@lpdjs/firestore-repo-service 2.3.0 → 2.3.1

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.
@@ -18,7 +18,7 @@
18
18
  }
19
19
  @media (prefers-color-scheme: dark) {
20
20
  body { background: #1d1d1f; color: #f5f5f7; }
21
- .card { background: #2c2c2e; }
21
+ .card { background: #2c2c2e !important; }
22
22
  input { background: #1d1d1f; color: #f5f5f7; border-color: #444; }
23
23
  input::placeholder { color: #888; }
24
24
  input:-webkit-autofill,
@@ -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","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","isPublic","path","patterns","p","wantsHtml","accept","extractBearer","m","firebaseAuth","config","cookieName","ttlDays","secure","sameSite","onUnauth","loginEnabled","loginPath","sessionPath","logoutPath","sessionHandler","logoutHandler","renderInlineLogin","error","pageCfg","prefix","inner","fullPath","sep","sessionAction","html","routes","publicPaths","middleware","next","action","token","rejectUnauthenticated","baseUser","context","isAuthExtension","allowAll"],"mappings":"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","file":"index.cjs","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\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; }\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 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 // 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 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(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 * 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\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 // Browsers send \"text/html\" early in their Accept header.\n // Fall back: treat GET requests with no Accept as HTML so platforms\n // that strip the header (or send \"*/*\") still get the login page.\n if (accept.includes(\"text/html\")) return true;\n if (!accept || accept === \"*/*\") return methodOf(req) === \"GET\";\n return false;\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 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 ): 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 const fullPath = `${prefix}${inner.startsWith(\"/\") ? inner : `/${inner}`}`;\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 });\n res\n .status(200)\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\n // 1. In-band action endpoints (work on ANY URL, no separate route needed).\n // Used by the inline login page since the helper can't know the function's\n // public URL prefix on Cloud Functions.\n if (loginEnabled && methodOf(req) === \"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 (works on Cloud Functions where there's no separate\n * `/__login` route reachable from the public URL).\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);\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","base","project","region","target","service","host","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","isPublic","path","patterns","p","wantsHtml","accept","extractBearer","m","firebaseAuth","config","cookieName","ttlDays","secure","sameSite","onUnauth","loginEnabled","loginPath","sessionPath","logoutPath","sessionHandler","logoutHandler","renderInlineLogin","error","pageCfg","prefix","inner","fullPath","sep","sessionAction","html","routes","publicPaths","middleware","next","action","token","rejectUnauthenticated","baseUser","context","isAuthExtension","allowAll"],"mappings":"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","file":"index.cjs","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\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 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 // 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 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(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 * 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\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 // Browsers send \"text/html\" early in their Accept header.\n // Fall back: treat GET requests with no Accept as HTML so platforms\n // that strip the header (or send \"*/*\") still get the login page.\n if (accept.includes(\"text/html\")) return true;\n if (!accept || accept === \"*/*\") return methodOf(req) === \"GET\";\n return false;\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 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 ): 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 const fullPath = `${prefix}${inner.startsWith(\"/\") ? inner : `/${inner}`}`;\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 });\n res\n .status(200)\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\n // 1. In-band action endpoints (work on ANY URL, no separate route needed).\n // Used by the inline login page since the helper can't know the function's\n // public URL prefix on Cloud Functions.\n if (loginEnabled && methodOf(req) === \"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 (works on Cloud Functions where there's no separate\n * `/__login` route reachable from the public URL).\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);\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"]}
@@ -18,7 +18,7 @@ function _(e,t){let o="";if(process.env.FUNCTIONS_EMULATOR==="true"){let s=proce
18
18
  }
19
19
  @media (prefers-color-scheme: dark) {
20
20
  body { background: #1d1d1f; color: #f5f5f7; }
21
- .card { background: #2c2c2e; }
21
+ .card { background: #2c2c2e !important; }
22
22
  input { background: #1d1d1f; color: #f5f5f7; border-color: #444; }
23
23
  input::placeholder { color: #888; }
24
24
  input:-webkit-autofill,
@@ -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","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","isPublic","path","patterns","p","wantsHtml","accept","extractBearer","m","firebaseAuth","config","cookieName","ttlDays","secure","sameSite","onUnauth","loginEnabled","loginPath","sessionPath","logoutPath","sessionHandler","logoutHandler","renderInlineLogin","error","pageCfg","prefix","inner","fullPath","sep","sessionAction","html","routes","publicPaths","middleware","next","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\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; }\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 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 // 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 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(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 * 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\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 // Browsers send \"text/html\" early in their Accept header.\n // Fall back: treat GET requests with no Accept as HTML so platforms\n // that strip the header (or send \"*/*\") still get the login page.\n if (accept.includes(\"text/html\")) return true;\n if (!accept || accept === \"*/*\") return methodOf(req) === \"GET\";\n return false;\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 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 ): 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 const fullPath = `${prefix}${inner.startsWith(\"/\") ? inner : `/${inner}`}`;\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 });\n res\n .status(200)\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\n // 1. In-band action endpoints (work on ANY URL, no separate route needed).\n // Used by the inline login page since the helper can't know the function's\n // public URL prefix on Cloud Functions.\n if (loginEnabled && methodOf(req) === \"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 (works on Cloud Functions where there's no separate\n * `/__login` route reachable from the public URL).\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);\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","base","project","region","target","service","host","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","isPublic","path","patterns","p","wantsHtml","accept","extractBearer","m","firebaseAuth","config","cookieName","ttlDays","secure","sameSite","onUnauth","loginEnabled","loginPath","sessionPath","logoutPath","sessionHandler","logoutHandler","renderInlineLogin","error","pageCfg","prefix","inner","fullPath","sep","sessionAction","html","routes","publicPaths","middleware","next","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\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 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 // 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 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(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 * 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\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 // Browsers send \"text/html\" early in their Accept header.\n // Fall back: treat GET requests with no Accept as HTML so platforms\n // that strip the header (or send \"*/*\") still get the login page.\n if (accept.includes(\"text/html\")) return true;\n if (!accept || accept === \"*/*\") return methodOf(req) === \"GET\";\n return false;\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 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 ): 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 const fullPath = `${prefix}${inner.startsWith(\"/\") ? inner : `/${inner}`}`;\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 });\n res\n .status(200)\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\n // 1. In-band action endpoints (work on ANY URL, no separate route needed).\n // Used by the inline login page since the helper can't know the function's\n // public URL prefix on Cloud Functions.\n if (loginEnabled && methodOf(req) === \"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 (works on Cloud Functions where there's no separate\n * `/__login` route reachable from the public URL).\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);\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lpdjs/firestore-repo-service",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "workspaces": [
5
5
  "test/functions"
6
6
  ],
@@ -231,4 +231,4 @@
231
231
  "@hono/zod-openapi": "^1.4.0",
232
232
  "hono": "^4.12.17"
233
233
  }
234
- }
234
+ }