0e7871ac0e
4a VOICE v1.1 committed (board start); lint is EXECUTABLE — banned list + no-exclamation law enforced in the engine (throws in test, drops in prod) and locked by tests. Curly-apostrophe variants covered. 4b mediaEngine: deterministic templates (MORNING WIRE, SIGNAL, STREAK WATCH, THE SETTLE, RECEIPTS, ARCHETYPE WATCH, LINE DISPATCH) filled ONLY from snapshot/ledger/streaks JSON. Record percentages never render under n>=20 (counts + 'Record building' below). Stark layer = curated committed library (content/stark-lines.json), day-rotated selection — selected, never generated. 4c /desk (founder-only: requireAuth + DESK_OWNERS email allowlist, deny-by-default): all formats as text + <=280-char pre-segmented tweets with per-tweet copy buttons + char counts, wire/numbers-only variants, DATA BRIEF block (structured day numbers) with copy-for-claude.ai. ntfy ping after the day's first snapshot: 'Desk pack ready'. 4d ghostPublisher: DRAFTS ONLY (status:'draft' test-locked), env-gated no-op, HS256 JWT via node crypto (zero new deps). POST /api/internal/ghost/drafts saves slate preview + settle drafts. Nothing anywhere auto-posts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
94 lines
2.9 KiB
JavaScript
94 lines
2.9 KiB
JavaScript
/* ============================================================
|
|
VYNDR 2.0 — app-shell routing config (§6, §12).
|
|
Plain CommonJS so the client AuthGate/HashRedirect import it
|
|
(allowJs) AND the Jest suite requires it directly (no transform).
|
|
|
|
AUTH NOTE: our auth is client-side Supabase (session in
|
|
localStorage via @supabase/supabase-js), and the existing
|
|
middleware.ts is locale-only. A Next server middleware can't read
|
|
that session, so the gate is enforced CLIENT-side by <AuthGate>.
|
|
This file is the single source of truth for which routes gate.
|
|
============================================================ */
|
|
|
|
/* Gated — require an authenticated user. Deliberately narrower than the
|
|
prototype's GATED set: the prototype also gated dashboard + scan, but those
|
|
are OUR free-scan acquisition funnel (anon/free users get 5 reads), so
|
|
gating them would be a monetization regression. We gate only the genuinely
|
|
personal surfaces (a user's own ledger, bets, account, alerts). */
|
|
const GATED_ROUTES = [
|
|
'/desk', // Session 63 (A1-S4) — the founder's media surface (+ backend allowlist)
|
|
'/ledger',
|
|
'/tracker',
|
|
'/account',
|
|
'/profile',
|
|
'/settings',
|
|
'/notifications',
|
|
'/invite',
|
|
];
|
|
|
|
/* Open — reachable without auth (marketing + the free funnel + auth itself). */
|
|
/* Session 57 (Phase 0): '/terminal' removed — the route now server-redirects
|
|
to /dashboard (fabricated surface retired), so it needs no gate exemption. */
|
|
const OPEN_ROUTES = [
|
|
'/',
|
|
'/dashboard',
|
|
'/slate',
|
|
'/scan',
|
|
'/compare',
|
|
'/game',
|
|
'/pricing',
|
|
'/blog',
|
|
'/article',
|
|
'/responsible-gambling',
|
|
'/help',
|
|
'/about',
|
|
'/terms',
|
|
'/privacy',
|
|
'/login',
|
|
'/signup',
|
|
'/auth',
|
|
'/forgot-password',
|
|
'/verify',
|
|
'/welcome',
|
|
'/offline',
|
|
'/upgrade',
|
|
];
|
|
|
|
/* Hash deep-link aliases (§C.3.4). The prototype was a single HTML file using
|
|
#scan / #terminal; we keep Next file-based routing and treat these hashes as
|
|
redirects so old share links and PWA shortcuts still resolve. */
|
|
const HASH_ALIASES = {
|
|
'#slate': '/dashboard',
|
|
'#dashboard': '/dashboard',
|
|
'#scan': '/scan',
|
|
'#terminal': '/dashboard', // retired surface (Session 57) — old links land on the slate
|
|
'#compare': '/compare',
|
|
'#ledger': '/ledger',
|
|
'#tracker': '/tracker',
|
|
'#account': '/account',
|
|
'#pricing': '/pricing',
|
|
'#blog': '/blog',
|
|
'#invite': '/invite',
|
|
'#notifications': '/notifications',
|
|
};
|
|
|
|
/** True when `pathname` falls under a gated route (exact or nested). */
|
|
function isGatedRoute(pathname) {
|
|
if (!pathname) return false;
|
|
return GATED_ROUTES.some((r) => pathname === r || pathname.startsWith(r + '/'));
|
|
}
|
|
|
|
/** Resolve a window.location.hash (e.g. "#scan") to a real route, or null. */
|
|
function resolveHashAlias(hash) {
|
|
if (!hash) return null;
|
|
return HASH_ALIASES[hash] || null;
|
|
}
|
|
|
|
module.exports = {
|
|
GATED_ROUTES,
|
|
OPEN_ROUTES,
|
|
HASH_ALIASES,
|
|
isGatedRoute,
|
|
resolveHashAlias,
|
|
};
|