From db25545a9a6a0695421e7656bcc25bdee983dde2 Mon Sep 17 00:00:00 2001 From: archipelago Date: Wed, 29 Jul 2026 12:13:21 -0400 Subject: [PATCH] docs(260729-gjd): pre-dispatch plan for demo IndeeHub iframe + signer + preinstall Co-Authored-By: Claude Fable 5 --- .../260729-gjd-PLAN.md | 348 ++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 .planning/quick/260729-gjd-demo-make-indee-tx1138-com-work-in-the-a/260729-gjd-PLAN.md diff --git a/.planning/quick/260729-gjd-demo-make-indee-tx1138-com-work-in-the-a/260729-gjd-PLAN.md b/.planning/quick/260729-gjd-demo-make-indee-tx1138-com-work-in-the-a/260729-gjd-PLAN.md new file mode 100644 index 00000000..f0e59037 --- /dev/null +++ b/.planning/quick/260729-gjd-demo-make-indee-tx1138-com-work-in-the-a/260729-gjd-PLAN.md @@ -0,0 +1,348 @@ +--- +phase: quick-260729-gjd +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - neode-ui/docker/nginx-demo.conf + - neode-ui/docker/indee-demo-signin.js + - neode-ui/Dockerfile.web + - docker-compose.demo.yml + - demo-deploy/docker-compose.yml + - neode-ui/src/composables/useDemoIntro.ts + - neode-ui/src/views/appSession/useAppIdentity.ts + - neode-ui/mock-backend.js +autonomous: true +requirements: [QUICK-260729-GJD] +must_haves: + truths: + - "A fresh demo session (clean browser, no localStorage) shows IndeeHub as an installed, running app in My Apps" + - "Launching IndeeHub in the demo renders the real indee.tx1138.com site inside the in-app iframe session (no new tab, no external interstitial)" + - "The embedded IndeeHub boots signed-in (active demo account visible, no login wall) and no identity-picker modal blocks the demo visitor" + - "The non-demo (real node) build is byte-for-byte unaffected in behavior: indeedhub launch, identity picker, and NIP-07 bridge all work as before" + - "The served demo content contains no occurrence of the private release-server IP (existing Docker build guards still pass)" + artifacts: + - "neode-ui/docker/nginx-demo.conf — new whole-origin reverse-proxy server block (port 2101) for indee.tx1138.com with framing headers stripped and sign-in script injected" + - "neode-ui/docker/indee-demo-signin.js — demo-only localStorage seeding script with a labelled throwaway demo nsec" + - "docker-compose.demo.yml and demo-deploy/docker-compose.yml — publish the new 2101 port" + - "neode-ui/src/composables/useDemoIntro.ts — indeedhub moved from external-tab to iframe launch via the :2101 proxy origin" + - "neode-ui/mock-backend.js — indeedhub present in staticDevApps as installed/running" + key_links: + - "demoAppUrl('indeedhub') → http://:2101/ → nginx :2101 server block → https://indee.tx1138.com upstream" + - "nginx sub_filter → /__demo/indee-demo-signin.js → seeds indeedhub-accounts + indeedhub-active-account localStorage keys → IndeeHub boot-restore logs the visitor in" + - "staticDevApps['indeedhub'] → structuredClone into per-session package-data → My Apps grid on fresh session" +--- + + +Make IndeeHub a first-class app in the PUBLIC DEMO only: (1) the real site +https://indee.tx1138.com/ renders inside the in-app iframe session (today it is +frame-busted by `X-Frame-Options: SAMEORIGIN` and opens externally), (2) a demo +visitor sees IndeeHub already signed in with a throwaway demo Nostr identity +(no login wall, no identity-picker modal), and (3) IndeeHub appears as an +already-installed, running app on a completely fresh demo session. + +Purpose: the demo currently punts IndeeHub to a new tab with a login wall — +the flagship media app looks broken/hostile to demo visitors. +Output: demo-scoped changes across nginx-demo.conf, a new sign-in seed script, +the two demo compose files, useDemoIntro.ts, useAppIdentity.ts, mock-backend.js. + +All behavior changes are gated behind IS_DEMO / demo-image build paths. The +real-node build must be completely unaffected. + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@CLAUDE.md +@neode-ui/src/composables/useDemoIntro.ts +@neode-ui/docker/nginx-demo.conf +@neode-ui/Dockerfile.web +@neode-ui/Dockerfile.backend +@docker-compose.demo.yml +@demo-deploy/docker-compose.yml +@neode-ui/src/views/appSession/appSessionConfig.ts +@neode-ui/src/views/appSession/useAppIdentity.ts +@neode-ui/src/views/appSession/useNostrBridge.ts +@neode-ui/src/stores/appLauncher.ts +@neode-ui/mock-backend.js + + + +Facts confirmed by inspection on 2026-07-29 (do not re-derive, but re-verify +the live-bundle details marked "verify at exec time"): + +- `curl -sI https://indee.tx1138.com/` → `X-Frame-Options: SAMEORIGIN`, no + Content-Security-Policy header today (strip both defensively). +- The live index.html loads a hashed module bundle from absolute-root paths + (`/assets/index-*.js`, `/icons/...`, `/manifest.json`). This is why the old + `/app/indeedhub/` path-prefix + sub_filter proxy broke (asset/router paths + escape the prefix). A WHOLE-ORIGIN proxy on a dedicated port has no such + problem — the SPA sees itself at `/` and every relative call just works. +- The live bundle (assets/index-BMWtjRCn.js) uses applesauce-accounts: + - localStorage key `"indeedhub-accounts"` = JSON array of serialized + accounts (`Fe.toJSON`/`Fe.fromJSON`), restored on boot before UI renders. + - localStorage key `"indeedhub-active-account"` = active account id. + - A private-key account class exists whose `fromJSON` does + `const t=Vn(e.signer.key); new mr(e.pubkey, new ai(t))` — i.e. shape is + `{ id, type: "", pubkey, signer: { key: "" } }` + plus common fields from `loadCommonFields` (verify exact `type` string and + common fields by grepping the live bundle for `static type` / the class's + `toJSON`). An `"extension"` account type also exists (fallback path). + - The bundle expects NIP-07 as a real `window.nostr` object ("Signer + extension missing" guard) — it does NOT contain the archipelago + `nostr-request` postMessage client. +- Parent-side NIP-07 plumbing already exists: `AppSession.vue` line ~423 + routes `nostr-request` messages to `useNostrBridge`, which calls + `node.nostr-pubkey` (mocked in mock-backend.js) and `node.nostr-sign` + (NOT implemented in mock-backend.js). Only needed for the fallback approach. +- `useAppIdentity.ts`: `isIdentityAwareApp('indeedhub')` is true → on iframe + load with no stored identity it opens the identity-picker modal. In the demo + this is a blocking modal the visitor shouldn't see. +- `appLauncher.ts openSession`: `IS_DEMO && isDemoExternal(appId)` is the only + thing forcing indeedhub external; `NEW_TAB_APP_IDS` is already bypassed when + `IS_DEMO && isDemoApp(appId)`. `AppSession.vue mustOpenNewTab` has the same + two-clause shape. Removing indeedhub from `DEMO_EXTERNAL_URLS` while keeping + `isDemoApp('indeedhub')` true flips it to the iframe path everywhere. +- `mock-backend.js`: per-visitor session state is initialized via + `md['package-data'] = structuredClone(staticDevApps)` (~line 5493), so + adding an entry to `staticDevApps` (~line 828) makes it installed on every + fresh session. `APP_PORTS`-style map at ~line 323 already has + `'indeedhub': 8190`; an icon exists at `/assets/img/app-icons/indeedhub.png`. +- `Dockerfile.web` copies `nginx-demo.conf` to `/etc/nginx/nginx.conf.template` + and runs `docker-entrypoint-custom.sh` (env substitution) — read the + entrypoint before editing so the new server block's nginx `$vars` survive + templating the same way the existing blocks' do. +- Both Docker builds already scrub + fail on any occurrence of the private + release-server IP; nothing in this change may hardcode host IPs — build the + iframe URL from `window.location.hostname`. +- `indeedhub/` at repo root is a git submodule (not checked out) — NEVER stage + any path under it. `indeedhub-demo/` is a prior standalone-build attempt + (clones the GitHub fork, builds with VITE env); this plan supersedes it by + proxying the LIVE site instead — leave that directory untouched. + + + + + + Task 1: nginx whole-origin proxy on :2101 + sign-in seed script + compose ports + neode-ui/docker/nginx-demo.conf, neode-ui/docker/indee-demo-signin.js, neode-ui/Dockerfile.web, docker-compose.demo.yml, demo-deploy/docker-compose.yml, neode-ui/docker/docker-entrypoint.sh + + Add a second `server` block to nginx-demo.conf: `listen 2101;` that is a + pure whole-origin reverse proxy of `https://indee.tx1138.com` — no path + prefix, no URL rewriting (this is the fix for the documented sub_filter + breakage: the SPA keeps its own absolute-root paths). In that block: + `location / { proxy_pass https://indee.tx1138.com; }` with + `proxy_ssl_server_name on;`, `proxy_ssl_name indee.tx1138.com;`, + `proxy_set_header Host indee.tx1138.com;`, + `proxy_http_version 1.1;` + WebSocket upgrade headers (reuse the existing + `$connection_upgrade` map), `proxy_hide_header X-Frame-Options;` and + `proxy_hide_header Content-Security-Policy;`. For HTML injection: + `proxy_set_header Accept-Encoding "";` (upstream must not gzip or + sub_filter no-ops), `sub_filter_types text/html;`, `sub_filter_once on;`, + `sub_filter '' '';` + — a classic (non-module) script injected at end of head still executes + BEFORE the SPA's deferred module bundle, which is what the seeding needs. + Add `location = /__demo/indee-demo-signin.js { root /usr/share/nginx/html; }` + (or alias) inside the 2101 server so the seed script is served same-origin + to the iframe. Update the comment block that currently explains why + IndeeHub is not proxied (lines ~106-109) to describe the new :2101 design. + + Create neode-ui/docker/indee-demo-signin.js: a small plain-JS classic + script, clearly headed with a comment stating it is PUBLIC-DEMO-ONLY and + that the embedded key is a freshly generated THROWAWAY demo identity, not + a real secret. Generate ONE fresh secp256k1 keypair at implementation time + (e.g. `node -e` with a tiny script using any available schnorr/secp lib, or + a one-off `npx` of nostr-tools in the scratchpad — the generator itself is + not committed) and embed hex sk + hex pk as constants. The script: if + `localStorage.getItem('indeedhub-accounts')` is empty/absent, write the + two keys IndeeHub's boot-restore reads — `indeedhub-accounts` (JSON array + with ONE serialized private-key account: verify the exact `type` string + and common-field shape against the live bundle per verified_findings, shape + `{ id, type, pubkey, signer: { key } }` + whatever `loadCommonFields` + round-trips, give it a friendly name/metadata like "Archy Demo" if the + shape supports it) and `indeedhub-active-account` (that account's id). + Because the script runs on the :2101 origin inside the iframe, this + touches only the proxied app's isolated storage. IndeeHub then restores + the account on boot and self-signs with its own bundled signer — no + window.nostr and no parent bridge required. Do NOT define a partial + `window.nostr` in this approach (a pubkey-only shim with a broken + signEvent causes worse failures than no shim). + + FALLBACK (only if live testing in Task-3 verification shows the seeded + account shape is not accepted): seed an `"extension"`-type account + instead, define a `window.nostr` postMessage client in this same script + (request/response protocol matching useNostrBridge: post + `{type:'nostr-request', id, method, params}` to `window.parent`, resolve on + `{type:'nostr-response', id, ...}`), and implement `node.nostr-sign` / + `identity.nostr-sign` in mock-backend.js with real schnorr signatures over + the same throwaway key (add `nostr-tools` to neode-ui dependencies — it is + pure JS and Dockerfile.backend runs `npm install` over package.json). + Prefer the primary approach; only fall back with evidence. + + Wire the plumbing: `EXPOSE 2101` in Dockerfile.web (the seed script is + already inside `neode-ui/` so the existing `COPY neode-ui/ ./` + + dist copy do NOT ship it — add an explicit + `COPY neode-ui/docker/indee-demo-signin.js /usr/share/nginx/html/__demo/indee-demo-signin.js` + in the nginx stage of Dockerfile.web; it lands only in the demo web image, + never in real-node artifacts). Publish the port in docker-compose.demo.yml + (`"2101:2101"` on neode-web) and demo-deploy/docker-compose.yml (use an + env-overridable mapping consistent with its existing `DEMO_WEB_PORT` + style, e.g. `"${DEMO_INDEE_PORT:-2101}:2101"`, and document it in that + file's header comment). Read docker-entrypoint.sh first and make sure the + new server block survives its template substitution exactly like the + existing blocks (same escaping convention for nginx `$` variables); touch + the entrypoint only if its substitution list needs it. + + Do not put any host IP in any of these files; upstream hostname + indee.tx1138.com is fine. + + + docker run --rm -v "$PWD/neode-ui/docker/nginx-demo.conf:/etc/nginx/nginx.conf:ro" nginx:alpine nginx -t (or, if docker unavailable locally, `nginx -t -c` via a podman run — config must parse). Plus: grep -c "2101" neode-ui/docker/nginx-demo.conf docker-compose.demo.yml demo-deploy/docker-compose.yml neode-ui/Dockerfile.web — each ≥1; grep -q "indee-demo-signin" neode-ui/docker/nginx-demo.conf && grep -qi "throwaway" neode-ui/docker/indee-demo-signin.js + + nginx config parses with the new :2101 whole-origin proxy block (framing headers stripped, sub_filter injection, WS upgrade); seed script exists with labelled throwaway demo key and idempotent localStorage seeding; both compose files publish 2101; demo web image copies the script and exposes the port; no host IPs added anywhere. + + + + Task 2: demo frontend — iframe launch via :2101 and no identity-picker wall + neode-ui/src/composables/useDemoIntro.ts, neode-ui/src/views/appSession/useAppIdentity.ts + + In useDemoIntro.ts: remove `indeedhub` from `DEMO_EXTERNAL_URLS` (delete + the map entirely if it becomes empty, simplifying `isDemoExternal` to + return false — keep the exported function so call sites in appLauncher.ts + and AppSession.vue compile unchanged). Make `demoAppUrl('indeedhub')` + return the proxied origin built at runtime: + `${window.location.protocol}//${window.location.hostname}:2101/` + (hostname, never a hardcoded host/IP — works on any deploy host). Keep + `isDemoApp('indeedhub')` true (it must stay in the demoable set so the + NEW_TAB bypass in appLauncher.openSession and AppSession.mustOpenNewTab + keeps routing it into the in-app iframe session, and so the install + button stays enabled). Update the file-header comment block that + currently documents the external-tab workaround to describe the :2101 + whole-origin proxy design instead. SSR-safety is not a concern (Vite SPA) + but guard `typeof window !== 'undefined'` if other tests import the module + in node context — check the existing unit tests under + src/views/appSession/__tests__/ and src/stores/__tests__/ for assertions + about indeedhub being demo-external and update them to the new behavior. + + In useAppIdentity.ts: gate the picker for the demo. Import IS_DEMO from + useDemoIntro and in `onIframeLoadIdentity` / `handleIdentityRequest`, + when IS_DEMO is true, never set `showIdentityPicker` — the demo visitor + must not be interrupted by an identity modal (the embedded IndeeHub is + already signed in via the seeded account from Task 1, and `sendIdentity`'s + `identity.sign` RPC is not what logs it in). Real-node behavior + (picker on first launch) is untouched because IS_DEMO is compile-time + false there. + + + cd neode-ui && npx vitest run src/views/appSession src/stores --silent 2>&1 | tail -5 (all green) && VITE_DEMO=1 npm run build && grep -rq "2101" dist/assets && npm run build && grep -rq "indee.tx1138.com" dist/assets && echo BUNDLE-OK + + Demo build (VITE_DEMO=1) bundle contains the :2101 launch logic (grep hit proves the build didn't silently no-op — per CLAUDE.md); plain build still compiles and demo-gated branches do not alter non-demo behavior; unit tests updated and green; launching indeedhub in demo resolves to the same-host :2101 origin in the iframe session; identity picker suppressed only under IS_DEMO. + + + + Task 3: mock backend — IndeeHub pre-installed on fresh demo sessions + neode-ui/mock-backend.js + + Add an `indeedhub` entry to `staticDevApps` in mock-backend.js using the + existing `staticApp({...})` helper: id `indeedhub`, title `Indeehub` + (match the existing title map at ~line 537 and APP_TITLES), a short/long + description consistent with the marketplace copy ("Bitcoin documentary + streaming platform" per the existing entry), `state: 'running'`, + `lanPort: 8190` (matches the existing port map), icon + `/assets/img/app-icons/indeedhub.png`. Because per-session demo state is + `structuredClone(staticDevApps)`, this alone makes it installed+running on + every fresh session. Then reconcile the rest of the mock so nothing + contradicts installed status: check the marketplace/available-apps mock + responses and any install/uninstall handlers (~lines 540-740, 1900-1960, + 4900+) for `indeedhub` entries that would render it as not-installed or + double-listed, and check `DEMO_APP_PAGES` does NOT grow an indeedhub + placeholder (the demo launch URL bypasses /app/indeedhub/ entirely — the + iframe goes to the :2101 origin). Keep the existing `node.nostr-pubkey` + mock as-is unless Task 1's fallback path was taken (in which case align + its pubkey with the throwaway demo key and add the sign handlers described + there). + + + cd neode-ui && node -e "const s=require('fs').readFileSync('mock-backend.js','utf8'); if(!/staticDevApps[\s\S]*?indeedhub:\s*staticApp/.test(s)) process.exit(1)" && (DEMO=1 timeout 20 node mock-backend.js & sleep 4; curl -s -X POST localhost:5959/rpc/v1 -H 'content-type: application/json' -d '{"method":"server.data","id":1}' -H 'cookie: demo=fresh' | grep -o '"indeedhub"' | head -1; kill %1 2>/dev/null) — expect an indeedhub hit in fresh-session package-data (adapt the RPC method/auth to what the mock actually serves; a login with the demo password first is fine) + + A fresh demo session's package-data includes indeedhub as installed and running with launchable UI; My Apps shows it without an install step; no duplicate/contradictory indeedhub listing in marketplace mocks; mock backend boots cleanly with DEMO=1. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| demo nginx :2101 → indee.tx1138.com | demo host proxies an external site; upstream content is served under the demo host | +| iframe (:2101 origin) ↔ parent (:2100 origin) | cross-origin; parent NIP-07 bridge only used in fallback path | +| public visitors → demo host | anyone can drive the proxy | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-gjd-01 | Spoofing | throwaway demo nostr key | low | accept | key is a labelled public demo identity by design; generated fresh, never a real user key; anyone extracting it can only impersonate "the demo visitor" | +| T-gjd-02 | Info disclosure | private release-server IP in served content | high | mitigate | no host IPs added in any changed file; iframe URL derived from window.location.hostname; existing Docker-build scrub+fail guards remain the backstop | +| T-gjd-03 | Tampering | open reverse proxy on :2101 | medium | mitigate | proxy is pinned to a single upstream host (proxy_pass fixed hostname + proxy_ssl_name), no dynamic upstreams, no request-driven destinations — it cannot be used as an open proxy | +| T-gjd-04 | Elevation | header stripping (X-Frame-Options/CSP) | low | accept | stripping applies only to the :2101 demo proxy of one known site, demo image only; real-node builds never carry this config | +| T-gjd-SC | Tampering | npm installs | low | accept | primary path adds no dependencies; fallback path adds only nostr-tools (well-known, verify on npmjs.com before install) | + + + +Local (executor, before commit): +1. nginx config parses (Task 1 verify). +2. Unit tests green; VITE_DEMO=1 build contains ":2101" logic; plain build + unaffected (Task 2 verify). Note: demo-gated strings are tree-shaken out of + the plain build — that is EXPECTED; the bundle-grep for demo strings must be + done on the VITE_DEMO=1 build, which is exactly what the demo Docker image + builds (Dockerfile.web defaults ARG VITE_DEMO=1). +3. Fresh-session mock package-data includes indeedhub (Task 3 verify). +4. Optional full-stack smoke: `docker compose -f docker-compose.demo.yml up + --build` locally, browse http://localhost:2100 in a private window → + login `entertoexit` → IndeeHub installed → launch → iframe renders the + proxied site from http://localhost:2101 with a signed-in account. +5. `git status` — confirm nothing under indeedhub/ is staged, ever. + +Post-deploy on vps2 (orchestrator deploys; verify on http://146.59.87.168:2100): +1. `curl -sI http://146.59.87.168:2101/` returns 200 with NO X-Frame-Options + header and the injected `indee-demo-signin.js` tag in the HTML body + (`curl -s http://146.59.87.168:2101/ | grep indee-demo-signin`). If the + port is unreachable, the vps2 firewall needs 2101 opened — flag to + orchestrator. +2. Fresh private browser window → :2100 → login → IndeeHub shows installed/ + running on the dashboard/My Apps without any install action. +3. Launch IndeeHub → renders inside the in-app iframe (panel/overlay), not a + new tab; content browsable; no identity-picker modal. +4. Signed-in check: IndeeHub header shows an active account (avatar/profile + instead of a sign-in button). If the seeded account shape was rejected + (login wall still visible), execute the documented fallback (extension + account + window.nostr shim + mock signer) and redeploy. +5. View-source/network spot-check: no occurrence of the private + release-server IP in any served response. +6. Repeat-visit check: reload the iframe once — a service worker registered by + IndeeHub may serve cached HTML without the injected tag on later loads; + that is acceptable because localStorage is already seeded on first load, + but confirm sign-in persists. + + + +- Demo visitor on a fresh browser sees IndeeHub installed, launches it into + the in-app iframe, and browses indee.tx1138.com content signed in — zero + clicks spent on install/login/identity modals. +- Real-node build behavior unchanged (all changes IS_DEMO- or demo-image-gated). +- No secrets committed beyond the labelled throwaway demo key; nothing staged + under indeedhub/; demo serves no private release-server IP. +- Work committed in focused commits (infra / frontend / mock) with the + Co-Authored-By trailer and pushed via gitea-ai per CLAUDE.md; docs left to + the orchestrator. + + + +Create `.planning/quick/260729-gjd-demo-make-indee-tx1138-com-work-in-the-a/260729-gjd-SUMMARY.md` when done. +