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 |
|
true |
|
|
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
fromJSONdoesconst 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 fromloadCommonFields(verify exacttypestring and common fields by grepping the live bundle forstatic type/ the class'stoJSON). An"extension"account type also exists (fallback path). - The bundle expects NIP-07 as a real
window.nostrobject ("Signer extension missing" guard) — it does NOT contain the archipelagonostr-requestpostMessage client.
- localStorage key
- Parent-side NIP-07 plumbing already exists:
AppSession.vueline ~423 routesnostr-requestmessages touseNostrBridge, which callsnode.nostr-pubkey(mocked in mock-backend.js) andnode.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_IDSis already bypassed whenIS_DEMO && isDemoApp(appId).AppSession.vue mustOpenNewTabhas the same two-clause shape. Removing indeedhub fromDEMO_EXTERNAL_URLSwhile keepingisDemoApp('indeedhub')true flips it to the iframe path everywhere.mock-backend.js: per-visitor session state is initialized viamd['package-data'] = structuredClone(staticDevApps)(~line 5493), so adding an entry tostaticDevApps(~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.webcopiesnginx-demo.confto/etc/nginx/nginx.conf.templateand runsdocker-entrypoint-custom.sh(env substitution) — read the entrypoint before editing so the new server block's nginx$varssurvive 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>
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.
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.
<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> |
Post-deploy on vps2 (orchestrator deploys; verify on http://146.59.87.168:2100):
curl -sI http://146.59.87.168:2101/returns 200 with NO X-Frame-Options header and the injectedindee-demo-signin.jstag 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.- Fresh private browser window → :2100 → login → IndeeHub shows installed/ running on the dashboard/My Apps without any install action.
- Launch IndeeHub → renders inside the in-app iframe (panel/overlay), not a new tab; content browsable; no identity-picker modal.
- 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.
- View-source/network spot-check: no occurrence of the private release-server IP in any served response.
- 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>