diff --git a/Dockerfile b/Dockerfile index 04bc574..729f78e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,11 @@ # ---- frontend ---- FROM node:22-bookworm-slim AS frontend-build +ARG VITE_BASE=/ WORKDIR /build/frontend COPY frontend/package*.json ./ RUN npm ci COPY frontend/ ./ -RUN npm run build +RUN VITE_BASE=$VITE_BASE npm run build # ---- server ---- FROM node:22-bookworm-slim AS server-build @@ -16,6 +17,7 @@ RUN npm run build && npm prune --omit=dev # ---- runtime ---- FROM node:22-bookworm-slim +ARG ROUTE_PREFIX= RUN apt-get update \ && apt-get install -y --no-install-recommends curl \ && rm -rf /var/lib/apt/lists/* @@ -29,8 +31,9 @@ USER node ENV NODE_ENV=production \ PORT=8199 \ DATA_DIR=/data \ - STATIC_DIR=/app/public + STATIC_DIR=/app/public \ + ROUTE_PREFIX=${ROUTE_PREFIX} EXPOSE 8199 HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ - CMD curl -fsS http://localhost:8199/api/health || exit 1 + CMD curl -fsS http://localhost:8199${ROUTE_PREFIX}/api/health || exit 1 CMD ["node", "dist/index.js"] diff --git a/README.md b/README.md index f570117..8802f19 100644 --- a/README.md +++ b/README.md @@ -60,47 +60,67 @@ npm test ## Deployment -Live at **`https://archy-x250-dev3.tail08d8f2.ts.net:8543`** (Tailscale-only — -this node has no public IP, so it isn't reachable from the open internet like -podsteadr is; anyone playing needs to be on the same tailnet). +Live at **`https://podsteadr.atobitcoin.io/regress/`** — same public host as +[podsteadr](http://146.59.87.168:3000/ssmithx/podsteadr), reached via a path +prefix under the existing domain/cert rather than its own subdomain (no DNS +access to add one). Migrated here from an earlier `archy-x250-dev3` trial +deploy (Tailscale-only Archipelago node) — see git history for that phase. - Single container (`Dockerfile`, server + built frontend in one image), - `podman run --restart unless-stopped`, data on a bind-mounted volume at - `/var/lib/archipelago/regress-data` on the node. -- Fronted by a dedicated nginx server block, `deploy/nginx-archy-node.conf`, - on its own port (**8543**) rather than being proxied under the node's main - dashboard (80/443). + `podman run --restart unless-stopped`, data bind-mounted at + `/var/lib/archipelago/regress-data` on the host, internal port **8199** + (bound to `127.0.0.1` only — nginx is the only way in). +- `deploy/nginx-podsteadr-regress.conf` — the `location /regress/` block + added to podsteadr's existing nginx site. - **Why a dedicated port, not `/app/regress/` like other installed apps:** - archipelago's own nginx config sets `Permissions-Policy: - geolocation=()` server-wide on its 80/443 blocks — inherited by every - `location` under them that doesn't set its own `add_header`s. Regress's - entire claim/link mechanic depends on `navigator.geolocation`, so being - proxied under those blocks would silently break the core game with no - clear error. A separate server block sidesteps this entirely and sets - `Permissions-Policy: geolocation=(self)` explicitly. +### Path-prefix deployment (`ROUTE_PREFIX` / `VITE_BASE`) - **Also found, not yet fixed:** the Archipelago dashboard's own app-iframe - element (`neode-ui/src/views/appSession/AppSessionFrame.vue`) has no - `allow="geolocation"` attribute, so even a geolocation-permitting app would - have geolocation blocked by the browser's default iframe permission - delegation if launched *inside* the dashboard iframe. This isn't - Regress-specific — it'd affect any app needing geolocation. Until that's - patched (a shared-component change, out of scope for this deploy), players - should open Regress directly (the URL above, or "open in new tab" from the - dashboard) rather than through the in-dashboard iframe. +Since `/` on this domain is already podsteadr, Regress needed to support +being served from a path prefix — this took two coordinated changes, both +required together: -- Non-standard port gotcha: nginx's `$host` variable strips the port before - forwarding, which breaks NIP-98 login (`u` tag URL won't match). Use - `proxy_set_header Host $http_host;` (preserves the original port) — already - set correctly in `deploy/nginx-archy-node.conf`. -- `PUBLIC_URL` passed to the container must include the `:8543` for the same - reason. +1. **Frontend build**: `VITE_BASE=/regress/` (Vite's `base` config) so built + asset URLs and the vendored `nostr-provider.js` script tag/data + attributes (via `%BASE_URL%` in `index.html`) resolve under the prefix. +2. **Backend runtime**: `ROUTE_PREFIX=/regress` env var — every route + (including `/api/health`) is registered under this prefix via Fastify's + plugin-encapsulation `{ prefix }` option (`app.ts`). + +**The nginx location must forward the full prefixed path unchanged — do +NOT strip it**, unlike podsteadr's own `/player/`, `/hls/`, etc. blocks +which all strip their prefix (`proxy_pass http://127.0.0.1:PORT/;` with a +trailing slash). Regress's NIP-98 login signs the *exact* URL it's about to +call, prefix included (`frontend/src/lib/api.ts#apiUrl`); if nginx stripped +the prefix before forwarding, the backend would reconstruct a different +(unprefixed) URL to check the signature against, and every login would fail +with "u tag does not match the request URL". So the location block uses +`proxy_pass http://127.0.0.1:8199;` — **no trailing path at all** — which +tells nginx to forward the original URI verbatim, prefix included. This is +covered by `server/src/routePrefix.test.ts`, including a negative test that +a signed-for-the-unprefixed-URL login is correctly rejected (proving the +prefix check is real, not accidentally bypassed). + +Deploying at root (no prefix) needs neither variable — both default to `''`/`'/'`. + +### Migrating the SQLite database between hosts + +The whole game state (places/claims/links/users) is one file, +`regress.sqlite3`. To move it: stop the container, copy the file (plus its +`-wal`/`-shm` siblings if present, or checkpoint first), start the new +container pointed at the copy. No export/import tooling needed — see git +history for the exact commands used for the archy-x250-dev3 → podsteadr +machine move. To redeploy after a code change: `git pull` in -`/var/lib/archipelago/regress-src` on the node, `podman build -t -localhost/regress:latest .`, then `podman run -d --name regress-app --replace ---restart unless-stopped -p 127.0.0.1:8199:8199 -v -/var/lib/archipelago/regress-data:/data -e -PUBLIC_URL=https://archy-x250-dev3.tail08d8f2.ts.net:8543 -localhost/regress:latest`. +`/var/lib/archipelago/regress-src` on the host, then: + +```bash +podman build --build-arg VITE_BASE=/regress/ --build-arg ROUTE_PREFIX=/regress \ + -t localhost/regress:latest . +podman run -d --name regress-app --replace --restart unless-stopped \ + -p 127.0.0.1:8199:8199 \ + -v /var/lib/archipelago/regress-data:/data \ + -e ROUTE_PREFIX=/regress \ + -e PUBLIC_URL=https://podsteadr.atobitcoin.io \ + localhost/regress:latest +``` diff --git a/deploy/nginx-podsteadr-regress.conf b/deploy/nginx-podsteadr-regress.conf new file mode 100644 index 0000000..14442df --- /dev/null +++ b/deploy/nginx-podsteadr-regress.conf @@ -0,0 +1,18 @@ +# Snippet added to podsteadr's existing /etc/nginx/sites-enabled/podsteadr, +# inside the existing `server { listen 443 ssl; ... }` block, alongside its +# /blossom/, /player/, /hls/, /whip/ locations. +# +# Unlike those locations, this one must NOT strip the /regress/ prefix — +# proxy_pass has no trailing path component, which tells nginx to forward +# the original request URI verbatim (prefix included). See README.md +# "Path-prefix deployment" for why: Regress's NIP-98 login signs the exact +# URL it calls, prefix included, and the backend (ROUTE_PREFIX=/regress) +# expects to see that same prefixed path. +location /regress/ { + proxy_pass http://127.0.0.1:8199; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; +} diff --git a/frontend/index.html b/frontend/index.html index 6f9a09f..9321800 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -14,7 +14,7 @@ window === window.top guard) — safe to always include. Provides window.nostr + auto sign-in via the node's selected nostr identity when opened inside the Archipelago shell. --> - +
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 4a50a78..9cf5247 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -6,8 +6,26 @@ export class ApiError extends Error { } } +// import.meta.env.BASE_URL is Vite's configured `base` (e.g. "/" or +// "/regress/") — API paths must resolve under it too when this app is +// deployed under a path prefix, not just its own static assets. +export function withBase(path: string): string { + const base = import.meta.env.BASE_URL.replace(/\/$/, ''); + return `${base}${path}`; +} + +/** + * Full absolute URL for a given API path, base-prefix included. NIP-98 login + * must sign exactly this — the same URL that will actually be requested — + * or the reverse proxy's forwarded path won't match the signature and every + * login under a path-prefixed deployment fails. + */ +export function apiUrl(path: string): string { + return `${location.origin}${withBase(path)}`; +} + async function request