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.
test-r8-prod.mjs. Four sub-phases:
nix_session cookie is set on .nixtech.app so it travels to every tenant subdomain. Pre-R8.1, hitting lumiere-coffee.nixtech.app/cafe/* with a get-coffee JWT rendered get-coffee data under lumiere's URL. The router (nix-router-worker) now sets x-nix-tenant-host: <subdomain> on every proxied request; Cafe auth() reads it and returns null on mismatch. Bug found mid-shipping: initial implementation used x-forwarded-host, which CF's edge clobbers to the upstream pages.dev hostname. Switched to a custom header CF leaves alone.tenant.auth.service.ts mints a fresh UUID on every login + UPDATE tenant_users.active_session_token; JWT payload carries the same UUID as sid. Refresh path preserves the existing sid (multi-tab in one browser doesn't kick itself). Cafe auth() compares JWT.sid against DB; mismatch → null → re-login. Skipped when JWT lacks sid (legacy / pre-deploy backwards compat) or DB column is NULL (user hasn't logged in fresh post-deploy).resetCashierPinAction + Reset PIN button + one-time PIN modal exist in lib/actions/cashiers.ts + app/(authed)/team/cashier-tab.tsx from the NIX-OS-67 R1.1 cashier-management era. Audit log entry cashier.pin_reset already wired. Marked done without code changes; visual proof captured in this gallery.commerce.pin_identities against the nix_active_cashier cookie. Unlock flow mints fresh sid via mintPinSessionToken, stores on the row, embeds in cookie. readActiveCashier validates on every POS page render — mismatch (another terminal unlocked the same PIN) returns null and bounces back to the lock screen. Manager-override path (actorKind='user') unaffected — that's tenant_users-driven and R8.2 covers it.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.
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