feat(demo): whole-origin IndeeHub proxy on :2101 with sign-in seeding

- nginx-demo.conf: new :2101 server block reverse-proxying the live
  indee.tx1138.com site with no path prefix (fixes the old sub_filter
  path-rewrite breakage), X-Frame-Options/CSP stripped, WS upgrade
  passthrough, and a demo sign-in script injected into <head>
- indee-demo-signin.js: PUBLIC-DEMO-ONLY seeder that writes a labelled
  throwaway "nsec" account (freshly generated keypair, not a secret) into
  the :2101 origin's indeedhub-accounts/indeedhub-active-account
  localStorage keys, idempotently, so IndeeHub boots signed in
- Dockerfile.web: copy the seeder into the demo web image, EXPOSE 2101
- docker-compose.demo.yml + demo-deploy/docker-compose.yml: publish 2101
  (DEMO_INDEE_PORT override documented in the thin deploy stack)

Verified: nginx -t clean in nginx:alpine; live proxy smoke shows 200 with
no framing headers, injected tag, seed script served, assets proxied.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-07-29 12:23:18 -04:00
parent e2278ad518
commit 69bc3d3f27
5 changed files with 119 additions and 6 deletions

View File

@ -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:

View File

@ -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

View File

@ -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"]

View File

@ -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
* <head> 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: <hex sk> } }) 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.
}
})()

View File

@ -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://<demo-host>: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 <head> (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 '</head>' '<script src="/__demo/indee-demo-signin.js"></script></head>';
}
}
}