Files
archy/.planning/quick/260729-gjd-demo-make-indee-tx1138-com-work-in-the-a/260729-gjd-PLAN.md
T

22 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
quick-260729-gjd 01 execute 1
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
true
QUICK-260729-GJD
truths artifacts key_links
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)
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
demoAppUrl('indeedhub') → http://<demo-host>: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.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@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

<verified_findings> 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: "<verify at exec time>", pubkey, signer: { key: "<hex sk>" } } 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. </verified_findings>
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 '</head>' '<script src="/__demo/indee-demo-signin.js"></script></head>';` — 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.

<threat_model>

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)
</threat_model>
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.

<success_criteria>

  • 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. </success_criteria>
Create `.planning/quick/260729-gjd-demo-make-indee-tx1138-com-work-in-the-a/260729-gjd-SUMMARY.md` when done.