@lpdjs/firestore-repo-service 2.6.13 → 2.6.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/{create-servers-BV-E4Rp-.d.cts → create-servers-C3CIViuc.d.cts} +5 -5
  2. package/dist/{create-servers-BhUavex0.d.ts → create-servers-O2stZ-yF.d.ts} +5 -5
  3. package/dist/{firebase-auth-t1CAR-lp.d.cts → firebase-auth-DZ8lb2Ec.d.cts} +9 -0
  4. package/dist/{firebase-auth-t1CAR-lp.d.ts → firebase-auth-DZ8lb2Ec.d.ts} +9 -0
  5. package/dist/{index-CXWiqnFs.d.cts → index-D0KRndyL.d.cts} +2 -2
  6. package/dist/{index-4gzZw5m_.d.ts → index-TTSjUi4p.d.ts} +2 -2
  7. package/dist/index.cjs +54 -54
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +8 -8
  10. package/dist/index.d.ts +8 -8
  11. package/dist/index.js +54 -54
  12. package/dist/index.js.map +1 -1
  13. package/dist/{openapi-D71F-INl.d.cts → openapi-BdatGOqK.d.cts} +1 -1
  14. package/dist/{openapi-Bq6KTWCR.d.ts → openapi-uTAic4s0.d.ts} +1 -1
  15. package/dist/{queue-DP7t5Jp7.d.ts → queue-B4uwef6c.d.ts} +1 -1
  16. package/dist/{queue-Bl-S-I-7.d.cts → queue-CQyWCQBk.d.cts} +1 -1
  17. package/dist/servers/admin/index.cjs +46 -46
  18. package/dist/servers/admin/index.cjs.map +1 -1
  19. package/dist/servers/admin/index.d.cts +3 -3
  20. package/dist/servers/admin/index.d.ts +3 -3
  21. package/dist/servers/admin/index.js +46 -46
  22. package/dist/servers/admin/index.js.map +1 -1
  23. package/dist/servers/auth/index.cjs +3 -3
  24. package/dist/servers/auth/index.cjs.map +1 -1
  25. package/dist/servers/auth/index.d.cts +1 -1
  26. package/dist/servers/auth/index.d.ts +1 -1
  27. package/dist/servers/auth/index.js +3 -3
  28. package/dist/servers/auth/index.js.map +1 -1
  29. package/dist/servers/crud/index.cjs +1 -1
  30. package/dist/servers/crud/index.cjs.map +1 -1
  31. package/dist/servers/crud/index.d.cts +5 -5
  32. package/dist/servers/crud/index.d.ts +5 -5
  33. package/dist/servers/crud/index.js +1 -1
  34. package/dist/servers/crud/index.js.map +1 -1
  35. package/dist/servers/hono/index.d.cts +1 -1
  36. package/dist/servers/hono/index.d.ts +1 -1
  37. package/dist/servers/index.cjs +74 -74
  38. package/dist/servers/index.cjs.map +1 -1
  39. package/dist/servers/index.d.cts +8 -8
  40. package/dist/servers/index.d.ts +8 -8
  41. package/dist/servers/index.js +74 -74
  42. package/dist/servers/index.js.map +1 -1
  43. package/dist/sync/bigquery.d.cts +2 -2
  44. package/dist/sync/bigquery.d.ts +2 -2
  45. package/dist/sync/index.cjs +11 -11
  46. package/dist/sync/index.cjs.map +1 -1
  47. package/dist/sync/index.d.cts +5 -5
  48. package/dist/sync/index.d.ts +5 -5
  49. package/dist/sync/index.js +11 -11
  50. package/dist/sync/index.js.map +1 -1
  51. package/dist/{types-BxsdnGDM.d.cts → types-BwH1C-wi.d.cts} +1 -1
  52. package/dist/{types-CmSUreTx.d.ts → types-DT7Nypd_.d.ts} +1 -1
  53. package/dist/{types-C_alF2Xe.d.cts → types-Qd4tuEq9.d.cts} +1 -1
  54. package/dist/{types-C6I3WtJS.d.ts → types-wzqw45jH.d.ts} +1 -1
  55. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/servers/utils/link-base.ts","../../../src/servers/auth/login-page.tsx","../../../src/servers/auth/session.ts","../../../src/servers/auth/firebase-auth.ts"],"names":["getLinkBase","req","staticBasePath","base","project","region","target","service","host","emulatorUrl","htmlEscape","value","jsonEscape","renderLoginPage","opts","showPassword","showGoogle","initialError","SESSION_COOKIE_DEFAULT","parseCookies","header","out","part","eq","key","buildSetCookie","name","segments","readJsonBody","body","createSessionHandler","cfg","res","idToken","expiresInMs","auth","authTimeRaw","authTime","sessionCookie","cookie","err","message","createLogoutHandler","cookieHeader","raw","session","decoded","expired","defaultLoginPage","mode","defaultUnauth","pathOf","idx","queryAction","q","url","methodOf","sanitizeNextPath","next","headerOf","CSRF_SAFE_METHODS","isSameOriginRequest","originHeader","isPublic","path","patterns","p","wantsHtml","extractBearer","m","firebaseAuth","config","cookieName","ttlDays","secure","sameSite","csrfProtection","onUnauth","loginEnabled","loginPath","sessionPath","logoutPath","sessionHandler","logoutHandler","renderInlineLogin","error","status","pageCfg","prefix","inner","fullPath","sep","sessionAction","html","routes","publicPaths","middleware","method","action","token","rejectUnauthenticated","baseUser","context","isAuthExtension","allowAll"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Compute the URL prefix used to build absolute paths from inside a\n * Firebase HTTPS function. Handles three deployment shapes uniformly:\n *\n * 1. **Firebase emulator** (`FUNCTIONS_EMULATOR=true`) — exposes functions at\n * `http://localhost:5001/{project}/{region}/{functionTarget}/...`. The\n * handler receives `req.url` *without* this prefix, so we rebuild it from\n * `GCLOUD_PROJECT`, `FUNCTION_REGION`, `FUNCTION_TARGET`.\n *\n * 2. **Cloud Functions v2 default URL** (`*.cloudfunctions.net/{name}`) —\n * Cloud Run terminates routing at the service name, so links must include\n * the `K_SERVICE` prefix. Detected via the `host` header containing\n * `cloudfunctions.net`.\n *\n * 3. **Custom domain / Hosting rewrite** — the proxy strips the prefix\n * before reaching the handler, so links are relative to the configured\n * `staticBasePath`.\n *\n * @param req The incoming request (needs `headers.host` / `hostname`).\n * @param staticBasePath The user-configured base path (e.g. `\"/api\"`).\n * @returns A path prefix (no trailing slash) suitable for prepending to\n * `req.url` to build a same-function absolute URL.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function getLinkBase(req: any, staticBasePath: string): string {\n const base = staticBasePath === \"/\" ? \"\" : staticBasePath.replace(/\\/$/, \"\");\n\n if (process.env[\"FUNCTIONS_EMULATOR\"] === \"true\") {\n const project =\n process.env[\"GCLOUD_PROJECT\"] ??\n process.env[\"GOOGLE_CLOUD_PROJECT\"] ??\n \"demo-project\";\n const region = process.env[\"FUNCTION_REGION\"] ?? \"us-central1\";\n // FUNCTION_TARGET uses dots (e.g. \"sync.functions.adminsync\") but the\n // emulator URL uses hyphens (\"sync-functions-adminsync\").\n const target = (process.env[\"FUNCTION_TARGET\"] ?? \"\").replace(/\\./g, \"-\");\n return `/${project}/${region}/${target}${base}`;\n }\n\n // Cloud Functions v2: K_SERVICE = function name = URL path prefix.\n // Only add it when accessed via cloudfunctions.net (not custom domains).\n // Cloud Run (Gen 2) lowercases service names, but K_SERVICE may still\n // carry the original mixed-case export name — normalise to lowercase\n // so that generated links match the canonical URL.\n const service = process.env[\"K_SERVICE\"];\n const host: string =\n req?.hostname ?? req?.headers?.[\"host\"] ?? \"\";\n if (service && typeof host === \"string\" && host.includes(\"cloudfunctions.net\")) {\n return `/${service.toLowerCase()}${base}`;\n }\n\n return base;\n}\n","/**\n * Login page renderer for `firebaseAuth`.\n * Standalone HTML — no JSX. Embeds the Firebase JS SDK from the official CDN\n * (modular v10) so users don't need a frontend build step.\n *\n * Flow:\n * 1. User signs in client-side (email/password or Google popup).\n * 2. We call `user.getIdToken(true)` and `POST` it to `{sessionPath}`.\n * 3. The server mints a session cookie and we redirect to `next`.\n */\n\ninterface LoginPageOptions {\n title: string;\n providers: (\"password\" | \"google\")[];\n apiKey: string;\n authDomain: string;\n sessionPath: string;\n next: string;\n error: string | null;\n /**\n * Firebase Auth emulator host (e.g. `127.0.0.1:9099`). When set, the client\n * SDK is pointed at the emulator via `connectAuthEmulator` so local sign-ins\n * match the server side (which the Admin SDK already routes to the emulator\n * through `FIREBASE_AUTH_EMULATOR_HOST`). A bare `host:port` is upgraded to\n * an `http://` URL.\n */\n authEmulatorHost?: string;\n}\n\n/** Normalise an emulator host (`host:port`) into a full `http://` URL. */\nfunction emulatorUrl(host: string | undefined): string {\n if (!host) return \"\";\n return /^https?:\\/\\//.test(host) ? host : `http://${host}`;\n}\n\nfunction htmlEscape(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\");\n}\n\nfunction jsonEscape(value: string): string {\n // Safe for embedding inside a <script> string literal.\n return JSON.stringify(value).slice(1, -1);\n}\n\nexport function renderLoginPage(opts: LoginPageOptions): string {\n const showPassword = opts.providers.includes(\"password\");\n const showGoogle = opts.providers.includes(\"google\");\n const initialError = opts.error ? htmlEscape(opts.error) : \"\";\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n <title>${htmlEscape(opts.title)}</title>\n <style>\n :root { color-scheme: light dark; }\n * { box-sizing: border-box; }\n body {\n margin: 0;\n min-height: 100vh;\n display: grid;\n place-items: center;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n background: #f5f5f7;\n color: #1d1d1f;\n }\n @media (prefers-color-scheme: dark) {\n body { background: #1d1d1f; color: #f5f5f7; }\n .card { background: #2c2c2e !important; }\n input { background: #1d1d1f; color: #f5f5f7; border-color: #444; }\n input::placeholder { color: #888; }\n input:-webkit-autofill,\n input:-webkit-autofill:hover,\n input:-webkit-autofill:focus,\n input:-webkit-autofill:active {\n -webkit-text-fill-color: #f5f5f7 !important;\n -webkit-box-shadow: 0 0 0 1000px #1d1d1f inset !important;\n caret-color: #f5f5f7;\n }\n .divider { color: #888; }\n .divider::before, .divider::after { background: #444; }\n }\n .card {\n width: min(420px, 92vw);\n padding: 32px;\n background: #fff;\n border-radius: 14px;\n box-shadow: 0 20px 50px rgba(0,0,0,.08);\n }\n h1 { font-size: 22px; margin: 0 0 6px; font-weight: 600; }\n p.sub { margin: 0 0 24px; opacity: .7; font-size: 14px; }\n label { display: block; font-size: 13px; margin-bottom: 6px; opacity: .8; }\n input {\n width: 100%; padding: 11px 12px;\n border: 1px solid #d2d2d7; border-radius: 8px;\n font-size: 15px; outline: none; background: #fff; color: #1d1d1f;\n margin-bottom: 14px;\n }\n input::placeholder { color: #86868b; }\n input:focus { border-color: #0071e3; box-shadow: 0 0 0 3px rgba(0,113,227,.15); }\n /* Force readable text on Chrome's autofill (otherwise the input keeps\n the autofill's white background but inherits the page's dark-mode text\n colour, producing white-on-white). */\n input:-webkit-autofill,\n input:-webkit-autofill:hover,\n input:-webkit-autofill:focus,\n input:-webkit-autofill:active {\n -webkit-text-fill-color: #1d1d1f !important;\n -webkit-box-shadow: 0 0 0 1000px #fff inset !important;\n caret-color: #1d1d1f;\n transition: background-color 9999s ease-out 0s;\n }\n button {\n width: 100%; padding: 11px 12px; border: none; border-radius: 8px;\n font-size: 15px; font-weight: 500; cursor: pointer;\n transition: opacity .15s, transform .05s;\n }\n button:active { transform: scale(.98); }\n button:disabled { opacity: .55; cursor: progress; }\n .btn-primary { background: #0071e3; color: #fff; }\n .btn-google {\n background: #fff; color: #1d1d1f; border: 1px solid #d2d2d7;\n display: flex; align-items: center; justify-content: center; gap: 8px;\n }\n @media (prefers-color-scheme: dark) {\n .btn-google { background: #2c2c2e; color: #f5f5f7; border-color: #444; }\n }\n .divider {\n display: flex; align-items: center; gap: 12px;\n margin: 16px 0; font-size: 12px; opacity: .55; text-transform: uppercase;\n }\n .divider::before, .divider::after {\n content: \"\"; flex: 1; height: 1px; background: #d2d2d7;\n }\n .err {\n margin: 0 0 14px; padding: 10px 12px;\n background: rgba(255,59,48,.12); color: #ff3b30;\n border-radius: 8px; font-size: 13px;\n display: ${initialError ? \"block\" : \"none\"};\n }\n .ok {\n margin: 0 0 14px; padding: 10px 12px;\n background: rgba(52,199,89,.12); color: #34c759;\n border-radius: 8px; font-size: 13px; display: none;\n }\n </style>\n</head>\n<body>\n <main class=\"card\">\n <h1>${htmlEscape(opts.title)}</h1>\n <p class=\"sub\">Sign in to continue.</p>\n <div id=\"err\" class=\"err\">${initialError}</div>\n <div id=\"ok\" class=\"ok\"></div>\n\n ${\n showPassword\n ? `<form id=\"pwd-form\" autocomplete=\"on\">\n <label for=\"email\">Email</label>\n <input id=\"email\" type=\"email\" name=\"email\" autocomplete=\"username\" required />\n <label for=\"password\">Password</label>\n <input id=\"password\" type=\"password\" name=\"password\" autocomplete=\"current-password\" required />\n <button class=\"btn-primary\" type=\"submit\" id=\"pwd-submit\">Sign in</button>\n </form>`\n : \"\"\n }\n\n ${showPassword && showGoogle ? `<div class=\"divider\">or</div>` : \"\"}\n\n ${\n showGoogle\n ? `<button class=\"btn-google\" type=\"button\" id=\"google-btn\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" aria-hidden=\"true\">\n <path fill=\"#4285F4\" d=\"M17.64 9.205c0-.638-.057-1.252-.164-1.841H9v3.481h4.844a4.14 4.14 0 0 1-1.796 2.716v2.259h2.908c1.702-1.567 2.684-3.875 2.684-6.615z\"/>\n <path fill=\"#34A853\" d=\"M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z\"/>\n <path fill=\"#FBBC05\" d=\"M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z\"/>\n <path fill=\"#EA4335\" d=\"M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z\"/>\n </svg>\n Continue with Google\n </button>`\n : \"\"\n }\n </main>\n\n <script type=\"module\">\n import { initializeApp } from \"https://www.gstatic.com/firebasejs/10.13.2/firebase-app.js\";\n import {\n getAuth,\n connectAuthEmulator,\n signInWithEmailAndPassword,\n signInWithPopup,\n GoogleAuthProvider,\n setPersistence,\n browserSessionPersistence,\n } from \"https://www.gstatic.com/firebasejs/10.13.2/firebase-auth.js\";\n\n const app = initializeApp({\n apiKey: \"${jsonEscape(opts.apiKey)}\",\n authDomain: \"${jsonEscape(opts.authDomain)}\",\n });\n const auth = getAuth(app);\n ${\n opts.authEmulatorHost\n ? `connectAuthEmulator(auth, ${JSON.stringify(\n emulatorUrl(opts.authEmulatorHost),\n )}, { disableWarnings: true });`\n : \"\"\n }\n // Don't persist client-side — the server-side session cookie is the source of truth.\n await setPersistence(auth, browserSessionPersistence).catch(() => {});\n\n const SESSION_PATH = \"${jsonEscape(opts.sessionPath)}\";\n const NEXT = ${JSON.stringify(opts.next)};\n\n // Defense-in-depth against open redirect (issue #07): only ever navigate\n // to a same-origin path, even though the server already sanitizes next.\n // Resolve against the current page URL (not just the origin) so a relative\n // \"next\" works behind any Cloud Functions / reverse-proxy path prefix.\n function safeNext(raw) {\n try {\n const u = new URL(raw, window.location.href);\n if (u.origin !== window.location.origin) return \"/\";\n return u.pathname + u.search + u.hash;\n } catch {\n return \"/\";\n }\n }\n\n const errEl = document.getElementById(\"err\");\n const okEl = document.getElementById(\"ok\");\n function showError(msg) {\n errEl.textContent = msg;\n errEl.style.display = \"block\";\n okEl.style.display = \"none\";\n }\n function showOk(msg) {\n okEl.textContent = msg;\n okEl.style.display = \"block\";\n errEl.style.display = \"none\";\n }\n\n async function exchangeForSession(user) {\n const idToken = await user.getIdToken(true);\n const res = await fetch(SESSION_PATH, {\n method: \"POST\",\n credentials: \"same-origin\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ idToken }),\n });\n if (!res.ok) {\n const data = await res.json().catch(() => ({}));\n throw new Error(data.error || \"Session exchange failed (\" + res.status + \")\");\n }\n // Sign out client-side immediately — we only needed the id token.\n try { await auth.signOut(); } catch {}\n showOk(\"Signed in. Redirecting…\");\n window.location.replace(safeNext(NEXT));\n }\n\n const pwdForm = document.getElementById(\"pwd-form\");\n if (pwdForm) {\n pwdForm.addEventListener(\"submit\", async (ev) => {\n ev.preventDefault();\n const submit = document.getElementById(\"pwd-submit\");\n submit.disabled = true;\n try {\n const email = document.getElementById(\"email\").value.trim();\n const password = document.getElementById(\"password\").value;\n const cred = await signInWithEmailAndPassword(auth, email, password);\n await exchangeForSession(cred.user);\n } catch (err) {\n showError(err && err.message ? err.message : String(err));\n submit.disabled = false;\n }\n });\n }\n\n const googleBtn = document.getElementById(\"google-btn\");\n if (googleBtn) {\n googleBtn.addEventListener(\"click\", async () => {\n googleBtn.disabled = true;\n try {\n const provider = new GoogleAuthProvider();\n const cred = await signInWithPopup(auth, provider);\n await exchangeForSession(cred.user);\n } catch (err) {\n showError(err && err.message ? err.message : String(err));\n googleBtn.disabled = false;\n }\n });\n }\n </script>\n</body>\n</html>`;\n}\n","/**\n * Session cookie + logout handlers for `firebaseAuth`.\n * Exchanges a Firebase ID token for an HttpOnly session cookie via the\n * Firebase Admin SDK (`createSessionCookie`), and clears it on logout.\n */\n\nimport type { RouteHandler } from \"../admin/router\";\nimport type { FirebaseAdminAuthLike } from \"./firebase-auth\";\n\nexport const SESSION_COOKIE_DEFAULT = \"__admin_session\";\n\ninterface SessionHandlerConfig {\n getAuth: () => FirebaseAdminAuthLike;\n cookieName: string;\n ttlDays: number;\n secure: boolean;\n sameSite: \"Strict\" | \"Lax\" | \"None\";\n}\n\ninterface LogoutHandlerConfig {\n getAuth: () => FirebaseAdminAuthLike;\n cookieName: string;\n secure: boolean;\n sameSite: \"Strict\" | \"Lax\" | \"None\";\n}\n\n// ---------------------------------------------------------------------------\n// Cookie utilities\n// ---------------------------------------------------------------------------\n\n/** Parse a `Cookie` header into a flat key→value map. Tolerant of malformed pairs. */\nexport function parseCookies(header: string): Record<string, string> {\n const out: Record<string, string> = {};\n if (!header) return out;\n for (const part of header.split(\";\")) {\n const eq = part.indexOf(\"=\");\n if (eq === -1) continue;\n const key = part.slice(0, eq).trim();\n if (!key) continue;\n let value = part.slice(eq + 1).trim();\n if (value.startsWith('\"') && value.endsWith('\"')) {\n value = value.slice(1, -1);\n }\n try {\n out[key] = decodeURIComponent(value);\n } catch {\n out[key] = value;\n }\n }\n return out;\n}\n\nfunction buildSetCookie(\n name: string,\n value: string,\n opts: {\n maxAgeSeconds: number;\n secure: boolean;\n sameSite: \"Strict\" | \"Lax\" | \"None\";\n path?: string;\n },\n): string {\n const segments = [\n `${name}=${value}`,\n `Path=${opts.path ?? \"/\"}`,\n `Max-Age=${opts.maxAgeSeconds}`,\n \"HttpOnly\",\n `SameSite=${opts.sameSite}`,\n ];\n if (opts.secure) segments.push(\"Secure\");\n return segments.join(\"; \");\n}\n\n/** Pull JSON body out of any Express-like request (works with `parseBody` already done by the host). */\nfunction readJsonBody(req: { body?: unknown }): Record<string, unknown> {\n const body = req.body;\n if (!body) return {};\n if (typeof body === \"string\") {\n try {\n return JSON.parse(body) as Record<string, unknown>;\n } catch {\n return {};\n }\n }\n if (typeof body === \"object\") return body as Record<string, unknown>;\n return {};\n}\n\n// ---------------------------------------------------------------------------\n// Handlers\n// ---------------------------------------------------------------------------\n\n/**\n * `POST /__session` — receives `{ idToken }`, verifies it via the Admin SDK,\n * mints a session cookie, and sets it on the response.\n */\nexport function createSessionHandler(cfg: SessionHandlerConfig): RouteHandler {\n return async (req, res) => {\n const body = readJsonBody(req);\n const idToken = typeof body.idToken === \"string\" ? body.idToken : \"\";\n if (!idToken) {\n res\n .status(400)\n .set(\"Content-Type\", \"application/json; charset=utf-8\")\n .send(JSON.stringify({ success: false, error: \"Missing idToken\" }));\n return;\n }\n\n const expiresInMs = cfg.ttlDays * 24 * 60 * 60 * 1000;\n try {\n const auth = cfg.getAuth();\n // Verify first so we surface auth errors before minting the cookie.\n const decoded = await auth.verifyIdToken(idToken, true);\n // Reject very old sign-ins to encourage fresh re-auth (Google guidance: < 5 min).\n const authTimeRaw = (decoded as { auth_time?: number }).auth_time;\n const authTime =\n typeof authTimeRaw === \"number\" ? authTimeRaw * 1000 : Date.now();\n if (Date.now() - authTime > 5 * 60 * 1000) {\n res\n .status(401)\n .set(\"Content-Type\", \"application/json; charset=utf-8\")\n .send(\n JSON.stringify({\n success: false,\n error: \"Recent sign-in required\",\n }),\n );\n return;\n }\n const sessionCookie = await auth.createSessionCookie(idToken, {\n expiresIn: expiresInMs,\n });\n const cookie = buildSetCookie(cfg.cookieName, encodeURIComponent(sessionCookie), {\n maxAgeSeconds: Math.floor(expiresInMs / 1000),\n secure: cfg.secure,\n sameSite: cfg.sameSite,\n });\n res\n .status(200)\n .set(\"Set-Cookie\", cookie)\n .set(\"Content-Type\", \"application/json; charset=utf-8\")\n .send(JSON.stringify({ success: true }));\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invalid idToken\";\n res\n .status(401)\n .set(\"Content-Type\", \"application/json; charset=utf-8\")\n .send(JSON.stringify({ success: false, error: message }));\n }\n };\n}\n\n/**\n * `POST /__logout` — clears the session cookie and revokes the user's refresh\n * tokens (best-effort; failure to revoke does not block the logout).\n */\nexport function createLogoutHandler(cfg: LogoutHandlerConfig): RouteHandler {\n return async (req, res) => {\n try {\n const cookieHeader = req.headers?.cookie;\n const raw = Array.isArray(cookieHeader) ? cookieHeader.join(\"; \") : cookieHeader;\n const cookies = parseCookies(typeof raw === \"string\" ? raw : \"\");\n const session = cookies[cfg.cookieName];\n if (session) {\n try {\n const auth = cfg.getAuth();\n const decoded = await auth.verifySessionCookie(session, false);\n await auth.revokeRefreshTokens(decoded.uid);\n } catch {\n /* best-effort */\n }\n }\n } finally {\n const expired = buildSetCookie(cfg.cookieName, \"\", {\n maxAgeSeconds: 0,\n secure: cfg.secure,\n sameSite: cfg.sameSite,\n });\n res\n .status(200)\n .set(\"Set-Cookie\", expired)\n .set(\"Content-Type\", \"application/json; charset=utf-8\")\n .send(JSON.stringify({ success: true }));\n }\n };\n}\n","/**\n * Firebase Auth helper for the admin & CRUD servers.\n *\n * Returns an {@link AuthExtension} ready to plug into `servers.admin()` or\n * `servers.crud()`. Supports two transport modes:\n *\n * - **`cookie`** — session cookie pattern (default for admin UIs). Mounts\n * `/__login`, `/__session`, `/__logout` routes; the page lets the user sign\n * in client-side with the Firebase JS SDK and exchanges the resulting ID\n * token for an HttpOnly session cookie via Firebase Admin SDK.\n * - **`bearer`** — verifies `Authorization: Bearer <idToken>` on every request\n * (default for REST APIs). No login routes mounted.\n * - **`both`** — accept either cookie or bearer.\n *\n * The helper is **agnostic** about authorization: pass an `allow` callback\n * returning whatever role/context shape you need. The result is exposed as\n * `req.user.context` to downstream middlewares and route handlers.\n *\n * @example Admin (cookie + role trio)\n * ```ts\n * import { firebaseAuth } from \"@lpdjs/firestore-repo-service/servers/auth\";\n * import { getAuth } from \"firebase-admin/auth\";\n *\n * servers.admin({\n * auth: firebaseAuth({\n * getAuth,\n * mode: \"cookie\",\n * apiKey: process.env.FIREBASE_WEB_API_KEY!,\n * authDomain: process.env.FIREBASE_AUTH_DOMAIN!,\n * allow: ({ email, claims }) => {\n * if (claims.superAdmin) return { role: \"superAdmin\" };\n * if (email?.endsWith(\"@solarpush.io\")) return { role: \"admin\" };\n * if (email) return { role: \"viewer\" };\n * return null;\n * },\n * }),\n * repos: { ... },\n * });\n * ```\n *\n * @example CRUD (bearer + business rules per repo)\n * ```ts\n * servers.crud({\n * auth: firebaseAuth({ getAuth, mode: \"bearer\", allow: (u) => u }),\n * repos: {\n * comments: {\n * repo: repos.comments,\n * rules: {\n * list: () => true,\n * get: ({ user, doc }) => doc.public || doc.authorId === user.uid,\n * create: ({ user }) => !!user.uid,\n * update: ({ user, doc }) => user.uid === doc.authorId,\n * delete: ({ user, doc }) => user.claims.role === \"moderator\",\n * },\n * },\n * },\n * });\n * ```\n */\n\nimport type { AnyReq, Middleware, RouteHandler } from \"../admin/router\";\nimport { getLinkBase } from \"../utils/link-base\";\nimport { renderLoginPage } from \"./login-page\";\nimport {\n createLogoutHandler,\n createSessionHandler,\n parseCookies,\n SESSION_COOKIE_DEFAULT,\n} from \"./session\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal Firebase Admin Auth surface needed by this helper.\n * Avoids a hard import of `firebase-admin/auth` so the package stays\n * decoupled from a specific firebase-admin version.\n */\nexport interface FirebaseAdminAuthLike {\n verifyIdToken(\n idToken: string,\n checkRevoked?: boolean,\n ): Promise<DecodedIdTokenLike>;\n verifySessionCookie(\n sessionCookie: string,\n checkRevoked?: boolean,\n ): Promise<DecodedIdTokenLike>;\n createSessionCookie(\n idToken: string,\n sessionCookieOptions: { expiresIn: number },\n ): Promise<string>;\n revokeRefreshTokens(uid: string): Promise<void>;\n}\n\nexport interface DecodedIdTokenLike {\n uid: string;\n email?: string;\n email_verified?: boolean;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [claim: string]: any;\n}\n\n/** Identity attached to every authenticated request as `req.user`. */\nexport interface AuthUser<TContext = unknown> {\n uid: string;\n email: string | null;\n emailVerified: boolean;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n claims: Record<string, any>;\n /** Result of the user-supplied `allow()` callback. */\n context: TContext;\n}\n\n/** A route descriptor mounted by `firebaseAuth` before the protected chain. */\nexport interface AuthRoute {\n method: \"GET\" | \"POST\";\n path: string;\n handler: RouteHandler;\n}\n\n/**\n * Returned by {@link firebaseAuth}. Servers detect this shape (vs.\n * `BasicAuthConfig` / raw `Middleware`) and mount the routes before pushing\n * the middleware onto the chain.\n */\nexport interface AuthExtension {\n readonly __authExtension: true;\n middleware: Middleware;\n /** Auxiliary routes (login page, session, logout). Empty in pure bearer mode. */\n routes: AuthRoute[];\n /** Path used to redirect unauthenticated browser requests. */\n loginPath: string;\n}\n\nexport type FirebaseAuthMode = \"cookie\" | \"bearer\" | \"both\";\n\n/** Provider configuration for the bundled login page. */\nexport interface FirebaseAuthLoginPageConfig {\n /** Page title. Default: \"Admin sign-in\". */\n title?: string;\n /**\n * Providers shown on the login page.\n * Default: `[\"password\", \"google\"]`.\n */\n providers?: (\"password\" | \"google\")[];\n}\n\nexport interface FirebaseAuthConfig<TContext = unknown> {\n /** Lazy getter for the Firebase Admin Auth instance. */\n getAuth: () => FirebaseAdminAuthLike;\n\n /** Transport mode. Default: `\"cookie\"`. */\n mode?: FirebaseAuthMode;\n\n /**\n * Authorization callback. Receives the verified token claims and returns:\n * - a context object → request is allowed, exposed as `req.user.context`,\n * - `null` → request is rejected (401 / redirect to login).\n *\n * If omitted, the default policy allows any authenticated user with\n * `context = null`.\n */\n allow?: (\n user: Omit<AuthUser, \"context\">,\n ) => TContext | null | Promise<TContext | null>;\n\n // ── Cookie mode options ────────────────────────────────────────────────\n /**\n * Whether to mount the bundled `/__login`, `/__session`, `/__logout`\n * routes. Default: `true` for `cookie`/`both`, `false` for `bearer`.\n */\n loginPage?: boolean | FirebaseAuthLoginPageConfig;\n\n /**\n * Firebase Web API key required by the JS SDK on the login page.\n * Mandatory when `loginPage` is enabled. Find it in your Firebase Console\n * under Project Settings → General → Web app config.\n */\n apiKey?: string;\n\n /**\n * Firebase Auth domain (e.g. `my-project.firebaseapp.com`).\n * Mandatory when `loginPage` is enabled.\n */\n authDomain?: string;\n\n /** Cookie name. Default: `__admin_session`. */\n cookieName?: string;\n\n /** Session cookie TTL in days. Default: `5` (Firebase max is 14). */\n sessionTtlDays?: number;\n\n /**\n * Cookie `Secure` flag. Default: `true`. Set to `false` only for local\n * development over HTTP.\n */\n secureCookie?: boolean;\n\n /** Cookie `SameSite`. Default: `\"Lax\"`. */\n sameSite?: \"Strict\" | \"Lax\" | \"None\";\n\n /**\n * Firebase Auth emulator host (e.g. `127.0.0.1:9099`). When set, the login\n * page's client SDK is pointed at the emulator via `connectAuthEmulator`,\n * matching the Admin SDK (which already targets the emulator when\n * `FIREBASE_AUTH_EMULATOR_HOST` is set). Defaults to that env var, so local\n * `firebase emulators:start` runs work out of the box; pass `\"\"` to force it\n * off.\n */\n authEmulatorHost?: string;\n\n /**\n * Enable the built-in CSRF defense: state-changing requests\n * (`POST`/`PUT`/`PATCH`/`DELETE`) authenticated by **session cookie** must\n * carry an `Origin`/`Referer` header matching the request host. Bearer\n * requests are exempt (a browser never auto-attaches an `Authorization`\n * header, so they are not CSRF-able).\n *\n * **Defaults to `true` only when `sameSite: \"None\"`** (and not in `bearer`\n * mode). With the default `Lax`/`Strict` cookie the browser already blocks\n * the session cookie on cross-site mutations, so the check is unnecessary\n * and is left off to avoid false positives behind proxies whose `Host`\n * differs from the public origin (e.g. Cloud Functions v2 / Cloud Run).\n * Set to `true` to force the check on regardless of `sameSite`, or `false`\n * to disable it even with `sameSite: \"None\"` (only if you implement your\n * own CSRF protection upstream).\n */\n csrfProtection?: boolean;\n\n /**\n * Behaviour when authentication fails or `allow()` returns `null`.\n * - `\"redirect\"` (default in cookie mode) → 302 to the login page,\n * - `\"401\"` (default in bearer mode) → JSON 401 response.\n */\n onUnauthenticated?: \"redirect\" | \"401\";\n\n /**\n * Routes that should bypass the auth middleware (matched against the path\n * after the basePath stripping). The auxiliary login routes are always\n * public regardless of this option.\n */\n publicPaths?: (string | RegExp)[];\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction defaultLoginPage(mode: FirebaseAuthMode): boolean {\n return mode === \"cookie\" || mode === \"both\";\n}\n\nfunction defaultUnauth(mode: FirebaseAuthMode): \"redirect\" | \"401\" {\n return mode === \"bearer\" ? \"401\" : \"redirect\";\n}\n\nfunction pathOf(req: AnyReq): string {\n const raw = req.path ?? req.url ?? \"/\";\n const idx = raw.indexOf(\"?\");\n return idx === -1 ? raw : raw.slice(0, idx);\n}\n\nfunction queryAction(req: AnyReq): string | null {\n const q = (req as { query?: Record<string, unknown> }).query;\n if (q && typeof q.__action === \"string\") return q.__action;\n // Fallback: parse from URL when query parsing isn't done by the runtime.\n const url = req.url ?? \"\";\n const idx = url.indexOf(\"?\");\n if (idx === -1) return null;\n const params = new URLSearchParams(url.slice(idx + 1));\n return params.get(\"__action\");\n}\n\nfunction methodOf(req: AnyReq): string {\n return String(req.method ?? \"GET\").toUpperCase();\n}\n\n/**\n * Strip a redirect / `next` target down to a safe **same-origin path**.\n *\n * Rejects protocol-relative (`//evil.com`), backslash-escaped (`/\\evil.com`)\n * and absolute (`https://…`) targets that would let an attacker turn the\n * post-login redirect into an open redirect / phishing vector (issue #07).\n * Anything suspicious collapses to `\"/\"`.\n */\nfunction sanitizeNextPath(next: string): string {\n if (typeof next !== \"string\" || next.length === 0) return \"/\";\n if (!next.startsWith(\"/\")) return \"/\";\n if (next.startsWith(\"//\") || next.startsWith(\"/\\\\\")) return \"/\";\n if (next.includes(\"://\")) return \"/\";\n return next;\n}\n\nfunction headerOf(req: AnyReq, name: string): string | undefined {\n const raw = req.headers?.[name];\n if (Array.isArray(raw)) return raw[0];\n return typeof raw === \"string\" ? raw : undefined;\n}\n\nconst CSRF_SAFE_METHODS = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Same-origin check used as a CSRF defense for state-changing requests.\n * Compares the request `Origin` (or `Referer`) host against the request host.\n * Returns `true` only when they match. A missing/invalid Origin fails closed.\n */\nfunction isSameOriginRequest(req: AnyReq): boolean {\n const originHeader = headerOf(req, \"origin\") ?? headerOf(req, \"referer\");\n if (!originHeader) return false;\n const host = headerOf(req, \"x-forwarded-host\") ?? headerOf(req, \"host\");\n if (!host) return false;\n try {\n return new URL(originHeader).host === host;\n } catch {\n return false;\n }\n}\n\nfunction isPublic(\n path: string,\n patterns: (string | RegExp)[] | undefined,\n): boolean {\n if (!patterns || patterns.length === 0) return false;\n for (const p of patterns) {\n if (typeof p === \"string\") {\n if (path === p || path.startsWith(p + \"/\")) return true;\n } else if (p.test(path)) {\n return true;\n }\n }\n return false;\n}\n\nfunction wantsHtml(req: AnyReq): boolean {\n const accept = String(req.headers?.accept ?? \"\");\n // Strict: only real browser navigations advertise `text/html`. Treating\n // `*/*` or a missing Accept header as HTML caused curl, healthchecks and\n // server-to-server fetches to receive a 200 login page instead of a clear\n // 401 (issue #16).\n return accept.includes(\"text/html\");\n}\n\nfunction extractBearer(req: AnyReq): string | null {\n const raw = req.headers?.authorization;\n const header = Array.isArray(raw) ? raw[0] : raw;\n if (!header) return null;\n const m = /^Bearer\\s+(.+)$/i.exec(header);\n return m ? m[1]!.trim() : null;\n}\n\nfunction rejectUnauthenticated(\n req: AnyReq,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n res: any,\n policy: \"redirect\" | \"401\",\n loginPath: string,\n): void {\n if (policy === \"redirect\" && wantsHtml(req)) {\n const target = encodeURIComponent(req.url ?? \"/\");\n res\n .status(302)\n .set(\"Location\", `${loginPath}?next=${target}`)\n .set(\"Cache-Control\", \"no-store\")\n .end();\n return;\n }\n res\n .status(401)\n .set(\"Content-Type\", \"application/json; charset=utf-8\")\n .send(JSON.stringify({ success: false, error: \"Unauthorized\" }));\n}\n\n// ---------------------------------------------------------------------------\n// Public factory\n// ---------------------------------------------------------------------------\n\n/**\n * Build a Firebase Auth extension for use with `servers.admin()` or\n * `servers.crud()`. See module-level docs for the full design and examples.\n */\nexport function firebaseAuth<TContext = unknown>(\n config: FirebaseAuthConfig<TContext>,\n): AuthExtension {\n const mode: FirebaseAuthMode = config.mode ?? \"cookie\";\n const cookieName = config.cookieName ?? SESSION_COOKIE_DEFAULT;\n const ttlDays = config.sessionTtlDays ?? 5;\n const secure = config.secureCookie ?? true;\n const sameSite = config.sameSite ?? \"Lax\";\n // The Origin/Referer (CSRF) check is only needed when `SameSite=None`:\n // with the default `Lax` (or `Strict`) the browser already refuses to send\n // the session cookie on cross-site state-changing requests, so a cross-site\n // POST/PUT/DELETE is unauthenticated and harmless. Enforcing the Origin\n // check unconditionally also breaks legitimate same-origin requests on\n // platforms where the container's `Host` differs from the public origin\n // (e.g. Cloud Functions v2 / Cloud Run, where `Host` is the internal\n // `*.run.app` host while `Origin` is the `*.cloudfunctions.net` domain).\n // Can be forced on/off explicitly via `csrfProtection`.\n const csrfProtection =\n config.csrfProtection ?? (mode !== \"bearer\" && sameSite === \"None\");\n if (mode !== \"bearer\" && sameSite === \"None\") {\n console.warn(\n \"[firebaseAuth] `sameSite: \\\"None\\\"` disables the browser's built-in \" +\n \"SameSite CSRF protection. \" +\n (csrfProtection\n ? \"The same-origin (Origin/Referer) check is active as the primary \" +\n \"defense — ensure state-changing requests are sent same-origin or \" +\n \"carry a bearer token.\"\n : \"`csrfProtection` is disabled — your endpoints are CSRF-exposed.\"),\n );\n }\n const onUnauth = config.onUnauthenticated ?? defaultUnauth(mode);\n const loginEnabled =\n config.loginPage === undefined\n ? defaultLoginPage(mode)\n : config.loginPage !== false;\n\n const loginPath = \"/__login\";\n const sessionPath = \"/__session\";\n const logoutPath = \"/__logout\";\n\n // ── Auxiliary handlers (kept in `routes` for hosting deployments\n // where users can mount them at known paths, AND invoked in-band by the\n // middleware on `?__action=session|logout` so vanilla Cloud Functions\n // — where there is no separate URL prefix per route — work too). ──────\n const sessionHandler = createSessionHandler({\n getAuth: config.getAuth,\n cookieName,\n ttlDays,\n secure,\n sameSite,\n });\n const logoutHandler = createLogoutHandler({\n getAuth: config.getAuth,\n cookieName,\n secure,\n sameSite,\n });\n\n function renderInlineLogin(\n req: AnyReq,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n res: any,\n error: string | null = null,\n status = 200,\n ): void {\n // Validate lazily (at request time) so module loading during Firebase CLI\n // analysis doesn't throw before env vars are injected.\n if (!config.apiKey || !config.authDomain) {\n throw new Error(\n \"[firebaseAuth] `apiKey` and `authDomain` are required when `loginPage` is enabled. \" +\n \"Find both in the Firebase Console under Project Settings → General → Web app config.\",\n );\n }\n const pageCfg: FirebaseAuthLoginPageConfig =\n typeof config.loginPage === \"object\" ? config.loginPage : {};\n // Build a same-function absolute URL: the function's external prefix\n // (Cloud Functions name, emulator project/region/target, or \"\" for\n // custom domains) + the in-router request path. The browser otherwise\n // resolves form actions relative to the public URL, which doesn't\n // include the function name on Cloud Functions.\n const prefix = getLinkBase(req, \"/\");\n const inner = req.url ?? \"/\";\n // Sanitize to a same-origin path so a crafted request URL (e.g.\n // `//evil.com/...` on a custom domain where the prefix is empty) cannot\n // turn the post-login redirect or the form action into an open redirect\n // (issue #07).\n const fullPath = sanitizeNextPath(\n `${prefix}${inner.startsWith(\"/\") ? inner : `/${inner}`}`,\n );\n const sep = fullPath.includes(\"?\") ? \"&\" : \"?\";\n const sessionAction = `${fullPath}${sep}__action=session`;\n const html = renderLoginPage({\n title: pageCfg.title ?? \"Admin sign-in\",\n providers: pageCfg.providers ?? [\"password\", \"google\"],\n apiKey: config.apiKey!,\n authDomain: config.authDomain!,\n sessionPath: sessionAction,\n next: fullPath,\n error,\n authEmulatorHost:\n config.authEmulatorHost ?? process.env[\"FIREBASE_AUTH_EMULATOR_HOST\"],\n });\n res\n .status(status)\n .set(\"Content-Type\", \"text/html; charset=utf-8\")\n .set(\"Cache-Control\", \"no-store\")\n .send(html);\n }\n\n // ── Auxiliary routes ─────────────────────────────────────────────────────\n const routes: AuthRoute[] = [];\n if (loginEnabled) {\n routes.push({\n method: \"GET\",\n path: loginPath,\n handler: (req, res) => {\n const error = (req.query?.error as string | undefined) ?? null;\n renderInlineLogin(req, res, error);\n },\n });\n routes.push({\n method: \"POST\",\n path: sessionPath,\n handler: sessionHandler,\n });\n routes.push({\n method: \"POST\",\n path: logoutPath,\n handler: logoutHandler,\n });\n }\n\n const publicPaths: (string | RegExp)[] = [\n ...(config.publicPaths ?? []),\n loginPath,\n sessionPath,\n logoutPath,\n ];\n\n // ── Middleware ───────────────────────────────────────────────────────────\n const middleware: Middleware = async (req, res, next) => {\n const path = pathOf(req);\n const method = methodOf(req);\n\n // 0. CSRF defense for cookie sessions (only active when `SameSite=None`,\n // or when `csrfProtection` is explicitly forced on — see config). With\n // the default Lax/Strict cookie the browser already blocks the session\n // cookie on cross-site state-changing requests, so this is skipped to\n // avoid breaking legitimate same-origin requests behind proxies. When\n // active it runs FIRST so it also protects the in-band\n // `?__action=logout|session` shortcut below (issues #05, #06).\n if (\n csrfProtection &&\n mode !== \"bearer\" &&\n !CSRF_SAFE_METHODS.has(method) &&\n !extractBearer(req) &&\n !isSameOriginRequest(req)\n ) {\n res\n .status(403)\n .set(\"Content-Type\", \"application/json; charset=utf-8\")\n .send(\n JSON.stringify({\n success: false,\n error: \"Origin check failed (possible CSRF)\",\n }),\n );\n return;\n }\n\n // 1. In-band action endpoints. The bundled login page posts the session\n // exchange (and logout) to the CURRENT url with `?__action=session`\n // (resp. `logout`) rather than to the mounted `/__session` route,\n // because on vanilla Cloud Functions the helper cannot reliably know\n // the function's public URL prefix to address a separate route. These\n // actions are inherently public (login/logout need no prior auth) and\n // are protected against cross-site abuse by the same-origin guard\n // above (issue #06).\n if (loginEnabled && method === \"POST\") {\n const action = queryAction(req);\n if (action === \"session\") {\n await sessionHandler(req, res);\n return;\n }\n if (action === \"logout\") {\n await logoutHandler(req, res);\n return;\n }\n }\n\n // 2. Public paths (mounted login routes, user-supplied allowlist).\n if (isPublic(path, publicPaths)) {\n await next();\n return;\n }\n\n let decoded: DecodedIdTokenLike | null = null;\n try {\n const auth = config.getAuth();\n\n // Try bearer first when allowed (cheaper, no cookie parsing).\n if (mode === \"bearer\" || mode === \"both\") {\n const token = extractBearer(req);\n if (token) {\n decoded = await auth.verifyIdToken(token, true);\n }\n }\n\n // Fall back to cookie when allowed.\n if (!decoded && (mode === \"cookie\" || mode === \"both\")) {\n const cookieHeader = req.headers?.cookie;\n const raw = Array.isArray(cookieHeader)\n ? cookieHeader.join(\"; \")\n : cookieHeader;\n const cookies = parseCookies(typeof raw === \"string\" ? raw : \"\");\n const session = cookies[cookieName];\n if (session) {\n decoded = await auth.verifySessionCookie(session, true);\n }\n }\n } catch {\n decoded = null;\n }\n\n if (!decoded) {\n rejectUnauthenticated(req, res);\n return;\n }\n\n const baseUser: Omit<AuthUser, \"context\"> = {\n uid: decoded.uid,\n email: typeof decoded.email === \"string\" ? decoded.email : null,\n emailVerified: !!decoded.email_verified,\n claims: decoded as Record<string, unknown>,\n };\n\n let context: TContext | null;\n try {\n context = config.allow\n ? await config.allow(baseUser)\n : (null as TContext | null);\n } catch {\n context = null;\n }\n\n if (config.allow && context === null) {\n rejectUnauthenticated(req, res);\n return;\n }\n\n (req as AnyReq & { user?: AuthUser<TContext> }).user = {\n ...baseUser,\n context: context as TContext,\n };\n\n await next();\n };\n\n /**\n * Reject according to the configured policy:\n * - cookie/both + GET HTML browser request → render the login page inline\n * on the SAME URL with a `401` status (works on Cloud Functions where\n * there's no separate `/__login` route reachable from the public URL).\n * A 401 keeps the response HTTP-compliant for monitoring/healthchecks\n * while browsers still render the page (issue #16).\n * - bearer mode or non-HTML clients → JSON 401.\n */\n function rejectUnauthenticated(\n req: AnyReq,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n res: any,\n ): void {\n if (\n onUnauth === \"redirect\" &&\n loginEnabled &&\n methodOf(req) === \"GET\" &&\n wantsHtml(req)\n ) {\n renderInlineLogin(req, res, null, 401);\n return;\n }\n res\n .status(401)\n .set(\"Content-Type\", \"application/json; charset=utf-8\")\n .send(JSON.stringify({ success: false, error: \"Unauthorized\" }));\n }\n\n return {\n __authExtension: true,\n middleware,\n routes,\n loginPath,\n };\n}\n\n/**\n * Type guard: detect an {@link AuthExtension} (vs. legacy\n * `BasicAuthConfig` / `Middleware`).\n */\nexport function isAuthExtension(value: unknown): value is AuthExtension {\n return (\n !!value &&\n typeof value === \"object\" &&\n (value as { __authExtension?: unknown }).__authExtension === true\n );\n}\n\n/**\n * Helper for explicitly opening a CRUD operation when the server has\n * `auth` defined (bypasses the default-deny policy).\n *\n * @example\n * ```ts\n * rules: { list: allowAll, get: allowAll }\n * ```\n */\nexport const allowAll = (): true => true;\n"]}
1
+ {"version":3,"sources":["../../../src/servers/utils/link-base.ts","../../../src/servers/auth/login-page.tsx","../../../src/servers/auth/session.ts","../../../src/servers/auth/firebase-auth.ts"],"names":["getLinkBase","req","staticBasePath","region","base","project","resolvedRegion","target","service","host","emulatorUrl","htmlEscape","value","jsonEscape","renderLoginPage","opts","showPassword","showGoogle","initialError","SESSION_COOKIE_DEFAULT","parseCookies","header","out","part","eq","key","buildSetCookie","name","segments","readJsonBody","body","createSessionHandler","cfg","res","idToken","expiresInMs","auth","authTimeRaw","authTime","sessionCookie","cookie","err","message","createLogoutHandler","cookieHeader","raw","session","decoded","expired","defaultLoginPage","mode","defaultUnauth","pathOf","idx","queryAction","q","url","methodOf","sanitizeNextPath","next","headerOf","CSRF_SAFE_METHODS","isSameOriginRequest","originHeader","isPublic","path","patterns","p","wantsHtml","extractBearer","m","firebaseAuth","config","cookieName","ttlDays","secure","sameSite","csrfProtection","onUnauth","loginEnabled","loginPath","sessionPath","logoutPath","sessionHandler","logoutHandler","renderInlineLogin","error","status","pageCfg","prefix","inner","fullPath","sep","sessionAction","html","routes","publicPaths","middleware","method","action","token","rejectUnauthenticated","baseUser","context","isAuthExtension","allowAll"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Compute the URL prefix used to build absolute paths from inside a\n * Firebase HTTPS function. Handles three deployment shapes uniformly:\n *\n * 1. **Firebase emulator** (`FUNCTIONS_EMULATOR=true`) — exposes functions at\n * `http://localhost:5001/{project}/{region}/{functionTarget}/...`. The\n * handler receives `req.url` *without* this prefix, so we rebuild it from\n * `GCLOUD_PROJECT`, `FUNCTION_REGION`, `FUNCTION_TARGET`.\n *\n * 2. **Cloud Functions v2 default URL** (`*.cloudfunctions.net/{name}`) —\n * Cloud Run terminates routing at the service name, so links must include\n * the `K_SERVICE` prefix. Detected via the `host` header containing\n * `cloudfunctions.net`.\n *\n * 3. **Custom domain / Hosting rewrite** — the proxy strips the prefix\n * before reaching the handler, so links are relative to the configured\n * `staticBasePath`.\n *\n * @param req The incoming request (needs `headers.host` / `hostname`).\n * @param staticBasePath The user-configured base path (e.g. `\"/api\"`).\n * @param region Explicit deployment region (e.g. `\"europe-west1\"`).\n * Used **only** in the emulator branch to build the\n * `/{project}/{region}/{target}` prefix; when omitted it\n * falls back to `FUNCTION_REGION` then `\"us-central1\"`.\n * Pass it (e.g. from `httpsOptions.region`) whenever the\n * function is deployed outside `us-central1`, since the\n * emulator does not reliably expose the region via env.\n * @returns A path prefix (no trailing slash) suitable for prepending to\n * `req.url` to build a same-function absolute URL.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function getLinkBase(\n req: any,\n staticBasePath: string,\n region?: string,\n): string {\n const base = staticBasePath === \"/\" ? \"\" : staticBasePath.replace(/\\/$/, \"\");\n\n if (process.env[\"FUNCTIONS_EMULATOR\"] === \"true\") {\n const project =\n process.env[\"GCLOUD_PROJECT\"] ??\n process.env[\"GOOGLE_CLOUD_PROJECT\"] ??\n \"demo-project\";\n const resolvedRegion =\n region ?? process.env[\"FUNCTION_REGION\"] ?? \"us-central1\";\n // FUNCTION_TARGET uses dots (e.g. \"sync.functions.adminsync\") but the\n // emulator URL uses hyphens (\"sync-functions-adminsync\").\n const target = (process.env[\"FUNCTION_TARGET\"] ?? \"\").replace(/\\./g, \"-\");\n return `/${project}/${resolvedRegion}/${target}${base}`;\n }\n\n // Cloud Functions v2: K_SERVICE = function name = URL path prefix.\n // Only add it when accessed via cloudfunctions.net (not custom domains).\n // Cloud Run (Gen 2) lowercases service names, but K_SERVICE may still\n // carry the original mixed-case export name — normalise to lowercase\n // so that generated links match the canonical URL.\n const service = process.env[\"K_SERVICE\"];\n const host: string =\n req?.hostname ?? req?.headers?.[\"host\"] ?? \"\";\n if (service && typeof host === \"string\" && host.includes(\"cloudfunctions.net\")) {\n return `/${service.toLowerCase()}${base}`;\n }\n\n return base;\n}\n\n/**\n * Normalise the `region` field of a Firebase `HttpsOptions` (which may be a\n * single region, an array, a CEL `Expression`, or a `ResetValue`) into a single\n * concrete region string for {@link getLinkBase}. Returns `undefined` for\n * anything that is not a plain string at config time.\n */\nexport function resolveRegion(region: unknown): string | undefined {\n if (typeof region === \"string\") return region;\n if (Array.isArray(region)) {\n const first = region[0];\n return typeof first === \"string\" ? first : undefined;\n }\n return undefined;\n}\n","/**\n * Login page renderer for `firebaseAuth`.\n * Standalone HTML — no JSX. Embeds the Firebase JS SDK from the official CDN\n * (modular v10) so users don't need a frontend build step.\n *\n * Flow:\n * 1. User signs in client-side (email/password or Google popup).\n * 2. We call `user.getIdToken(true)` and `POST` it to `{sessionPath}`.\n * 3. The server mints a session cookie and we redirect to `next`.\n */\n\ninterface LoginPageOptions {\n title: string;\n providers: (\"password\" | \"google\")[];\n apiKey: string;\n authDomain: string;\n sessionPath: string;\n next: string;\n error: string | null;\n /**\n * Firebase Auth emulator host (e.g. `127.0.0.1:9099`). When set, the client\n * SDK is pointed at the emulator via `connectAuthEmulator` so local sign-ins\n * match the server side (which the Admin SDK already routes to the emulator\n * through `FIREBASE_AUTH_EMULATOR_HOST`). A bare `host:port` is upgraded to\n * an `http://` URL.\n */\n authEmulatorHost?: string;\n}\n\n/** Normalise an emulator host (`host:port`) into a full `http://` URL. */\nfunction emulatorUrl(host: string | undefined): string {\n if (!host) return \"\";\n return /^https?:\\/\\//.test(host) ? host : `http://${host}`;\n}\n\nfunction htmlEscape(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\");\n}\n\nfunction jsonEscape(value: string): string {\n // Safe for embedding inside a <script> string literal.\n return JSON.stringify(value).slice(1, -1);\n}\n\nexport function renderLoginPage(opts: LoginPageOptions): string {\n const showPassword = opts.providers.includes(\"password\");\n const showGoogle = opts.providers.includes(\"google\");\n const initialError = opts.error ? htmlEscape(opts.error) : \"\";\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n <title>${htmlEscape(opts.title)}</title>\n <style>\n :root { color-scheme: light dark; }\n * { box-sizing: border-box; }\n body {\n margin: 0;\n min-height: 100vh;\n display: grid;\n place-items: center;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n background: #f5f5f7;\n color: #1d1d1f;\n }\n @media (prefers-color-scheme: dark) {\n body { background: #1d1d1f; color: #f5f5f7; }\n .card { background: #2c2c2e !important; }\n input { background: #1d1d1f; color: #f5f5f7; border-color: #444; }\n input::placeholder { color: #888; }\n input:-webkit-autofill,\n input:-webkit-autofill:hover,\n input:-webkit-autofill:focus,\n input:-webkit-autofill:active {\n -webkit-text-fill-color: #f5f5f7 !important;\n -webkit-box-shadow: 0 0 0 1000px #1d1d1f inset !important;\n caret-color: #f5f5f7;\n }\n .divider { color: #888; }\n .divider::before, .divider::after { background: #444; }\n }\n .card {\n width: min(420px, 92vw);\n padding: 32px;\n background: #fff;\n border-radius: 14px;\n box-shadow: 0 20px 50px rgba(0,0,0,.08);\n }\n h1 { font-size: 22px; margin: 0 0 6px; font-weight: 600; }\n p.sub { margin: 0 0 24px; opacity: .7; font-size: 14px; }\n label { display: block; font-size: 13px; margin-bottom: 6px; opacity: .8; }\n input {\n width: 100%; padding: 11px 12px;\n border: 1px solid #d2d2d7; border-radius: 8px;\n font-size: 15px; outline: none; background: #fff; color: #1d1d1f;\n margin-bottom: 14px;\n }\n input::placeholder { color: #86868b; }\n input:focus { border-color: #0071e3; box-shadow: 0 0 0 3px rgba(0,113,227,.15); }\n /* Force readable text on Chrome's autofill (otherwise the input keeps\n the autofill's white background but inherits the page's dark-mode text\n colour, producing white-on-white). */\n input:-webkit-autofill,\n input:-webkit-autofill:hover,\n input:-webkit-autofill:focus,\n input:-webkit-autofill:active {\n -webkit-text-fill-color: #1d1d1f !important;\n -webkit-box-shadow: 0 0 0 1000px #fff inset !important;\n caret-color: #1d1d1f;\n transition: background-color 9999s ease-out 0s;\n }\n button {\n width: 100%; padding: 11px 12px; border: none; border-radius: 8px;\n font-size: 15px; font-weight: 500; cursor: pointer;\n transition: opacity .15s, transform .05s;\n }\n button:active { transform: scale(.98); }\n button:disabled { opacity: .55; cursor: progress; }\n .btn-primary { background: #0071e3; color: #fff; }\n .btn-google {\n background: #fff; color: #1d1d1f; border: 1px solid #d2d2d7;\n display: flex; align-items: center; justify-content: center; gap: 8px;\n }\n @media (prefers-color-scheme: dark) {\n .btn-google { background: #2c2c2e; color: #f5f5f7; border-color: #444; }\n }\n .divider {\n display: flex; align-items: center; gap: 12px;\n margin: 16px 0; font-size: 12px; opacity: .55; text-transform: uppercase;\n }\n .divider::before, .divider::after {\n content: \"\"; flex: 1; height: 1px; background: #d2d2d7;\n }\n .err {\n margin: 0 0 14px; padding: 10px 12px;\n background: rgba(255,59,48,.12); color: #ff3b30;\n border-radius: 8px; font-size: 13px;\n display: ${initialError ? \"block\" : \"none\"};\n }\n .ok {\n margin: 0 0 14px; padding: 10px 12px;\n background: rgba(52,199,89,.12); color: #34c759;\n border-radius: 8px; font-size: 13px; display: none;\n }\n </style>\n</head>\n<body>\n <main class=\"card\">\n <h1>${htmlEscape(opts.title)}</h1>\n <p class=\"sub\">Sign in to continue.</p>\n <div id=\"err\" class=\"err\">${initialError}</div>\n <div id=\"ok\" class=\"ok\"></div>\n\n ${\n showPassword\n ? `<form id=\"pwd-form\" autocomplete=\"on\">\n <label for=\"email\">Email</label>\n <input id=\"email\" type=\"email\" name=\"email\" autocomplete=\"username\" required />\n <label for=\"password\">Password</label>\n <input id=\"password\" type=\"password\" name=\"password\" autocomplete=\"current-password\" required />\n <button class=\"btn-primary\" type=\"submit\" id=\"pwd-submit\">Sign in</button>\n </form>`\n : \"\"\n }\n\n ${showPassword && showGoogle ? `<div class=\"divider\">or</div>` : \"\"}\n\n ${\n showGoogle\n ? `<button class=\"btn-google\" type=\"button\" id=\"google-btn\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" aria-hidden=\"true\">\n <path fill=\"#4285F4\" d=\"M17.64 9.205c0-.638-.057-1.252-.164-1.841H9v3.481h4.844a4.14 4.14 0 0 1-1.796 2.716v2.259h2.908c1.702-1.567 2.684-3.875 2.684-6.615z\"/>\n <path fill=\"#34A853\" d=\"M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z\"/>\n <path fill=\"#FBBC05\" d=\"M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z\"/>\n <path fill=\"#EA4335\" d=\"M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z\"/>\n </svg>\n Continue with Google\n </button>`\n : \"\"\n }\n </main>\n\n <script type=\"module\">\n import { initializeApp } from \"https://www.gstatic.com/firebasejs/10.13.2/firebase-app.js\";\n import {\n getAuth,\n connectAuthEmulator,\n signInWithEmailAndPassword,\n signInWithPopup,\n GoogleAuthProvider,\n setPersistence,\n browserSessionPersistence,\n } from \"https://www.gstatic.com/firebasejs/10.13.2/firebase-auth.js\";\n\n const app = initializeApp({\n apiKey: \"${jsonEscape(opts.apiKey)}\",\n authDomain: \"${jsonEscape(opts.authDomain)}\",\n });\n const auth = getAuth(app);\n ${\n opts.authEmulatorHost\n ? `connectAuthEmulator(auth, ${JSON.stringify(\n emulatorUrl(opts.authEmulatorHost),\n )}, { disableWarnings: true });`\n : \"\"\n }\n // Don't persist client-side — the server-side session cookie is the source of truth.\n await setPersistence(auth, browserSessionPersistence).catch(() => {});\n\n const SESSION_PATH = \"${jsonEscape(opts.sessionPath)}\";\n const NEXT = ${JSON.stringify(opts.next)};\n\n // Defense-in-depth against open redirect (issue #07): only ever navigate\n // to a same-origin path, even though the server already sanitizes next.\n // Resolve against the current page URL (not just the origin) so a relative\n // \"next\" works behind any Cloud Functions / reverse-proxy path prefix.\n function safeNext(raw) {\n try {\n const u = new URL(raw, window.location.href);\n if (u.origin !== window.location.origin) return \"/\";\n return u.pathname + u.search + u.hash;\n } catch {\n return \"/\";\n }\n }\n\n const errEl = document.getElementById(\"err\");\n const okEl = document.getElementById(\"ok\");\n function showError(msg) {\n errEl.textContent = msg;\n errEl.style.display = \"block\";\n okEl.style.display = \"none\";\n }\n function showOk(msg) {\n okEl.textContent = msg;\n okEl.style.display = \"block\";\n errEl.style.display = \"none\";\n }\n\n async function exchangeForSession(user) {\n const idToken = await user.getIdToken(true);\n const res = await fetch(SESSION_PATH, {\n method: \"POST\",\n credentials: \"same-origin\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ idToken }),\n });\n if (!res.ok) {\n const data = await res.json().catch(() => ({}));\n throw new Error(data.error || \"Session exchange failed (\" + res.status + \")\");\n }\n // Sign out client-side immediately — we only needed the id token.\n try { await auth.signOut(); } catch {}\n showOk(\"Signed in. Redirecting…\");\n window.location.replace(safeNext(NEXT));\n }\n\n const pwdForm = document.getElementById(\"pwd-form\");\n if (pwdForm) {\n pwdForm.addEventListener(\"submit\", async (ev) => {\n ev.preventDefault();\n const submit = document.getElementById(\"pwd-submit\");\n submit.disabled = true;\n try {\n const email = document.getElementById(\"email\").value.trim();\n const password = document.getElementById(\"password\").value;\n const cred = await signInWithEmailAndPassword(auth, email, password);\n await exchangeForSession(cred.user);\n } catch (err) {\n showError(err && err.message ? err.message : String(err));\n submit.disabled = false;\n }\n });\n }\n\n const googleBtn = document.getElementById(\"google-btn\");\n if (googleBtn) {\n googleBtn.addEventListener(\"click\", async () => {\n googleBtn.disabled = true;\n try {\n const provider = new GoogleAuthProvider();\n const cred = await signInWithPopup(auth, provider);\n await exchangeForSession(cred.user);\n } catch (err) {\n showError(err && err.message ? err.message : String(err));\n googleBtn.disabled = false;\n }\n });\n }\n </script>\n</body>\n</html>`;\n}\n","/**\n * Session cookie + logout handlers for `firebaseAuth`.\n * Exchanges a Firebase ID token for an HttpOnly session cookie via the\n * Firebase Admin SDK (`createSessionCookie`), and clears it on logout.\n */\n\nimport type { RouteHandler } from \"../admin/router\";\nimport type { FirebaseAdminAuthLike } from \"./firebase-auth\";\n\nexport const SESSION_COOKIE_DEFAULT = \"__admin_session\";\n\ninterface SessionHandlerConfig {\n getAuth: () => FirebaseAdminAuthLike;\n cookieName: string;\n ttlDays: number;\n secure: boolean;\n sameSite: \"Strict\" | \"Lax\" | \"None\";\n}\n\ninterface LogoutHandlerConfig {\n getAuth: () => FirebaseAdminAuthLike;\n cookieName: string;\n secure: boolean;\n sameSite: \"Strict\" | \"Lax\" | \"None\";\n}\n\n// ---------------------------------------------------------------------------\n// Cookie utilities\n// ---------------------------------------------------------------------------\n\n/** Parse a `Cookie` header into a flat key→value map. Tolerant of malformed pairs. */\nexport function parseCookies(header: string): Record<string, string> {\n const out: Record<string, string> = {};\n if (!header) return out;\n for (const part of header.split(\";\")) {\n const eq = part.indexOf(\"=\");\n if (eq === -1) continue;\n const key = part.slice(0, eq).trim();\n if (!key) continue;\n let value = part.slice(eq + 1).trim();\n if (value.startsWith('\"') && value.endsWith('\"')) {\n value = value.slice(1, -1);\n }\n try {\n out[key] = decodeURIComponent(value);\n } catch {\n out[key] = value;\n }\n }\n return out;\n}\n\nfunction buildSetCookie(\n name: string,\n value: string,\n opts: {\n maxAgeSeconds: number;\n secure: boolean;\n sameSite: \"Strict\" | \"Lax\" | \"None\";\n path?: string;\n },\n): string {\n const segments = [\n `${name}=${value}`,\n `Path=${opts.path ?? \"/\"}`,\n `Max-Age=${opts.maxAgeSeconds}`,\n \"HttpOnly\",\n `SameSite=${opts.sameSite}`,\n ];\n if (opts.secure) segments.push(\"Secure\");\n return segments.join(\"; \");\n}\n\n/** Pull JSON body out of any Express-like request (works with `parseBody` already done by the host). */\nfunction readJsonBody(req: { body?: unknown }): Record<string, unknown> {\n const body = req.body;\n if (!body) return {};\n if (typeof body === \"string\") {\n try {\n return JSON.parse(body) as Record<string, unknown>;\n } catch {\n return {};\n }\n }\n if (typeof body === \"object\") return body as Record<string, unknown>;\n return {};\n}\n\n// ---------------------------------------------------------------------------\n// Handlers\n// ---------------------------------------------------------------------------\n\n/**\n * `POST /__session` — receives `{ idToken }`, verifies it via the Admin SDK,\n * mints a session cookie, and sets it on the response.\n */\nexport function createSessionHandler(cfg: SessionHandlerConfig): RouteHandler {\n return async (req, res) => {\n const body = readJsonBody(req);\n const idToken = typeof body.idToken === \"string\" ? body.idToken : \"\";\n if (!idToken) {\n res\n .status(400)\n .set(\"Content-Type\", \"application/json; charset=utf-8\")\n .send(JSON.stringify({ success: false, error: \"Missing idToken\" }));\n return;\n }\n\n const expiresInMs = cfg.ttlDays * 24 * 60 * 60 * 1000;\n try {\n const auth = cfg.getAuth();\n // Verify first so we surface auth errors before minting the cookie.\n const decoded = await auth.verifyIdToken(idToken, true);\n // Reject very old sign-ins to encourage fresh re-auth (Google guidance: < 5 min).\n const authTimeRaw = (decoded as { auth_time?: number }).auth_time;\n const authTime =\n typeof authTimeRaw === \"number\" ? authTimeRaw * 1000 : Date.now();\n if (Date.now() - authTime > 5 * 60 * 1000) {\n res\n .status(401)\n .set(\"Content-Type\", \"application/json; charset=utf-8\")\n .send(\n JSON.stringify({\n success: false,\n error: \"Recent sign-in required\",\n }),\n );\n return;\n }\n const sessionCookie = await auth.createSessionCookie(idToken, {\n expiresIn: expiresInMs,\n });\n const cookie = buildSetCookie(cfg.cookieName, encodeURIComponent(sessionCookie), {\n maxAgeSeconds: Math.floor(expiresInMs / 1000),\n secure: cfg.secure,\n sameSite: cfg.sameSite,\n });\n res\n .status(200)\n .set(\"Set-Cookie\", cookie)\n .set(\"Content-Type\", \"application/json; charset=utf-8\")\n .send(JSON.stringify({ success: true }));\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invalid idToken\";\n res\n .status(401)\n .set(\"Content-Type\", \"application/json; charset=utf-8\")\n .send(JSON.stringify({ success: false, error: message }));\n }\n };\n}\n\n/**\n * `POST /__logout` — clears the session cookie and revokes the user's refresh\n * tokens (best-effort; failure to revoke does not block the logout).\n */\nexport function createLogoutHandler(cfg: LogoutHandlerConfig): RouteHandler {\n return async (req, res) => {\n try {\n const cookieHeader = req.headers?.cookie;\n const raw = Array.isArray(cookieHeader) ? cookieHeader.join(\"; \") : cookieHeader;\n const cookies = parseCookies(typeof raw === \"string\" ? raw : \"\");\n const session = cookies[cfg.cookieName];\n if (session) {\n try {\n const auth = cfg.getAuth();\n const decoded = await auth.verifySessionCookie(session, false);\n await auth.revokeRefreshTokens(decoded.uid);\n } catch {\n /* best-effort */\n }\n }\n } finally {\n const expired = buildSetCookie(cfg.cookieName, \"\", {\n maxAgeSeconds: 0,\n secure: cfg.secure,\n sameSite: cfg.sameSite,\n });\n res\n .status(200)\n .set(\"Set-Cookie\", expired)\n .set(\"Content-Type\", \"application/json; charset=utf-8\")\n .send(JSON.stringify({ success: true }));\n }\n };\n}\n","/**\n * Firebase Auth helper for the admin & CRUD servers.\n *\n * Returns an {@link AuthExtension} ready to plug into `servers.admin()` or\n * `servers.crud()`. Supports two transport modes:\n *\n * - **`cookie`** — session cookie pattern (default for admin UIs). Mounts\n * `/__login`, `/__session`, `/__logout` routes; the page lets the user sign\n * in client-side with the Firebase JS SDK and exchanges the resulting ID\n * token for an HttpOnly session cookie via Firebase Admin SDK.\n * - **`bearer`** — verifies `Authorization: Bearer <idToken>` on every request\n * (default for REST APIs). No login routes mounted.\n * - **`both`** — accept either cookie or bearer.\n *\n * The helper is **agnostic** about authorization: pass an `allow` callback\n * returning whatever role/context shape you need. The result is exposed as\n * `req.user.context` to downstream middlewares and route handlers.\n *\n * @example Admin (cookie + role trio)\n * ```ts\n * import { firebaseAuth } from \"@lpdjs/firestore-repo-service/servers/auth\";\n * import { getAuth } from \"firebase-admin/auth\";\n *\n * servers.admin({\n * auth: firebaseAuth({\n * getAuth,\n * mode: \"cookie\",\n * apiKey: process.env.FIREBASE_WEB_API_KEY!,\n * authDomain: process.env.FIREBASE_AUTH_DOMAIN!,\n * allow: ({ email, claims }) => {\n * if (claims.superAdmin) return { role: \"superAdmin\" };\n * if (email?.endsWith(\"@solarpush.io\")) return { role: \"admin\" };\n * if (email) return { role: \"viewer\" };\n * return null;\n * },\n * }),\n * repos: { ... },\n * });\n * ```\n *\n * @example CRUD (bearer + business rules per repo)\n * ```ts\n * servers.crud({\n * auth: firebaseAuth({ getAuth, mode: \"bearer\", allow: (u) => u }),\n * repos: {\n * comments: {\n * repo: repos.comments,\n * rules: {\n * list: () => true,\n * get: ({ user, doc }) => doc.public || doc.authorId === user.uid,\n * create: ({ user }) => !!user.uid,\n * update: ({ user, doc }) => user.uid === doc.authorId,\n * delete: ({ user, doc }) => user.claims.role === \"moderator\",\n * },\n * },\n * },\n * });\n * ```\n */\n\nimport type { AnyReq, Middleware, RouteHandler } from \"../admin/router\";\nimport { getLinkBase } from \"../utils/link-base\";\nimport { renderLoginPage } from \"./login-page\";\nimport {\n createLogoutHandler,\n createSessionHandler,\n parseCookies,\n SESSION_COOKIE_DEFAULT,\n} from \"./session\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal Firebase Admin Auth surface needed by this helper.\n * Avoids a hard import of `firebase-admin/auth` so the package stays\n * decoupled from a specific firebase-admin version.\n */\nexport interface FirebaseAdminAuthLike {\n verifyIdToken(\n idToken: string,\n checkRevoked?: boolean,\n ): Promise<DecodedIdTokenLike>;\n verifySessionCookie(\n sessionCookie: string,\n checkRevoked?: boolean,\n ): Promise<DecodedIdTokenLike>;\n createSessionCookie(\n idToken: string,\n sessionCookieOptions: { expiresIn: number },\n ): Promise<string>;\n revokeRefreshTokens(uid: string): Promise<void>;\n}\n\nexport interface DecodedIdTokenLike {\n uid: string;\n email?: string;\n email_verified?: boolean;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [claim: string]: any;\n}\n\n/** Identity attached to every authenticated request as `req.user`. */\nexport interface AuthUser<TContext = unknown> {\n uid: string;\n email: string | null;\n emailVerified: boolean;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n claims: Record<string, any>;\n /** Result of the user-supplied `allow()` callback. */\n context: TContext;\n}\n\n/** A route descriptor mounted by `firebaseAuth` before the protected chain. */\nexport interface AuthRoute {\n method: \"GET\" | \"POST\";\n path: string;\n handler: RouteHandler;\n}\n\n/**\n * Returned by {@link firebaseAuth}. Servers detect this shape (vs.\n * `BasicAuthConfig` / raw `Middleware`) and mount the routes before pushing\n * the middleware onto the chain.\n */\nexport interface AuthExtension {\n readonly __authExtension: true;\n middleware: Middleware;\n /** Auxiliary routes (login page, session, logout). Empty in pure bearer mode. */\n routes: AuthRoute[];\n /** Path used to redirect unauthenticated browser requests. */\n loginPath: string;\n}\n\nexport type FirebaseAuthMode = \"cookie\" | \"bearer\" | \"both\";\n\n/** Provider configuration for the bundled login page. */\nexport interface FirebaseAuthLoginPageConfig {\n /** Page title. Default: \"Admin sign-in\". */\n title?: string;\n /**\n * Providers shown on the login page.\n * Default: `[\"password\", \"google\"]`.\n */\n providers?: (\"password\" | \"google\")[];\n}\n\nexport interface FirebaseAuthConfig<TContext = unknown> {\n /** Lazy getter for the Firebase Admin Auth instance. */\n getAuth: () => FirebaseAdminAuthLike;\n\n /** Transport mode. Default: `\"cookie\"`. */\n mode?: FirebaseAuthMode;\n\n /**\n * Authorization callback. Receives the verified token claims and returns:\n * - a context object → request is allowed, exposed as `req.user.context`,\n * - `null` → request is rejected (401 / redirect to login).\n *\n * If omitted, the default policy allows any authenticated user with\n * `context = null`.\n */\n allow?: (\n user: Omit<AuthUser, \"context\">,\n ) => TContext | null | Promise<TContext | null>;\n\n // ── Cookie mode options ────────────────────────────────────────────────\n /**\n * Whether to mount the bundled `/__login`, `/__session`, `/__logout`\n * routes. Default: `true` for `cookie`/`both`, `false` for `bearer`.\n */\n loginPage?: boolean | FirebaseAuthLoginPageConfig;\n\n /**\n * Firebase Web API key required by the JS SDK on the login page.\n * Mandatory when `loginPage` is enabled. Find it in your Firebase Console\n * under Project Settings → General → Web app config.\n */\n apiKey?: string;\n\n /**\n * Firebase Auth domain (e.g. `my-project.firebaseapp.com`).\n * Mandatory when `loginPage` is enabled.\n */\n authDomain?: string;\n\n /** Cookie name. Default: `__admin_session`. */\n cookieName?: string;\n\n /** Session cookie TTL in days. Default: `5` (Firebase max is 14). */\n sessionTtlDays?: number;\n\n /**\n * Cookie `Secure` flag. Default: `true`. Set to `false` only for local\n * development over HTTP.\n */\n secureCookie?: boolean;\n\n /** Cookie `SameSite`. Default: `\"Lax\"`. */\n sameSite?: \"Strict\" | \"Lax\" | \"None\";\n\n /**\n * Deployment region (e.g. `\"europe-west1\"`). Used to build the login page's\n * same-function URLs **under the Firebase emulator**, which does not reliably\n * expose the region via env — without it the emulator prefix falls back to\n * `us-central1`, so the session POST 404s when deployed elsewhere. Has no\n * effect in production (the prefix is derived from the request there). When\n * created via `servers.admin/crud()` this defaults to `httpsOptions.region`.\n */\n region?: string;\n\n /**\n * Firebase Auth emulator host (e.g. `127.0.0.1:9099`). When set, the login\n * page's client SDK is pointed at the emulator via `connectAuthEmulator`,\n * matching the Admin SDK (which already targets the emulator when\n * `FIREBASE_AUTH_EMULATOR_HOST` is set). Defaults to that env var, so local\n * `firebase emulators:start` runs work out of the box; pass `\"\"` to force it\n * off.\n */\n authEmulatorHost?: string;\n\n /**\n * Enable the built-in CSRF defense: state-changing requests\n * (`POST`/`PUT`/`PATCH`/`DELETE`) authenticated by **session cookie** must\n * carry an `Origin`/`Referer` header matching the request host. Bearer\n * requests are exempt (a browser never auto-attaches an `Authorization`\n * header, so they are not CSRF-able).\n *\n * **Defaults to `true` only when `sameSite: \"None\"`** (and not in `bearer`\n * mode). With the default `Lax`/`Strict` cookie the browser already blocks\n * the session cookie on cross-site mutations, so the check is unnecessary\n * and is left off to avoid false positives behind proxies whose `Host`\n * differs from the public origin (e.g. Cloud Functions v2 / Cloud Run).\n * Set to `true` to force the check on regardless of `sameSite`, or `false`\n * to disable it even with `sameSite: \"None\"` (only if you implement your\n * own CSRF protection upstream).\n */\n csrfProtection?: boolean;\n\n /**\n * Behaviour when authentication fails or `allow()` returns `null`.\n * - `\"redirect\"` (default in cookie mode) → 302 to the login page,\n * - `\"401\"` (default in bearer mode) → JSON 401 response.\n */\n onUnauthenticated?: \"redirect\" | \"401\";\n\n /**\n * Routes that should bypass the auth middleware (matched against the path\n * after the basePath stripping). The auxiliary login routes are always\n * public regardless of this option.\n */\n publicPaths?: (string | RegExp)[];\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction defaultLoginPage(mode: FirebaseAuthMode): boolean {\n return mode === \"cookie\" || mode === \"both\";\n}\n\nfunction defaultUnauth(mode: FirebaseAuthMode): \"redirect\" | \"401\" {\n return mode === \"bearer\" ? \"401\" : \"redirect\";\n}\n\nfunction pathOf(req: AnyReq): string {\n const raw = req.path ?? req.url ?? \"/\";\n const idx = raw.indexOf(\"?\");\n return idx === -1 ? raw : raw.slice(0, idx);\n}\n\nfunction queryAction(req: AnyReq): string | null {\n const q = (req as { query?: Record<string, unknown> }).query;\n if (q && typeof q.__action === \"string\") return q.__action;\n // Fallback: parse from URL when query parsing isn't done by the runtime.\n const url = req.url ?? \"\";\n const idx = url.indexOf(\"?\");\n if (idx === -1) return null;\n const params = new URLSearchParams(url.slice(idx + 1));\n return params.get(\"__action\");\n}\n\nfunction methodOf(req: AnyReq): string {\n return String(req.method ?? \"GET\").toUpperCase();\n}\n\n/**\n * Strip a redirect / `next` target down to a safe **same-origin path**.\n *\n * Rejects protocol-relative (`//evil.com`), backslash-escaped (`/\\evil.com`)\n * and absolute (`https://…`) targets that would let an attacker turn the\n * post-login redirect into an open redirect / phishing vector (issue #07).\n * Anything suspicious collapses to `\"/\"`.\n */\nfunction sanitizeNextPath(next: string): string {\n if (typeof next !== \"string\" || next.length === 0) return \"/\";\n if (!next.startsWith(\"/\")) return \"/\";\n if (next.startsWith(\"//\") || next.startsWith(\"/\\\\\")) return \"/\";\n if (next.includes(\"://\")) return \"/\";\n return next;\n}\n\nfunction headerOf(req: AnyReq, name: string): string | undefined {\n const raw = req.headers?.[name];\n if (Array.isArray(raw)) return raw[0];\n return typeof raw === \"string\" ? raw : undefined;\n}\n\nconst CSRF_SAFE_METHODS = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Same-origin check used as a CSRF defense for state-changing requests.\n * Compares the request `Origin` (or `Referer`) host against the request host.\n * Returns `true` only when they match. A missing/invalid Origin fails closed.\n */\nfunction isSameOriginRequest(req: AnyReq): boolean {\n const originHeader = headerOf(req, \"origin\") ?? headerOf(req, \"referer\");\n if (!originHeader) return false;\n const host = headerOf(req, \"x-forwarded-host\") ?? headerOf(req, \"host\");\n if (!host) return false;\n try {\n return new URL(originHeader).host === host;\n } catch {\n return false;\n }\n}\n\nfunction isPublic(\n path: string,\n patterns: (string | RegExp)[] | undefined,\n): boolean {\n if (!patterns || patterns.length === 0) return false;\n for (const p of patterns) {\n if (typeof p === \"string\") {\n if (path === p || path.startsWith(p + \"/\")) return true;\n } else if (p.test(path)) {\n return true;\n }\n }\n return false;\n}\n\nfunction wantsHtml(req: AnyReq): boolean {\n const accept = String(req.headers?.accept ?? \"\");\n // Strict: only real browser navigations advertise `text/html`. Treating\n // `*/*` or a missing Accept header as HTML caused curl, healthchecks and\n // server-to-server fetches to receive a 200 login page instead of a clear\n // 401 (issue #16).\n return accept.includes(\"text/html\");\n}\n\nfunction extractBearer(req: AnyReq): string | null {\n const raw = req.headers?.authorization;\n const header = Array.isArray(raw) ? raw[0] : raw;\n if (!header) return null;\n const m = /^Bearer\\s+(.+)$/i.exec(header);\n return m ? m[1]!.trim() : null;\n}\n\nfunction rejectUnauthenticated(\n req: AnyReq,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n res: any,\n policy: \"redirect\" | \"401\",\n loginPath: string,\n): void {\n if (policy === \"redirect\" && wantsHtml(req)) {\n const target = encodeURIComponent(req.url ?? \"/\");\n res\n .status(302)\n .set(\"Location\", `${loginPath}?next=${target}`)\n .set(\"Cache-Control\", \"no-store\")\n .end();\n return;\n }\n res\n .status(401)\n .set(\"Content-Type\", \"application/json; charset=utf-8\")\n .send(JSON.stringify({ success: false, error: \"Unauthorized\" }));\n}\n\n// ---------------------------------------------------------------------------\n// Public factory\n// ---------------------------------------------------------------------------\n\n/**\n * Build a Firebase Auth extension for use with `servers.admin()` or\n * `servers.crud()`. See module-level docs for the full design and examples.\n */\nexport function firebaseAuth<TContext = unknown>(\n config: FirebaseAuthConfig<TContext>,\n): AuthExtension {\n const mode: FirebaseAuthMode = config.mode ?? \"cookie\";\n const cookieName = config.cookieName ?? SESSION_COOKIE_DEFAULT;\n const ttlDays = config.sessionTtlDays ?? 5;\n const secure = config.secureCookie ?? true;\n const sameSite = config.sameSite ?? \"Lax\";\n // The Origin/Referer (CSRF) check is only needed when `SameSite=None`:\n // with the default `Lax` (or `Strict`) the browser already refuses to send\n // the session cookie on cross-site state-changing requests, so a cross-site\n // POST/PUT/DELETE is unauthenticated and harmless. Enforcing the Origin\n // check unconditionally also breaks legitimate same-origin requests on\n // platforms where the container's `Host` differs from the public origin\n // (e.g. Cloud Functions v2 / Cloud Run, where `Host` is the internal\n // `*.run.app` host while `Origin` is the `*.cloudfunctions.net` domain).\n // Can be forced on/off explicitly via `csrfProtection`.\n const csrfProtection =\n config.csrfProtection ?? (mode !== \"bearer\" && sameSite === \"None\");\n if (mode !== \"bearer\" && sameSite === \"None\") {\n console.warn(\n \"[firebaseAuth] `sameSite: \\\"None\\\"` disables the browser's built-in \" +\n \"SameSite CSRF protection. \" +\n (csrfProtection\n ? \"The same-origin (Origin/Referer) check is active as the primary \" +\n \"defense — ensure state-changing requests are sent same-origin or \" +\n \"carry a bearer token.\"\n : \"`csrfProtection` is disabled — your endpoints are CSRF-exposed.\"),\n );\n }\n const onUnauth = config.onUnauthenticated ?? defaultUnauth(mode);\n const loginEnabled =\n config.loginPage === undefined\n ? defaultLoginPage(mode)\n : config.loginPage !== false;\n\n const loginPath = \"/__login\";\n const sessionPath = \"/__session\";\n const logoutPath = \"/__logout\";\n\n // ── Auxiliary handlers (kept in `routes` for hosting deployments\n // where users can mount them at known paths, AND invoked in-band by the\n // middleware on `?__action=session|logout` so vanilla Cloud Functions\n // — where there is no separate URL prefix per route — work too). ──────\n const sessionHandler = createSessionHandler({\n getAuth: config.getAuth,\n cookieName,\n ttlDays,\n secure,\n sameSite,\n });\n const logoutHandler = createLogoutHandler({\n getAuth: config.getAuth,\n cookieName,\n secure,\n sameSite,\n });\n\n function renderInlineLogin(\n req: AnyReq,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n res: any,\n error: string | null = null,\n status = 200,\n ): void {\n // Validate lazily (at request time) so module loading during Firebase CLI\n // analysis doesn't throw before env vars are injected.\n if (!config.apiKey || !config.authDomain) {\n throw new Error(\n \"[firebaseAuth] `apiKey` and `authDomain` are required when `loginPage` is enabled. \" +\n \"Find both in the Firebase Console under Project Settings → General → Web app config.\",\n );\n }\n const pageCfg: FirebaseAuthLoginPageConfig =\n typeof config.loginPage === \"object\" ? config.loginPage : {};\n // Build a same-function absolute URL: the function's external prefix\n // (Cloud Functions name, emulator project/region/target, or \"\" for\n // custom domains) + the in-router request path. The browser otherwise\n // resolves form actions relative to the public URL, which doesn't\n // include the function name on Cloud Functions.\n const prefix = getLinkBase(req, \"/\", config.region);\n const inner = req.url ?? \"/\";\n // Sanitize to a same-origin path so a crafted request URL (e.g.\n // `//evil.com/...` on a custom domain where the prefix is empty) cannot\n // turn the post-login redirect or the form action into an open redirect\n // (issue #07).\n const fullPath = sanitizeNextPath(\n `${prefix}${inner.startsWith(\"/\") ? inner : `/${inner}`}`,\n );\n const sep = fullPath.includes(\"?\") ? \"&\" : \"?\";\n const sessionAction = `${fullPath}${sep}__action=session`;\n const html = renderLoginPage({\n title: pageCfg.title ?? \"Admin sign-in\",\n providers: pageCfg.providers ?? [\"password\", \"google\"],\n apiKey: config.apiKey!,\n authDomain: config.authDomain!,\n sessionPath: sessionAction,\n next: fullPath,\n error,\n authEmulatorHost:\n config.authEmulatorHost ?? process.env[\"FIREBASE_AUTH_EMULATOR_HOST\"],\n });\n res\n .status(status)\n .set(\"Content-Type\", \"text/html; charset=utf-8\")\n .set(\"Cache-Control\", \"no-store\")\n .send(html);\n }\n\n // ── Auxiliary routes ─────────────────────────────────────────────────────\n const routes: AuthRoute[] = [];\n if (loginEnabled) {\n routes.push({\n method: \"GET\",\n path: loginPath,\n handler: (req, res) => {\n const error = (req.query?.error as string | undefined) ?? null;\n renderInlineLogin(req, res, error);\n },\n });\n routes.push({\n method: \"POST\",\n path: sessionPath,\n handler: sessionHandler,\n });\n routes.push({\n method: \"POST\",\n path: logoutPath,\n handler: logoutHandler,\n });\n }\n\n const publicPaths: (string | RegExp)[] = [\n ...(config.publicPaths ?? []),\n loginPath,\n sessionPath,\n logoutPath,\n ];\n\n // ── Middleware ───────────────────────────────────────────────────────────\n const middleware: Middleware = async (req, res, next) => {\n const path = pathOf(req);\n const method = methodOf(req);\n\n // 0. CSRF defense for cookie sessions (only active when `SameSite=None`,\n // or when `csrfProtection` is explicitly forced on — see config). With\n // the default Lax/Strict cookie the browser already blocks the session\n // cookie on cross-site state-changing requests, so this is skipped to\n // avoid breaking legitimate same-origin requests behind proxies. When\n // active it runs FIRST so it also protects the in-band\n // `?__action=logout|session` shortcut below (issues #05, #06).\n if (\n csrfProtection &&\n mode !== \"bearer\" &&\n !CSRF_SAFE_METHODS.has(method) &&\n !extractBearer(req) &&\n !isSameOriginRequest(req)\n ) {\n res\n .status(403)\n .set(\"Content-Type\", \"application/json; charset=utf-8\")\n .send(\n JSON.stringify({\n success: false,\n error: \"Origin check failed (possible CSRF)\",\n }),\n );\n return;\n }\n\n // 1. In-band action endpoints. The bundled login page posts the session\n // exchange (and logout) to the CURRENT url with `?__action=session`\n // (resp. `logout`) rather than to the mounted `/__session` route,\n // because on vanilla Cloud Functions the helper cannot reliably know\n // the function's public URL prefix to address a separate route. These\n // actions are inherently public (login/logout need no prior auth) and\n // are protected against cross-site abuse by the same-origin guard\n // above (issue #06).\n if (loginEnabled && method === \"POST\") {\n const action = queryAction(req);\n if (action === \"session\") {\n await sessionHandler(req, res);\n return;\n }\n if (action === \"logout\") {\n await logoutHandler(req, res);\n return;\n }\n }\n\n // 2. Public paths (mounted login routes, user-supplied allowlist).\n if (isPublic(path, publicPaths)) {\n await next();\n return;\n }\n\n let decoded: DecodedIdTokenLike | null = null;\n try {\n const auth = config.getAuth();\n\n // Try bearer first when allowed (cheaper, no cookie parsing).\n if (mode === \"bearer\" || mode === \"both\") {\n const token = extractBearer(req);\n if (token) {\n decoded = await auth.verifyIdToken(token, true);\n }\n }\n\n // Fall back to cookie when allowed.\n if (!decoded && (mode === \"cookie\" || mode === \"both\")) {\n const cookieHeader = req.headers?.cookie;\n const raw = Array.isArray(cookieHeader)\n ? cookieHeader.join(\"; \")\n : cookieHeader;\n const cookies = parseCookies(typeof raw === \"string\" ? raw : \"\");\n const session = cookies[cookieName];\n if (session) {\n decoded = await auth.verifySessionCookie(session, true);\n }\n }\n } catch {\n decoded = null;\n }\n\n if (!decoded) {\n rejectUnauthenticated(req, res);\n return;\n }\n\n const baseUser: Omit<AuthUser, \"context\"> = {\n uid: decoded.uid,\n email: typeof decoded.email === \"string\" ? decoded.email : null,\n emailVerified: !!decoded.email_verified,\n claims: decoded as Record<string, unknown>,\n };\n\n let context: TContext | null;\n try {\n context = config.allow\n ? await config.allow(baseUser)\n : (null as TContext | null);\n } catch {\n context = null;\n }\n\n if (config.allow && context === null) {\n rejectUnauthenticated(req, res);\n return;\n }\n\n (req as AnyReq & { user?: AuthUser<TContext> }).user = {\n ...baseUser,\n context: context as TContext,\n };\n\n await next();\n };\n\n /**\n * Reject according to the configured policy:\n * - cookie/both + GET HTML browser request → render the login page inline\n * on the SAME URL with a `401` status (works on Cloud Functions where\n * there's no separate `/__login` route reachable from the public URL).\n * A 401 keeps the response HTTP-compliant for monitoring/healthchecks\n * while browsers still render the page (issue #16).\n * - bearer mode or non-HTML clients → JSON 401.\n */\n function rejectUnauthenticated(\n req: AnyReq,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n res: any,\n ): void {\n if (\n onUnauth === \"redirect\" &&\n loginEnabled &&\n methodOf(req) === \"GET\" &&\n wantsHtml(req)\n ) {\n renderInlineLogin(req, res, null, 401);\n return;\n }\n res\n .status(401)\n .set(\"Content-Type\", \"application/json; charset=utf-8\")\n .send(JSON.stringify({ success: false, error: \"Unauthorized\" }));\n }\n\n return {\n __authExtension: true,\n middleware,\n routes,\n loginPath,\n };\n}\n\n/**\n * Type guard: detect an {@link AuthExtension} (vs. legacy\n * `BasicAuthConfig` / `Middleware`).\n */\nexport function isAuthExtension(value: unknown): value is AuthExtension {\n return (\n !!value &&\n typeof value === \"object\" &&\n (value as { __authExtension?: unknown }).__authExtension === true\n );\n}\n\n/**\n * Helper for explicitly opening a CRUD operation when the server has\n * `auth` defined (bypasses the default-deny policy).\n *\n * @example\n * ```ts\n * rules: { list: allowAll, get: allowAll }\n * ```\n */\nexport const allowAll = (): true => true;\n"]}
@@ -1,4 +1,4 @@
1
- 'use strict';var crypto=require('crypto'),zod=require('zod'),firestore=require('firebase-admin/firestore');function Pe(e){let t=[],r=e.replace(/[.*+?^${}()|[\]\\]/g,o=>o===":"?o:`\\${o}`).replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g,(o,s)=>(t.push(s),"([^/]+)"));return {pattern:new RegExp(`^${r}$`),paramNames:t}}function ve(e){let t=e.path??e.url??"/",r=t.indexOf("?");return r===-1?t:t.slice(0,r)}var W=class{constructor(){this.routes=[];this.middlewares=[];this.notFoundHandler=(t,r)=>{r.status(404).send("Not Found");};this.errorHandler=(t,r,o)=>{console.error("[MiniRouter]",t),o.status(500).send("Internal Server Error");};}use(t){return this.middlewares.push(t),this}get(t,r){return this.addRoute("GET",t,r)}post(t,r){return this.addRoute("POST",t,r)}put(t,r){return this.addRoute("PUT",t,r)}patch(t,r){return this.addRoute("PATCH",t,r)}delete(t,r){return this.addRoute("DELETE",t,r)}onNotFound(t){return this.notFoundHandler=t,this}onError(t){return this.errorHandler=t,this}addRoute(t,r,o){let{pattern:s,paramNames:i}=Pe(r);return this.routes.push({method:t.toUpperCase(),pattern:s,paramNames:i,handler:o}),this}async handle(t,r){let o=(t.method??"GET").toUpperCase(),s=ve(t),i=null,d={};for(let g of this.routes){if(g.method!==o)continue;let k=s.match(g.pattern);if(k){i=g,d={},g.paramNames.forEach((N,C)=>{d[N]=decodeURIComponent(k[C+1]??"");});break}}let y=Object.assign(t,{params:d}),h=i?i.handler:this.notFoundHandler;try{await this.runMiddlewareChain(y,r,h);}catch(g){this.errorHandler(g,t,r);}}async runMiddlewareChain(t,r,o){let s=0,i=async()=>{if(s<this.middlewares.length){let d=this.middlewares[s++];await d(t,r,i);}else await o(t,r);};await i();}};function V(e,t){let r=t==="/"?"":t.replace(/\/$/,"");if(process.env.FUNCTIONS_EMULATOR==="true"){let i=process.env.GCLOUD_PROJECT??process.env.GOOGLE_CLOUD_PROJECT??"demo-project",d=process.env.FUNCTION_REGION??"us-central1",y=(process.env.FUNCTION_TARGET??"").replace(/\./g,"-");return `/${i}/${d}/${y}${r}`}let o=process.env.K_SERVICE,s=e?.hostname??e?.headers?.host??"";return o&&typeof s=="string"&&s.includes("cloudfunctions.net")?`/${o.toLowerCase()}${r}`:r}function J(e){return !!e&&typeof e=="object"&&e.__authExtension===true}var ue="preserve";function le(){return ue}function Te(e){return typeof e=="object"&&e!==null&&typeof e._seconds=="number"&&typeof e._nanoseconds=="number"}function pe(e){if(e==null)return null;if(e instanceof Date)return Number.isNaN(e.getTime())?null:e;if(e instanceof firestore.Timestamp)return e.toDate();if(Te(e))return new Date(e._seconds*1e3+Math.floor(e._nanoseconds/1e6));if(typeof e=="string"){let t=new Date(e);return Number.isNaN(t.getTime())?null:t}if(typeof e=="number"){let t=new Date(e);return Number.isNaN(t.getTime())?null:t}return null}function fe(e){return e}var Ne=new Set(["<","<=",">",">=","!="]),$e=new Set(["array-contains","array-contains-any"]);function Y(e){return e==="desc"?"DESCENDING":"ASCENDING"}function je(e){let t=e.split("/").filter(Boolean);return t[t.length-1]??e}function Fe(e,t,r,o,s){let i=[],d=new Set;for(let h of o)if(h.op==="=="||h.op==="in"||h.op==="not-in"){if(d.has(h.field))continue;d.add(h.field),i.push({fieldPath:h.field,order:"ASCENDING"});}for(let h of o)if($e.has(h.op)){if(d.has(h.field))continue;d.add(h.field),i.push({fieldPath:h.field,arrayConfig:"CONTAINS"});}for(let h of o)if(Ne.has(h.op)){if(d.has(h.field))continue;d.add(h.field);let g=s?.field===h.field?Y(s.dir):"ASCENDING";i.push({fieldPath:h.field,order:g});}if(s&&!d.has(s.field)&&i.push({fieldPath:s.field,order:Y(s.dir)}),i.length===1&&r)return Be(e,t,i[0]);let y=s&&i.some(h=>h.fieldPath===s.field)?Y(s.dir):"ASCENDING";return i.push({fieldPath:"__name__",order:y}),_e(e,t,r,i)}function _e(e,t,r,o,s="(default)"){let i=`projects/${e}/databases/${s}/collectionGroups/${t}/indexes/_`,d=[...ne(1,i),...Z(2,r?2:1)];for(let g of o)d.push(...ge(3,me(g)));let y=s==="(default)"?"-default-":s,h=encodeURIComponent(ye(d));return `https://console.firebase.google.com/project/${e}/firestore/databases/${y}/indexes?create_composite=${h}`}function He(e){return e.match(/https:\/\/console\.firebase\.google\.com[^\s)"]*/)?.[0]}function ee(e){let t=[],r=e>>>0;for(;r>=128;)t.push(r&127|128),r>>>=7;return t.push(r&127),t}function te(e,t){return e<<3|t}function ne(e,t){let r=Array.from(new TextEncoder().encode(t));return [te(e,2),...ee(r.length),...r]}function Z(e,t){return [te(e,0),...ee(t)]}function ge(e,t){return [te(e,2),...ee(t.length),...t]}function me(e){let t=[...ne(1,e.fieldPath)];return e.arrayConfig==="CONTAINS"?t.push(...Z(3,1)):t.push(...Z(2,e.order==="DESCENDING"?2:1)),t}function ye(e){let t=String.fromCharCode(...e),r;if(typeof Buffer<"u")r=Buffer.from(e).toString("base64");else if(typeof btoa<"u")r=btoa(t);else throw new Error("No base64 encoder available");return r.replace(/=+$/,"")}function Be(e,t,r,o="(default)"){let s=`projects/${e}/databases/${o}/collectionGroups/${t}/fields/${r.fieldPath}`,i=[...ne(1,s),...Z(2,2),...ge(3,me(r))],d=o==="(default)"?"-default-":o,y=encodeURIComponent(ye(i));return `https://console.firebase.google.com/project/${e}/firestore/databases/${d}/indexes/automatic?create_exemption=${y}`}function ze(e){let t=e,r=[t?.firestore?.projectId,t?.firestore?.app?.options?.projectId,t?.firestore?._settings?.projectId,t?.firestore?.databaseId?.projectId,t?._firestore?.projectId];for(let s of r)if(typeof s=="string"&&s.length>0)return s;return process.env.GCLOUD_PROJECT||process.env.GOOGLE_CLOUD_PROJECT||process.env.FIREBASE_PROJECT_ID||void 0}function Ue(e){let t=e;return t?t.code===9?true:typeof t.message=="string"?t.message.includes("requires an index"):false:false}function he(e,t){let r=e??{},o=Ue(e),s;if(o&&(s=r.message?He(r.message):void 0,!s)){let i=ze(t.ref);if(i){let d=je(t.path);s=Fe(i,d,t.isGroup,t.filters,t.sort);}}return {type:o?"index":"error",message:o?"This query requires a composite index that does not exist yet.":r.message??"Query failed",indexUrl:s}}function se(e,t,r=200){let o=fe(t);e.status(r).set("Content-Type","application/json; charset=utf-8").send(JSON.stringify(o));}function L(e,t,r,o=200){se(e,{success:true,data:t,meta:r},o);}function R(e,t,r=400){se(e,{success:false,error:t},r);}function re(e,t,r,o,s){let i=he(t,r),d=i.type==="index",y=d?424:500,g={success:false,error:d?i.message:s&&t instanceof Error?t.message:o};d&&(g.errorType="index",i.indexUrl&&(g.indexUrl=i.indexUrl)),se(e,g,y);}var oe="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";function qe(){let e="";for(;e.length<20;){let t=crypto.randomBytes(20);for(let r=0;r<t.length&&e.length<20;r++){let o=t[r];o>=oe.length*4||(e+=oe.charAt(o%oe.length));}}return e}function q(e){let t=e._def??e.def;if(!t)return e;let r=t.typeName??t.type;if(r==="ZodDate"||r==="date")return zod.z.preprocess(o=>pe(o)??o,e);if(r==="ZodObject"||r==="object"){let o=e.shape,s={};for(let[i,d]of Object.entries(o))s[i]=q(d);return zod.z.object(s)}if(r==="ZodArray"||r==="array"){let o=t.element??t.type;if(o)return zod.z.array(q(o))}if(r==="ZodOptional"||r==="optional"){let o=t.innerType;if(o)return q(o).optional()}if(r==="ZodNullable"||r==="nullable"){let o=t.innerType;if(o)return q(o).nullable()}if(r==="ZodDefault"||r==="default"){let o=t.innerType,s=t.defaultValue;if(o){let i=q(o);return typeof s=="function"?i.default(s()):i.default(s)}}return e}function Ge(e,t,r=[]){let o=e.shape,s={},i=t&&t.length>0?t:Object.keys(o);for(let d of i){if(r.includes(d))continue;let y=d.split(".")[0];y&&o[y]&&(s[y]=o[y]);}return zod.z.object(s)}function be(e,t,r,o=false,s=[]){try{let i=Ge(e,r,s),d=o?i.partial():i;return {success:!0,data:(le()==="normalize"?q(d):d).parse(t)}}catch(i){return i instanceof zod.z.ZodError?{success:false,error:`Validation failed: ${i.issues.map(y=>`${y.path.join(".")}: ${y.message}`).join(", ")}`}:{success:false,error:"Validation failed"}}}function Me(e,t){let r=[],o=t?new Set(t):null,s={eq:"==",ne:"!=",lt:"<",lte:"<=",gt:">",gte:">=",in:"in",nin:"not-in",contains:"array-contains",containsAny:"array-contains-any"};for(let[i,d]of Object.entries(e)){if(d===void 0||["cursor","limit","pageSize","orderBy","orderDir","select"].includes(i))continue;let y=Array.isArray(d)?d[0]:d;if(y===void 0||y==="")continue;let h=i.match(/^(\w+)__(\w+)$/),g,k="==";if(h&&h[1]&&h[2]){g=h[1];let C=h[2];if(s[C])k=s[C];else continue}else if(!h)g=i;else continue;if(o&&!o.has(g))continue;let N=y;k==="in"||k==="not-in"||k==="array-contains-any"?N=y.split(",").map(C=>we(C.trim())):N=we(y),r.push({field:g,op:k,value:N});}return r}function we(e){if(e==="true")return true;if(e==="false")return false;if(e==="null")return null;let t=Number(e);return !isNaN(t)&&e!==""?t:e}function Q(e){return e?{docId:e.id}:null}async function Re(e,t){if(!t||typeof t!="object")return;let r=t.docId;if(typeof r=="string")try{let o=e.repo.ref;if(typeof o.doc!="function")return;let s=await o.doc(r).get();return s.exists?s:void 0}catch{return}}function xe(e,t,r,o=false){async function s(c,u,l,n){if(!o)return true;let f=u.rules?.[l];if(!f)return R(c,`Operation "${l}" is not allowed for this repository`,403),false;try{return await f(n)?!0:(R(c,"Forbidden",403),!1)}catch(p){let m=r&&p instanceof Error?p.message:"Forbidden";return R(c,m,403),false}}async function i(c,u,l,n){if(!o)return n;let f=c.rules?.filter;if(!f)return n;let p=[];for(let m of n)try{await f({user:u,doc:m,params:l})&&p.push(m);}catch{}return p}function d(c){return c?.user??null}function y(c,u){return !c||!e[c]?(R(u,`Repository "${c}" not found`,404),null):e[c]}function h(c,u){if(!u)return;let l=c[u];if(typeof l!="string"||!l)return;let n=l.split("/").filter(Boolean),f=[];for(let p=1;p<n.length;p+=2)f.push(n[p]);return f.length>0?f:void 0}async function g(c,u){let l=`by${c.documentKey.charAt(0).toUpperCase()}${c.documentKey.slice(1)}`,n=c.repo.get[l];if(typeof n=="function")try{let p=await n(u);if(p)return p}catch{}return (await c.repo.query.by({where:[[c.documentKey,"==",u]],limit:1}))[0]??null}async function k(c,u){let l=c.params||{},n=y(l.repoName,u);if(!n)return;let f=d(c);if(!await s(u,n,"list",{user:f,query:c.query??{},params:l}))return;let p=[],m;try{let a=c.query??{},w=Math.min(Number(a.pageSize)||n.pageSize,100),S=a.cursor,O=a.direction?.toLowerCase()==="prev"?"prev":"next",b=a.orderBy,v=a.orderDir?.toLowerCase()==="desc"?"desc":"asc",T=a.select,x=T?T.split(",").map(D=>D.trim()):void 0,A;n.allowedIncludes&&a.includes&&(A=(typeof a.includes=="string"?a.includes.split(",").map(K=>K.trim()):Array.isArray(a.includes)?a.includes:[]).filter(K=>typeof K=="string"&&n.allowedIncludes.includes(K)),A?.length===0&&(A=void 0));let P=Me(a,n.filterableFields);p=P.map(D=>({field:D.field,op:D.op,value:String(D.value??"")})),b&&(m={field:b,dir:v});let F={pageSize:w,direction:O};if(S)try{let D=typeof S=="string"?JSON.parse(S):S;F.cursor=await Re(n,D);}catch{}if(b){if(n.orderableFields&&!n.orderableFields.includes(b)){R(u,`Field not orderable: ${b}`,400);return}F.orderBy=[{field:b,direction:v}];}P.length>0&&(F.where=P.map(D=>[D.field,D.op,D.value])),x&&(F.select=x),A&&(F.include=A);let U=await n.repo.query.paginate(F),Oe={items:await i(n,d(c),l,U.data),hasNextPage:U.hasNextPage,hasPrevPage:U.hasPrevPage,nextCursor:Q(U.nextCursor),prevCursor:Q(U.prevCursor)};L(u,Oe,{pageSize:w,hasMore:U.hasNextPage});}catch(a){re(u,a,{ref:n.repo.ref,path:n.path,isGroup:!!n.isGroup,filters:p,sort:m},"Failed to fetch documents",r);}}async function N(c,u){let l=c.params||{},n=y(l.repoName,u);if(!n)return;let f=d(c);if(!await s(u,n,"list",{user:f,query:c.body??{},params:l}))return;let p=[],m;try{let a=c.body??{},w=Math.min(a.pageSize||n.pageSize,100),S=a.direction==="prev"?"prev":"next";a.where&&(p=a.where.map(x=>({field:String(x[0]),op:x[1],value:String(x[2]??"")}))),a.orderBy&&a.orderBy[0]&&(m={field:a.orderBy[0].field,dir:a.orderBy[0].direction==="desc"?"desc":"asc"});let O={pageSize:w,direction:S};if(a.cursor)try{let x=typeof a.cursor=="string"?JSON.parse(a.cursor):a.cursor;O.cursor=await Re(n,x);}catch{}if(n.allowedIncludes&&a.includes&&a.includes.length>0){let x=a.includes.filter(A=>typeof A=="string"?n.allowedIncludes.includes(A):typeof A=="object"&&A!==null&&"relation"in A&&typeof A.relation=="string"?n.allowedIncludes.includes(A.relation):!1);x.length>0&&(O.include=x);}if(a.where&&a.where.length>0){if(n.filterableFields){let x=new Set(n.filterableFields),A=a.where.filter(P=>!x.has(P[0]));if(A.length>0){R(u,`Fields not filterable: ${A.map(P=>P[0]).join(", ")}`,400);return}}O.where=a.where;}if(a.orWhere&&a.orWhere.length>0){if(n.filterableFields){let x=new Set(n.filterableFields),A=a.orWhere.filter(P=>!x.has(P[0]));if(A.length>0){R(u,`Fields not filterable: ${A.map(P=>P[0]).join(", ")}`,400);return}}O.orWhere=a.orWhere;}if(a.orWhereGroups&&a.orWhereGroups.length>0){if(n.filterableFields){let x=new Set(n.filterableFields);for(let A of a.orWhereGroups){let P=A.filter(F=>!x.has(F[0]));if(P.length>0){R(u,`Fields not filterable: ${P.map(F=>F[0]).join(", ")}`,400);return}}}O.orWhereGroups=a.orWhereGroups;}if(a.orderBy&&a.orderBy.length>0){if(n.orderableFields){let x=new Set(n.orderableFields),A=a.orderBy.filter(P=>!x.has(P.field));if(A.length>0){R(u,`Fields not orderable: ${A.map(P=>P.field).join(", ")}`,400);return}}O.orderBy=a.orderBy;}a.select&&a.select.length>0&&(O.select=a.select);let b=await n.repo.query.paginate(O),T={items:await i(n,d(c),l,b.data),hasNextPage:b.hasNextPage,hasPrevPage:b.hasPrevPage,nextCursor:Q(b.nextCursor),prevCursor:Q(b.prevCursor)};L(u,T,{pageSize:w,hasMore:b.hasNextPage});}catch(a){re(u,a,{ref:n.repo.ref,path:n.path,isGroup:!!n.isGroup,filters:p,sort:m},"Failed to query documents",r);}}async function C(c,u){let l=c.params||{},n=y(l.repoName,u);if(!n)return;let f=l.id;if(!f){R(u,"Document ID required",400);return}try{let p=await g(n,f);if(!p){R(u,"Document not found",404);return}let m=d(c);if(!await s(u,n,"get",{user:m,doc:p,params:l}))return;if(o&&n.rules?.filter)try{if(!await n.rules.filter({user:m,doc:p,params:l})){R(u,"Document not found",404);return}}catch{R(u,"Document not found",404);return}L(u,p);}catch(p){re(u,p,{ref:n.repo.ref,path:n.path,isGroup:!!n.isGroup,filters:[{field:n.documentKey,op:"==",value:f}]},"Failed to fetch document",r);}}async function $(c,u){let l=c.params||{},n=y(l.repoName,u);if(n)try{let f=c.body??{},p=d(c);if(!await s(u,n,"create",{user:p,body:f,params:l}))return;let m=be(n.schema,f,n.createFields,!1,n.systemKeys);if(!m.success){R(u,m.error,400);return}if(n.validate){let w=await n.validate(m.data,"create");if(w){R(u,w,400);return}}let a;if(n.isGroup&&n.parentKeys&&n.parentKeys.length>0){let w={...m.data};n.createdKey&&(w[n.createdKey]=new Date);let S=n.parentKeys.filter(v=>!w[v]);if(S.length>0){R(u,`Missing parent key(s) for subcollection create: ${S.join(", ")}`,400);return}let O=n.parentKeys.map(v=>w[v]),b=w[n.documentKey]||qe();a=await n.repo.set(...O,b,w);}else a=await n.repo.create(m.data);L(u,a,void 0,201);}catch(f){let p=r&&f instanceof Error?f.message:"Failed to create document";R(u,p,500);}}async function z(c,u,l){let n=c.params||{},f=y(n.repoName,u);if(!f)return;let p=n.id;if(!p){R(u,"Document ID required",400);return}try{let m=c.body??{},a=await g(f,p);if(!a){R(u,"Document not found",404);return}let w=d(c);if(!await s(u,f,"update",{user:w,doc:a,body:m,params:n}))return;let S=be(f.schema,m,f.mutableFields,l,f.systemKeys);if(!S.success){R(u,S.error,400);return}if(f.validate){let v=await f.validate(S.data,"update");if(v){R(u,v,400);return}}let O=h(a,f.pathKey)??[p],b=await f.repo.update(...O,S.data);L(u,b);}catch(m){let a=r&&m instanceof Error?m.message:"Failed to update document";R(u,a,500);}}async function H(c,u){let l=c.params||{},n=y(l.repoName,u);if(!n)return;if(!n.allowDelete){R(u,"Delete not allowed for this repository",403);return}let f=l.id;if(!f){R(u,"Document ID required",400);return}try{let p=await g(n,f);if(!p){R(u,"Document not found",404);return}let m=d(c);if(!await s(u,n,"delete",{user:m,doc:p,params:l}))return;let a=h(p,n.pathKey)??[f];await n.repo.delete(...a),L(u,{deleted:!0});}catch(p){let m=r&&p instanceof Error?p.message:"Failed to delete document";R(u,m,500);}}function I(c,u){u.status(204).set("Access-Control-Allow-Methods","GET, POST, PUT, PATCH, DELETE, OPTIONS").set("Access-Control-Allow-Headers","Content-Type, Authorization").set("Access-Control-Max-Age","86400").send("");}return {handleList:k,handleQuery:N,handleGet:C,handleCreate:$,handleUpdate:z,handleDelete:H,handleOptions:I}}function ie(e){try{return zod.z.toJSONSchema(e,{target:"openapi-3.1",unrepresentable:"any",override:t=>{let r=t.zodSchema?._zod?.def;r&&(r.type==="date"?(t.jsonSchema.type="string",t.jsonSchema.format="date-time"):r.type==="bigint"&&(t.jsonSchema.type="string",t.jsonSchema.format="int64"));}})}catch(t){return typeof console<"u"&&console.warn&&console.warn("[generateOpenAPISpec] Failed to convert Zod schema to JSON Schema; falling back to {type:object}.",t),{type:"object"}}}function j(e){return {$ref:`#/components/schemas/${e}`}}function E(e){return {description:e,content:{"application/json":{schema:j("ErrorResponse")}}}}function M(e,t){return {description:e,content:{"application/json":{schema:{type:"object",properties:{success:{type:"boolean",enum:[true]},data:t},required:["success","data"]}}}}}function Ae(e){return {description:"Paginated list of documents",content:{"application/json":{schema:{type:"object",properties:{success:{type:"boolean",enum:[true]},data:{type:"object",properties:{items:{type:"array",items:e},nextCursor:{oneOf:[{type:"object"},{type:"null"}]},prevCursor:{oneOf:[{type:"object"},{type:"null"}]},hasNextPage:{type:"boolean"},hasPrevPage:{type:"boolean"}},required:["items","hasNextPage","hasPrevPage"]},meta:{type:"object",properties:{pageSize:{type:"integer"},hasMore:{type:"boolean"},cursor:{oneOf:[{type:"string"},{type:"null"}]}}}},required:["success","data"]}}}}}function Ke(e){return [{name:"pageSize",in:"query",schema:{type:"integer",default:e.pageSize,maximum:100},description:"Number of items per page"},{name:"cursor",in:"query",schema:{type:"string"},description:"Base64 pagination cursor"},{name:"orderBy",in:"query",schema:{type:"string"},description:"Field name to order by"},{name:"orderDir",in:"query",schema:{type:"string",enum:["asc","desc"]},description:"Order direction"},{name:"select",in:"query",schema:{type:"string"},description:"Comma-separated list of fields to return"}]}function We(e){let t=e.filterableFields??Object.keys(e.schema.shape),r=["eq","ne","lt","lte","gt","gte","in","nin","contains"],o=[];for(let s of t){o.push({name:s,in:"query",schema:{type:"string"},description:`Filter by ${s} (equality)`});for(let i of r)o.push({name:`${s}__${i}`,in:"query",schema:{type:"string"},description:`Filter ${s} with operator ${i}`});}return o}function Je(){return {type:"object",properties:{where:{type:"array",items:{type:"array",items:{},minItems:3,maxItems:3},description:"AND conditions: [field, operator, value][]"},orWhere:{type:"array",items:{type:"array",items:{},minItems:3,maxItems:3},description:"Simple OR conditions (each independently OR'd)"},orWhereGroups:{type:"array",items:{type:"array",items:{type:"array",items:{},minItems:3,maxItems:3}},description:"Advanced OR groups (AND within, OR across groups)"},orderBy:{type:"array",items:{type:"object",properties:{field:{type:"string"},direction:{type:"string",enum:["asc","desc"]}},required:["field"]}},select:{type:"array",items:{type:"string"},description:"Fields to select (projection)"},pageSize:{type:"integer",maximum:100,description:"Number of items per page"},cursor:{oneOf:[{type:"string"},{type:"object"}],description:"Pagination cursor"},direction:{type:"string",enum:["next","prev"],description:"Pagination direction"},includes:{type:"array",items:{oneOf:[{type:"string"},{type:"object",properties:{relation:{type:"string"},select:{type:"array",items:{type:"string"}}},required:["relation"]}]},description:"Relations to include (populate)"}}}}function Ze(e,t,r,o,s){let i={},d=e.name,y=`${t}/${e.name}`,h=`${y}/{${e.documentKey}}`,g={name:e.documentKey,in:"path",required:true,schema:{type:"string"},description:"Unique document identifier"};i[y]={get:{operationId:`list${B(e.name)}`,summary:`List ${e.name} (paginated)`,tags:[d],parameters:[...Ke(e),...We(e)],responses:{200:Ae(j(r)),500:E("Internal server error")}},post:{operationId:`create${B(e.name)}`,summary:`Create a ${_(e.name)}`,tags:[d],requestBody:{required:true,content:{"application/json":{schema:j(o??r)}}},responses:{201:M("Document created",j(r)),400:E("Validation error"),500:E("Internal server error")}}},i[`${y}/query`]={post:{operationId:`query${B(e.name)}`,summary:`Query ${e.name} with advanced filters`,tags:[d],requestBody:{required:true,content:{"application/json":{schema:j("QueryRequestBody")}}},responses:{200:Ae(j(r)),400:E("Invalid query"),500:E("Internal server error")}}};let k={};return k.get={operationId:`get${B(_(e.name))}`,summary:`Get a single ${_(e.name)}`,tags:[d],parameters:[g],responses:{200:M("Document found",j(r)),404:E("Document not found"),500:E("Internal server error")}},k.put={operationId:`update${B(_(e.name))}`,summary:`Update a ${_(e.name)} (full replace)`,tags:[d],parameters:[g],requestBody:{required:true,content:{"application/json":{schema:j(s??r)}}},responses:{200:M("Document updated",j(r)),400:E("Validation error"),404:E("Document not found"),500:E("Internal server error")}},k.patch={operationId:`patch${B(_(e.name))}`,summary:`Partially update a ${_(e.name)}`,tags:[d],parameters:[g],requestBody:{required:true,content:{"application/json":{schema:{allOf:[j(s??r)],description:"All fields are optional for partial updates"}}}},responses:{200:M("Document patched",j(r)),400:E("Validation error"),404:E("Document not found"),500:E("Internal server error")}},e.allowDelete&&(k.delete={operationId:`delete${B(_(e.name))}`,summary:`Delete a ${_(e.name)}`,tags:[d],parameters:[g],responses:{200:M("Document deleted",{type:"object",properties:{id:{type:"string"}}}),404:E("Document not found"),500:E("Internal server error")}}),i[h]=k,i}function ce(e,t,r={}){let{title:o="CRUD API",version:s="1.0.0",description:i,servers:d,auth:y=false}=r,h=t==="/"?"":t.replace(/\/$/,""),g={},k={},N=[];g.ErrorResponse={type:"object",properties:{success:{type:"boolean",enum:[false]},error:{type:"string"}},required:["success","error"]},g.QueryRequestBody=Je();for(let[H,I]of Object.entries(e)){let c=B(_(H)),u=`${c}Create`,l=`${c}Update`;g[c]=ie(I.schema);let n=S=>{let O=S&&S.length>0?S:Object.keys(I.schema.shape),b={};for(let v of O){let T=v.split(".")[0];T&&I.schema.shape[T]&&!I.systemKeys.includes(T)&&(b[T]=I.schema.shape[T]);}return b},f=null,p=n(I.createFields);Object.keys(p).length>0&&(g[u]=ie(zod.z.object(p)),f=u);let m=null,a=n(I.mutableFields);Object.keys(a).length>0&&(g[l]=ie(zod.z.object(a)),m=l);let w=Ze(I,h,c,f,m);Object.assign(k,w),N.push({name:H,description:`Operations on ${H} (collection: ${I.path})`});}let C={},$;return y==="basic"?(C.basicAuth={type:"http",scheme:"basic"},$=[{basicAuth:[]}]):y==="bearer"&&(C.bearerAuth={type:"http",scheme:"bearer",bearerFormat:"JWT"},$=[{bearerAuth:[]}]),{openapi:"3.1.0",info:{title:o,version:s,...i?{description:i}:{}},...d&&d.length>0?{servers:d}:{},paths:k,components:{schemas:g,...Object.keys(C).length>0?{securitySchemes:C}:{}},...$?{security:$}:{},tags:N}}function B(e){return e.charAt(0).toUpperCase()+e.slice(1)}function _(e){return e.endsWith("ies")?e.slice(0,-3)+"y":e.endsWith("ses")||e.endsWith("xes")||e.endsWith("zes")?e.slice(0,-2):e.endsWith("s")&&!e.endsWith("ss")?e.slice(0,-1):e}var ke="https://cdn.jsdelivr.net",Ce="https://www.gstatic.com",Qe=["default-src 'self'",`script-src 'self' 'unsafe-inline' 'unsafe-eval' ${ke} ${Ce}`,`style-src 'self' 'unsafe-inline' ${ke}`,`connect-src 'self' ${Ce} https://*.googleapis.com https://*.firebaseio.com https://identitytoolkit.googleapis.com`,"img-src 'self' data:","font-src 'self' data:","frame-ancestors 'none'","base-uri 'self'","form-action 'self'"].join("; ");function Se(e={}){let t=e.csp===void 0?Qe:e.csp,r=e.frameOptions===void 0?"DENY":e.frameOptions,o=e.referrerPolicy===void 0?"no-referrer":e.referrerPolicy,s=e.cacheControl===void 0?"private, no-store":e.cacheControl;return async(i,d,y)=>{typeof d?.setHeader=="function"&&(d.setHeader("X-Content-Type-Options","nosniff"),r&&d.setHeader("X-Frame-Options",r),o&&d.setHeader("Referrer-Policy",o),s&&d.setHeader("Cache-Control",s),t&&d.setHeader("Content-Security-Policy",t)),await y();}}function Ve(e,t){return `<!DOCTYPE html>
1
+ 'use strict';var crypto=require('crypto'),zod=require('zod'),firestore=require('firebase-admin/firestore');function Pe(e){let t=[],r=e.replace(/[.*+?^${}()|[\]\\]/g,o=>o===":"?o:`\\${o}`).replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g,(o,s)=>(t.push(s),"([^/]+)"));return {pattern:new RegExp(`^${r}$`),paramNames:t}}function ve(e){let t=e.path??e.url??"/",r=t.indexOf("?");return r===-1?t:t.slice(0,r)}var W=class{constructor(){this.routes=[];this.middlewares=[];this.notFoundHandler=(t,r)=>{r.status(404).send("Not Found");};this.errorHandler=(t,r,o)=>{console.error("[MiniRouter]",t),o.status(500).send("Internal Server Error");};}use(t){return this.middlewares.push(t),this}get(t,r){return this.addRoute("GET",t,r)}post(t,r){return this.addRoute("POST",t,r)}put(t,r){return this.addRoute("PUT",t,r)}patch(t,r){return this.addRoute("PATCH",t,r)}delete(t,r){return this.addRoute("DELETE",t,r)}onNotFound(t){return this.notFoundHandler=t,this}onError(t){return this.errorHandler=t,this}addRoute(t,r,o){let{pattern:s,paramNames:i}=Pe(r);return this.routes.push({method:t.toUpperCase(),pattern:s,paramNames:i,handler:o}),this}async handle(t,r){let o=(t.method??"GET").toUpperCase(),s=ve(t),i=null,d={};for(let g of this.routes){if(g.method!==o)continue;let k=s.match(g.pattern);if(k){i=g,d={},g.paramNames.forEach((N,C)=>{d[N]=decodeURIComponent(k[C+1]??"");});break}}let y=Object.assign(t,{params:d}),h=i?i.handler:this.notFoundHandler;try{await this.runMiddlewareChain(y,r,h);}catch(g){this.errorHandler(g,t,r);}}async runMiddlewareChain(t,r,o){let s=0,i=async()=>{if(s<this.middlewares.length){let d=this.middlewares[s++];await d(t,r,i);}else await o(t,r);};await i();}};function V(e,t,r){let o=t==="/"?"":t.replace(/\/$/,"");if(process.env.FUNCTIONS_EMULATOR==="true"){let d=process.env.GCLOUD_PROJECT??process.env.GOOGLE_CLOUD_PROJECT??"demo-project",y=process.env.FUNCTION_REGION??"us-central1",h=(process.env.FUNCTION_TARGET??"").replace(/\./g,"-");return `/${d}/${y}/${h}${o}`}let s=process.env.K_SERVICE,i=e?.hostname??e?.headers?.host??"";return s&&typeof i=="string"&&i.includes("cloudfunctions.net")?`/${s.toLowerCase()}${o}`:o}function J(e){return !!e&&typeof e=="object"&&e.__authExtension===true}var ue="preserve";function le(){return ue}function Te(e){return typeof e=="object"&&e!==null&&typeof e._seconds=="number"&&typeof e._nanoseconds=="number"}function pe(e){if(e==null)return null;if(e instanceof Date)return Number.isNaN(e.getTime())?null:e;if(e instanceof firestore.Timestamp)return e.toDate();if(Te(e))return new Date(e._seconds*1e3+Math.floor(e._nanoseconds/1e6));if(typeof e=="string"){let t=new Date(e);return Number.isNaN(t.getTime())?null:t}if(typeof e=="number"){let t=new Date(e);return Number.isNaN(t.getTime())?null:t}return null}function fe(e){return e}var Ne=new Set(["<","<=",">",">=","!="]),$e=new Set(["array-contains","array-contains-any"]);function Y(e){return e==="desc"?"DESCENDING":"ASCENDING"}function je(e){let t=e.split("/").filter(Boolean);return t[t.length-1]??e}function Fe(e,t,r,o,s){let i=[],d=new Set;for(let h of o)if(h.op==="=="||h.op==="in"||h.op==="not-in"){if(d.has(h.field))continue;d.add(h.field),i.push({fieldPath:h.field,order:"ASCENDING"});}for(let h of o)if($e.has(h.op)){if(d.has(h.field))continue;d.add(h.field),i.push({fieldPath:h.field,arrayConfig:"CONTAINS"});}for(let h of o)if(Ne.has(h.op)){if(d.has(h.field))continue;d.add(h.field);let g=s?.field===h.field?Y(s.dir):"ASCENDING";i.push({fieldPath:h.field,order:g});}if(s&&!d.has(s.field)&&i.push({fieldPath:s.field,order:Y(s.dir)}),i.length===1&&r)return Be(e,t,i[0]);let y=s&&i.some(h=>h.fieldPath===s.field)?Y(s.dir):"ASCENDING";return i.push({fieldPath:"__name__",order:y}),_e(e,t,r,i)}function _e(e,t,r,o,s="(default)"){let i=`projects/${e}/databases/${s}/collectionGroups/${t}/indexes/_`,d=[...ne(1,i),...Z(2,r?2:1)];for(let g of o)d.push(...ge(3,me(g)));let y=s==="(default)"?"-default-":s,h=encodeURIComponent(ye(d));return `https://console.firebase.google.com/project/${e}/firestore/databases/${y}/indexes?create_composite=${h}`}function He(e){return e.match(/https:\/\/console\.firebase\.google\.com[^\s)"]*/)?.[0]}function ee(e){let t=[],r=e>>>0;for(;r>=128;)t.push(r&127|128),r>>>=7;return t.push(r&127),t}function te(e,t){return e<<3|t}function ne(e,t){let r=Array.from(new TextEncoder().encode(t));return [te(e,2),...ee(r.length),...r]}function Z(e,t){return [te(e,0),...ee(t)]}function ge(e,t){return [te(e,2),...ee(t.length),...t]}function me(e){let t=[...ne(1,e.fieldPath)];return e.arrayConfig==="CONTAINS"?t.push(...Z(3,1)):t.push(...Z(2,e.order==="DESCENDING"?2:1)),t}function ye(e){let t=String.fromCharCode(...e),r;if(typeof Buffer<"u")r=Buffer.from(e).toString("base64");else if(typeof btoa<"u")r=btoa(t);else throw new Error("No base64 encoder available");return r.replace(/=+$/,"")}function Be(e,t,r,o="(default)"){let s=`projects/${e}/databases/${o}/collectionGroups/${t}/fields/${r.fieldPath}`,i=[...ne(1,s),...Z(2,2),...ge(3,me(r))],d=o==="(default)"?"-default-":o,y=encodeURIComponent(ye(i));return `https://console.firebase.google.com/project/${e}/firestore/databases/${d}/indexes/automatic?create_exemption=${y}`}function ze(e){let t=e,r=[t?.firestore?.projectId,t?.firestore?.app?.options?.projectId,t?.firestore?._settings?.projectId,t?.firestore?.databaseId?.projectId,t?._firestore?.projectId];for(let s of r)if(typeof s=="string"&&s.length>0)return s;return process.env.GCLOUD_PROJECT||process.env.GOOGLE_CLOUD_PROJECT||process.env.FIREBASE_PROJECT_ID||void 0}function Ue(e){let t=e;return t?t.code===9?true:typeof t.message=="string"?t.message.includes("requires an index"):false:false}function he(e,t){let r=e??{},o=Ue(e),s;if(o&&(s=r.message?He(r.message):void 0,!s)){let i=ze(t.ref);if(i){let d=je(t.path);s=Fe(i,d,t.isGroup,t.filters,t.sort);}}return {type:o?"index":"error",message:o?"This query requires a composite index that does not exist yet.":r.message??"Query failed",indexUrl:s}}function se(e,t,r=200){let o=fe(t);e.status(r).set("Content-Type","application/json; charset=utf-8").send(JSON.stringify(o));}function L(e,t,r,o=200){se(e,{success:true,data:t,meta:r},o);}function R(e,t,r=400){se(e,{success:false,error:t},r);}function re(e,t,r,o,s){let i=he(t,r),d=i.type==="index",y=d?424:500,g={success:false,error:d?i.message:s&&t instanceof Error?t.message:o};d&&(g.errorType="index",i.indexUrl&&(g.indexUrl=i.indexUrl)),se(e,g,y);}var oe="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";function qe(){let e="";for(;e.length<20;){let t=crypto.randomBytes(20);for(let r=0;r<t.length&&e.length<20;r++){let o=t[r];o>=oe.length*4||(e+=oe.charAt(o%oe.length));}}return e}function q(e){let t=e._def??e.def;if(!t)return e;let r=t.typeName??t.type;if(r==="ZodDate"||r==="date")return zod.z.preprocess(o=>pe(o)??o,e);if(r==="ZodObject"||r==="object"){let o=e.shape,s={};for(let[i,d]of Object.entries(o))s[i]=q(d);return zod.z.object(s)}if(r==="ZodArray"||r==="array"){let o=t.element??t.type;if(o)return zod.z.array(q(o))}if(r==="ZodOptional"||r==="optional"){let o=t.innerType;if(o)return q(o).optional()}if(r==="ZodNullable"||r==="nullable"){let o=t.innerType;if(o)return q(o).nullable()}if(r==="ZodDefault"||r==="default"){let o=t.innerType,s=t.defaultValue;if(o){let i=q(o);return typeof s=="function"?i.default(s()):i.default(s)}}return e}function Ge(e,t,r=[]){let o=e.shape,s={},i=t&&t.length>0?t:Object.keys(o);for(let d of i){if(r.includes(d))continue;let y=d.split(".")[0];y&&o[y]&&(s[y]=o[y]);}return zod.z.object(s)}function be(e,t,r,o=false,s=[]){try{let i=Ge(e,r,s),d=o?i.partial():i;return {success:!0,data:(le()==="normalize"?q(d):d).parse(t)}}catch(i){return i instanceof zod.z.ZodError?{success:false,error:`Validation failed: ${i.issues.map(y=>`${y.path.join(".")}: ${y.message}`).join(", ")}`}:{success:false,error:"Validation failed"}}}function Me(e,t){let r=[],o=t?new Set(t):null,s={eq:"==",ne:"!=",lt:"<",lte:"<=",gt:">",gte:">=",in:"in",nin:"not-in",contains:"array-contains",containsAny:"array-contains-any"};for(let[i,d]of Object.entries(e)){if(d===void 0||["cursor","limit","pageSize","orderBy","orderDir","select"].includes(i))continue;let y=Array.isArray(d)?d[0]:d;if(y===void 0||y==="")continue;let h=i.match(/^(\w+)__(\w+)$/),g,k="==";if(h&&h[1]&&h[2]){g=h[1];let C=h[2];if(s[C])k=s[C];else continue}else if(!h)g=i;else continue;if(o&&!o.has(g))continue;let N=y;k==="in"||k==="not-in"||k==="array-contains-any"?N=y.split(",").map(C=>we(C.trim())):N=we(y),r.push({field:g,op:k,value:N});}return r}function we(e){if(e==="true")return true;if(e==="false")return false;if(e==="null")return null;let t=Number(e);return !isNaN(t)&&e!==""?t:e}function Q(e){return e?{docId:e.id}:null}async function Re(e,t){if(!t||typeof t!="object")return;let r=t.docId;if(typeof r=="string")try{let o=e.repo.ref;if(typeof o.doc!="function")return;let s=await o.doc(r).get();return s.exists?s:void 0}catch{return}}function xe(e,t,r,o=false){async function s(c,u,l,n){if(!o)return true;let f=u.rules?.[l];if(!f)return R(c,`Operation "${l}" is not allowed for this repository`,403),false;try{return await f(n)?!0:(R(c,"Forbidden",403),!1)}catch(p){let m=r&&p instanceof Error?p.message:"Forbidden";return R(c,m,403),false}}async function i(c,u,l,n){if(!o)return n;let f=c.rules?.filter;if(!f)return n;let p=[];for(let m of n)try{await f({user:u,doc:m,params:l})&&p.push(m);}catch{}return p}function d(c){return c?.user??null}function y(c,u){return !c||!e[c]?(R(u,`Repository "${c}" not found`,404),null):e[c]}function h(c,u){if(!u)return;let l=c[u];if(typeof l!="string"||!l)return;let n=l.split("/").filter(Boolean),f=[];for(let p=1;p<n.length;p+=2)f.push(n[p]);return f.length>0?f:void 0}async function g(c,u){let l=`by${c.documentKey.charAt(0).toUpperCase()}${c.documentKey.slice(1)}`,n=c.repo.get[l];if(typeof n=="function")try{let p=await n(u);if(p)return p}catch{}return (await c.repo.query.by({where:[[c.documentKey,"==",u]],limit:1}))[0]??null}async function k(c,u){let l=c.params||{},n=y(l.repoName,u);if(!n)return;let f=d(c);if(!await s(u,n,"list",{user:f,query:c.query??{},params:l}))return;let p=[],m;try{let a=c.query??{},w=Math.min(Number(a.pageSize)||n.pageSize,100),S=a.cursor,O=a.direction?.toLowerCase()==="prev"?"prev":"next",b=a.orderBy,v=a.orderDir?.toLowerCase()==="desc"?"desc":"asc",T=a.select,x=T?T.split(",").map(D=>D.trim()):void 0,A;n.allowedIncludes&&a.includes&&(A=(typeof a.includes=="string"?a.includes.split(",").map(K=>K.trim()):Array.isArray(a.includes)?a.includes:[]).filter(K=>typeof K=="string"&&n.allowedIncludes.includes(K)),A?.length===0&&(A=void 0));let P=Me(a,n.filterableFields);p=P.map(D=>({field:D.field,op:D.op,value:String(D.value??"")})),b&&(m={field:b,dir:v});let F={pageSize:w,direction:O};if(S)try{let D=typeof S=="string"?JSON.parse(S):S;F.cursor=await Re(n,D);}catch{}if(b){if(n.orderableFields&&!n.orderableFields.includes(b)){R(u,`Field not orderable: ${b}`,400);return}F.orderBy=[{field:b,direction:v}];}P.length>0&&(F.where=P.map(D=>[D.field,D.op,D.value])),x&&(F.select=x),A&&(F.include=A);let U=await n.repo.query.paginate(F),Oe={items:await i(n,d(c),l,U.data),hasNextPage:U.hasNextPage,hasPrevPage:U.hasPrevPage,nextCursor:Q(U.nextCursor),prevCursor:Q(U.prevCursor)};L(u,Oe,{pageSize:w,hasMore:U.hasNextPage});}catch(a){re(u,a,{ref:n.repo.ref,path:n.path,isGroup:!!n.isGroup,filters:p,sort:m},"Failed to fetch documents",r);}}async function N(c,u){let l=c.params||{},n=y(l.repoName,u);if(!n)return;let f=d(c);if(!await s(u,n,"list",{user:f,query:c.body??{},params:l}))return;let p=[],m;try{let a=c.body??{},w=Math.min(a.pageSize||n.pageSize,100),S=a.direction==="prev"?"prev":"next";a.where&&(p=a.where.map(x=>({field:String(x[0]),op:x[1],value:String(x[2]??"")}))),a.orderBy&&a.orderBy[0]&&(m={field:a.orderBy[0].field,dir:a.orderBy[0].direction==="desc"?"desc":"asc"});let O={pageSize:w,direction:S};if(a.cursor)try{let x=typeof a.cursor=="string"?JSON.parse(a.cursor):a.cursor;O.cursor=await Re(n,x);}catch{}if(n.allowedIncludes&&a.includes&&a.includes.length>0){let x=a.includes.filter(A=>typeof A=="string"?n.allowedIncludes.includes(A):typeof A=="object"&&A!==null&&"relation"in A&&typeof A.relation=="string"?n.allowedIncludes.includes(A.relation):!1);x.length>0&&(O.include=x);}if(a.where&&a.where.length>0){if(n.filterableFields){let x=new Set(n.filterableFields),A=a.where.filter(P=>!x.has(P[0]));if(A.length>0){R(u,`Fields not filterable: ${A.map(P=>P[0]).join(", ")}`,400);return}}O.where=a.where;}if(a.orWhere&&a.orWhere.length>0){if(n.filterableFields){let x=new Set(n.filterableFields),A=a.orWhere.filter(P=>!x.has(P[0]));if(A.length>0){R(u,`Fields not filterable: ${A.map(P=>P[0]).join(", ")}`,400);return}}O.orWhere=a.orWhere;}if(a.orWhereGroups&&a.orWhereGroups.length>0){if(n.filterableFields){let x=new Set(n.filterableFields);for(let A of a.orWhereGroups){let P=A.filter(F=>!x.has(F[0]));if(P.length>0){R(u,`Fields not filterable: ${P.map(F=>F[0]).join(", ")}`,400);return}}}O.orWhereGroups=a.orWhereGroups;}if(a.orderBy&&a.orderBy.length>0){if(n.orderableFields){let x=new Set(n.orderableFields),A=a.orderBy.filter(P=>!x.has(P.field));if(A.length>0){R(u,`Fields not orderable: ${A.map(P=>P.field).join(", ")}`,400);return}}O.orderBy=a.orderBy;}a.select&&a.select.length>0&&(O.select=a.select);let b=await n.repo.query.paginate(O),T={items:await i(n,d(c),l,b.data),hasNextPage:b.hasNextPage,hasPrevPage:b.hasPrevPage,nextCursor:Q(b.nextCursor),prevCursor:Q(b.prevCursor)};L(u,T,{pageSize:w,hasMore:b.hasNextPage});}catch(a){re(u,a,{ref:n.repo.ref,path:n.path,isGroup:!!n.isGroup,filters:p,sort:m},"Failed to query documents",r);}}async function C(c,u){let l=c.params||{},n=y(l.repoName,u);if(!n)return;let f=l.id;if(!f){R(u,"Document ID required",400);return}try{let p=await g(n,f);if(!p){R(u,"Document not found",404);return}let m=d(c);if(!await s(u,n,"get",{user:m,doc:p,params:l}))return;if(o&&n.rules?.filter)try{if(!await n.rules.filter({user:m,doc:p,params:l})){R(u,"Document not found",404);return}}catch{R(u,"Document not found",404);return}L(u,p);}catch(p){re(u,p,{ref:n.repo.ref,path:n.path,isGroup:!!n.isGroup,filters:[{field:n.documentKey,op:"==",value:f}]},"Failed to fetch document",r);}}async function $(c,u){let l=c.params||{},n=y(l.repoName,u);if(n)try{let f=c.body??{},p=d(c);if(!await s(u,n,"create",{user:p,body:f,params:l}))return;let m=be(n.schema,f,n.createFields,!1,n.systemKeys);if(!m.success){R(u,m.error,400);return}if(n.validate){let w=await n.validate(m.data,"create");if(w){R(u,w,400);return}}let a;if(n.isGroup&&n.parentKeys&&n.parentKeys.length>0){let w={...m.data};n.createdKey&&(w[n.createdKey]=new Date);let S=n.parentKeys.filter(v=>!w[v]);if(S.length>0){R(u,`Missing parent key(s) for subcollection create: ${S.join(", ")}`,400);return}let O=n.parentKeys.map(v=>w[v]),b=w[n.documentKey]||qe();a=await n.repo.set(...O,b,w);}else a=await n.repo.create(m.data);L(u,a,void 0,201);}catch(f){let p=r&&f instanceof Error?f.message:"Failed to create document";R(u,p,500);}}async function z(c,u,l){let n=c.params||{},f=y(n.repoName,u);if(!f)return;let p=n.id;if(!p){R(u,"Document ID required",400);return}try{let m=c.body??{},a=await g(f,p);if(!a){R(u,"Document not found",404);return}let w=d(c);if(!await s(u,f,"update",{user:w,doc:a,body:m,params:n}))return;let S=be(f.schema,m,f.mutableFields,l,f.systemKeys);if(!S.success){R(u,S.error,400);return}if(f.validate){let v=await f.validate(S.data,"update");if(v){R(u,v,400);return}}let O=h(a,f.pathKey)??[p],b=await f.repo.update(...O,S.data);L(u,b);}catch(m){let a=r&&m instanceof Error?m.message:"Failed to update document";R(u,a,500);}}async function H(c,u){let l=c.params||{},n=y(l.repoName,u);if(!n)return;if(!n.allowDelete){R(u,"Delete not allowed for this repository",403);return}let f=l.id;if(!f){R(u,"Document ID required",400);return}try{let p=await g(n,f);if(!p){R(u,"Document not found",404);return}let m=d(c);if(!await s(u,n,"delete",{user:m,doc:p,params:l}))return;let a=h(p,n.pathKey)??[f];await n.repo.delete(...a),L(u,{deleted:!0});}catch(p){let m=r&&p instanceof Error?p.message:"Failed to delete document";R(u,m,500);}}function I(c,u){u.status(204).set("Access-Control-Allow-Methods","GET, POST, PUT, PATCH, DELETE, OPTIONS").set("Access-Control-Allow-Headers","Content-Type, Authorization").set("Access-Control-Max-Age","86400").send("");}return {handleList:k,handleQuery:N,handleGet:C,handleCreate:$,handleUpdate:z,handleDelete:H,handleOptions:I}}function ie(e){try{return zod.z.toJSONSchema(e,{target:"openapi-3.1",unrepresentable:"any",override:t=>{let r=t.zodSchema?._zod?.def;r&&(r.type==="date"?(t.jsonSchema.type="string",t.jsonSchema.format="date-time"):r.type==="bigint"&&(t.jsonSchema.type="string",t.jsonSchema.format="int64"));}})}catch(t){return typeof console<"u"&&console.warn&&console.warn("[generateOpenAPISpec] Failed to convert Zod schema to JSON Schema; falling back to {type:object}.",t),{type:"object"}}}function j(e){return {$ref:`#/components/schemas/${e}`}}function E(e){return {description:e,content:{"application/json":{schema:j("ErrorResponse")}}}}function M(e,t){return {description:e,content:{"application/json":{schema:{type:"object",properties:{success:{type:"boolean",enum:[true]},data:t},required:["success","data"]}}}}}function Ae(e){return {description:"Paginated list of documents",content:{"application/json":{schema:{type:"object",properties:{success:{type:"boolean",enum:[true]},data:{type:"object",properties:{items:{type:"array",items:e},nextCursor:{oneOf:[{type:"object"},{type:"null"}]},prevCursor:{oneOf:[{type:"object"},{type:"null"}]},hasNextPage:{type:"boolean"},hasPrevPage:{type:"boolean"}},required:["items","hasNextPage","hasPrevPage"]},meta:{type:"object",properties:{pageSize:{type:"integer"},hasMore:{type:"boolean"},cursor:{oneOf:[{type:"string"},{type:"null"}]}}}},required:["success","data"]}}}}}function Ke(e){return [{name:"pageSize",in:"query",schema:{type:"integer",default:e.pageSize,maximum:100},description:"Number of items per page"},{name:"cursor",in:"query",schema:{type:"string"},description:"Base64 pagination cursor"},{name:"orderBy",in:"query",schema:{type:"string"},description:"Field name to order by"},{name:"orderDir",in:"query",schema:{type:"string",enum:["asc","desc"]},description:"Order direction"},{name:"select",in:"query",schema:{type:"string"},description:"Comma-separated list of fields to return"}]}function We(e){let t=e.filterableFields??Object.keys(e.schema.shape),r=["eq","ne","lt","lte","gt","gte","in","nin","contains"],o=[];for(let s of t){o.push({name:s,in:"query",schema:{type:"string"},description:`Filter by ${s} (equality)`});for(let i of r)o.push({name:`${s}__${i}`,in:"query",schema:{type:"string"},description:`Filter ${s} with operator ${i}`});}return o}function Je(){return {type:"object",properties:{where:{type:"array",items:{type:"array",items:{},minItems:3,maxItems:3},description:"AND conditions: [field, operator, value][]"},orWhere:{type:"array",items:{type:"array",items:{},minItems:3,maxItems:3},description:"Simple OR conditions (each independently OR'd)"},orWhereGroups:{type:"array",items:{type:"array",items:{type:"array",items:{},minItems:3,maxItems:3}},description:"Advanced OR groups (AND within, OR across groups)"},orderBy:{type:"array",items:{type:"object",properties:{field:{type:"string"},direction:{type:"string",enum:["asc","desc"]}},required:["field"]}},select:{type:"array",items:{type:"string"},description:"Fields to select (projection)"},pageSize:{type:"integer",maximum:100,description:"Number of items per page"},cursor:{oneOf:[{type:"string"},{type:"object"}],description:"Pagination cursor"},direction:{type:"string",enum:["next","prev"],description:"Pagination direction"},includes:{type:"array",items:{oneOf:[{type:"string"},{type:"object",properties:{relation:{type:"string"},select:{type:"array",items:{type:"string"}}},required:["relation"]}]},description:"Relations to include (populate)"}}}}function Ze(e,t,r,o,s){let i={},d=e.name,y=`${t}/${e.name}`,h=`${y}/{${e.documentKey}}`,g={name:e.documentKey,in:"path",required:true,schema:{type:"string"},description:"Unique document identifier"};i[y]={get:{operationId:`list${B(e.name)}`,summary:`List ${e.name} (paginated)`,tags:[d],parameters:[...Ke(e),...We(e)],responses:{200:Ae(j(r)),500:E("Internal server error")}},post:{operationId:`create${B(e.name)}`,summary:`Create a ${_(e.name)}`,tags:[d],requestBody:{required:true,content:{"application/json":{schema:j(o??r)}}},responses:{201:M("Document created",j(r)),400:E("Validation error"),500:E("Internal server error")}}},i[`${y}/query`]={post:{operationId:`query${B(e.name)}`,summary:`Query ${e.name} with advanced filters`,tags:[d],requestBody:{required:true,content:{"application/json":{schema:j("QueryRequestBody")}}},responses:{200:Ae(j(r)),400:E("Invalid query"),500:E("Internal server error")}}};let k={};return k.get={operationId:`get${B(_(e.name))}`,summary:`Get a single ${_(e.name)}`,tags:[d],parameters:[g],responses:{200:M("Document found",j(r)),404:E("Document not found"),500:E("Internal server error")}},k.put={operationId:`update${B(_(e.name))}`,summary:`Update a ${_(e.name)} (full replace)`,tags:[d],parameters:[g],requestBody:{required:true,content:{"application/json":{schema:j(s??r)}}},responses:{200:M("Document updated",j(r)),400:E("Validation error"),404:E("Document not found"),500:E("Internal server error")}},k.patch={operationId:`patch${B(_(e.name))}`,summary:`Partially update a ${_(e.name)}`,tags:[d],parameters:[g],requestBody:{required:true,content:{"application/json":{schema:{allOf:[j(s??r)],description:"All fields are optional for partial updates"}}}},responses:{200:M("Document patched",j(r)),400:E("Validation error"),404:E("Document not found"),500:E("Internal server error")}},e.allowDelete&&(k.delete={operationId:`delete${B(_(e.name))}`,summary:`Delete a ${_(e.name)}`,tags:[d],parameters:[g],responses:{200:M("Document deleted",{type:"object",properties:{id:{type:"string"}}}),404:E("Document not found"),500:E("Internal server error")}}),i[h]=k,i}function ce(e,t,r={}){let{title:o="CRUD API",version:s="1.0.0",description:i,servers:d,auth:y=false}=r,h=t==="/"?"":t.replace(/\/$/,""),g={},k={},N=[];g.ErrorResponse={type:"object",properties:{success:{type:"boolean",enum:[false]},error:{type:"string"}},required:["success","error"]},g.QueryRequestBody=Je();for(let[H,I]of Object.entries(e)){let c=B(_(H)),u=`${c}Create`,l=`${c}Update`;g[c]=ie(I.schema);let n=S=>{let O=S&&S.length>0?S:Object.keys(I.schema.shape),b={};for(let v of O){let T=v.split(".")[0];T&&I.schema.shape[T]&&!I.systemKeys.includes(T)&&(b[T]=I.schema.shape[T]);}return b},f=null,p=n(I.createFields);Object.keys(p).length>0&&(g[u]=ie(zod.z.object(p)),f=u);let m=null,a=n(I.mutableFields);Object.keys(a).length>0&&(g[l]=ie(zod.z.object(a)),m=l);let w=Ze(I,h,c,f,m);Object.assign(k,w),N.push({name:H,description:`Operations on ${H} (collection: ${I.path})`});}let C={},$;return y==="basic"?(C.basicAuth={type:"http",scheme:"basic"},$=[{basicAuth:[]}]):y==="bearer"&&(C.bearerAuth={type:"http",scheme:"bearer",bearerFormat:"JWT"},$=[{bearerAuth:[]}]),{openapi:"3.1.0",info:{title:o,version:s,...i?{description:i}:{}},...d&&d.length>0?{servers:d}:{},paths:k,components:{schemas:g,...Object.keys(C).length>0?{securitySchemes:C}:{}},...$?{security:$}:{},tags:N}}function B(e){return e.charAt(0).toUpperCase()+e.slice(1)}function _(e){return e.endsWith("ies")?e.slice(0,-3)+"y":e.endsWith("ses")||e.endsWith("xes")||e.endsWith("zes")?e.slice(0,-2):e.endsWith("s")&&!e.endsWith("ss")?e.slice(0,-1):e}var ke="https://cdn.jsdelivr.net",Ce="https://www.gstatic.com",Qe=["default-src 'self'",`script-src 'self' 'unsafe-inline' 'unsafe-eval' ${ke} ${Ce}`,`style-src 'self' 'unsafe-inline' ${ke}`,`connect-src 'self' ${Ce} https://*.googleapis.com https://*.firebaseio.com https://identitytoolkit.googleapis.com`,"img-src 'self' data:","font-src 'self' data:","frame-ancestors 'none'","base-uri 'self'","form-action 'self'"].join("; ");function Se(e={}){let t=e.csp===void 0?Qe:e.csp,r=e.frameOptions===void 0?"DENY":e.frameOptions,o=e.referrerPolicy===void 0?"no-referrer":e.referrerPolicy,s=e.cacheControl===void 0?"private, no-store":e.cacheControl;return async(i,d,y)=>{typeof d?.setHeader=="function"&&(d.setHeader("X-Content-Type-Options","nosniff"),r&&d.setHeader("X-Frame-Options",r),o&&d.setHeader("Referrer-Policy",o),s&&d.setHeader("Cache-Control",s),t&&d.setHeader("Content-Security-Policy",t)),await y();}}function Ve(e,t){return `<!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8" />