From 445f08a5c17bb16e786bf07c93ea19d1a876cc3b Mon Sep 17 00:00:00 2001 From: archipelago Date: Mon, 22 Jun 2026 16:34:12 -0400 Subject: [PATCH] feat(demo): iframe asset-rewrite proxy, AIUI mockArchy, QR 2s, dummy mints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IndeeHub + Mempool: nginx reverse-proxy + strip X-Frame-Options/CSP + sub_filter rewrite of absolute asset paths so the frame-busting SPAs load in the iframe (mempool.space remains best-effort — third-party CSP/ws may still limit it). - AIUI iframe gets ?mockArchy in demo → its built-in mock node data loads. - Pay-with-mobile QR: invoice settles after ~2s (backend gate keyed by payment_hash) and the poll tightened to 1s, so the QR is visible before auto-pay. - Wallet settings: dummy Cashu mints (4) + Fedimint federations (2, 222,500 sats), interactive per session (streaming.list/configure-mints, wallet.fedimint-list/ join/balance). Co-Authored-By: Claude Opus 4.8 (1M context) --- neode-ui/docker/nginx-demo.conf | 43 ++++++++++++++++++++ neode-ui/mock-backend.js | 50 ++++++++++++++++++++++-- neode-ui/src/composables/useDemoIntro.ts | 14 +++---- neode-ui/src/views/Chat.vue | 8 +++- neode-ui/src/views/PeerFiles.vue | 2 +- 5 files changed, 103 insertions(+), 14 deletions(-) diff --git a/neode-ui/docker/nginx-demo.conf b/neode-ui/docker/nginx-demo.conf index 5c55bde8..01bfa11e 100644 --- a/neode-ui/docker/nginx-demo.conf +++ b/neode-ui/docker/nginx-demo.conf @@ -94,6 +94,49 @@ http { proxy_request_buffering off; } + # IndeeHub: reverse-proxy the real site same-origin, strip framing headers, + # and rewrite its absolute asset paths (/assets, /, src, href) to the + # /app/indeedhub/ prefix so the SPA loads inside the iframe. + location /app/indeedhub/ { + proxy_pass https://indee.tx1138.com/; + proxy_http_version 1.1; + proxy_set_header Host indee.tx1138.com; + proxy_set_header Accept-Encoding ""; + proxy_ssl_server_name on; + proxy_hide_header X-Frame-Options; + proxy_hide_header Content-Security-Policy; + proxy_hide_header Content-Security-Policy-Report-Only; + sub_filter_types text/html text/css application/javascript application/json; + sub_filter_once off; + sub_filter 'href="/' 'href="/app/indeedhub/'; + sub_filter 'src="/' 'src="/app/indeedhub/'; + sub_filter "href='/" "href='/app/indeedhub/"; + sub_filter "src='/" "src='/app/indeedhub/"; + sub_filter 'from"/' 'from"/app/indeedhub/'; + sub_filter 'url(/' 'url(/app/indeedhub/'; + } + + # Mempool: same approach. NOTE mempool.space is a strict third-party app — + # its data/websocket calls may still be blocked; iframe is best-effort. + location /app/mempool/ { + proxy_pass https://mempool.space/; + proxy_http_version 1.1; + proxy_set_header Host mempool.space; + proxy_set_header Accept-Encoding ""; + proxy_ssl_server_name on; + proxy_hide_header X-Frame-Options; + proxy_hide_header Content-Security-Policy; + proxy_hide_header Content-Security-Policy-Report-Only; + sub_filter_types text/html text/css application/javascript application/json; + sub_filter_once off; + sub_filter 'href="/' 'href="/app/mempool/'; + sub_filter 'src="/' 'src="/app/mempool/'; + sub_filter "href='/" "href='/app/mempool/"; + sub_filter "src='/" "src='/app/mempool/"; + sub_filter 'from"/' 'from"/app/mempool/'; + sub_filter 'url(/' 'url(/app/mempool/'; + } + # Proxy every other app UI (/app//) to the mock backend, which serves # the per-app mock UIs (bitcoin-ui, electrumx, lnd, fedimint) and the # generic "Not available in the demo" notice for the rest. diff --git a/neode-ui/mock-backend.js b/neode-ui/mock-backend.js index 39b13e18..a71b7301 100755 --- a/neode-ui/mock-backend.js +++ b/neode-ui/mock-backend.js @@ -149,6 +149,10 @@ const SEED_WALLET = { } function randomHex(bytes) { return Array.from({length: bytes}, () => Math.floor(Math.random()*256).toString(16).padStart(2,'0')).join('') } +// Tracks when a content invoice was requested, so the demo can leave the QR on +// screen for a couple of seconds before reporting it paid. +const invoiceRequestedAt = new Map() + const SEED_BTCRELAY = { settings: { enabled_for_peers: true, @@ -216,7 +220,20 @@ function seedUserState() { } function seedMockState() { - return { analyticsEnabled: false, nodeVisibility: 'discoverable' } + return { + analyticsEnabled: false, + nodeVisibility: 'discoverable', + cashuMints: [ + 'https://mint.minibits.cash/Bitcoin', + 'https://stablenut.umint.cash', + 'https://mint.coinos.io', + 'https://8333.space:3338', + ], + federations: [ + { federation_id: 'fed1' + randomHex(28), name: 'Archipelago Federation', balance_sats: 180_000 }, + { federation_id: 'fed1' + randomHex(28), name: 'Bitcoin Park Mint', balance_sats: 42_500 }, + ], + } } console.log(`[Auth] Dev mode: ${DEV_MODE}${DEMO ? ' (DEMO multi-session)' : ''}`) @@ -1981,17 +1998,42 @@ app.post('/rpc/v1', (req, res) => { } case 'content.request-invoice': { const price = params?.price_sats || 500 + const payment_hash = randomHex(32) + invoiceRequestedAt.set(payment_hash, Date.now()) return res.json({ result: { bolt11: 'lntb' + price + '1p' + randomHex(50), - payment_hash: randomHex(32), + payment_hash, price_sats: price, }, }) } case 'content.invoice-status': { - // Settle after the first poll so the QR flow shows a realistic wait. - return res.json({ result: { paid: true } }) + // Demo: leave the QR on screen for ~2s before the invoice "settles". + const at = invoiceRequestedAt.get(params?.payment_hash) + const paid = !at || (Date.now() - at) >= 2000 + return res.json({ result: { paid } }) + } + + // ── Wallet settings: Cashu mints + Fedimint federations (interactive) ── + case 'streaming.list-mints': { + return res.json({ result: { mints: mockState.cashuMints } }) + } + case 'streaming.configure-mints': { + if (Array.isArray(params?.mints)) mockState.cashuMints = params.mints + return res.json({ result: { success: true, mints: mockState.cashuMints } }) + } + case 'wallet.fedimint-list': { + return res.json({ result: { federations: mockState.federations } }) + } + case 'wallet.fedimint-join': { + const fed = { federation_id: 'fed1' + randomHex(28), name: 'Joined Federation', balance_sats: 0 } + mockState.federations.push(fed) + return res.json({ result: { success: true, ...fed } }) + } + case 'wallet.fedimint-balance': { + const total = (mockState.federations || []).reduce((s, f) => s + (f.balance_sats || 0), 0) + return res.json({ result: { balance_sats: total } }) } case 'content.request-onchain': { const price = params?.price_sats || 500 diff --git a/neode-ui/src/composables/useDemoIntro.ts b/neode-ui/src/composables/useDemoIntro.ts index 4bc4533e..38fee9a4 100644 --- a/neode-ui/src/composables/useDemoIntro.ts +++ b/neode-ui/src/composables/useDemoIntro.ts @@ -52,15 +52,15 @@ export function clearDemoIntroSeen(): void { // Only these apps actually do something in the demo (a mock UI or a real // external site). Everything else shows "No demo" on a disabled install button // and is not launchable. -const DEMO_EXTERNAL_URLS: Record = { - // Real, public sites loaded directly in the in-app iframe. - indeedhub: 'https://indee.tx1138.com/', - mempool: 'https://mempool.space/testnet', - 'mempool-web': 'https://mempool.space/testnet', -} +const DEMO_EXTERNAL_URLS: Record = {} -// Apps with a same-origin mock UI served by the demo backend. +// Apps loaded in the in-app iframe via a same-origin path. IndeeHub and Mempool +// are reverse-proxied by nginx (X-Frame-Options/CSP stripped + asset paths +// rewritten) so the frame-busting real sites can be embedded. const DEMO_MOCK_UI: Record = { + indeedhub: '/app/indeedhub/', + mempool: '/app/mempool/', + 'mempool-web': '/app/mempool/', 'bitcoin-knots': '/app/bitcoin-knots/', 'bitcoin-core': '/app/bitcoin-core/', bitcoin: '/app/bitcoin-core/', diff --git a/neode-ui/src/views/Chat.vue b/neode-ui/src/views/Chat.vue index e847d61d..0d110d0f 100644 --- a/neode-ui/src/views/Chat.vue +++ b/neode-ui/src/views/Chat.vue @@ -62,6 +62,7 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue' import { useRouter } from 'vue-router' import { useI18n } from 'vue-i18n' import { ContextBroker } from '@/services/contextBroker' +import { IS_DEMO } from '@/composables/useDemoIntro' const { t } = useI18n() @@ -71,9 +72,12 @@ const aiuiConnected = ref(false) let broker: ContextBroker | null = null const aiuiUrl = computed(() => { + // Demo: ?mockArchy makes AIUI use its built-in mock node data (apps, system, + // network, wallet, bitcoin, files) and &seed pre-loads the example chats. + const demo = IS_DEMO ? '&mockArchy=1&seed=1' : '' const envUrl = import.meta.env.VITE_AIUI_URL - if (envUrl) return `${envUrl}?embedded=true&hideClose=true` - if (import.meta.env.PROD) return '/aiui/?embedded=true&hideClose=true' + if (envUrl) return `${envUrl}?embedded=true&hideClose=true${demo}` + if (import.meta.env.PROD || IS_DEMO) return `/aiui/?embedded=true&hideClose=true${demo}` return '' }) diff --git a/neode-ui/src/views/PeerFiles.vue b/neode-ui/src/views/PeerFiles.vue index 7a6cdb70..4f0ff5f9 100644 --- a/neode-ui/src/views/PeerFiles.vue +++ b/neode-ui/src/views/PeerFiles.vue @@ -1304,7 +1304,7 @@ async function payWithLightning() { function scheduleInvoicePoll() { if (invoicePollTimer) clearTimeout(invoicePollTimer) - invoicePollTimer = setTimeout(pollInvoice, 3000) + invoicePollTimer = setTimeout(pollInvoice, 1000) } async function pollInvoice() {