diff --git a/apps/machine/.env.example b/apps/machine/.env.example index 66541dc..ec66f81 100644 --- a/apps/machine/.env.example +++ b/apps/machine/.env.example @@ -73,6 +73,18 @@ VITE_SPIRE_SEED= # Show "Under Service" screen and block all transactions # VITE_MAINTENANCE_MODE=true +# ============================================================================= +# Public Web Demo +# ============================================================================= + +# Set ONLY for the browser demo build (atm.demo.aiolabs.dev). Leave blank on +# every real machine. When set it: +# - keeps the mouse cursor visible (kiosk builds hide it) +# - mints one extra, never-used LNbits wallet named with this exact string, +# so the throwaway accounts the demo creates (one per page load, each with +# its own ephemeral identity) can be swept by name instead of guessed at. +# VITE_DEMO_TAG=bitspire-web-demo + # ============================================================================= # Mock Fallback (Production Safety) # ============================================================================= diff --git a/apps/machine/package.json b/apps/machine/package.json index 7f68db8..cefc699 100644 --- a/apps/machine/package.json +++ b/apps/machine/package.json @@ -15,6 +15,7 @@ "dev:vite": "vite", "electron:dev": "tsc -p electron/tsconfig.json && tsc -p electron/tsconfig.preload.json && electron dist-electron/main.js", "build": "vue-tsc --noEmit && vite build && tsc -p electron/tsconfig.json && tsc -p electron/tsconfig.preload.json && npx esbuild electron/fund-atm.ts --bundle --platform=node --format=cjs --external:better-sqlite3 --outfile=dist-electron/fund-atm.bundle.cjs", + "build:web": "vite build", "build:electron": "pnpm build && electron-builder", "preview": "vite preview", "typecheck": "vue-tsc --noEmit", diff --git a/apps/machine/src/main.ts b/apps/machine/src/main.ts index 5f39e0a..8679c87 100644 --- a/apps/machine/src/main.ts +++ b/apps/machine/src/main.ts @@ -24,6 +24,13 @@ const router = createRouter({ ], }) +// Kiosk chrome (hidden cursor) is the default — every real machine is a +// touchscreen. The public web demo (VITE_DEMO_TAG) runs in a normal browser, +// where an invisible pointer just reads as broken. +if (!import.meta.env.VITE_DEMO_TAG) { + document.documentElement.classList.add('kiosk') +} + // Create Pinia store const pinia = createPinia() diff --git a/apps/machine/src/services/lightning.ts b/apps/machine/src/services/lightning.ts index eb2e1c7..aa6b52c 100644 --- a/apps/machine/src/services/lightning.ts +++ b/apps/machine/src/services/lightning.ts @@ -505,6 +505,31 @@ export async function initializeLightningServices(options?: { } console.log('[Lightning] LNbits wallet:', lnbitsWalletId) + // ── Public web demo: stamp the throwaway account so it can be swept ────── + // The browser demo (atm.demo.aiolabs.dev) runs with an EPHEMERAL identity — + // a fresh keypair per page load — so LNbits mints a new account + a fresh + // auto-credited wallet for every visitor. That isolation is the point (a + // single baked-in key would be credited exactly once and then drain), but it + // leaves throwaway accounts behind, and nothing in an auto-created row says + // "demo": pubkey-set/prvkey-NULL also describes a real ATM. + // + // A nostr pubkey can't carry a marker (you'd have to grind a vanity prefix, + // far too slow to do on page load), and the account/wallet the server + // auto-creates isn't nameable by the client. So we mint one extra, + // never-used wallet whose NAME is the tag: sweeping is then an exact string + // match on wallet name rather than a heuristic about what looks disposable. + // + // Unset on every real machine, so this is inert outside the demo build. The + // call is fire-and-forget: losing the marker degrades cleanup, not the demo. + const demoTag = (import.meta.env.VITE_DEMO_TAG as string | undefined)?.trim() + if (demoTag) { + void lnbits + .createWallet(demoTag) + // Never log the reply — create_wallet returns adminkey/inkey. + .then(() => console.log('[Lightning] Demo marker wallet created:', demoTag)) + .catch((e) => console.warn('[Lightning] Demo marker wallet failed:', e)) + } + // #70 P1: pull operator pubkey + fee config from LNbits over the authenticated // transport (spirekeeper#41 `get_machine_config`). A seed-only machine has no // VITE_OPERATOR_PUBKEYS, so without this it can't trust its fee config and sits diff --git a/apps/machine/src/style.css b/apps/machine/src/style.css index 621cf82..288f0dc 100644 --- a/apps/machine/src/style.css +++ b/apps/machine/src/style.css @@ -1,10 +1,13 @@ @import 'tailwindcss'; @import 'tw-animate-css'; -/* Hide cursor completely on touchscreen kiosk */ -*, -*::before, -*::after { +/* Hide cursor completely on touchscreen kiosk. + Scoped to .kiosk (set on by main.ts) so the public web demo, which + runs in an ordinary browser with a mouse, keeps a visible pointer. */ +.kiosk, +.kiosk *, +.kiosk *::before, +.kiosk *::after { cursor: none !important; } diff --git a/package.json b/package.json index 1cf98b1..6499319 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "scripts": { "dev": "turbo dev", "build": "turbo build", + "build:web": "turbo build:web", "test": "turbo test", "lint": "turbo lint", "format": "prettier --write .", diff --git a/packages/lnbits/src/client.ts b/packages/lnbits/src/client.ts index cb3645d..ad212a8 100644 --- a/packages/lnbits/src/client.ts +++ b/packages/lnbits/src/client.ts @@ -37,6 +37,7 @@ import type { CreateInvoiceBody, PayInvoiceBody, WalletInfo, + CreatedWallet, MachineConfigResponse, SubscribePaymentsBody, SubscribeAck, @@ -211,6 +212,17 @@ export class LnbitsClient { return data ?? [] } + /** + * Create an additional wallet on the calling account (`create_wallet`). + * + * Account-scoped (AUTH_ACCOUNT): the envelope carries no `wallet_id`, which + * is what makes the server resolve auth to the Account rather than a Wallet. + * NOT wrapped in `idempotent()` — a retry would mint a duplicate wallet. + */ + async createWallet(name: string): Promise { + return this.sendRpc('create_wallet', { body: { name } }) + } + /** Pull server-delivered machine config (operator pubkey + fee config) over * the authenticated transport — spirekeeper's `get_machine_config` RPC * (bitspire#70 P1). Lets a seed-only ATM configure itself with no per-machine diff --git a/packages/lnbits/src/index.ts b/packages/lnbits/src/index.ts index 9618a2f..e1e87c1 100644 --- a/packages/lnbits/src/index.ts +++ b/packages/lnbits/src/index.ts @@ -66,6 +66,7 @@ export type { CreateInvoiceBody, PayInvoiceBody, WalletInfo, + CreatedWallet, SubscribePaymentsBody, SubscribeAck, SubscribePush, diff --git a/packages/lnbits/src/types.ts b/packages/lnbits/src/types.ts index dd5e7a9..fdebb95 100644 --- a/packages/lnbits/src/types.ts +++ b/packages/lnbits/src/types.ts @@ -112,6 +112,15 @@ export interface WalletInfo { balance: number } +/** Reply shape of the `create_wallet` RPC — unlike WalletInfo it carries the + * fresh wallet's keys, so never log it verbatim. */ +export interface CreatedWallet { + id: string + name: string + adminkey: string + inkey: string +} + // ============================================================================ // Subscriptions // ============================================================================ diff --git a/turbo.json b/turbo.json index d6d2fb5..a5a9376 100644 --- a/turbo.json +++ b/turbo.json @@ -5,6 +5,11 @@ "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**", "!.next/cache/**"] }, + "build:web": { + "dependsOn": ["^build"], + "outputs": ["dist/**"], + "env": ["VITE_*"] + }, "dev": { "cache": false, "persistent": true