diff --git a/demo-deploy/docker-compose.yml b/demo-deploy/docker-compose.yml index 0f657af0..64ea99cd 100644 --- a/demo-deploy/docker-compose.yml +++ b/demo-deploy/docker-compose.yml @@ -13,6 +13,8 @@ # IMAGE_TAG image tag to pull (default: demo) # ANTHROPIC_API_KEY optional — enables the AI chat panel # DEMO_WEB_PORT host port for the UI (default 2100) +# DEMO_INDEE_PORT host port for the IndeeHub demo proxy (default 2101; +# must stay 2101 unless the UI's demoAppUrl changes) services: neode-backend: @@ -42,6 +44,8 @@ services: container_name: archy-demo-web ports: - "${DEMO_WEB_PORT:-2100}:80" + # IndeeHub whole-origin demo proxy (nginx :2101 in the web image) + - "${DEMO_INDEE_PORT:-2101}:2101" environment: ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} depends_on: diff --git a/docker-compose.demo.yml b/docker-compose.demo.yml index cb083fd7..bca2b7c3 100644 --- a/docker-compose.demo.yml +++ b/docker-compose.demo.yml @@ -47,6 +47,8 @@ services: container_name: archy-demo-web ports: - "2100:80" + # IndeeHub whole-origin demo proxy (nginx :2101 in nginx-demo.conf) + - "2101:2101" depends_on: - neode-backend restart: unless-stopped diff --git a/neode-ui/Dockerfile.web b/neode-ui/Dockerfile.web index 9eafa714..ff00b812 100644 --- a/neode-ui/Dockerfile.web +++ b/neode-ui/Dockerfile.web @@ -57,9 +57,12 @@ COPY neode-ui/docker/nginx-demo.conf /etc/nginx/nginx.conf.template COPY neode-ui/docker/docker-entrypoint.sh /docker-entrypoint-custom.sh RUN chmod +x /docker-entrypoint-custom.sh +# IndeeHub demo sign-in seeder, injected by the :2101 whole-origin proxy +# (nginx-demo.conf). Demo image only — never in real-node artifacts. +COPY neode-ui/docker/indee-demo-signin.js /usr/share/nginx/html/__demo/indee-demo-signin.js -# Expose port -EXPOSE 80 +# Expose ports (80 = demo UI, 2101 = IndeeHub whole-origin demo proxy) +EXPOSE 80 2101 # Substitute ANTHROPIC_API_KEY at runtime, then start nginx ENTRYPOINT ["/docker-entrypoint-custom.sh"] diff --git a/neode-ui/docker/indee-demo-signin.js b/neode-ui/docker/indee-demo-signin.js new file mode 100644 index 00000000..4be0171c --- /dev/null +++ b/neode-ui/docker/indee-demo-signin.js @@ -0,0 +1,58 @@ +/** + * PUBLIC-DEMO-ONLY IndeeHub sign-in seeder. + * + * Served at /__demo/indee-demo-signin.js on the :2101 IndeeHub whole-origin + * demo proxy (see nginx-demo.conf) and injected into the proxied site's HTML + * via sub_filter. It only ever runs on the demo's :2101 origin inside + * the demo iframe — it never ships in real-node artifacts. + * + * THROWAWAY DEMO IDENTITY — NOT A SECRET. The embedded secp256k1 keypair was + * freshly generated for the public demo (2026-07-29) and has never belonged + * to any real user. Its whole purpose is to be a shared, public "demo + * visitor" Nostr identity so the embedded IndeeHub boots signed in with no + * login wall. Anyone extracting this key can only impersonate the demo + * visitor, by design (threat T-gjd-01: accepted). + * + * How it works: IndeeHub's bundle (applesauce-accounts) restores accounts on + * boot from localStorage key "indeedhub-accounts" (JSON array of serialized + * accounts; a "nsec" private-key account deserializes as + * { id, type: "nsec", pubkey, metadata, signer: { key: } }) and + * activates the account whose id is stored under "indeedhub-active-account". + * This classic script executes before the SPA's deferred module bundle, so + * seeding here is visible to that boot-restore. Seeding is idempotent: an + * existing non-empty account list is never overwritten. + */ +;(function () { + 'use strict' + + var ACCOUNTS_KEY = 'indeedhub-accounts' + var ACTIVE_KEY = 'indeedhub-active-account' + + // Throwaway demo keypair (see header — public by design, not a secret). + var DEMO_SK_HEX = 'ce2ffa96f99968beffc789cbba5d8b52f4a3020454dcaf77c2b553961bf5a8c9' + var DEMO_PK_HEX = '7261540160244ec65ce0bf86ba03997e9b1b3b35c277e416bf1c7ba4271fee31' + var DEMO_ACCOUNT_ID = 'archy-demo-visitor' + + try { + var existing = null + try { + existing = JSON.parse(localStorage.getItem(ACCOUNTS_KEY)) + } catch (e) { + existing = null + } + if (Array.isArray(existing) && existing.length > 0) return + + var account = { + id: DEMO_ACCOUNT_ID, + type: 'nsec', + pubkey: DEMO_PK_HEX, + metadata: { name: 'Archy Demo' }, + signer: { key: DEMO_SK_HEX }, + } + localStorage.setItem(ACCOUNTS_KEY, JSON.stringify([account])) + localStorage.setItem(ACTIVE_KEY, DEMO_ACCOUNT_ID) + } catch (e) { + // localStorage unavailable (e.g. blocked third-party storage) — the demo + // visitor just sees IndeeHub's normal signed-out state. + } +})() diff --git a/neode-ui/docker/nginx-demo.conf b/neode-ui/docker/nginx-demo.conf index 0c2a8c3b..392b9d00 100644 --- a/neode-ui/docker/nginx-demo.conf +++ b/neode-ui/docker/nginx-demo.conf @@ -103,10 +103,14 @@ http { proxy_request_buffering off; } - # IndeeHub is no longer proxied same-origin — the sub_filter rewrite - # approach broke the SPA's runtime-built asset URLs. The demo now opens - # the real site (https://indee.tx1138.com/) externally instead, via - # DEMO_EXTERNAL_URLS in useDemoIntro.ts. + # IndeeHub is NOT proxied under a /app/indeedhub/ path prefix — the + # old sub_filter path-rewrite approach broke the SPA's runtime-built + # absolute-root asset URLs. Instead, a dedicated WHOLE-ORIGIN reverse + # proxy of https://indee.tx1138.com listens on :2101 (see the second + # server block below): the SPA sees itself at '/' so every asset and + # router path just works, framing headers are stripped, and a demo + # sign-in seed script is injected. useDemoIntro.demoAppUrl points the + # in-app iframe at http://:2101/. # Mempool is NOT proxied upstream anymore — the mock backend serves a # branded placeholder page for it (see DEMO_APP_PAGES in mock-backend.js), @@ -177,4 +181,46 @@ http { add_header Cache-Control "public, immutable"; } } + + # ── IndeeHub whole-origin demo proxy (:2101) ──────────────────────────── + # Pure reverse proxy of the LIVE https://indee.tx1138.com site on its own + # port — no path prefix, no URL rewriting, so the SPA's absolute-root + # asset/router paths work untouched. The upstream's X-Frame-Options + # (SAMEORIGIN) and any CSP are stripped so the demo can embed it in the + # in-app iframe session, and a PUBLIC-DEMO-ONLY sign-in seed script is + # injected into the HTML (it seeds a labelled throwaway demo Nostr + # account into the :2101 origin's isolated localStorage, so IndeeHub boots + # signed in). Upstream is pinned to a single fixed hostname — this cannot + # be used as an open proxy. + server { + listen 2101; + server_name _; + + # Demo sign-in seeder, served same-origin to the proxied SPA. + location = /__demo/indee-demo-signin.js { + root /usr/share/nginx/html; + add_header Cache-Control "no-store"; + } + + location / { + proxy_pass https://indee.tx1138.com; + proxy_ssl_server_name on; + proxy_ssl_name indee.tx1138.com; + proxy_set_header Host indee.tx1138.com; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 86400; + + # Allow embedding in the demo's iframe session. + proxy_hide_header X-Frame-Options; + proxy_hide_header Content-Security-Policy; + + # HTML injection: upstream must not compress or sub_filter no-ops. + # (sub_filter applies to text/html by default — exactly what we want.) + proxy_set_header Accept-Encoding ""; + sub_filter_once on; + sub_filter '' ''; + } + } }