feat: support a public browser demo of the kiosk #88

Merged
padreug merged 3 commits from feat/web-demo into dev 2026-09-06 18:04:50 +00:00
10 changed files with 80 additions and 4 deletions

View file

@ -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)
# =============================================================================

View file

@ -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",

View file

@ -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()

View file

@ -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

View file

@ -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 <html> 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;
}

View file

@ -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 .",

View file

@ -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<CreatedWallet> {
return this.sendRpc<CreatedWallet>('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

View file

@ -66,6 +66,7 @@ export type {
CreateInvoiceBody,
PayInvoiceBody,
WalletInfo,
CreatedWallet,
SubscribePaymentsBody,
SubscribeAck,
SubscribePush,

View file

@ -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
// ============================================================================

View file

@ -5,6 +5,11 @@
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"build:web": {
"dependsOn": ["^build"],
"outputs": ["dist/**"],
"env": ["VITE_*"]
},
"dev": {
"cache": false,
"persistent": true