← All tests

R8 — Auth / security trio PROD

Three security asks from Narong's 2026-05-07 call notes bundled into R8 because they share the auth surface. Per-tenant token scope (R8.1), single-session-per-user enforcement on web (R8.2) + on PIN identities (R8.4), and PIN reset for cashiers (R8.3 — recon caught the button + flow already shipped during R1.1). Mid-arc the R8.1 host check failed in prod because Cloudflare's edge fetch overrides x-forwarded-host when nix-router proxies to *.workers.dev — switched the router to set a custom x-nix-tenant-host header instead.

4/4 R8 prod checks green via test-r8-prod.mjs. Four sub-phases:

All R8 prod checks (4)

Walkthrough screenshots

The mid-arc bug (R8.1 host signal)

Initial implementation:
  const host = headers().get("x-forwarded-host") ?? headers().get("host") ?? "";
  const subdomainMatch = host.match(/^([^.]+)\.nixtech\.app(?::\d+)?$/i);
  if (subdomainMatch && subdomainMatch[1] !== tenant.code) return null;

Symptom on prod: lumiere-coffee.nixtech.app/cafe/dashboard rendered FINE
with a get-coffee JWT cookie. R8.1 didn't fire.

Diagnostic via temporary /api/debug-r8 endpoint:
  "x-forwarded-host": "nix-cafe.narongix.workers.dev"
  "host":             "nix-cafe.narongix.workers.dev"

Root cause: nix-router sets x-forwarded-host = inbound URL.host (e.g.
"lumiere-coffee.nixtech.app"). But the router's `fetch(targetUrl, ...)`
to the upstream pages.dev/workers.dev re-enters the Cloudflare edge,
which OVERRIDES x-forwarded-host with the upstream's own hostname. The
router's value gets clobbered before Next.js SSR sees it.

Fix: router sets a custom `x-nix-tenant-host: <subdomain>` header that
CF doesn't touch (it only rewrites known X-Forwarded-* headers on the
edge fetch). Cafe auth() reads x-nix-tenant-host instead.

Lesson worth keeping: when proxying through Cloudflare to another CF
property, custom-name headers survive but standard X-Forwarded-* may
not. Use a NIX-prefixed header for any signal that MUST round-trip
through the router.

Code surface

nix-outdoor-sales-backend (commit 3b6233d):
  migrations/20260508190000_tenant_users_active_session_token.ts        NEW
  migrations/20260508200000_pin_identities_active_session_token.ts      NEW
  migrate.js                                                            +2 entries
  src/tenant/tenant.auth.service.ts                                     mint sid on login, preserve on refresh

nix-cafe (commits 50164a9 + a6509c6):
  lib/auth.ts                                                           R8.1 host check + R8.2 sid validate
  lib/auth/active-cashier.ts                                            R8.4 sid in payload + readActiveCashier validate
  lib/db/schema.ts                                                      activeSessionToken on tenantUsers + commercePinIdentities
  lib/db/pin_identities.ts                                              mintPinSessionToken + getPinSessionToken
  lib/actions/register.ts                                               unlock mints + embeds sid in cookie

nix-router-worker (deployed via wrangler, no git push):
  src/index.js                                                          set x-nix-tenant-host on every proxied request