@fonderie/auth 7.12.0 → 7.13.0
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.
- package/brain/signatures.md +2 -2
- package/dist/index.cjs +345 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -66
- package/dist/index.d.ts +4 -66
- package/dist/index.js +346 -19
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +8 -8
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.js +8 -8
- package/dist/middlewares/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/services/password.ts","../src/index.ts","../src/routes.ts","../src/middlewares/require-email-login.ts","../src/middlewares/validate.ts","../src/services/rate-limit.ts","../src/schemas.ts","../src/services/cookies.ts","../src/controllers/mfa.controller.ts","../src/config.ts","../src/services/jwt.ts","../src/services/mfa.ts","../src/services/mfa-crypto.ts","../src/dtos/user.ts","../src/models/user.model.ts","../src/services/request-location.ts","../src/models/session.model.ts","../src/models/backup-code.model.ts","../src/models/login-event.model.ts","../src/services/request-meta.ts","../src/controllers/auth.controller.ts","../src/services/cooldown.ts","../src/services/email.ts","../src/models/password-reset.model.ts","../src/models/email-verification.model.ts","../src/models/phone-verification.model.ts","../src/controllers/user.controller.ts","../src/dtos/login-activity.ts","../src/controllers/oauth.controller.ts","../src/models/consumed-token.model.ts","../src/services/config-guard.ts","../src/middlewares/session.ts","../src/admin.ts","../src/module.ts","../src/templates.ts","../src/middlewares/require-auth.ts","../src/migrate.ts","../src/services/retention.ts"],"sourcesContent":["import bcrypt from 'bcryptjs';\n\nconst ROUNDS = 12;\n\nexport async function hashPassword(plain: string): Promise<string> {\n\treturn bcrypt.hash(plain, ROUNDS);\n}\n\nexport async function verifyPassword(plain: string, hash: string): Promise<boolean> {\n\treturn bcrypt.compare(plain, hash);\n}\n\nexport interface IVerifyResult {\n\tvalid: boolean;\n\t// True when the password validated only via the legacy verifier — the caller\n\t// should re-store it as a bcrypt hash (rehash-on-login).\n\tneedsRehash: boolean;\n}\n\n// The login-path verifier: `verifyPassword` (the bcrypt primitive) PLUS an\n// optional foreign-hash fallback for apps migrating onto Fonderie auth. Tries\n// bcrypt first; if that fails and a `legacyVerify` is provided, falls back to it\n// and flags `needsRehash` so the caller can re-store the password as bcrypt\n// (rehash-on-login). `bcrypt.compare` returns false rather than throwing on a\n// non-bcrypt hash, so the fallback is reached cleanly; the legacy verifier is\n// wrapped defensively in case a custom implementation throws.\nexport async function verifyPasswordForLogin(\n\tplain: string,\n\thash: string,\n\tlegacyVerify?: (plain: string, hash: string) => boolean | Promise<boolean>,\n): Promise<IVerifyResult> {\n\tif (await verifyPassword(plain, hash)) return { valid: true, needsRehash: false };\n\n\tif (legacyVerify) {\n\t\tlet legacyOk = false;\n\t\ttry {\n\t\t\tlegacyOk = await legacyVerify(plain, hash);\n\t\t} catch {\n\t\t\tlegacyOk = false;\n\t\t}\n\t\tif (legacyOk) return { valid: true, needsRehash: true };\n\t}\n\n\treturn { valid: false, needsRehash: false };\n}\n","// ── Public API ───────────────────────────────────────────────────\nexport type { IUser, ISession, IMfaChallenge } from './types';\nexport { AuthModule } from './module';\nexport type { IAuthConfig, IAuthSecrets, IAuthRuntimeConfig, IDataExportContributor } from './config';\nexport { AUTH_CONFIG_KEYS, MESSAGE_KEYS } from './config';\nexport type { AuthMessageKey } from './config';\n// Built-in default templates for auth's notifications. Pass to courier via\n// config.templates.defaults so the emails render out of the box; override any\n// key per-app with a DB row / FS file.\nexport { DEFAULT_TEMPLATES } from './templates';\n\n// DTOs\nexport type { IUserDTO } from './dtos/user';\nexport { toUserDTO } from './dtos/user';\n\nexport type {\n\tILoginEventDTO,\n\tILoginHistoryPageDTO,\n\tISessionDTO,\n} from './dtos/login-activity';\nexport {\n\tsanitizeLocation,\n\tresolveLocation,\n} from './services/request-location';\nexport type {\n\tIRequestLocation,\n\tILocationRequest,\n\tLocationResolver,\n} from './services/request-location';\n\n// Request validation — schemas are the enforced contract for every\n// body-taking route; exported for docs generation and typed clients.\nexport { validate } from './middlewares/validate';\nexport * as schemas from './schemas';\nexport type { RegisterInput, LoginInput, ResetPasswordInput, ChangePasswordInput } from './schemas';\n\n// Guards — used by other modules and user route handlers\nexport { withSession } from './middlewares/session';\nexport { requireAuth } from './middlewares/require-auth';\n\n// Utilities\nexport { normalizeEmail, normalizeEmailSafe } from './services/email';\n\n// Migration — import an existing user base (preserves identity; pairs with the\n// `legacyVerify` config option for rehash-on-login).\nexport { importUser } from './migrate';\nexport { purgeSoftDeletedUsers, startUserRetention } from './services/retention';\nexport type { IPurgeOptions, IUserRetentionScheduleOptions } from './services/retention';\nexport type { IImportUser } from './migrate';\n\n// Production-readiness — validate the auth config (fatal on a weak jwtSecret in\n// production). Runs automatically on AuthModule construction; exported for an\n// app's own preflight.\nexport { validateAuthConfig } from './services/config-guard';\n\n// Brute-force protection — on by default; see services/rate-limit.ts\nexport { buildAuthIpLimiter, buildAuthAccountLimiter } from './services/rate-limit';\nexport type { IAuthRateLimitConfig, AuthLimitedRoute } from './services/rate-limit';\nexport type { IAdminUserDTO } from './admin';\nexport { describeAuthAdminRoutes, toAdminUserDTO, toAdminUserPageDTO } from './admin';\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport type { EventBus } from '@fonderie/events';\nimport type { IAuthConfig, AuthRouteId } from './config';\n\nimport { requireAuth, requireAnyAuth, requireVerified } from '@fonderie/core/middlewares';\nimport { requireEmailLogin } from './middlewares/require-email-login';\nimport { validate } from './middlewares/validate';\nimport { buildAuthIpLimiter, buildAuthAccountLimiter } from './services/rate-limit';\nimport {\n\tloginSchema,\n\tverifySchema,\n\trefreshSchema,\n\tmfaTokenSchema,\n\tregisterSchema,\n\tupdateEmailSchema,\n\tupdatePhoneSchema,\n\tupdateProfileSchema,\n\tresetPasswordSchema,\n\tchangePasswordSchema,\n\tforgotPasswordSchema,\n\tupdatePreferencesSchema,\n\tappleNativeSchema,\n} from './schemas';\n\nimport { mfaController } from './controllers/mfa.controller';\nimport { authController } from './controllers/auth.controller';\nimport { userController } from './controllers/user.controller';\nimport { oauthController } from './controllers/oauth.controller';\n\ntype RouteDefinition = [string, string, ...Middleware[]];\n\nexport function buildAuthRoutes(\n\tstore: IStoreAdapter,\n\tconfig: IAuthConfig,\n\tbus?: EventBus,\n): RouteDefinition[] {\n\tconst user = userController(store, config, bus);\n\tconst auth = authController(store, config, bus);\n\tconst oauth = oauthController(store, config, bus);\n\tconst mfa = mfaController(store, config, config.appName ?? 'Fonderie', bus);\n\n\t// Opt-in verification gate: only enforces email/phone verification when\n\t// requireVerification: true is set in config. Defaults to false so that\n\t// auth can be used without a courier/email provider.\n\tconst verifyGate: Middleware = config.requireVerification\n\t\t? requireVerified\n\t\t: (_ctx, next) => next();\n\n\t// Brute-force protection — on by default, backed by the module's own\n\t// store; see services/rate-limit.ts. Null when disabled via config.\n\tconst passthrough: Middleware = (_ctx, next) => next();\n\t// IP limit runs before validate(); account limit runs after (keys on the\n\t// validated, bounded email — never raw attacker input).\n\tconst ipLimit = (route: Parameters<typeof buildAuthIpLimiter>[0]): Middleware =>\n\t\tbuildAuthIpLimiter(route, store, config.rateLimit) ?? passthrough;\n\tconst acctLimit = (route: Parameters<typeof buildAuthAccountLimiter>[0]): Middleware =>\n\t\tbuildAuthAccountLimiter(route, store, config.rateLimit) ?? passthrough;\n\n\t// Apply an optional per-route method/path override (config.routes) keyed by a\n\t// stable id, so an app can match an existing frontend's contract without a shim.\n\tconst R = (id: AuthRouteId, method: string, path: string, ...handlers: Middleware[]): RouteDefinition => {\n\t\tconst o = config.routes?.[id];\n\t\tif (!o) return [method, path, ...handlers];\n\t\tif (typeof o === 'string') return [method, o, ...handlers];\n\t\treturn [o.method ?? method, o.path ?? path, ...handlers];\n\t};\n\n\tconst routes: RouteDefinition[] = [\n\t\t// Which sign-in methods this deployment can actually honour (Public).\n\t\t//\n\t\t// A frontend has to decide which buttons to draw, and the only honest\n\t\t// source is the side holding the credentials. The alternative — a\n\t\t// build-time flag in the app — stores the same fact twice and lets the\n\t\t// two disagree; the symptom is a user clicking a provider the server\n\t\t// cannot complete and landing on the provider's error page, which the\n\t\t// app cannot explain.\n\t\t//\n\t\t// Public and unauthenticated on purpose: the login screen needs it\n\t\t// before anyone has signed in. It discloses nothing a visitor could not\n\t\t// learn by looking at the buttons, and deliberately nothing else — no\n\t\t// client ids, no redirect URIs, no module inventory.\n\t\tR('providers', 'GET', '/auth/providers', async () =>\n\t\t\tsetApiResponse(HTTP.OK, 'AUTH_PROVIDERS', 'Sign-in methods available here', {\n\t\t\t\tproviders: [...config.providers],\n\t\t\t}),\n\t\t),\n\n\t\t// Registration & Login (Public)\n\t\tR('register', 'POST', '/auth/register', ipLimit('register'), validate(registerSchema), auth.register),\n\t\tR('login', 'POST', '/auth/login', ipLimit('login'), validate(loginSchema), acctLimit('login'), auth.login),\n\n\t\t// Token Management (Public)\n\t\tR('refresh', 'POST', '/auth/refresh', validate(refreshSchema), auth.refresh),\n\n\t\t// Email — Password Recovery (Public)\n\t\tR('forgotPassword', 'POST', '/auth/email/forgot', ipLimit('forgot'), validate(forgotPasswordSchema), acctLimit('forgot'), auth.forgotPassword),\n\t\tR('resetPassword', 'POST', '/auth/email/reset', ipLimit('reset'), validate(resetPasswordSchema), auth.resetPassword),\n\n\t\t// Verification (email or phone, determined by loginMethod). requireAnyAuth,\n\t\t// not requireAuth: the phone-OTP flow reaches these with a short-lived\n\t\t// pending token (login/register no longer issue real tokens before the OTP\n\t\t// round-trip); the controllers reject pending tokens for any other flow.\n\t\t// ipLimit guards the 6-digit OTP against brute force (like mfaVerify).\n\t\tR('verifyEmail', 'POST', '/auth/verify', ipLimit('verify'), requireAnyAuth, validate(verifySchema), auth.verify),\n\t\tR('sendVerification', 'GET', '/auth/send-verification', requireAnyAuth, auth.sendVerification),\n\n\t\t// Account Management (Protected)\n\t\tR('logout', 'POST', '/auth/logout', requireAuth, validate(refreshSchema), auth.logout),\n\n\t\t// User Profile (Protected; writes also gate on requireVerification)\n\t\tR('me', 'GET', '/users', requireAuth, user.me),\n\t\tR('updateProfile', 'PUT', '/users/profile', requireAuth, verifyGate, validate(updateProfileSchema), user.updateProfile),\n\t\tR('updatePreferences', 'PUT', '/users/preferences', requireAuth, verifyGate, validate(updatePreferencesSchema), user.updatePreferences),\n\t\tR('updateEmail', 'PUT', '/users/email', requireAuth, verifyGate, validate(updateEmailSchema), user.updateEmail),\n\t\tR('updatePhone', 'PUT', '/users/phone', requireAuth, verifyGate, validate(updatePhoneSchema), user.updatePhone),\n\t\tR('changePassword', 'PUT', '/users/password', requireAuth, validate(changePasswordSchema), user.changePassword),\n\t\t// Disconnect an OAuth provider. requireAuth only — the controller\n\t\t// refuses when it would leave the account with no way to sign in.\n\t\tR('unlinkOauth', 'DELETE', '/auth/oauth/:provider', requireAuth, user.unlinkOauth),\n\n\t\tR('deleteMe', 'DELETE', '/users', requireAuth, verifyGate, user.deleteMe),\n\t\tR('exportMe', 'GET', '/users/export', requireAuth, user.exportMe),\n\n\t\t// Security surfaces (Protected; the caller's own login history + sessions).\n\t\t// The literal /others route is registered before /:id so it isn't\n\t\t// captured as an id.\n\t\tR('loginHistory', 'GET', '/auth/login-history', requireAuth, user.loginHistory),\n\t\tR('listSessions', 'GET', '/auth/sessions', requireAuth, user.listSessions),\n\t\tR('terminateOtherSessions', 'DELETE', '/auth/sessions/others', requireAuth, user.terminateOtherSessions),\n\t\tR('terminateSession', 'DELETE', '/auth/sessions/:id', requireAuth, user.terminateSession),\n\n\t\t// MFA (email sessions only — requireVerified is always enforced here\n\t\t// because MFA is a security feature and email verification is meaningful)\n\t\tR('mfaSetup', 'POST', '/auth/mfa/setup', requireAuth, requireEmailLogin, requireVerified, mfa.setup),\n\t\t// /auth/mfa/verify accepts both mfaPending tokens (TOTP/backup-code login)\n\t\t// and full tokens (setup confirmation), so requireAnyAuth is used here.\n\t\tR('mfaVerify', 'POST', '/auth/mfa/verify', ipLimit('mfaVerify'), requireAnyAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.verify),\n\t\tR('mfaDisable', 'POST', '/auth/mfa/disable', requireAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.disable),\n\t\tR('mfaBackupCodes', 'POST', '/auth/mfa/backup-codes', requireAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.regenerateBackupCodes),\n\t];\n\n\tif (config.providers.includes('google')) {\n\t\troutes.push(\n\t\t\t['GET', '/auth/google', oauth.googleInit],\n\t\t\t// ipLimit: an unauthenticated caller can otherwise force one outbound\n\t\t\t// token-exchange to Google per request (the state check doesn't gate it).\n\t\t\t['GET', '/auth/google/callback', ipLimit('login'), oauth.googleCallback],\n\t\t);\n\t}\n\n\tif (config.providers.includes('apple')) {\n\t\troutes.push(\n\t\t\t// Web redirect flow. The callback is a POST (response_mode=form_post);\n\t\t\t// its urlencoded body is parsed inside the controller. ipLimit for the\n\t\t\t// same reason as Google's callback (bounds outbound token exchanges).\n\t\t\t['GET', '/auth/apple', oauth.appleInit],\n\t\t\t['POST', '/auth/apple/callback', ipLimit('login'), oauth.appleCallback],\n\t\t\t// Native (iOS) flow: the app posts the identityToken from the Apple\n\t\t\t// sheet. ipLimit guards token brute-forcing; validate bounds the body.\n\t\t\t['POST', '/auth/apple/native', ipLimit('login'), validate(appleNativeSchema), oauth.appleNative],\n\t\t);\n\t}\n\n\treturn routes;\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\n\nexport const requireEmailLogin: Middleware = async (ctx, next) => {\n\tif (ctx.user!.loginMethod !== 'email') {\n\t\treturn setApiResponse(\n\t\t\tHTTP.FORBIDDEN,\n\t\t\t'EMAIL_LOGIN_REQUIRED',\n\t\t\t'This action requires email authentication',\n\t\t);\n\t}\n\treturn next();\n};\n","// Moved to @fonderie/core/middlewares so every package shares one validation\n// middleware (same 422 envelope, same parse semantics). Re-exported here for\n// back-compat.\nexport { validate } from '@fonderie/core/middlewares';\n","import type { Middleware } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport {\n\tbyBodyField,\n\tbyIp,\n\trateLimit,\n\tStoreAdapterStore,\n\ttype IRateLimitRule,\n\ttype IRateLimitStore,\n} from '@fonderie/rate-limit';\n\n// Per-route brute-force protection for auth. Defaults are deliberately\n// conservative and, crucially, ON without the caller asking — the whole point\n// of shipping this in the module is that \"add login\" gets throttling too.\n//\n// Two phases per route, wired at different points in the chain:\n// - IP limit runs BEFORE validate() — a cheap guard that sheds floods\n// before they reach schema parsing.\n// - Account (body-field) limit runs AFTER validate() — so it keys on a\n// bounded, well-typed identifier, never raw attacker input.\n\nexport interface IAuthRateLimitConfig {\n\t// Override the backing store. Defaults to StoreAdapterStore over the\n\t// module's own IStoreAdapter — distributed-correct with zero extra infra.\n\tstore?: IRateLimitStore;\n\t// Per-route rule overrides; set a route to false to disable just that one.\n\trules?: Partial<Record<AuthLimitedRoute, IRateLimitRule | false>>;\n}\n\nexport type AuthLimitedRoute = 'login' | 'register' | 'forgot' | 'reset' | 'verify' | 'mfaVerify';\n\n// capacity = burst; refillPerSec = capacity / windowSeconds.\nconst min = (n: number) => n * 60;\nconst DEFAULTS: Record<\n\tAuthLimitedRoute,\n\t{ ip: IRateLimitRule; account?: IRateLimitRule; accountField?: string }\n> = {\n\t// login: 10/15min per IP AND 5/15min per account (credential stuffing\n\t// rotates IPs, so the per-account bucket is the one that bites).\n\tlogin: {\n\t\tip: { capacity: 10, refillPerSec: 10 / min(15) },\n\t\taccount: { capacity: 5, refillPerSec: 5 / min(15) },\n\t\taccountField: 'email',\n\t},\n\t// register: 5/hour per IP — signup abuse is IP-shaped.\n\tregister: { ip: { capacity: 5, refillPerSec: 5 / min(60) } },\n\t// forgot: 5/hour per IP AND 3/hour per account (email-bombing protection).\n\tforgot: {\n\t\tip: { capacity: 5, refillPerSec: 5 / min(60) },\n\t\taccount: { capacity: 3, refillPerSec: 3 / min(60) },\n\t\taccountField: 'email',\n\t},\n\t// reset: 10/15min per IP — the reset PIN is a 6-digit code looked up\n\t// globally, so an unthrottled endpoint is brute-forceable into an account\n\t// takeover. There's no email in the reset body to key an account bucket on,\n\t// so this IP bucket is the guard. (Moving reset to a high-entropy opaque\n\t// token is tracked as a follow-up hardening.)\n\treset: { ip: { capacity: 10, refillPerSec: 10 / min(15) } },\n\t// verify: 10/15min per IP — /auth/verify checks a 6-digit email/phone OTP,\n\t// and in the phone flow that OTP is the login credential itself.\n\tverify: { ip: { capacity: 10, refillPerSec: 10 / min(15) } },\n\t// mfaVerify: 10/15min per IP — TOTP brute-force.\n\tmfaVerify: { ip: { capacity: 10, refillPerSec: 10 / min(15) } },\n};\n\nfunction resolve(\n\troute: AuthLimitedRoute,\n\tstore: IStoreAdapter,\n\tconfig: IAuthRateLimitConfig | false | undefined,\n): {\n\tstore: IRateLimitStore;\n\tipRule: IRateLimitRule;\n\taccount?: { rule: IRateLimitRule; field: string };\n} | null {\n\tif (config === false) return null;\n\tconst override = config?.rules?.[route];\n\tif (override === false) return null;\n\n\tconst backing = config?.store ?? new StoreAdapterStore(store);\n\tconst def = DEFAULTS[route];\n\treturn {\n\t\tstore: backing,\n\t\tipRule: override ?? def.ip,\n\t\t...(def.account && def.accountField\n\t\t\t? { account: { rule: def.account, field: def.accountField } }\n\t\t\t: {}),\n\t};\n}\n\n// Phase 1: IP limit, placed BEFORE validate().\nexport function buildAuthIpLimiter(\n\troute: AuthLimitedRoute,\n\tstore: IStoreAdapter,\n\tconfig: IAuthRateLimitConfig | false | undefined,\n): Middleware | null {\n\tconst r = resolve(route, store, config);\n\tif (!r) return null;\n\treturn rateLimit({ store: r.store, rule: r.ipRule, key: byIp(`auth:${route}`) });\n}\n\n// Phase 2: account limit, placed AFTER validate() so the keyed field is clean.\nexport function buildAuthAccountLimiter(\n\troute: AuthLimitedRoute,\n\tstore: IStoreAdapter,\n\tconfig: IAuthRateLimitConfig | false | undefined,\n): Middleware | null {\n\tconst r = resolve(route, store, config);\n\tif (!r || !r.account) return null;\n\treturn rateLimit({\n\t\tstore: r.store,\n\t\trule: r.account.rule,\n\t\tkey: byBodyField(`auth:${route}`, r.account.field),\n\t});\n}\n","import { z } from 'zod';\n\n// Request schemas — the validation contract for every body-taking auth route.\n// Wired into routes.ts via validate(); exported so docs generators and typed\n// clients read the same source of truth the runtime enforces.\n//\n// Passwords are capped at 128 chars: bcrypt truncates at 72 bytes anyway, and\n// an explicit cap stops oversized inputs from reaching the hash function.\n\nconst email = z.string().trim().pipe(z.email());\nconst password = z.string().min(8, 'password must be at least 8 characters').max(128);\n// E.164-ish, tolerant of spaces/dashes/parens — controllers normalize further.\nconst phone = z\n\t.string()\n\t.refine((v) => /^\\+?[1-9]\\d{6,14}$/.test(v.replace(/[\\s\\-()]/g, '')), 'Invalid phone number');\nconst sixDigitPin = z\n\t.string()\n\t.trim()\n\t.regex(/^\\d{6}$/, 'must be a 6-digit code');\n\nexport const registerSchema = z.union([\n\tz.object({\n\t\temail,\n\t\tpassword,\n\t\tfirstName: z.string().max(100).nullish(),\n\t\tlastName: z.string().max(100).nullish(),\n\t}),\n\tz.object({ phone }),\n]);\n\nexport const loginSchema = z.union([\n\tz.object({ email, password: z.string().min(1).max(128) }),\n\tz.object({ phone }),\n]);\n\n// Native Sign in with Apple: the iOS app posts the identityToken it got from\n// the native Apple sheet. Bounded length — it's a JWT, never megabytes.\nexport const appleNativeSchema = z.object({\n\tidentityToken: z.string().min(1).max(8192),\n\t// Optional nonce the app bound to the native request; when present we check\n\t// it against the token's `nonce` claim to defend against replay.\n\tnonce: z.string().max(256).optional(),\n});\n\n// refreshToken may come from the body or the refresh_token cookie.\nexport const refreshSchema = z.object({ refreshToken: z.string().min(1).optional() });\n\nexport const forgotPasswordSchema = z.object({ email });\n\n// Reset by the 6-digit pin (typed from the email; route is IP-rate-limited)\n// or by the high-entropy token (from the email's reset link; not\n// brute-forceable, so it needs no rate limit).\nexport const resetPasswordSchema = z.union([\n\tz.object({ pin: sixDigitPin, password }),\n\tz.object({ token: z.string().trim().min(32, 'invalid reset token'), password }),\n]);\n\nexport const verifySchema = z.object({ token: sixDigitPin });\n\nexport const updateProfileSchema = z\n\t.object({\n\t\tfirstName: z.string().max(100).nullable().optional(),\n\t\tlastName: z.string().max(100).nullable().optional(),\n\t\tavatarUrl: z.string().trim().pipe(z.url()).nullable().optional(),\n\t})\n\t.refine(\n\t\t(o) => Object.values(o).some((v) => v !== undefined),\n\t\t'Provide at least one of: firstName, lastName, avatarUrl',\n\t);\n\nexport const updatePreferencesSchema = z\n\t.object({\n\t\tlocale: z.string().max(35).optional(),\n\t\ttimezone: z.string().max(64).optional(),\n\t\t// Typed at last — these four accepted `unknown`, letting null and\n\t\t// arbitrary JSON into stored preferences that the user DTO then served\n\t\t// against string-typed client fields.\n\t\tnotifications: z\n\t\t\t.object({\n\t\t\t\temail: z.boolean().optional(),\n\t\t\t\tinApp: z.boolean().optional(),\n\t\t\t\tsms: z.boolean().optional(),\n\t\t\t\tpush: z.boolean().optional(),\n\t\t\t})\n\t\t\t.optional(),\n\t\temailDigest: z.string().max(20).optional(),\n\t\tdateFormat: z.string().max(30).optional(),\n\t\ttimeFormat: z.string().max(30).optional(),\n\t})\n\t.refine(\n\t\t(o) => Object.values(o).some((v) => v !== undefined),\n\t\t'Provide at least one preference field',\n\t);\n\nexport const updateEmailSchema = z.object({ email });\n\nexport const updatePhoneSchema = z.object({ phone });\n\nexport const changePasswordSchema = z.object({\n\tcurrentPassword: z.string().min(1).max(128),\n\tnewPassword: password,\n});\n\n// TOTP or backup code — verify/disable accept either; both are short strings.\nexport const mfaTokenSchema = z.object({ token: z.string().trim().min(6).max(64) });\n\nexport type RegisterInput = z.infer<typeof registerSchema>;\nexport type LoginInput = z.infer<typeof loginSchema>;\nexport type ResetPasswordInput = z.infer<typeof resetPasswordSchema>;\nexport type ChangePasswordInput = z.infer<typeof changePasswordSchema>;\n","import type { IAuthConfig } from '../config';\n\n// Single source of truth for the auth token cookie pair, so every login\n// path (email, phone, OAuth, MFA, refresh) sets identical attributes.\n//\n// Secure defaults to NODE_ENV === 'production': in production the tokens\n// must never travel over plain HTTP; local HTTP dev keeps working without\n// config. Override with secureCookies when the environment variable doesn't\n// reflect reality (e.g. HTTPS in staging, TLS-terminating proxy quirks).\n\nfunction secureAttr(config: IAuthConfig): string {\n\tconst secure = config.secureCookies ?? process.env['NODE_ENV'] === 'production';\n\treturn secure ? '; Secure' : '';\n}\n\n// Returns ONE string per cookie. Each must go out as its own `Set-Cookie`\n// header — joining them (e.g. with \", \") is invalid HTTP and mangles the\n// distinct Paths. Callers append each via `jsonWithCookies`.\nexport function tokenPairCookies(\n\taccessToken: string,\n\trefreshToken: string,\n\tconfig: IAuthConfig,\n): string[] {\n\tconst secure = secureAttr(config);\n\treturn [\n\t\t`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/${secure}`,\n\t\t`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh${secure}`,\n\t];\n}\n\nexport function clearedTokenCookies(config: IAuthConfig): string[] {\n\tconst secure = secureAttr(config);\n\treturn [\n\t\t`access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0${secure}`,\n\t\t`refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0${secure}`,\n\t];\n}\n\n// Build a Headers object with each cookie as its OWN Set-Cookie entry. Pass it\n// straight to `Response.json(body, { status, headers: cookieHeaders(...) })`. A\n// plain `{ 'Set-Cookie': [...] }` init coalesces them into one comma-joined\n// header (invalid); appending onto a Headers instance keeps them distinct.\nexport function cookieHeaders(cookies: string[]): Headers {\n\tconst headers = new Headers();\n\tfor (const c of cookies) headers.append('Set-Cookie', c);\n\treturn headers;\n}\n","import { tokenPairCookies, cookieHeaders } from '../services/cookies';\nimport QRCode from 'qrcode';\n\nimport { setApiResponse, HTTP, background } from '@fonderie/core';\nimport type { IFonderieContext, ICourierMessage } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport type { EventBus } from '@fonderie/events';\nimport { NOTIFICATION_EVENT } from '@fonderie/events';\n\nimport type { IAuthConfig } from '../config';\nimport { MESSAGE_KEYS } from '../config';\nimport { issueTokenPair, refreshTokenExpiry } from '../services/jwt';\nimport {\n\tgenerateTotpSecret,\n\tgenerateTotpUri,\n\tverifyTotpToken,\n\tgenerateBackupCodes,\n} from '../services/mfa';\nimport { hashPassword, verifyPassword } from '../services/password';\nimport { makeMfaCipher } from '../services/mfa-crypto';\nimport { toUserDTO } from '../dtos/user';\nimport { UserModel } from '../models/user.model';\nimport { SessionModel } from '../models/session.model';\nimport { BackupCodeModel } from '../models/backup-code.model';\nimport { LoginEventModel } from '../models/login-event.model';\nimport { requestMeta } from '../services/request-meta';\n\nexport function mfaController(\n\tstore: IStoreAdapter,\n\tconfig: IAuthConfig,\n\tissuer: string,\n\tbus?: EventBus,\n) {\n\tconst users = new UserModel(store);\n\tconst sessions = new SessionModel(store, config.location);\n\tconst backupCodes = new BackupCodeModel(store);\n\tconst loginEvents = new LoginEventModel(store, config.location);\n\t// Encrypts/decrypts TOTP secrets at rest. Passthrough when no key is set.\n\tconst mfaCipher = makeMfaCipher(config.mfaSecretKey);\n\n\treturn {\n\t\t// ── 1. Setup ───────────────────────────────────────────────\n\t\tsetup: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst secret = generateTotpSecret();\n\t\t\tconst uri = generateTotpUri(ctx.user!.email ?? ctx.user!.id, secret, issuer);\n\t\t\tconst plainCodes = generateBackupCodes();\n\t\t\tconst codeHashes = await Promise.all(plainCodes.map((c) => hashPassword(c)));\n\n\t\t\tconst qr = await QRCode.toDataURL(uri);\n\n\t\t\tawait Promise.all([\n\t\t\t\tusers.saveMfaPendingSecret(ctx.user!.id, mfaCipher.encrypt(secret)),\n\t\t\t\tbackupCodes.replace(ctx.user!.id, codeHashes),\n\t\t\t]);\n\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.OK,\n\t\t\t\t'MFA_SETUP_INITIATED',\n\t\t\t\t'Scan the QR code with your authenticator app.',\n\t\t\t\t{\n\t\t\t\t\tqr,\n\t\t\t\t\t// uri — expose when adding a \"manual entry\" flow in the UI (otpauth:// URI lets\n\t\t\t\t\t// users add the credential by typing the secret instead of scanning the QR code)\n\t\t\t\t\tbackupCodes: plainCodes,\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\t// ── 2. Verify (setup confirm · TOTP login · backup code login) ──\n\t\tverify: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst token = body?.['token'];\n\t\t\t// Only the login-completion path (a pending MFA challenge) belongs in\n\t\t\t// login history; enrollment confirmation is not a login attempt.\n\t\t\tconst isLoginCompletion = ctx.user!.mfaPending === true;\n\t\t\tconst meta = requestMeta(ctx);\n\t\t\tconst failedMfa = async () =>\n\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\tuserId: ctx.user!.id,\n\t\t\t\t\temailAttempted: ctx.user!.email,\n\t\t\t\t\tmethod: 'mfa',\n\t\t\t\t\toutcome: 'failed',\n\t\t\t\t\tfailureReason: 'invalid_code',\n\t\t\t\t\t...meta,\n\t\t\t\t});\n\n\t\t\tif (typeof token !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'token is required');\n\t\t\t}\n\n\t\t\tconst pendingCipher = await users.getMfaPendingSecret(ctx.user!.id);\n\t\t\tconst pendingSecret = pendingCipher ? mfaCipher.decrypt(pendingCipher) : null;\n\n\t\t\tif (pendingSecret) {\n\t\t\t\t// ── Setup confirmation (TOTP only — backup codes cannot confirm setup) ──\n\t\t\t\tif (!verifyTotpToken(token, pendingSecret)) {\n\t\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_CODE', 'Invalid MFA token');\n\t\t\t\t}\n\t\t\t\tawait users.confirmMfaSecret(ctx.user!.id);\n\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\t\ttype: MESSAGE_KEYS.mfaEnabled,\n\t\t\t\t\t\tlocale: ctx.user!.locale,\n\t\t\t\t\t\tdata: {},\n\t\t\t\t\t\trecipient: { email: ctx.user!.email, phone: null, deviceToken: null },\n\t\t\t\t\t} satisfies ICourierMessage));\n\n\t\t\t\treturn setApiResponse(HTTP.OK, 'MFA_VERIFIED', 'MFA verified successfully.', {\n\t\t\t\t\tmfaEnabled: true,\n\t\t\t\t});\n\t\t\t} else if (/^[A-Z0-9]{8}$/i.test(token)) {\n\t\t\t\t// ── Backup code consumption ────────────────────────────────────────────\n\t\t\t\tif (!ctx.user!.mfaPending) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t\t\t'MFA_NOT_PENDING',\n\t\t\t\t\t\t'Use the mfaToken from the login response',\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (!ctx.user!.mfaEnabled) {\n\t\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'MFA_NOT_CONFIGURED', 'MFA not configured');\n\t\t\t\t}\n\n\t\t\t\tconst unused = await backupCodes.findUnused(ctx.user!.id);\n\t\t\t\tconst checks = await Promise.all(\n\t\t\t\t\tunused.map(async (row) => ({\n\t\t\t\t\t\tid: row.id,\n\t\t\t\t\t\tmatch: await verifyPassword(token.toUpperCase(), row.codeHash),\n\t\t\t\t\t})),\n\t\t\t\t);\n\t\t\t\tconst matched = checks.find((r) => r.match);\n\n\t\t\t\tif (!matched) {\n\t\t\t\t\tif (isLoginCompletion) await failedMfa();\n\t\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_CODE', 'Invalid backup code');\n\t\t\t\t}\n\n\t\t\t\tawait backupCodes.consume(matched.id);\n\t\t\t} else {\n\t\t\t\t// ── TOTP login verification ───────────────────────────────────────────\n\t\t\t\tif (!ctx.user!.mfaPending) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t\t\t'MFA_NOT_PENDING',\n\t\t\t\t\t\t'Use the mfaToken from the login response',\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst storedSecret = await users.getMfaSecret(ctx.user!.id);\n\t\t\t\tif (!storedSecret) {\n\t\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'MFA_NOT_CONFIGURED', 'MFA not configured');\n\t\t\t\t}\n\t\t\t\tconst secret = mfaCipher.decrypt(storedSecret);\n\t\t\t\tif (!verifyTotpToken(token, secret)) {\n\t\t\t\t\tif (isLoginCompletion) await failedMfa();\n\t\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_CODE', 'Invalid MFA token');\n\t\t\t\t}\n\t\t\t\tif (!ctx.user!.mfaEnabled) {\n\t\t\t\t\tawait users.enableMfa(ctx.user!.id);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst { accessToken, refreshToken, sid } = issueTokenPair(ctx.user!.id, config, {\n\t\t\t\tloginMethod: ctx.user!.loginMethod,\n\t\t\t\tphoneVerified: ctx.user!.phoneVerified,\n\t\t\t});\n\t\t\tawait sessions.create(ctx.user!.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);\n\t\t\tif (isLoginCompletion) {\n\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\tuserId: ctx.user!.id,\n\t\t\t\t\temailAttempted: ctx.user!.email,\n\t\t\t\t\tmethod: 'mfa',\n\t\t\t\t\toutcome: 'success',\n\t\t\t\t\t...meta,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst fullUser = await users.findById(ctx.user!.id);\n\t\t\tif (!fullUser) {\n\t\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'User not found after MFA verify');\n\t\t\t}\n\n\t\t\treturn Response.json(\n\t\t\t\t{\n\t\t\t\t\treason: 'MFA_VERIFIED',\n\t\t\t\t\texplanation: 'MFA verified successfully.',\n\t\t\t\t\tresult: {\n\t\t\t\t\t\ttokens: { access: accessToken, refresh: refreshToken },\n\t\t\t\t\t\tuser: toUserDTO(fullUser, ctx.user!.phoneVerified),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\theaders: cookieHeaders(tokenPairCookies(accessToken, refreshToken, config)),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\t// ── 3. Regenerate backup codes ────────────────────────────\n\t\tregenerateBackupCodes: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst token = body?.['token'];\n\n\t\t\tif (typeof token !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'TOTP token is required');\n\t\t\t}\n\n\t\t\tif (!ctx.user!.mfaEnabled) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'MFA_NOT_ENABLED', 'MFA is not enabled');\n\t\t\t}\n\n\t\t\tconst storedSecret = await users.getMfaSecret(ctx.user!.id);\n\t\t\tconst secret = storedSecret ? mfaCipher.decrypt(storedSecret) : null;\n\t\t\tif (!secret || !verifyTotpToken(token, secret)) {\n\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_CODE', 'Invalid TOTP code');\n\t\t\t}\n\n\t\t\tconst plainCodes = generateBackupCodes();\n\t\t\tconst codeHashes = await Promise.all(plainCodes.map((c) => hashPassword(c)));\n\t\t\tawait backupCodes.replace(ctx.user!.id, codeHashes);\n\n\t\t\tawait background(bus\n\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\ttype: MESSAGE_KEYS.mfaBackupCodesRegenerated,\n\t\t\t\t\tlocale: ctx.user!.locale,\n\t\t\t\t\tdata: {},\n\t\t\t\t\trecipient: { email: ctx.user!.email, phone: null, deviceToken: null },\n\t\t\t\t} satisfies ICourierMessage));\n\n\t\t\treturn setApiResponse(HTTP.OK, 'BACKUP_CODES_REGENERATED', 'Backup codes regenerated.', {\n\t\t\t\tbackupCodes: plainCodes,\n\t\t\t});\n\t\t},\n\n\t\t// ── 4. Disable ─────────────────────────────────────────────\n\t\tdisable: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst token = body?.['token'];\n\n\t\t\tif (typeof token !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'TOTP token is required');\n\t\t\t}\n\n\t\t\tconst user = await users.findById(ctx.user!.id);\n\t\t\tif (!user || !user.mfaEnabled) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'MFA_NOT_ENABLED', 'MFA is not enabled');\n\t\t\t}\n\n\t\t\t// Fetched on demand like mfa.verify — the encrypted secret no longer\n\t\t\t// rides along on every user row.\n\t\t\tconst storedSecret = await users.getMfaSecret(ctx.user!.id);\n\t\t\tconst secret = storedSecret ? mfaCipher.decrypt(storedSecret) : null;\n\t\t\tif (!secret || !verifyTotpToken(token, secret)) {\n\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_CODE', 'Invalid TOTP code');\n\t\t\t}\n\n\t\t\tawait Promise.all([users.disableMfa(ctx.user!.id), backupCodes.deleteByUser(ctx.user!.id)]);\n\n\t\t\tawait background(bus\n\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\ttype: MESSAGE_KEYS.mfaDisabled,\n\t\t\t\t\tlocale: user.locale,\n\t\t\t\t\tdata: {},\n\t\t\t\t\trecipient: { email: user.email, phone: null, deviceToken: null },\n\t\t\t\t} satisfies ICourierMessage));\n\n\t\t\treturn setApiResponse(HTTP.OK, 'MFA_DISABLED', 'MFA disabled successfully.');\n\t\t},\n\t};\n}\n","import type { LocationResolver } from './services/request-location';\nimport type { IAuthRateLimitConfig } from './services/rate-limit';\nexport const DEFAULT_VERIFICATION_COOLDOWN = 5 * 60 * 1000; // 5 minutes\nexport const DEFAULT_SESSION_DURATION = '7d';\n\n// Boot-time only — never resolvable at runtime\nexport interface IAuthSecrets {\n\tjwtSecret: string;\n\t// Optional 32-byte key (64 hex chars, e.g. `openssl rand -hex 32`) that\n\t// encrypts TOTP secrets at rest (AES-256-GCM). Unset → secrets are stored\n\t// plaintext (backward-compatible). Set it in production; losing it makes\n\t// existing MFA secrets unrecoverable (users must re-enroll).\n\tmfaSecretKey?: string;\n\tgoogle?: {\n\t\tclientId: string;\n\t\tclientSecret: string;\n\t\tredirectUri: string;\n\t};\n\t// Sign in with Apple. Unlike Google, Apple's client secret is not a static\n\t// string — it's a short-lived ES256 JWT minted from a .p8 key at token\n\t// exchange, so the config carries the signing material, not a secret.\n\tapple?: {\n\t\t// Services ID (Sign in with Apple identifier) — the `aud` for the WEB\n\t\t// redirect flow and the `sub` of the client-secret JWT.\n\t\tclientId: string;\n\t\t// Apple Developer Team ID — the `iss` of the client-secret JWT.\n\t\tteamId: string;\n\t\t// Key ID of the .p8 signing key — the `kid` header of the client secret.\n\t\tkeyId: string;\n\t\t// Contents of the .p8 private key (PEM, `-----BEGIN PRIVATE KEY----- …`).\n\t\tprivateKey: string;\n\t\t// Web form_post callback URL registered with Apple.\n\t\tredirectUri: string;\n\t\t// iOS bundle identifiers whose NATIVE identityToken we accept. Native Sign\n\t\t// in with Apple mints id_tokens with `aud` = the app's bundle id (NOT the\n\t\t// Services ID), so each must be allow-listed for POST /auth/apple/native.\n\t\tnativeClientIds?: string[];\n\t};\n}\n\n// Behavioral — safe to expose to admin dashboard\nexport interface IAuthRuntimeConfig {\n\tsessionDuration?: string;\n\tverificationCooldown?: number;\n\tmfa?: boolean;\n\trequireVerification?: boolean;\n}\n\n// Type-checked: adding/renaming a field in IAuthRuntimeConfig breaks this at compile time\nexport const AUTH_CONFIG_KEYS: Record<keyof IAuthRuntimeConfig, string> = {\n\tsessionDuration: 'auth.session.duration',\n\tverificationCooldown: 'auth.verification.cooldown',\n\tmfa: 'auth.mfa.enabled',\n\trequireVerification: 'auth.verification.required',\n} as const;\n\nexport const MESSAGE_KEYS = {\n\temailRegistration: 'email-registration',\n\temailVerification: 'email-verification',\n\tpasswordReset: 'password-reset',\n\tphoneOtp: 'phone-otp',\n\tmfaEnabled: 'mfa-enabled',\n\tmfaDisabled: 'mfa-disabled',\n\tmfaBackupCodesRegenerated: 'mfa-backup-codes-regenerated',\n\temailChanged: 'email-changed',\n\tphoneChanged: 'phone-changed',\n\t// Changing HOW an account can be signed into is a security event for the\n\t// account's owner, who may not be the person doing it. Both directions are\n\t// notified for the same reason password changes are.\n\t// A password was REVOKED because the account it sat on had never been\n\t// verified and an OAuth identity proved ownership of the mailbox. The\n\t// recipient is now known to own that address, so this reaches the right\n\t// person — and it is the only way they learn a password stopped working.\n\tpasswordRevoked: 'password-revoked',\n\t// Welcome for an account CREATED by an OAuth sign-in. Distinct from\n\t// emailRegistration, which carries a verification PIN — an OAuth signup\n\t// arrives already verified by the provider and has nothing to confirm.\n\toauthRegistration: 'oauth-registration',\n\toauthLinked: 'oauth-linked',\n\toauthUnlinked: 'oauth-unlinked',\n} as const;\n\nexport type AuthMessageKey = (typeof MESSAGE_KEYS)[keyof typeof MESSAGE_KEYS];\n\nexport const EVENT_KEYS = {\n\tuserRegistered: 'fonderie.user.registered',\n\tuserDeleted: 'fonderie.user.deleted',\n\t// Emitted by purgeSoftDeletedUsers (given a bus) once the row is hard-deleted.\n\tuserPurged: 'fonderie.user.purged',\n\temailVerified: 'fonderie.user.email_verified',\n\tpasswordChanged: 'fonderie.user.password_changed',\n} as const;\n\nexport type AuthEventKey = (typeof EVENT_KEYS)[keyof typeof EVENT_KEYS];\n\nexport interface IAuthConfig extends IAuthSecrets, IAuthRuntimeConfig {\n\t// Optional resolver: where is this request from? Auth calls it when it\n\t// records an auth event — every login attempt, every registration, and every\n\t// new session — and stores the result on that row (country / region / city\n\t// / time zone, plus network facts if the resolver knows them). Called at\n\t// most once per request. Absent ⇒ no location, exactly as before. The result\n\t// is sanitized and bounded; a resolver that throws or takes longer than\n\t// 500 ms leaves the row without a location and never fails the request.\n\t// In-process resolvers never approach that; the cap exists for a DB-backed\n\t// or hosted-API resolver (richer data: ISP, ASN, proxy/VPN).\n\t// On Vercel/Cloudflare: ({ headers }) => geoFromHeaders(headers, { trust })\n\t// from @fonderie/geo — zero infrastructure.\n\tlocation?: LocationResolver;\n\t// Adds the Secure attribute to auth cookies. Defaults to\n\t// NODE_ENV === 'production'; set explicitly when that heuristic is wrong.\n\tsecureCookies?: boolean;\n\t// Brute-force protection — ON by default, backed by the module's own\n\t// store adapter (distributed-correct across instances with zero config).\n\t// Inject a store (e.g. RedisStore) for high-throughput deployments,\n\t// override individual rules, or set false to disable entirely.\n\trateLimit?: IAuthRateLimitConfig | false;\n\t// Access-token lifetime (jsonwebtoken duration string). Default '24h'.\n\t// Access tokens are session-bound and die on logout / rotation /\n\t// password change regardless of this value; shorten it to bound the\n\t// window of a stolen token whose session is still alive.\n\taccessTokenDuration?: string;\n\tproviders: ('email' | 'phone' | 'google' | 'github' | 'apple')[];\n\tappName?: string;\n\t// Base URL for the password-reset LINK. When set, forgot-password emits a\n\t// ready-built `resetUrl` (base + `token=<high-entropy token>`) in the\n\t// notification payload, so a template can offer a click-to-reset link\n\t// instead of only the 6-digit code. Unset → resetUrl is '' and only the\n\t// pin flows (unchanged behaviour). The token backs POST /auth/email/reset\n\t// with `{ token, password }`.\n\tpasswordResetUrl?: string;\n\tresolve?: (ctx: { meta: Record<string, unknown> }) => Partial<IAuthRuntimeConfig>;\n\t// Override the HTTP path (and optionally method) of any auth route, keyed by a\n\t// stable id. Lets an app match an existing frontend's contract without a\n\t// gateway/shim, e.g. `{ forgotPassword: '/auth/forgot-password', updateProfile:\n\t// { method: 'PATCH', path: '/users/me' } }`. A bare string overrides the path;\n\t// an object can also change the method. Unset routes keep their defaults.\n\troutes?: Partial<Record<AuthRouteId, AuthRouteOverride>>;\n\t// Migrating an existing app onto Fonderie auth? Fonderie stores bcrypt\n\t// hashes, so a user imported with a foreign hash (argon2, scrypt, pbkdf2,\n\t// a framework's format, …) can't log in via the built-in bcrypt check.\n\t// Provide `legacyVerify` to validate that foreign hash on login; on the\n\t// first successful login Fonderie transparently re-stores the password as\n\t// bcrypt (rehash-on-login), so the legacy verifier is only ever hit once\n\t// per migrated user. Return true iff `plain` matches `hash`. If the imported\n\t// hashes are already bcrypt, you don't need this — the built-in check\n\t// accepts them and re-stores at the current cost factor automatically.\n\tlegacyVerify?: (plain: string, hash: string) => boolean | Promise<boolean>;\n\t// SAR export contributors from other modules (see IDataExportContributor).\n\tdataExportContributors?: IDataExportContributor[];\n}\n\n// A module's contribution to the per-user data export (SAR). The app wires\n// these (e.g. from @fonderie/workspaces) so auth can aggregate data owned by\n// other packages without importing them.\nexport interface IDataExportContributor {\n\tname: string;\n\tcollect: (userId: string) => Promise<unknown> | unknown;\n}\n\n// Stable ids for every auth route, for the `routes` path/method override map.\nexport type AuthRouteId =\n\t| 'providers' | 'unlinkOauth'\n\t| 'register' | 'login' | 'refresh'\n\t| 'forgotPassword' | 'resetPassword'\n\t| 'verifyEmail' | 'sendVerification'\n\t| 'logout'\n\t| 'me' | 'updateProfile' | 'updatePreferences' | 'updateEmail' | 'updatePhone' | 'changePassword' | 'deleteMe' | 'exportMe'\n\t| 'loginHistory' | 'listSessions' | 'terminateSession' | 'terminateOtherSessions'\n\t| 'mfaSetup' | 'mfaVerify' | 'mfaDisable' | 'mfaBackupCodes';\n\nexport type AuthRouteOverride = string | { method?: string; path?: string };\n","import { randomUUID } from 'node:crypto';\nimport jwt, { type SignOptions } from 'jsonwebtoken';\n\nimport type { IAuthConfig } from '../config';\n\nexport interface TokenPair {\n\taccessToken: string;\n\trefreshToken: string;\n\t// Server-side session id both tokens are bound to. Store it on the\n\t// fonderie_sessions row — deleting that row revokes the access token\n\t// immediately, not just the refresh token.\n\tsid: string;\n}\n\nexport interface IAccessPayload {\n\tsub: string; // userId\n\ttype: 'access';\n\tloginMethod: 'email' | 'phone' | 'google' | 'apple';\n\tphoneVerified: boolean;\n\tmfaPending?: boolean;\n\t// Absent only on legacy tokens issued before session binding and on\n\t// short-lived mfaPending tokens (no session exists yet at that point).\n\tsid?: string;\n}\n\nexport interface IRefreshPayload {\n\tsub: string;\n\ttype: 'refresh';\n\tloginMethod: 'email' | 'phone' | 'google' | 'apple';\n\tphoneVerified: boolean;\n\tsid?: string;\n}\n\nexport interface ITokenOptions {\n\tloginMethod: 'email' | 'phone' | 'google' | 'apple';\n\tphoneVerified?: boolean;\n}\n\nexport function issueMfaPendingToken(\n\tuserId: string,\n\tconfig: IAuthConfig,\n\tloginMethod: 'email' | 'phone',\n): string {\n\treturn jwt.sign(\n\t\t{\n\t\t\tsub: userId,\n\t\t\ttype: 'access',\n\t\t\tloginMethod,\n\t\t\tphoneVerified: false,\n\t\t\tmfaPending: true,\n\t\t} satisfies IAccessPayload,\n\t\tconfig.jwtSecret,\n\t\t{ expiresIn: '5m' },\n\t);\n}\n\nexport function issueTokenPair(\n\tuserId: string,\n\tconfig: IAuthConfig,\n\toptions: ITokenOptions,\n): TokenPair {\n\tconst duration = config.sessionDuration ?? '7d';\n\tconst accessDuration = config.accessTokenDuration ?? '24h';\n\tconst loginMethod = options.loginMethod;\n\tconst phoneVerified = options.phoneVerified ?? false;\n\tconst sid = randomUUID();\n\n\tconst accessToken = jwt.sign(\n\t\t{ sub: userId, type: 'access', loginMethod, phoneVerified, sid } satisfies IAccessPayload,\n\t\tconfig.jwtSecret,\n\t\t{ expiresIn: accessDuration } as SignOptions,\n\t);\n\n\tconst refreshToken = jwt.sign(\n\t\t{ sub: userId, type: 'refresh', loginMethod, phoneVerified, sid } satisfies IRefreshPayload,\n\t\tconfig.jwtSecret,\n\t\t{ expiresIn: duration } as SignOptions,\n\t);\n\n\treturn { accessToken, refreshToken, sid };\n}\n\nexport function refreshTokenExpiry(token: string): Date {\n\tconst decoded = jwt.decode(token) as { exp?: number } | null;\n\treturn decoded?.exp\n\t\t? new Date(decoded.exp * 1000)\n\t\t: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);\n}\n\nexport function verifyToken(\n\ttoken: string,\n\tconfig: IAuthConfig,\n): IAccessPayload | IRefreshPayload | null {\n\ttry {\n\t\treturn jwt.verify(token, config.jwtSecret) as IAccessPayload | IRefreshPayload;\n\t} catch {\n\t\treturn null;\n\t}\n}\n","import { createHmac, randomBytes } from 'node:crypto';\n\nimport { constantTimeEqual } from '@fonderie/core';\n\n// ── TOTP (RFC 6238) — no external dependency ─────────────────────\n\nconst STEP = 30; // seconds per window\nconst DRIFT = 1; // ±1 window tolerance\nconst DIGITS = 6;\n\nfunction hotp(secret: string, counter: number): string {\n\tconst buf = Buffer.alloc(8);\n\tlet c = counter;\n\n\tfor (let i = 7; i >= 0; i--) {\n\t\tbuf[i] = c & 0xff;\n\t\tc >>= 8;\n\t}\n\n\tconst key = Buffer.from(base32Decode(secret));\n\tconst hmac = createHmac('sha1', key).update(buf).digest();\n\tconst offset = (hmac[19] ?? 0) & 0x0f;\n\tconst code =\n\t\t((((hmac[offset] ?? 0) & 0x7f) << 24) |\n\t\t\t(((hmac[offset + 1] ?? 0) & 0xff) << 16) |\n\t\t\t(((hmac[offset + 2] ?? 0) & 0xff) << 8) |\n\t\t\t((hmac[offset + 3] ?? 0) & 0xff)) %\n\t\tMath.pow(10, DIGITS);\n\n\treturn code.toString().padStart(DIGITS, '0');\n}\n\nfunction timeCounter(): number {\n\treturn Math.floor(Date.now() / 1000 / STEP);\n}\n\n// RFC 4648 base32 decode — uppercase alphabet only\nfunction base32Decode(input: string): Buffer {\n\tconst ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n\tconst clean = input.toUpperCase().replace(/=+$/, '');\n\tlet bits = 0;\n\tlet value = 0;\n\n\tconst output: number[] = [];\n\tfor (const char of clean) {\n\t\tconst idx = ALPHABET.indexOf(char);\n\t\tif (idx === -1) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tvalue = (value << 5) | idx;\n\t\tbits += 5;\n\n\t\tif (bits >= 8) {\n\t\t\toutput.push((value >>> (bits - 8)) & 0xff);\n\t\t\tbits -= 8;\n\t\t}\n\t}\n\n\treturn Buffer.from(output);\n}\n\nfunction base32Encode(buf: Buffer): string {\n\tconst ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n\tlet bits = 0;\n\tlet value = 0;\n\tlet output = '';\n\n\tfor (const byte of buf) {\n\t\tvalue = (value << 8) | byte;\n\t\tbits += 8;\n\t\twhile (bits >= 5) {\n\t\t\toutput += ALPHABET[(value >>> (bits - 5)) & 0x1f];\n\t\t\tbits -= 5;\n\t\t}\n\t}\n\n\tif (bits > 0) {\n\t\toutput += ALPHABET[(value << (5 - bits)) & 0x1f];\n\t}\n\n\treturn output;\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\nexport function generateTotpSecret(): string {\n\treturn base32Encode(randomBytes(20));\n}\n\nexport function generateTotpUri(email: string, secret: string, issuer: string): string {\n\tconst params = new URLSearchParams({\n\t\tsecret,\n\t\tissuer,\n\t\talgorithm: 'SHA1',\n\t\tdigits: String(DIGITS),\n\t\tperiod: String(STEP),\n\t});\n\n\treturn `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(email)}?${params}`;\n}\n\nexport function verifyTotpToken(token: string, secret: string): boolean {\n\tconst t = timeCounter();\n\tfor (let i = -DRIFT; i <= DRIFT; i++) {\n\t\t// Constant-time compare (shared) — a plain === leaks how many leading\n\t\t// digits matched through response timing.\n\t\tif (constantTimeEqual(hotp(secret, t + i), token)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\n\treturn false;\n}\n\nexport function generateTotpCode(secret: string): string {\n\treturn hotp(secret, timeCounter());\n}\n\nexport function generateBackupCodes(count = 8): string[] {\n\treturn Array.from({ length: count }, () => randomBytes(4).toString('hex').toUpperCase());\n}\n","import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\n\n// At-rest encryption for TOTP secrets. Unlike passwords, a TOTP secret must stay\n// reversible (the server recomputes codes from it), so it can't be hashed — it's\n// encrypted with a key the app holds instead. AES-256-GCM; output is\n// `mfa.v1:iv:tag:ciphertext`, all hex. Authenticated (tampering fails on decrypt).\n//\n// Backward compatible by design:\n// - No key configured → passthrough. Secrets are stored plaintext exactly as\n// before this change, so upgrading without a key is a no-op.\n// - With a key, `decrypt` still passes through any value lacking the `mfa.v1:`\n// prefix, so pre-existing plaintext secrets keep working and are re-encrypted\n// the next time the user re-runs MFA setup (lazy migration). A base32 TOTP\n// secret never starts with the prefix, so the two are unambiguous.\nconst PREFIX = 'mfa.v1:';\n\nexport interface IMfaCipher {\n\tencrypt(plain: string): string;\n\tdecrypt(stored: string): string;\n}\n\nexport function makeMfaCipher(keyHex?: string): IMfaCipher {\n\tif (!keyHex) {\n\t\t// Passthrough: no key, no encryption. `decrypt` is identity — a legacy\n\t\t// plaintext secret returns unchanged; a prefixed value (written when a key\n\t\t// was previously set, now removed) can't be recovered and will simply fail\n\t\t// TOTP verification, which is the correct outcome for a lost key.\n\t\treturn { encrypt: (plain) => plain, decrypt: (stored) => stored };\n\t}\n\n\tconst key = Buffer.from(keyHex, 'hex');\n\tif (key.length !== 32) {\n\t\tthrow new Error('[auth] mfaSecretKey must be 32 bytes (64 hex chars, e.g. `openssl rand -hex 32`)');\n\t}\n\n\treturn {\n\t\tencrypt(plain) {\n\t\t\tconst iv = randomBytes(12);\n\t\t\tconst cipher = createCipheriv('aes-256-gcm', key, iv);\n\t\t\tconst enc = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]);\n\t\t\tconst tag = cipher.getAuthTag();\n\t\t\treturn `${PREFIX}${iv.toString('hex')}:${tag.toString('hex')}:${enc.toString('hex')}`;\n\t\t},\n\t\tdecrypt(stored) {\n\t\t\tif (!stored.startsWith(PREFIX)) return stored; // legacy plaintext — pass through\n\t\t\tconst [ivHex, tagHex, dataHex] = stored.slice(PREFIX.length).split(':');\n\t\t\tif (!ivHex || !tagHex || !dataHex) {\n\t\t\t\tthrow new Error('[auth] malformed mfa secret ciphertext');\n\t\t\t}\n\t\t\tconst decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(ivHex, 'hex'));\n\t\t\tdecipher.setAuthTag(Buffer.from(tagHex, 'hex'));\n\t\t\treturn Buffer.concat([\n\t\t\t\tdecipher.update(Buffer.from(dataHex, 'hex')),\n\t\t\t\tdecipher.final(),\n\t\t\t]).toString('utf8');\n\t\t},\n\t};\n}\n","import { stringOrEmpty, booleanOrFalse, dateOrEmpty } from '@fonderie/core';\n\nimport type { IUser, IUserPreferences } from '../types';\n\nexport interface IUserDTO {\n\tid: string;\n\temail: string;\n\tfirstName: string;\n\tlastName: string;\n\tphone: string;\n\tprofileImageUrl: string;\n\tisActive: boolean;\n\tlastLogin: string;\n\tpreferences: IUserPreferences;\n\tisEmailVerified: boolean;\n\tisPhoneVerified: boolean;\n\tmfaEnabled: boolean;\n\t// OAuth provider linked to this account ('google', 'apple'), or '' if none.\n\tprovider: string;\n\t// Whether a password is set. Together with `provider` this is everything a\n\t// settings screen needs to render sign-in methods AND to know that\n\t// disconnecting the provider is possible — an account with no password has\n\t// no other credential, so unlinking is refused (409 PASSWORD_REQUIRED).\n\t// Exposed as a boolean precisely so the hash never can be.\n\thasPassword: boolean;\n\tsuspended: boolean;\n\twhitelist: boolean;\n\tipWhitelist: string[];\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\nconst DEFAULT_PREFERENCES: IUserPreferences = {\n\tlocale: 'en-US',\n\ttimezone: 'UTC',\n\tnotifications: { email: true, inApp: true, sms: false, push: false },\n\temailDigest: 'immediate',\n\tdateFormat: 'MM/DD/YYYY',\n\ttimeFormat: 'hh:mm A',\n};\n\n// Historical rows may hold garbage preferences (the update schema accepted\n// `unknown` for four keys until it was typed). Only well-typed values\n// survive into the DTO, and notifications deep-merge over the defaults so a\n// partial stored object can't shrink the shape the client type promises.\nfunction cleanPreferences(prefs: IUserPreferences): Partial<IUserPreferences> {\n\tconst out: Partial<IUserPreferences> = {};\n\tif (typeof prefs.locale === 'string') out.locale = prefs.locale;\n\tif (typeof prefs.timezone === 'string') out.timezone = prefs.timezone;\n\tif (typeof prefs.emailDigest === 'string') out.emailDigest = prefs.emailDigest;\n\tif (typeof prefs.dateFormat === 'string') out.dateFormat = prefs.dateFormat;\n\tif (typeof prefs.timeFormat === 'string') out.timeFormat = prefs.timeFormat;\n\treturn out;\n}\n\nfunction cleanNotifications(value: unknown): Partial<IUserPreferences['notifications']> {\n\tif (!value || typeof value !== 'object') return {};\n\tconst out: Partial<IUserPreferences['notifications']> = {};\n\tfor (const key of ['email', 'inApp', 'sms', 'push'] as const) {\n\t\tconst v = (value as Record<string, unknown>)[key];\n\t\tif (typeof v === 'boolean') out[key] = v;\n\t}\n\treturn out;\n}\n\nexport function toUserDTO(user: IUser, phoneVerified = false): IUserDTO {\n\tconst prefs = user.preferences ?? ({} as IUserPreferences);\n\tconst cleaned = cleanPreferences(prefs);\n\treturn {\n\t\tid: stringOrEmpty(user.id),\n\t\temail: stringOrEmpty(user.email),\n\t\tfirstName: stringOrEmpty(user.firstName),\n\t\tlastName: stringOrEmpty(user.lastName),\n\t\tphone: stringOrEmpty(user.phone),\n\t\tprofileImageUrl: stringOrEmpty(user.profileImageUrl),\n\t\tisActive: typeof user.isActive === 'boolean' ? user.isActive : true,\n\t\tlastLogin: dateOrEmpty(user.lastLogin),\n\t\tpreferences: {\n\t\t\t...DEFAULT_PREFERENCES,\n\t\t\t...cleaned,\n\t\t\tnotifications: {\n\t\t\t\t...DEFAULT_PREFERENCES.notifications,\n\t\t\t\t...cleanNotifications(prefs.notifications),\n\t\t\t},\n\t\t\tlocale: user.locale || cleaned.locale || 'en-US',\n\t\t\ttimezone: user.timezone || cleaned.timezone || 'UTC',\n\t\t},\n\t\tisEmailVerified: user.emailVerifiedAt !== null,\n\t\tisPhoneVerified: phoneVerified,\n\t\tmfaEnabled: booleanOrFalse(user.mfaEnabled),\n\t\tprovider: stringOrEmpty(user.provider),\n\t\thasPassword: user.passwordHash !== null && user.passwordHash !== undefined,\n\t\tsuspended: booleanOrFalse(user.suspended),\n\t\twhitelist: booleanOrFalse(user.whitelist),\n\t\tipWhitelist: Array.isArray(user.ipWhitelist) ? user.ipWhitelist : [],\n\t\tcreatedAt: dateOrEmpty(user.createdAt),\n\t\tupdatedAt: dateOrEmpty(user.updatedAt),\n\t};\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { IUser } from '../types';\n\nexport interface IUserUpdateFields {\n\tfirstName?: string;\n\tlastName?: string;\n\tphoneNumber?: string;\n\tavatarUrl?: string;\n\tlocale?: string;\n\ttimezone?: string;\n\tpreferences?: unknown;\n}\n\nconst USER_COLUMNS = `\n\tid,\n\temail,\n\tpassword_hash AS \"passwordHash\",\n\tfirst_name AS \"firstName\",\n\tlast_name AS \"lastName\",\n\tphone,\n\tprofile_image_url AS \"profileImageUrl\",\n\tlocale,\n\ttimezone,\n\tis_active AS \"isActive\",\n\tlast_login AS \"lastLogin\",\n\tpreferences,\n\tsuspended,\n\twhitelist,\n\tip_whitelist AS \"ipWhitelist\",\n\tmfa_enabled AS \"mfaEnabled\",\n\temail_verified_at AS \"emailVerifiedAt\",\n\tprovider,\n\tdeleted_at AS \"deletedAt\",\n\tcreated_at AS \"createdAt\",\n\tupdated_at AS \"updatedAt\"\n`;\n\nexport class UserModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\t// One page of users, newest first. Keyset-paginated on (created_at, id) —\n\t// the same cursor contract as login history and the audit log. The +1\n\t// over-fetch that detects a next page lives here so no outer clamp can\n\t// shave it off. Soft-deleted rows are excluded, as in every other finder.\n\tasync list(query: IUserListQuery = {}): Promise<IUserPage> {\n\t\tconst limit = Math.min(query.limit ?? 50, MAX_LIST_LIMIT);\n\t\tconst params: unknown[] = [];\n\t\tconst where: string[] = [query.deleted ? 'deleted_at IS NOT NULL' : 'deleted_at IS NULL'];\n\n\t\tif (query.cursor) {\n\t\t\tparams.push(query.cursor.createdAt, query.cursor.id);\n\t\t\twhere.push(`(created_at, id) < ($${params.length - 1}::timestamptz, $${params.length}::uuid)`);\n\t\t}\n\n\t\tparams.push(limit + 1);\n\t\tconst rows = await this.store.query<IUser & { createdAtRaw?: string }>(\n\t\t\t`SELECT ${USER_COLUMNS}, created_at::text AS \"createdAtRaw\"\n\t\t\t FROM fonderie_users\n\t\t\t WHERE ${where.join(' AND ')}\n\t\t\t ORDER BY created_at DESC, id DESC\n\t\t\t LIMIT $${params.length}`,\n\t\t\tparams,\n\t\t);\n\t\treturn { users: rows.slice(0, limit), hasMore: rows.length > limit };\n\t}\n\n\t// For operator views only: a soft-deleted account still resolves, with\n\t// deletedAt set, so the console can say \"deleted on …\" instead of \"no such\n\t// user\". Nothing that authenticates or acts on an account may use this.\n\tasync findByIdIncludingDeleted(id: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findById(id: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE id = $1 AND deleted_at IS NULL`,\n\t\t\t[id],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findByEmail(email: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE email = $1 AND deleted_at IS NULL`,\n\t\t\t[email],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findByPhone(phone: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NULL`,\n\t\t\t[phone],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findOrCreateByPhone(\n\t\tphone: string,\n\t\tfirstName: string | null = null,\n\t\tlastName: string | null = null,\n\t): Promise<{ id: string }> {\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`INSERT INTO fonderie_users (phone, first_name, last_name)\n\t\t\tVALUES ($1, $2, $3)\n\t\t\tON CONFLICT (phone) DO UPDATE\n\t\t\tSET first_name = COALESCE(EXCLUDED.first_name, fonderie_users.first_name),\n\t\t\t last_name = COALESCE(EXCLUDED.last_name, fonderie_users.last_name),\n\t\t\t updated_at = now()\n\t\t\tRETURNING id`,\n\t\t\t[phone, firstName, lastName],\n\t\t);\n\t\treturn row!;\n\t}\n\n\tasync create(\n\t\temail: string,\n\t\tpasswordHash: string,\n\t\tfirstName: string | null,\n\t\tlastName: string | null,\n\t): Promise<{ id: string } | null> {\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`INSERT INTO fonderie_users (email, password_hash, first_name, last_name)\n\t\t\tVALUES ($1, $2, $3, $4)\n\t\t\tRETURNING id`,\n\t\t\t[email.toLowerCase().trim(), passwordHash, firstName, lastName],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync update(id: string, fields: IUserUpdateFields): Promise<{ id: string } | null> {\n\t\tconst columnMap: Record<string, string> = {\n\t\t\tfirstName: 'first_name',\n\t\t\tlastName: 'last_name',\n\t\t\tphoneNumber: 'phone',\n\t\t\tavatarUrl: 'profile_image_url',\n\t\t\tlocale: 'locale',\n\t\t\ttimezone: 'timezone',\n\t\t\tpreferences: 'preferences',\n\t\t};\n\n\t\tconst sets: string[] = [];\n\t\tconst values: unknown[] = [];\n\n\t\tfor (const [key, col] of Object.entries(columnMap)) {\n\t\t\tif ((fields as Record<string, unknown>)[key] !== undefined) {\n\t\t\t\tvalues.push((fields as Record<string, unknown>)[key]);\n\t\t\t\tsets.push(`${col} = $${values.length}`);\n\t\t\t}\n\t\t}\n\n\t\tvalues.push(id);\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`UPDATE fonderie_users SET ${sets.join(', ')}, updated_at = now() WHERE id = $${values.length} AND deleted_at IS NULL RETURNING id`,\n\t\t\tvalues,\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\t/**\n\t * Clear the OAuth provider, but ONLY when the account can still be signed\n\t * into afterwards.\n\t *\n\t * An account created through Google has no password. Unlinking it without\n\t * checking would not be \"removing a sign-in method\" — it would be locking\n\t * the owner out permanently, which is account deletion wearing a friendlier\n\t * label. The guard lives in the WHERE clause so the check and the write are\n\t * one atomic statement: a password cannot be removed between them.\n\t *\n\t * Returns false when the account has no password (nothing was changed) so\n\t * the caller can say why rather than reporting a silent success.\n\t */\n\tasync clearProvider(id: string): Promise<boolean> {\n\t\tconst rows = await this.store.query<{ id: string }>(\n\t\t\t`UPDATE fonderie_users\n\t\t\t SET provider = NULL, provider_id = NULL\n\t\t\t WHERE id = $1\n\t\t\t AND password_hash IS NOT NULL\n\t\t\t AND provider IS NOT NULL\n\t\t\t RETURNING id`,\n\t\t\t[id],\n\t\t);\n\t\treturn rows.length > 0;\n\t}\n\n\tasync updatePassword(id: string, passwordHash: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_users SET password_hash = $1 WHERE id = $2`, [\n\t\t\tpasswordHash,\n\t\t\tid,\n\t\t]);\n\t}\n\n\tasync markEmailVerified(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET email_verified_at = now(), updated_at = now() WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\t// The operator's lock: login, refresh and the session middleware all refuse a\n\t// suspended user. Returns false when there is no such user.\n\tasync setSuspended(id: string, suspended: boolean): Promise<boolean> {\n\t\tconst rows = await this.store.query<{ id: string }>(\n\t\t\t`UPDATE fonderie_users SET suspended = $2, updated_at = now() WHERE id = $1 AND deleted_at IS NULL RETURNING id`,\n\t\t\t[id, suspended],\n\t\t);\n\t\treturn rows.length > 0;\n\t}\n\n\tasync softDelete(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET deleted_at = now(), updated_at = now() WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync saveMfaSecret(id: string, secret: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_users SET mfa_secret = $1 WHERE id = $2`, [secret, id]);\n\t}\n\n\tasync saveMfaPendingSecret(id: string, secret: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users\n\t\t\t SET mfa_secret_pending = $1, mfa_secret_pending_expires_at = now() + interval '15 minutes'\n\t\t\t WHERE id = $2`,\n\t\t\t[secret, id],\n\t\t);\n\t}\n\n\tasync getMfaPendingSecret(id: string): Promise<string | null> {\n\t\tconst [row] = await this.store.query<{ mfa_secret_pending: string | null }>(\n\t\t\t`SELECT mfa_secret_pending FROM fonderie_users\n\t\t\t WHERE id = $1 AND mfa_secret_pending_expires_at > now()`,\n\t\t\t[id],\n\t\t);\n\t\treturn row?.mfa_secret_pending ?? null;\n\t}\n\n\tasync confirmMfaSecret(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users\n\t\t\t SET mfa_secret = mfa_secret_pending,\n\t\t\t mfa_secret_pending = NULL,\n\t\t\t mfa_secret_pending_expires_at = NULL,\n\t\t\t mfa_enabled = true,\n\t\t\t updated_at = now()\n\t\t\t WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync enableMfa(id: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_users SET mfa_enabled = true WHERE id = $1`, [id]);\n\t}\n\n\tasync disableMfa(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET mfa_enabled = false, mfa_secret = NULL, updated_at = now() WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync updateEmail(id: string, email: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET email = $1, email_verified_at = NULL, updated_at = now() WHERE id = $2`,\n\t\t\t[email.toLowerCase().trim(), id],\n\t\t);\n\t}\n\n\tasync updatePhone(id: string, phone: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET phone = $1, updated_at = now() WHERE id = $2`,\n\t\t\t[phone, id],\n\t\t);\n\t}\n\n\tasync updatePreferences(\n\t\tid: string,\n\t\tfields: {\n\t\t\tlocale?: string;\n\t\t\ttimezone?: string;\n\t\t\tpatch?: Record<string, unknown>;\n\t\t},\n\t): Promise<{ id: string } | null> {\n\t\tconst sets: string[] = [];\n\t\tconst values: unknown[] = [];\n\n\t\tif (fields.locale !== undefined) {\n\t\t\tvalues.push(fields.locale);\n\t\t\tsets.push(`locale = $${values.length}`);\n\t\t}\n\t\tif (fields.timezone !== undefined) {\n\t\t\tvalues.push(fields.timezone);\n\t\t\tsets.push(`timezone = $${values.length}`);\n\t\t}\n\t\tif (fields.patch && Object.keys(fields.patch).length > 0) {\n\t\t\tvalues.push(JSON.stringify(fields.patch));\n\t\t\tsets.push(`preferences = preferences || $${values.length}::jsonb`);\n\t\t}\n\n\t\tif (sets.length === 0) return null;\n\n\t\tvalues.push(id);\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`UPDATE fonderie_users SET ${sets.join(', ')}, updated_at = now() WHERE id = $${values.length} AND deleted_at IS NULL RETURNING id`,\n\t\t\tvalues,\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync getMfaSecret(id: string): Promise<string | null> {\n\t\tconst [row] = await this.store.query<{ mfa_secret: string | null }>(\n\t\t\t`SELECT mfa_secret FROM fonderie_users WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t\treturn row?.mfa_secret ?? null;\n\t}\n\n\t/**\n\t * Link an OAuth identity to the account owning `email`, creating it if there\n\t * is none.\n\t *\n\t * Returns what HAPPENED, not just who it happened to. Three outcomes hide\n\t * behind one upsert, and callers must tell them apart:\n\t *\n\t * • `inserted` — a brand-new account. Whoever provisions on signup (the\n\t * personal workspace, a wallet, a welcome email) must run, exactly as\n\t * it does for a password registration. This is the case that was\n\t * previously invisible: the upsert returned an id and looked identical\n\t * to a returning user signing in, so OAuth signups silently skipped\n\t * every signup side effect.\n\t * • `previousProvider` differs — an existing account gained, or switched,\n\t * a way to sign in. A security event for the owner.\n\t * • `previousProvider` is the same — an ordinary login. Nothing to do, and\n\t * notifying here would email the user on every single sign-in.\n\t *\n\t * The prior state is read in a CTE of the SAME statement, so it sees the\n\t * snapshot from before the insert. Reading it in a separate query would\n\t * race two concurrent logins into both seeing \"new\".\n\t *\n\t * SECURITY — linking is BY EMAIL, so this merges into whatever row already\n\t * holds the address. If that row was never verified, its password was never\n\t * proven to belong to the mailbox; anyone can register an address they do\n\t * not own. The provider HAS proven ownership. So an unverified password is\n\t * revoked at the moment of linking: without that, linking sets\n\t * email_verified_at and the earlier registrant is left holding a working\n\t * password on an account now treated as verified.\n\t *\n\t * The legitimate user loses nothing but a password reset — they own the\n\t * mailbox. Someone who does not own it cannot receive that mail.\n\t */\n\tasync upsertByProvider(\n\t\temail: string,\n\t\tprovider: string,\n\t\tproviderId: string,\n\t): Promise<{\n\t\tid: string;\n\t\tinserted: boolean;\n\t\tpreviousProvider: string | null;\n\t\tclearedUnverifiedPassword: boolean;\n\t} | null> {\n\t\tconst [row] = await this.store.query<{\n\t\t\tid: string;\n\t\t\tinserted: boolean;\n\t\t\tpreviousProvider: string | null;\n\t\t\tclearedUnverifiedPassword: boolean;\n\t\t}>(\n\t\t\t`WITH prior AS (\n\t\t\t\tSELECT provider, email_verified_at, password_hash IS NOT NULL AS had_password\n\t\t\t\t FROM fonderie_users WHERE email = $1\n\t\t\t), upserted AS (\n\t\t\t\tINSERT INTO fonderie_users (email, email_verified_at, provider, provider_id)\n\t\t\t\tVALUES ($1, now(), $2, $3)\n\t\t\t\tON CONFLICT (email) DO UPDATE\n\t\t\t\tSET provider = $2, provider_id = $3,\n\t\t\t\t email_verified_at = COALESCE(fonderie_users.email_verified_at, now()),\n\t\t\t\t -- A password on an UNVERIFIED account is a claim, not a\n\t\t\t\t -- credential: nobody ever proved that mailbox belongs to\n\t\t\t\t -- whoever set it. The provider has now proven it belongs to\n\t\t\t\t -- the person signing in. Drop the unproven claim rather than\n\t\t\t\t -- promote it — otherwise linking marks the account verified\n\t\t\t\t -- and silently hands the earlier registrant a working\n\t\t\t\t -- password on a now-trusted account.\n\t\t\t\t password_hash = CASE\n\t\t\t\t WHEN fonderie_users.email_verified_at IS NULL THEN NULL\n\t\t\t\t ELSE fonderie_users.password_hash\n\t\t\t\t END\n\t\t\t\tRETURNING id\n\t\t\t)\n\t\t\tSELECT upserted.id,\n\t\t\t NOT EXISTS (SELECT 1 FROM prior) AS inserted,\n\t\t\t (SELECT provider FROM prior) AS \"previousProvider\",\n\t\t\t -- True only when a password was actually revoked, so the\n\t\t\t -- caller can tell the owner their sign-in method changed.\n\t\t\t COALESCE((SELECT had_password AND email_verified_at IS NULL FROM prior), false)\n\t\t\t AS \"clearedUnverifiedPassword\"\n\t\t\tFROM upserted`,\n\t\t\t[email, provider, providerId],\n\t\t);\n\t\treturn row ?? null;\n\t}\n}\n\nconst MAX_LIST_LIMIT = 200;\n\nexport interface IUserListQuery {\n\tlimit?: number;\n\tcursor?: { createdAt: string; id: string };\n\t// true ⇒ ONLY soft-deleted accounts (an operator view; every other finder\n\t// excludes them). They stay until the retention purge hard-deletes them.\n\tdeleted?: boolean;\n}\n\nexport interface IUserPage {\n\t// Carries createdAtRaw (created_at::text) so the cursor keeps the\n\t// microsecond precision a Date round-trip would drop.\n\tusers: Array<IUser & { createdAtRaw?: string }>;\n\thasMore: boolean;\n}\n","// Optional location enrichment for auth events (login history, registrations,\n// sessions).\n//\n// Auth never resolves a location itself and imports no geo code: the app hands\n// in a resolver (IAuthConfig.location). Typical choices — the platform's\n// edge headers via @fonderie/geo's geoFromHeaders (zero infrastructure), a\n// self-hosted table, or a hosted IP API that also knows the network (ISP, ASN,\n// proxy/VPN, hosting). Field names match @fonderie/geo's GeoLocation, so its\n// result can be returned as-is.\n//\n// Whatever the resolver returns is treated as untrusted input: every field is\n// type-checked and bounded before it is stored, and a resolver that throws or\n// stalls costs the login nothing — the row is written without a location.\n\n/** Where a login attempt came from. Every field is optional; unknown is null. */\nexport interface IRequestLocation {\n\tcountry?: string | null; // ISO-3166-1 alpha-2\n\tcountryName?: string | null;\n\tsubdivision?: string | null; // ISO-3166-2 region part (QC, CA, ENG)\n\tsubdivisionName?: string | null;\n\tcity?: string | null;\n\t/** Postal / ZIP code as the provider reports it (94043, K1A 0B1, SW1A). Approximate. */\n\tpostalCode?: string | null;\n\tcontinent?: string | null; // two-letter continent code\n\ttimeZone?: string | null; // IANA name\n\tlatitude?: number | null;\n\tlongitude?: number | null;\n\t/** How far off the point may be, in kilometres (MaxMind's accuracy_radius).\n\t * Lets a UI say \"near Mountain View (±20 km)\" instead of implying precision. */\n\taccuracyRadius?: number | null;\n\t/** A stable, language-neutral place key (MaxMind/GeoNames id). Lets a reader\n\t * see the stored place in their own language later, and lets an app map it\n\t * to its own regions. Names above are a snapshot; this is the reference. */\n\tgeonameId?: number | null;\n\t/** Network facts — only resolvers backed by an IP-intelligence API know these. */\n\tisp?: string | null;\n\torg?: string | null;\n\tasn?: string | null; // e.g. \"AS15169\"\n\tmobile?: boolean | null;\n\tproxy?: boolean | null;\n\thosting?: boolean | null;\n}\n\nexport interface ILocationRequest {\n\t/** The resolved client IP (adapter-resolved, proxy-aware), or null. */\n\tip: string | null;\n\t/** The incoming request's headers — for platform geolocation headers. */\n\theaders: Headers;\n}\n\nexport type LocationResolver = (\n\treq: ILocationRequest,\n) => IRequestLocation | null | undefined | Promise<IRequestLocation | null | undefined>;\n\n// How long auth waits for the resolver before writing the row without a\n// location. Internal, deliberately not exported. It is zero-cost for in-process\n// resolvers (edge headers return in microseconds) and exists for the ones that\n// are not: @fonderie/geo's PostgresGeoProvider does a DB query that can hang\n// on an exhausted pool, and an app may choose a hosted IP-intelligence API for\n// richer data (ISP, ASN, proxy/VPN, accuracy radius). A legitimate lookup\n// answers in a few to ~150 ms; the session write that triggers resolution is\n// on the login path, so the cap bounds the worst-case delay a slow provider\n// can add to a sign-in.\nconst LOCATION_TIMEOUT_MS = 500;\n\nconst TEXT_MAX = 128;\n\nfunction str(v: unknown, re?: RegExp): string | null {\n\tif (typeof v !== 'string') return null;\n\tconst t = v.trim();\n\tif (t.length === 0 || t.length > TEXT_MAX) return null;\n\treturn re && !re.test(t) ? null : t;\n}\nfunction upperCode(v: unknown, re: RegExp): string | null {\n\tconst t = str(v, re);\n\treturn t ? t.toUpperCase() : null;\n}\nfunction bool(v: unknown): boolean | null {\n\treturn typeof v === 'boolean' ? v : null;\n}\n// Two decimals ≈ 1 km: plenty for \"which city\", and a login history should not\n// keep more precision than the city it names.\nfunction coord(v: unknown, limit: number): number | null {\n\tif (typeof v !== 'number' || !Number.isFinite(v) || Math.abs(v) > limit) return null;\n\treturn Math.round(v * 100) / 100;\n}\n\n// Kilometres, a positive whole number. MaxMind's values run 1–1000; anything\n// beyond the planet's half-circumference is not a radius.\nfunction radius(v: unknown): number | null {\n\tif (typeof v !== 'number' || !Number.isFinite(v) || v <= 0 || v > 20_000) return null;\n\treturn Math.round(v);\n}\n\n// GeoNames ids are positive integers well inside 2^31.\nfunction placeId(v: unknown): number | null {\n\tconst n = typeof v === 'string' && /^\\d{1,10}$/.test(v) ? Number(v) : v;\n\treturn typeof n === 'number' && Number.isInteger(n) && n > 0 && n <= 2_147_483_647 ? n : null;\n}\n\n/** Keep only well-formed, bounded fields. Returns null when nothing survives. */\nexport function sanitizeLocation(input: unknown): IRequestLocation | null {\n\tif (!input || typeof input !== 'object') return null;\n\tconst i = input as Record<string, unknown>;\n\tconst out: IRequestLocation = {\n\t\tcountry: upperCode(i['country'], /^[A-Za-z]{2}$/),\n\t\tcountryName: str(i['countryName']),\n\t\tsubdivision: upperCode(i['subdivision'], /^[A-Za-z0-9]{1,3}$/),\n\t\tsubdivisionName: str(i['subdivisionName']),\n\t\tcity: str(i['city']),\n\t\tpostalCode: upperCode(i['postalCode'], /^[A-Za-z0-9][A-Za-z0-9 -]{0,11}$/),\n\t\tcontinent: upperCode(i['continent'], /^[A-Za-z]{2}$/),\n\t\ttimeZone: str(i['timeZone'], /^(UTC|[A-Za-z_]+(\\/[A-Za-z0-9_+-]+)+)$/),\n\t\tlatitude: coord(i['latitude'], 90),\n\t\tlongitude: coord(i['longitude'], 180),\n\t\taccuracyRadius: radius(i['accuracyRadius']),\n\t\tgeonameId: placeId(i['geonameId']),\n\t\tisp: str(i['isp']),\n\t\torg: str(i['org']),\n\t\tasn: upperCode(i['asn'], /^AS\\d{1,10}$/i),\n\t\tmobile: bool(i['mobile']),\n\t\tproxy: bool(i['proxy']),\n\t\thosting: bool(i['hosting']),\n\t};\n\t// Drop nulls so the stored JSON carries only what is known.\n\tconst known = Object.fromEntries(Object.entries(out).filter(([, v]) => v !== null)) as IRequestLocation;\n\treturn Object.keys(known).length > 0 ? known : null;\n}\n\n/** Run the resolver bounded by a timeout; never throws. */\n// One resolution per request: a sign-in writes a login-event row AND a session\n// row, and both ask. Keyed on the request's Headers object (unique per\n// request, collected with it), so the resolver — possibly a paid API — runs\n// once.\nconst perRequest = new WeakMap<Headers, { resolver: LocationResolver; result: Promise<IRequestLocation | null> }>();\n\nexport function resolveLocation(\n\tresolver: LocationResolver | undefined,\n\treq: ILocationRequest,\n\ttimeoutMs = LOCATION_TIMEOUT_MS,\n): Promise<IRequestLocation | null> {\n\tif (!resolver) return Promise.resolve(null);\n\tconst cached = perRequest.get(req.headers);\n\tif (cached && cached.resolver === resolver) return cached.result;\n\tconst result = resolveOnce(resolver, req, timeoutMs);\n\tperRequest.set(req.headers, { resolver, result });\n\treturn result;\n}\n\nasync function resolveOnce(\n\tresolver: LocationResolver,\n\treq: ILocationRequest,\n\ttimeoutMs: number,\n): Promise<IRequestLocation | null> {\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\ttry {\n\t\tconst result = await Promise.race([\n\t\t\tPromise.resolve().then(() => resolver(req)),\n\t\t\tnew Promise<null>((resolve) => {\n\t\t\t\ttimer = setTimeout(() => resolve(null), timeoutMs);\n\t\t\t}),\n\t\t]);\n\t\treturn sanitizeLocation(result);\n\t} catch (err) {\n\t\tconsole.warn('[auth] location resolver failed; recording without a location:', err);\n\t\treturn null;\n\t} finally {\n\t\tif (timer) clearTimeout(timer);\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IRequestMeta } from '../services/request-meta';\nimport { type IRequestLocation, type LocationResolver, resolveLocation } from '../services/request-location';\n\nexport class SessionModel {\n\t// `locate` is IAuthConfig.location. Absent ⇒ sessions carry no location.\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate locate?: LocationResolver,\n\t) {}\n\n\tasync create(\n\t\tuserId: string,\n\t\ttoken: string,\n\t\texpiresAt: Date,\n\t\tsid?: string,\n\t\tmeta?: IRequestMeta,\n\t): Promise<void> {\n\t\tconst location =\n\t\t\tthis.locate && meta?.headers\n\t\t\t\t? await resolveLocation(this.locate, { ip: meta.ipAddress, headers: meta.headers })\n\t\t\t\t: null;\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid, user_agent, ip_address, location)\n\t\t\tVALUES ($1, $2, $3, $4, $5, $6, $7)\n\t\t\tON CONFLICT (token) DO NOTHING`,\n\t\t\t[\n\t\t\t\tuserId,\n\t\t\t\ttoken,\n\t\t\t\texpiresAt,\n\t\t\t\tsid ?? null,\n\t\t\t\tmeta?.userAgent ?? null,\n\t\t\t\tmeta?.ipAddress ?? null,\n\t\t\t\tlocation ? JSON.stringify(location) : null,\n\t\t\t],\n\t\t);\n\t}\n\n\tasync delete(token: string): Promise<void> {\n\t\tawait this.store.query(`DELETE FROM fonderie_sessions WHERE token = $1`, [token]);\n\t}\n\n\t// Delete the session an access token is bound to (by its sid claim). Lets\n\t// logout revoke the current session without the client resending the refresh\n\t// token — the access token already identifies its session.\n\tasync deleteBySid(sid: string): Promise<void> {\n\t\tawait this.store.query(`DELETE FROM fonderie_sessions WHERE sid = $1`, [sid]);\n\t}\n\n\t// Revoke every session for a user (e.g. on password change). Access tokens\n\t// bound to these sessions via the sid claim die on their next request.\n\tasync deleteByUser(userId: string): Promise<void> {\n\t\tawait this.store.query(`DELETE FROM fonderie_sessions WHERE user_id = $1`, [userId]);\n\t}\n\n\t// Session metadata for a user (no tokens) — for the data-export / SAR bundle.\n\tasync listByUser(userId: string): Promise<\n\t\tArray<{ id: string; userAgent: string | null; ipAddress: string | null; createdAt: Date; expiresAt: Date }>\n\t> {\n\t\treturn this.store.query(\n\t\t\t`SELECT id,\n\t\t\t user_agent AS \"userAgent\",\n\t\t\t ip_address AS \"ipAddress\",\n\t\t\t created_at AS \"createdAt\",\n\t\t\t expires_at AS \"expiresAt\"\n\t\t\t FROM fonderie_sessions\n\t\t\t WHERE user_id = $1\n\t\t\t ORDER BY created_at DESC`,\n\t\t\t[userId],\n\t\t);\n\t}\n\n\t// Live sessions for the Active Sessions screen: unexpired only, carrying the\n\t// sid so the caller can flag the current one (row.sid === the JWT's sid).\n\t// No token — that never leaves the server.\n\tasync listLiveByUser(userId: string): Promise<\n\t\tArray<{\n\t\t\tid: string;\n\t\t\tsid: string | null;\n\t\t\tuserAgent: string | null;\n\t\t\tipAddress: string | null;\n\t\t\tlocation: IRequestLocation | null;\n\t\t\tcreatedAt: Date;\n\t\t\texpiresAt: Date;\n\t\t}>\n\t> {\n\t\treturn this.store.query(\n\t\t\t`SELECT id, sid,\n\t\t\t user_agent AS \"userAgent\",\n\t\t\t ip_address AS \"ipAddress\",\n\t\t\t location,\n\t\t\t created_at AS \"createdAt\",\n\t\t\t expires_at AS \"expiresAt\"\n\t\t\t FROM fonderie_sessions\n\t\t\t WHERE user_id = $1 AND expires_at > now()\n\t\t\t ORDER BY created_at DESC`,\n\t\t\t[userId],\n\t\t);\n\t}\n\n\t// Terminate one session by row id, scoped to its owner (a user can never\n\t// delete another user's session). Returns whether a row was removed.\n\tasync terminateById(userId: string, id: string): Promise<boolean> {\n\t\tconst rows = await this.store.query<{ id: string }>(\n\t\t\t`DELETE FROM fonderie_sessions WHERE id = $1 AND user_id = $2 RETURNING id`,\n\t\t\t[id, userId],\n\t\t);\n\t\treturn rows.length > 0;\n\t}\n\n\t// Terminate all of a user's sessions except the current one (by sid).\n\t// Returns the number of sessions removed.\n\tasync terminateOthers(userId: string, keepSid: string): Promise<number> {\n\t\tconst rows = await this.store.query<{ id: string }>(\n\t\t\t`DELETE FROM fonderie_sessions\n\t\t\t WHERE user_id = $1 AND (sid IS DISTINCT FROM $2)\n\t\t\t RETURNING id`,\n\t\t\t[userId, keepSid],\n\t\t);\n\t\treturn rows.length;\n\t}\n\n\tasync exists(token: string): Promise<boolean> {\n\t\tconst rows = await this.store.query<{ id: string }>(\n\t\t\t`SELECT id FROM fonderie_sessions WHERE token = $1 AND expires_at > now()`,\n\t\t\t[token],\n\t\t);\n\t\treturn rows.length > 0;\n\t}\n\n\t// Liveness check for session-bound access tokens (by the sid claim).\n\tasync aliveBySid(sid: string): Promise<boolean> {\n\t\tconst rows = await this.store.query<{ id: string }>(\n\t\t\t`SELECT id FROM fonderie_sessions WHERE sid = $1 AND expires_at > now()`,\n\t\t\t[sid],\n\t\t);\n\t\treturn rows.length > 0;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nexport class BackupCodeModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync replace(userId: string, codeHashes: string[]): Promise<void> {\n\t\tawait this.store.transaction(async (tx) => {\n\t\t\tawait tx.query(`DELETE FROM fonderie_mfa_backup_codes WHERE user_id = $1`, [userId]);\n\t\t\tif (codeHashes.length === 0) return;\n\t\t\tconst placeholders = codeHashes.map((_, i) => `($1, $${i + 2})`).join(', ');\n\t\t\tawait tx.query(\n\t\t\t\t`INSERT INTO fonderie_mfa_backup_codes (user_id, code_hash) VALUES ${placeholders}`,\n\t\t\t\t[userId, ...codeHashes],\n\t\t\t);\n\t\t});\n\t}\n\n\tasync findUnused(userId: string): Promise<{ id: string; codeHash: string }[]> {\n\t\tconst rows = await this.store.query<{ id: string; code_hash: string }>(\n\t\t\t`SELECT id, code_hash FROM fonderie_mfa_backup_codes\n\t\t\t WHERE user_id = $1 AND used_at IS NULL`,\n\t\t\t[userId],\n\t\t);\n\t\treturn rows.map((r) => ({ id: r.id, codeHash: r.code_hash }));\n\t}\n\n\tasync consume(id: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_mfa_backup_codes SET used_at = now() WHERE id = $1`, [\n\t\t\tid,\n\t\t]);\n\t}\n\n\tasync deleteByUser(userId: string): Promise<void> {\n\t\tawait this.store.query(`DELETE FROM fonderie_mfa_backup_codes WHERE user_id = $1`, [userId]);\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport { background } from '@fonderie/core';\n\nimport {\n\ttype IRequestLocation,\n\ttype LocationResolver,\n\tresolveLocation,\n} from '../services/request-location';\n\n// One row per login ATTEMPT — success or failure — across every method.\n// Append-only: rows survive logout/expiry (unlike fonderie_sessions, which is\n// the live-session list and the single source of truth for Active Sessions).\n// user_id is nullable so unknown-email attempts still record.\n// 'registration' marks the sign-up request itself: with verification not\n// enforced the account is live from that moment, so it is the first — sometimes\n// only — record of where the user came from.\nexport type LoginMethod = 'password' | 'phone' | 'mfa' | 'oauth-google' | 'oauth-apple' | 'registration';\nexport type LoginOutcome = 'success' | 'failed';\n\nexport interface ILoginEventInput {\n\tuserId: string | null;\n\temailAttempted?: string | null;\n\tmethod: LoginMethod;\n\toutcome: LoginOutcome;\n\tfailureReason?: string | null;\n\tipAddress: string | null;\n\tuserAgent: string | null;\n\t/** Request headers, handed to the location resolver. Not stored. */\n\theaders?: Headers;\n}\n\nexport class LoginEventModel {\n\t// `locate` is IAuthConfig.location. Absent ⇒ rows carry no location,\n\t// exactly as before.\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate locate?: LocationResolver,\n\t) {}\n\n\tasync record(e: ILoginEventInput): Promise<void> {\n\t\tconst location = this.locate\n\t\t\t? await resolveLocation(this.locate, { ip: e.ipAddress, headers: e.headers ?? new Headers() })\n\t\t\t: null;\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_login_events\n\t\t\t (user_id, email_attempted, method, outcome, failure_reason, ip_address, user_agent, location)\n\t\t\t VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n\t\t\t[\n\t\t\t\te.userId,\n\t\t\t\te.emailAttempted ?? null,\n\t\t\t\te.method,\n\t\t\t\te.outcome,\n\t\t\t\te.failureReason ?? null,\n\t\t\t\te.ipAddress,\n\t\t\t\te.userAgent,\n\t\t\t\tlocation ? JSON.stringify(location) : null,\n\t\t\t],\n\t\t);\n\t}\n\n\t// Never throws — recording history must not fail a login. It IS awaited,\n\t// though: this is a security audit trail (who signed in, from where), and a\n\t// detached write is abandoned when a serverless instance freezes after the\n\t// response, losing the row entirely. background() decides whether waiting is\n\t// actually necessary for the current runtime.\n\tasync recordSafe(e: ILoginEventInput): Promise<void> {\n\t\tawait background(this.record(e));\n\t}\n\n\t// One page of a user's own login history, newest first. Keyset-paginated on\n\t// (created_at, id) — mirrors audit's event list so the two log UIs share a\n\t// cursor contract. The +1 over-fetch that detects a next page lives here so\n\t// no outer clamp can shave it off.\n\tasync listByUser(query: ILoginEventQuery): Promise<ILoginEventPage> {\n\t\tconst limit = Math.min(query.limit ?? 50, MAX_HISTORY_LIMIT);\n\t\tconst params: unknown[] = [query.userId];\n\t\tconst where: string[] = ['user_id = $1'];\n\n\t\tif (query.outcome) {\n\t\t\tparams.push(query.outcome);\n\t\t\twhere.push(`outcome = $${params.length}`);\n\t\t}\n\t\tif (query.from) {\n\t\t\tparams.push(query.from);\n\t\t\twhere.push(`created_at >= $${params.length}`);\n\t\t}\n\t\tif (query.to) {\n\t\t\tparams.push(query.to);\n\t\t\twhere.push(`created_at <= $${params.length}`);\n\t\t}\n\t\tif (query.cursor) {\n\t\t\tparams.push(query.cursor.createdAt, query.cursor.id);\n\t\t\twhere.push(`(created_at, id) < ($${params.length - 1}::timestamptz, $${params.length}::uuid)`);\n\t\t}\n\n\t\tparams.push(limit + 1);\n\t\tconst rows = await this.store.query<ILoginEventRow>(\n\t\t\t`SELECT id, method, outcome, failure_reason AS \"failureReason\",\n\t\t\t ip_address AS \"ipAddress\", user_agent AS \"userAgent\", location,\n\t\t\t created_at AS \"createdAt\", created_at::text AS \"createdAtRaw\"\n\t\t\t FROM fonderie_login_events\n\t\t\t WHERE ${where.join(' AND ')}\n\t\t\t ORDER BY created_at DESC, id DESC\n\t\t\t LIMIT $${params.length}`,\n\t\t\tparams,\n\t\t);\n\t\treturn { events: rows.slice(0, limit), hasMore: rows.length > limit };\n\t}\n}\n\nconst MAX_HISTORY_LIMIT = 200;\n\nexport interface ILoginEventQuery {\n\tuserId: string;\n\toutcome?: LoginOutcome;\n\tfrom?: Date;\n\tto?: Date;\n\tcursor?: { createdAt: string; id: string } | null;\n\tlimit?: number;\n}\n\nexport interface ILoginEventRow {\n\tid: string;\n\tmethod: LoginMethod;\n\toutcome: LoginOutcome;\n\tfailureReason: string | null;\n\tipAddress: string | null;\n\tuserAgent: string | null;\n\tlocation: IRequestLocation | null;\n\tcreatedAt: Date;\n\t// created_at::text — full microsecond precision for the keyset cursor\n\t// (node-pg parses timestamptz into a millisecond Date, which would make the\n\t// cursor skip same-millisecond rows between pages).\n\tcreatedAtRaw: string;\n}\n\nexport interface ILoginEventPage {\n\tevents: ILoginEventRow[];\n\thasMore: boolean;\n}\n","import type { IFonderieContext } from '@fonderie/core';\n\n// The request metadata auth persists for security surfaces (session rows,\n// login events): effective client IP and raw user-agent. IP comes from\n// ctx.meta['clientIp'], which the adapters resolve with explicit proxy trust\n// (resolveClientIp in @fonderie/core) — never re-derive forwarding logic here.\n// The user-agent is stored raw and parsed at display time.\nexport interface IRequestMeta {\n\tipAddress: string | null;\n\tuserAgent: string | null;\n\t/** The request's headers, for the optional location resolver. Never stored. */\n\theaders?: Headers;\n}\n\n// Cap the stored UA: real ones are <300 chars; anything longer is noise or abuse.\nconst UA_MAX = 512;\n\n// Warn ONCE per process. The condition is a property of how the app is wired,\n// not of a particular request, so repeating it per login would bury it.\nlet warnedAboutMissingIdentity = false;\n\n/**\n * Warn when a context carries no caller identity at all.\n *\n * A real HTTP request arriving through an adapter has a user-agent, and a\n * resolved client IP. BOTH being absent means the context was built by hand —\n * almost always `fonderie.handle(new Request(...))` called directly, with the\n * caller's headers dropped and no `{ meta: { clientIp } }` seed.\n *\n * The consequence is invisible until someone looks at it: login history shows\n * \"Unknown device\" and no IP for those sign-ins, and only for those, so it\n * reads as a display bug rather than missing security data. It happened on an\n * OAuth callback — the method an attacker is most likely to use, and the one\n * whose history therefore matters most.\n *\n * Only one of the two missing is NOT reported: a request can legitimately lack\n * a user-agent, and an IP can be unresolvable on some transports. Both missing\n * together is the signature of a synthesized request, which is why that is the\n * condition rather than either alone.\n */\nfunction warnIfIdentityless(ctx: IFonderieContext): void {\n\tif (warnedAboutMissingIdentity) return;\n\twarnedAboutMissingIdentity = true;\n\tconsole.warn(\n\t\t`[auth] recording a security event with no client IP and no user-agent for ` +\n\t\t\t`${ctx.request.method} ${new URL(ctx.request.url).pathname}. Login history and ` +\n\t\t\t`session rows will show \"Unknown device\" with no IP for these. This is what a ` +\n\t\t\t`hand-built request looks like: if you call fonderie.handle() directly, forward ` +\n\t\t\t`the caller's user-agent header and pass the resolved IP as ` +\n\t\t\t`\\`handle(req, { meta: { clientIp } })\\` — resolve it with resolveClientIp from ` +\n\t\t\t`@fonderie/core/middlewares rather than reading the socket, or a proxied ` +\n\t\t\t`deployment records the proxy. Reported once per process.`,\n\t);\n}\n\nexport function requestMeta(ctx: IFonderieContext): IRequestMeta {\n\tconst ip = ctx.meta['clientIp'];\n\tconst ua = ctx.request.headers.get('user-agent');\n\tconst meta: IRequestMeta = {\n\t\tipAddress: typeof ip === 'string' && ip.length > 0 ? ip : null,\n\t\tuserAgent: ua && ua.length > 0 ? ua.slice(0, UA_MAX) : null,\n\t\theaders: ctx.request.headers,\n\t};\n\tif (meta.ipAddress === null && meta.userAgent === null) warnIfIdentityless(ctx);\n\treturn meta;\n}\n\n// Test seam: the warning fires once per process by design, which would make\n// every test after the first silently pass.\nexport function __resetIdentityWarningForTests(): void {\n\twarnedAboutMissingIdentity = false;\n}\n","import { tokenPairCookies, clearedTokenCookies, cookieHeaders } from '../services/cookies';\nimport { randomBytes, randomInt } from 'node:crypto';\n\nimport type { EventBus } from '@fonderie/events';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport { NOTIFICATION_EVENT } from '@fonderie/events';\nimport { setApiResponse, HTTP, background } from '@fonderie/core';\nimport type { IFonderieContext, ICourierMessage } from '@fonderie/core';\n\nimport { EVENT_KEYS } from '../config';\nimport { toUserDTO } from '../dtos/user';\nimport type { IAuthConfig } from '../config';\nimport { UserModel } from '../models/user.model';\nimport { checkCooldown } from '../services/cooldown';\nimport { SessionModel } from '../models/session.model';\nimport { LoginEventModel } from '../models/login-event.model';\nimport { requestMeta } from '../services/request-meta';\nimport { hashPassword, verifyPasswordForLogin } from '../services/password';\nimport { normalizeEmailSafe } from '../services/email';\nimport { PasswordResetModel } from '../models/password-reset.model';\nimport { DEFAULT_VERIFICATION_COOLDOWN, MESSAGE_KEYS } from '../config';\nimport { EmailVerificationModel } from '../models/email-verification.model';\nimport { PhoneVerificationModel } from '../models/phone-verification.model';\nimport {\n\tissueTokenPair,\n\tissueMfaPendingToken,\n\tverifyToken,\n\trefreshTokenExpiry,\n} from '../services/jwt';\n\nfunction normalizePhone(phone: string): string {\n\treturn phone.trim().replace(/[\\s()\\-\\.]/g, '');\n}\n\nfunction isValidPhone(phone: unknown): phone is string {\n\treturn typeof phone === 'string' && /^\\+?[1-9]\\d{6,14}$/.test(normalizePhone(phone));\n}\n\nfunction extractRefreshToken(ctx: IFonderieContext): string | null {\n\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\tif (typeof body?.['refreshToken'] === 'string') {\n\t\treturn body['refreshToken'] as string;\n\t}\n\n\tconst cookie = ctx.request.headers.get('cookie') ?? '';\n\tconst match = cookie.match(/(?:^|;\\s*)refresh_token=([^;]+)/);\n\n\treturn match?.[1] ?? null;\n}\n\nexport function authController(store: IStoreAdapter, config: IAuthConfig, bus?: EventBus) {\n\tconst users = new UserModel(store);\n\tconst sessions = new SessionModel(store, config.location);\n\tconst loginEvents = new LoginEventModel(store, config.location);\n\tconst passwordReset = new PasswordResetModel(store);\n\tconst emailVerif = new EmailVerificationModel(store);\n\tconst phoneVerif = new PhoneVerificationModel(store);\n\n\tconst OTP_TTL_MS = 10 * 60 * 1000;\n\n\treturn {\n\t\tregister: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst { email, password, phone, firstName = null, lastName = null } = body ?? {};\n\n\t\t\t// ── Email branch takes priority (cheaper than SMS) ───────\n\t\t\tif (typeof email === 'string' && typeof password === 'string') {\n\t\t\t\tconst normalizedEmail = normalizeEmailSafe(email);\n\t\t\t\tif (!normalizedEmail) {\n\t\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid email address');\n\t\t\t\t}\n\n\t\t\t\tif (password.length < 8) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t\t'password must be at least 8 characters',\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tconst existing = await users.findByEmail(normalizedEmail);\n\t\t\t\tif (existing) {\n\t\t\t\t\treturn setApiResponse(HTTP.CONFLICT, 'USER_ALREADY_EXISTS', 'Email already registered');\n\t\t\t\t}\n\n\t\t\t\tconst passwordHash = await hashPassword(password);\n\t\t\t\tconst row = await users.create(\n\t\t\t\t\tnormalizedEmail,\n\t\t\t\t\tpasswordHash,\n\t\t\t\t\tfirstName as string | null,\n\t\t\t\t\tlastName as string | null,\n\t\t\t\t);\n\n\t\t\t\tif (!row) {\n\t\t\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Registration failed');\n\t\t\t\t}\n\n\t\t\t\tconst pin = randomInt(100000, 1000000).toString();\n\t\t\t\tconst expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24);\n\t\t\t\tawait emailVerif.create(row.id, pin, expiresAt);\n\n\t\t\t\tconst user = await users.findById(row.id);\n\t\t\t\tif (!user) {\n\t\t\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Registration failed');\n\t\t\t\t}\n\n\t\t\t\tconst reqId = ctx.meta['requestId'] as string | undefined;\n\t\t\t\tconst reqOpts = reqId !== undefined ? { requestId: reqId } : undefined;\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(\n\t\t\t\t\t\tNOTIFICATION_EVENT,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: MESSAGE_KEYS.emailRegistration,\n\t\t\t\t\t\t\tlocale: user.locale,\n\t\t\t\t\t\t\tdata: { pin, firstName: firstName ?? '' },\n\t\t\t\t\t\t\trecipient: { email: normalizedEmail, phone: null, deviceToken: null },\n\t\t\t\t\t\t} satisfies ICourierMessage,\n\t\t\t\t\t\treqOpts,\n\t\t\t\t\t));\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(\n\t\t\t\t\t\tEVENT_KEYS.userRegistered,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tuserId: user.id,\n\t\t\t\t\t\t\temail: user.email,\n\t\t\t\t\t\t\tfirstName: user.firstName,\n\t\t\t\t\t\t\tlastName: user.lastName,\n\t\t\t\t\t\t\tloginMethod: 'email' as const,\n\t\t\t\t\t\t},\n\t\t\t\t\t\treqOpts,\n\t\t\t\t\t));\n\n\t\t\t\tconst { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {\n\t\t\t\t\tloginMethod: 'email',\n\t\t\t\t});\n\t\t\t\tconst registerMeta = requestMeta(ctx);\n\t\t\t\tawait sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, registerMeta);\n\t\t\t\t// Where the account was created from — the user's first entry in\n\t\t\t\t// their own history. Location resolves once for this request (the\n\t\t\t\t// session above already asked).\n\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\tuserId: user.id,\n\t\t\t\t\temailAttempted: normalizedEmail,\n\t\t\t\t\tmethod: 'registration',\n\t\t\t\t\toutcome: 'success',\n\t\t\t\t\t...registerMeta,\n\t\t\t\t});\n\n\t\t\t\tconst resolvedRegister = { ...config, ...config.resolve?.(ctx) };\n\t\t\t\tconst requiresVerification = !!(resolvedRegister.requireVerification) && !user.emailVerifiedAt;\n\n\t\t\t\treturn Response.json(\n\t\t\t\t\t{\n\t\t\t\t\t\treason: 'USER_EMAIL_REGISTERED',\n\t\t\t\t\t\texplanation: 'Account created. Check your email for a verification code.',\n\t\t\t\t\t\tresult: {\n\t\t\t\t\t\t\ttokens: { access: accessToken, refresh: refreshToken },\n\t\t\t\t\t\t\tuser: toUserDTO(user),\n\t\t\t\t\t\t\trequiresVerification,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tstatus: 201,\n\t\t\t\t\t\theaders: cookieHeaders(tokenPairCookies(accessToken, refreshToken, config)),\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// ── Phone branch ──────────────────────────────────────────\n\t\t\tif (isValidPhone(phone)) {\n\t\t\t\tconst existing = await users.findByPhone(normalizePhone(phone));\n\t\t\t\tif (existing) {\n\t\t\t\t\treturn setApiResponse(HTTP.CONFLICT, 'USER_ALREADY_EXISTS', 'Phone already registered');\n\t\t\t\t}\n\n\t\t\t\tconst { id } = await users.findOrCreateByPhone(\n\t\t\t\t\tnormalizePhone(phone),\n\t\t\t\t\t(firstName as string | null) ?? null,\n\t\t\t\t\t(lastName as string | null) ?? null,\n\t\t\t\t);\n\n\t\t\t\tconst otp = randomInt(100000, 1000000).toString();\n\t\t\t\tconst expiresAt = new Date(Date.now() + OTP_TTL_MS);\n\t\t\t\tawait phoneVerif.upsert(id, normalizePhone(phone), otp, expiresAt);\n\n\t\t\t\tconst user = await users.findById(id);\n\t\t\t\tif (!user) {\n\t\t\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Registration failed');\n\t\t\t\t}\n\n\t\t\t\tconst reqId2 = ctx.meta['requestId'] as string | undefined;\n\t\t\t\tconst reqOpts2 = reqId2 !== undefined ? { requestId: reqId2 } : undefined;\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(\n\t\t\t\t\t\tNOTIFICATION_EVENT,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: MESSAGE_KEYS.phoneOtp,\n\t\t\t\t\t\t\tlocale: user.locale,\n\t\t\t\t\t\t\tdata: { otp },\n\t\t\t\t\t\t\trecipient: { email: null, phone: normalizePhone(phone), deviceToken: null },\n\t\t\t\t\t\t} satisfies ICourierMessage,\n\t\t\t\t\t\treqOpts2,\n\t\t\t\t\t));\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(\n\t\t\t\t\t\tEVENT_KEYS.userRegistered,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tuserId: user.id,\n\t\t\t\t\t\t\temail: user.email,\n\t\t\t\t\t\t\tfirstName: user.firstName,\n\t\t\t\t\t\t\tlastName: user.lastName,\n\t\t\t\t\t\t\tloginMethod: 'phone' as const,\n\t\t\t\t\t\t},\n\t\t\t\t\t\treqOpts2,\n\t\t\t\t\t));\n\n\t\t\t\t// No session and no full tokens before the OTP round-trip: possession\n\t\t\t\t// of the phone is the ONLY credential in this flow, so issuing real\n\t\t\t\t// tokens here would authenticate anyone who typed the number. Mirror\n\t\t\t\t// the MFA flow instead — a short-lived pending token that only\n\t\t\t\t// /auth/verify and /auth/send-verification accept; the real token\n\t\t\t\t// pair is issued by verify() once the OTP matches.\n\t\t\t\tconst otpToken = issueMfaPendingToken(user.id, config, 'phone');\n\n\t\t\t\treturn Response.json(\n\t\t\t\t\t{\n\t\t\t\t\t\treason: 'USER_PHONE_REGISTERED',\n\t\t\t\t\t\texplanation: 'Account created. A verification code has been sent to your phone.',\n\t\t\t\t\t\tresult: {\n\t\t\t\t\t\t\totpToken,\n\t\t\t\t\t\t\tuser: toUserDTO(user),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{ status: 202 },\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t'Provide email + password or a valid phone number',\n\t\t\t);\n\t\t},\n\n\t\tlogin: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst meta = requestMeta(ctx);\n\n\t\t\t// ── Email branch takes priority (cheaper than SMS) ────────\n\t\t\tif (typeof body?.['email'] === 'string' && typeof body?.['password'] === 'string') {\n\t\t\t\tconst { email: rawEmail, password } = body as { email: string; password: string };\n\t\t\t\tconst email = normalizeEmailSafe(rawEmail);\n\t\t\t\tif (!email) {\n\t\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid email address');\n\t\t\t\t}\n\n\t\t\t\tconst user = await users.findByEmail(email);\n\t\t\t\tif (!user || !user.passwordHash) {\n\t\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\t\tuserId: null,\n\t\t\t\t\t\temailAttempted: email,\n\t\t\t\t\t\tmethod: 'password',\n\t\t\t\t\t\toutcome: 'failed',\n\t\t\t\t\t\tfailureReason: 'unknown_email',\n\t\t\t\t\t\t...meta,\n\t\t\t\t\t});\n\t\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_CREDENTIALS', 'Invalid credentials');\n\t\t\t\t}\n\n\t\t\t\tconst { valid, needsRehash } = await verifyPasswordForLogin(\n\t\t\t\t\tpassword,\n\t\t\t\t\tuser.passwordHash,\n\t\t\t\t\tconfig.legacyVerify,\n\t\t\t\t);\n\t\t\t\tif (!valid) {\n\t\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\t\tuserId: user.id,\n\t\t\t\t\t\temailAttempted: email,\n\t\t\t\t\t\tmethod: 'password',\n\t\t\t\t\t\toutcome: 'failed',\n\t\t\t\t\t\tfailureReason: 'bad_password',\n\t\t\t\t\t\t...meta,\n\t\t\t\t\t});\n\t\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_CREDENTIALS', 'Invalid credentials');\n\t\t\t\t}\n\n\t\t\t\t// Rehash-on-login: a password validated via the legacy verifier is\n\t\t\t\t// re-stored as bcrypt so the foreign hash is used at most once.\n\t\t\t\tif (needsRehash) {\n\t\t\t\t\tawait users.updatePassword(user.id, await hashPassword(password));\n\t\t\t\t}\n\n\t\t\t\tif (user.suspended) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t\t\t'ACCOUNT_SUSPENDED',\n\t\t\t\t\t\t'Account suspended. Please contact support.',\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tif (user.mfaEnabled) {\n\t\t\t\t\t// Password verified; MFA still pending. The completed login is\n\t\t\t\t\t// recorded on mfa/verify — recording success here would log a\n\t\t\t\t\t// login that hasn't happened yet.\n\t\t\t\t\tconst mfaToken = issueMfaPendingToken(user.id, config, 'email');\n\t\t\t\t\treturn setApiResponse(HTTP.OK, 'MFA_REQUIRED', 'Multi-factor authentication required', {\n\t\t\t\t\t\tmfaToken,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tconst { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {\n\t\t\t\t\tloginMethod: 'email',\n\t\t\t\t});\n\t\t\t\tawait sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);\n\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\tuserId: user.id,\n\t\t\t\t\temailAttempted: email,\n\t\t\t\t\tmethod: 'password',\n\t\t\t\t\toutcome: 'success',\n\t\t\t\t\t...meta,\n\t\t\t\t});\n\n\t\t\t\tconst resolvedLogin = { ...config, ...config.resolve?.(ctx) };\n\t\t\t\tconst requiresVerification = !!(resolvedLogin.requireVerification) && !user.emailVerifiedAt;\n\n\t\t\t\treturn Response.json(\n\t\t\t\t\t{\n\t\t\t\t\t\treason: 'USER_EMAIL_LOGIN',\n\t\t\t\t\t\texplanation: 'Login successful.',\n\t\t\t\t\t\tresult: {\n\t\t\t\t\t\t\ttokens: { access: accessToken, refresh: refreshToken },\n\t\t\t\t\t\t\tuser: toUserDTO(user),\n\t\t\t\t\t\t\trequiresVerification,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tstatus: 200,\n\t\t\t\t\t\theaders: cookieHeaders(tokenPairCookies(accessToken, refreshToken, config)),\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// ── Phone branch ──────────────────────────────────────────\n\t\t\tconst phone = body?.['phone'];\n\t\t\tif (isValidPhone(phone)) {\n\t\t\t\tconst user = await users.findByPhone(normalizePhone(phone));\n\t\t\t\tif (!user) {\n\t\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_CREDENTIALS', 'Invalid credentials');\n\t\t\t\t}\n\t\t\t\tif (user.suspended) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t\t\t'ACCOUNT_SUSPENDED',\n\t\t\t\t\t\t'Account suspended. Please contact support.',\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tconst otp = randomInt(100000, 1000000).toString();\n\t\t\t\tconst expiresAt = new Date(Date.now() + OTP_TTL_MS);\n\t\t\t\tawait phoneVerif.upsert(user.id, normalizePhone(phone), otp, expiresAt);\n\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\t\ttype: MESSAGE_KEYS.phoneOtp,\n\t\t\t\t\t\tlocale: user.locale,\n\t\t\t\t\t\tdata: { otp },\n\t\t\t\t\t\trecipient: { email: null, phone: normalizePhone(phone), deviceToken: null },\n\t\t\t\t\t} satisfies ICourierMessage));\n\n\t\t\t\t// Same rule as phone registration: the OTP IS the credential, so no\n\t\t\t\t// session/full tokens until verify() confirms it. Also no user DTO —\n\t\t\t\t// before proof of possession, the caller has only typed a phone\n\t\t\t\t// number, and returning profile data would leak PII to anyone who\n\t\t\t\t// knows (or guesses) a registered number.\n\t\t\t\tconst otpToken = issueMfaPendingToken(user.id, config, 'phone');\n\n\t\t\t\treturn Response.json(\n\t\t\t\t\t{\n\t\t\t\t\t\treason: 'USER_PHONE_OTP_SENT',\n\t\t\t\t\t\texplanation: 'A verification code has been sent to your phone.',\n\t\t\t\t\t\tresult: { otpToken },\n\t\t\t\t\t},\n\t\t\t\t\t{ status: 202 },\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t'Provide email + password or a valid phone number',\n\t\t\t);\n\t\t},\n\n\t\tlogout: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\t// Kill the session this request is authenticated on — by its sid claim,\n\t\t\t// which is always present under requireAuth. This is the reliable path:\n\t\t\t// clients that don't resend the refresh token (most, since only the\n\t\t\t// access token is persisted) previously left the session row alive, so\n\t\t\t// it lingered in Active Sessions and its refresh token stayed valid.\n\t\t\tconst sid = (ctx.user as { sid?: string | null } | null)?.sid;\n\t\t\tif (sid) {\n\t\t\t\tawait sessions.deleteBySid(sid).catch(() => undefined);\n\t\t\t}\n\t\t\t// Also honour an explicitly-passed refresh token (covers pre-sid tokens\n\t\t\t// and callers that log out a specific refresh session).\n\t\t\tconst token = extractRefreshToken(ctx);\n\t\t\tif (token) {\n\t\t\t\tawait sessions.delete(token).catch(() => undefined);\n\t\t\t}\n\n\t\t\treturn Response.json(\n\t\t\t\t{ reason: 'USER_LOGOUT', explanation: 'Logged out successfully.' },\n\t\t\t\t{\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\theaders: cookieHeaders(clearedTokenCookies(config)),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\trefresh: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst token = extractRefreshToken(ctx);\n\t\t\tif (!token) {\n\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_PARAMETER', 'No refresh token provided');\n\t\t\t}\n\n\t\t\tconst payload = verifyToken(token, config);\n\t\t\tif (!payload || payload.type !== 'refresh') {\n\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'TOKEN_REFRESH_FAILED', 'Invalid refresh token');\n\t\t\t}\n\n\t\t\tconst valid = await sessions.exists(token);\n\t\t\tif (!valid) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNAUTHORIZED,\n\t\t\t\t\t'TOKEN_REFRESH_FAILED',\n\t\t\t\t\t'Session expired or already revoked',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst user = await users.findById(payload.sub);\n\t\t\tif (!user || user.suspended || user.deletedAt) {\n\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t\t}\n\n\t\t\tawait sessions.delete(token);\n\t\t\tconst { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {\n\t\t\t\tloginMethod: payload.loginMethod ?? 'email',\n\t\t\t\tphoneVerified: payload.phoneVerified ?? false,\n\t\t\t});\n\t\t\tawait sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, requestMeta(ctx));\n\n\t\t\treturn Response.json(\n\t\t\t\t{\n\t\t\t\t\treason: 'TOKENS_REFRESHED',\n\t\t\t\t\texplanation: 'Tokens refreshed successfully.',\n\t\t\t\t\tresult: { tokens: { access: accessToken, refresh: refreshToken } },\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\theaders: cookieHeaders(tokenPairCookies(accessToken, refreshToken, config)),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\tforgotPassword: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst rawEmail = body?.['email'];\n\n\t\t\tif (typeof rawEmail !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'email is required');\n\t\t\t}\n\n\t\t\tconst email = normalizeEmailSafe(rawEmail);\n\t\t\tif (!email) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid email address');\n\t\t\t}\n\n\t\t\tconst user = await users.findByEmail(email);\n\t\t\tif (!user) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t'PASSWORD_RESET_EMAIL_SENT',\n\t\t\t\t\t'Password reset email sent (if account exists).',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst resolved = { ...config, ...config.resolve?.(ctx) };\n\t\t\tconst cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;\n\t\t\tconst remaining = checkCooldown(await passwordReset.findLastSentAt(user.id), cooldown);\n\t\t\tif (remaining > 0) {\n\t\t\t\t// Silently skip the send: a 429 here would fire only for existing\n\t\t\t\t// accounts, letting an attacker enumerate users by requesting twice.\n\t\t\t\t// The response must be indistinguishable from the not-found branch.\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t'PASSWORD_RESET_EMAIL_SENT',\n\t\t\t\t\t'Password reset email sent (if account exists).',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst pin = randomInt(100000, 1000000).toString();\n\t\t\t// High-entropy token backing a reset LINK (32 bytes → 64 hex chars).\n\t\t\t// Not brute-forceable, so its reset path needs no rate limit.\n\t\t\tconst token = randomBytes(32).toString('hex');\n\t\t\tconst expiresAt = new Date(Date.now() + 1000 * 60 * 60);\n\t\t\tawait passwordReset.create(user.id, pin, token, expiresAt);\n\n\t\t\t// Ready-built reset link when the app configured a base URL; '' means\n\t\t\t// the app only surfaces the pin (a blank {{resetUrl}} renders empty).\n\t\t\tconst base = resolved.passwordResetUrl;\n\t\t\tconst resetUrl = base ? `${base}${base.includes('?') ? '&' : '?'}token=${token}` : '';\n\n\t\t\tawait background(bus\n\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\ttype: MESSAGE_KEYS.passwordReset,\n\t\t\t\t\tlocale: user.locale,\n\t\t\t\t\trecipient: { email, phone: null, deviceToken: null },\n\t\t\t\t\tdata: { pin, token, resetUrl },\n\t\t\t\t} satisfies ICourierMessage));\n\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.OK,\n\t\t\t\t'PASSWORD_RESET_EMAIL_SENT',\n\t\t\t\t'Password reset email sent (if account exists).',\n\t\t\t);\n\t\t},\n\n\t\tresetPassword: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst raw = body?.['pin'];\n\t\t\tconst password = body?.['password'];\n\n\t\t\tconst rawToken = body?.['token'];\n\n\t\t\tif (typeof password !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'password is required');\n\t\t\t}\n\t\t\tif (password.length < 8) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'password must be at least 8 characters',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Two credentials redeem a reset: the strong TOKEN (from the email\n\t\t\t// link — not brute-forceable) or the 6-digit PIN (route is\n\t\t\t// IP-rate-limited). Token takes precedence when both are supplied.\n\t\t\tlet row: { userId: string; expiresAt: Date } | null;\n\t\t\tif (typeof rawToken === 'string' && rawToken.trim().length >= 32) {\n\t\t\t\trow = await passwordReset.findByToken(rawToken.trim());\n\t\t\t} else {\n\t\t\t\tif (typeof raw !== 'string' || !/^\\d{6}$/.test(raw.trim())) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t\t'a 6-digit pin or a reset token is required',\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\trow = await passwordReset.findByPin(raw.trim());\n\t\t\t}\n\n\t\t\tif (!row || new Date() > row.expiresAt) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'PASSWORD_RESET_FAILED',\n\t\t\t\t\t'Invalid or expired reset credentials',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst passwordHash = await hashPassword(password);\n\t\t\tawait store.transaction(async (tx) => {\n\t\t\t\tawait Promise.all([\n\t\t\t\t\ttx.query(`UPDATE fonderie_users SET password_hash = $1 WHERE id = $2`, [\n\t\t\t\t\t\tpasswordHash,\n\t\t\t\t\t\trow.userId,\n\t\t\t\t\t]),\n\t\t\t\t\ttx.query(`DELETE FROM fonderie_password_resets WHERE user_id = $1`, [row.userId]),\n\t\t\t\t\t// Revoke every session: the reset exists because the account may\n\t\t\t\t\t// be compromised — a stolen session must not survive it.\n\t\t\t\t\ttx.query(`DELETE FROM fonderie_sessions WHERE user_id = $1`, [row.userId]),\n\t\t\t\t]);\n\t\t\t});\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PASSWORD_RESET_SUCCESSFUL', 'Password reset successfully.');\n\t\t},\n\n\t\tverify: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\t// requireAnyAuth admits pending tokens so the phone-OTP flow can reach\n\t\t\t// this route pre-authentication — but a pending token is ONLY valid for\n\t\t\t// that flow. An email mfaPending token (password verified, MFA not) must\n\t\t\t// finish MFA at /auth/mfa/verify, not side-step into email verification.\n\t\t\tif (ctx.user!.mfaPending && ctx.user!.loginMethod !== 'phone') {\n\t\t\t\treturn setApiResponse(HTTP.FORBIDDEN, 'MFA_REQUIRED', 'Complete MFA verification to continue');\n\t\t\t}\n\n\t\t\t// ── Email early-exit: no pin needed if already verified ──\n\t\t\tif (ctx.user!.loginMethod !== 'phone' && ctx.user!.emailVerifiedAt) {\n\t\t\t\treturn setApiResponse(HTTP.OK, 'VERIFIED', 'Email verified successfully.', {\n\t\t\t\t\tverified: true,\n\t\t\t\t\temail: ctx.user!.email,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst raw = body?.['token'];\n\n\t\t\tif (typeof raw !== 'string' || !/^\\d{6}$/.test(raw.trim())) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'token must be a 6-digit code',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst pin = raw.trim();\n\n\t\t\t// ── Phone branch ─────────────────────────────────────────\n\t\t\tif (ctx.user!.loginMethod === 'phone') {\n\t\t\t\t// A phone sign-in COMPLETES here, not in login(): /auth/login only\n\t\t\t\t// sends the code and issues a pending token, because possession of\n\t\t\t\t// the phone is the sole credential. That is why login history missed\n\t\t\t\t// this path for months — an audit of \"where do logins happen\" reads\n\t\t\t\t// login() and the OAuth callbacks, and never a route named verify.\n\t\t\t\tconst phoneMeta = requestMeta(ctx);\n\t\t\t\tconst record = await phoneVerif.findByUser(ctx.user!.id, pin);\n\t\t\t\tif (!record) {\n\t\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\t\tuserId: ctx.user!.id,\n\t\t\t\t\t\temailAttempted: null,\n\t\t\t\t\t\tmethod: 'phone',\n\t\t\t\t\t\toutcome: 'failed',\n\t\t\t\t\t\tfailureReason: 'invalid_pin',\n\t\t\t\t\t\t...phoneMeta,\n\t\t\t\t\t});\n\t\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'VERIFICATION_FAILED', 'Invalid or expired pin');\n\t\t\t\t}\n\t\t\t\tif (new Date() > record.expiresAt) {\n\t\t\t\t\tawait phoneVerif.deleteByUser(ctx.user!.id);\n\t\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\t\tuserId: ctx.user!.id,\n\t\t\t\t\t\temailAttempted: null,\n\t\t\t\t\t\tmethod: 'phone',\n\t\t\t\t\t\toutcome: 'failed',\n\t\t\t\t\t\tfailureReason: 'expired_pin',\n\t\t\t\t\t\t...phoneMeta,\n\t\t\t\t\t});\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t\t'VERIFICATION_FAILED',\n\t\t\t\t\t\t'Verification code expired',\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tawait phoneVerif.deleteByUser(ctx.user!.id);\n\n\t\t\t\tconst { accessToken, refreshToken, sid } = issueTokenPair(ctx.user!.id, config, {\n\t\t\t\t\tloginMethod: 'phone',\n\t\t\t\t\tphoneVerified: true,\n\t\t\t\t});\n\t\t\t\tawait sessions.create(ctx.user!.id, refreshToken, refreshTokenExpiry(refreshToken), sid, phoneMeta);\n\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\tuserId: ctx.user!.id,\n\t\t\t\t\temailAttempted: null,\n\t\t\t\t\tmethod: 'phone',\n\t\t\t\t\toutcome: 'success',\n\t\t\t\t\t...phoneMeta,\n\t\t\t\t});\n\n\t\t\t\tconst verifiedUser = await users.findById(ctx.user!.id);\n\t\t\t\tif (!verifiedUser) {\n\t\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'User not found');\n\t\t\t\t}\n\t\t\t\tif (verifiedUser.suspended) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t\t\t'ACCOUNT_SUSPENDED',\n\t\t\t\t\t\t'Account suspended. Please contact support.',\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\treturn Response.json(\n\t\t\t\t\t{\n\t\t\t\t\t\treason: 'VERIFIED',\n\t\t\t\t\t\texplanation: 'Phone verified successfully.',\n\t\t\t\t\t\tresult: {\n\t\t\t\t\t\t\ttokens: { access: accessToken, refresh: refreshToken },\n\t\t\t\t\t\t\tuser: toUserDTO(verifiedUser, true),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tstatus: 200,\n\t\t\t\t\t\theaders: cookieHeaders(tokenPairCookies(accessToken, refreshToken, config)),\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// ── Email branch ─────────────────────────────────────────\n\t\t\tconst row = await emailVerif.findByUser(ctx.user!.id, pin);\n\t\t\tif (!row) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'VERIFICATION_FAILED', 'Invalid or expired pin');\n\t\t\t}\n\t\t\tif (new Date() > row.expiresAt) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'VERIFICATION_FAILED', 'Pin expired');\n\t\t\t}\n\n\t\t\tawait store.transaction(async (tx) => {\n\t\t\t\tawait Promise.all([\n\t\t\t\t\ttx.query(\n\t\t\t\t\t\t`UPDATE fonderie_users SET email_verified_at = now(), updated_at = now() WHERE id = $1`,\n\t\t\t\t\t\t[ctx.user!.id],\n\t\t\t\t\t),\n\t\t\t\t\ttx.query(`DELETE FROM fonderie_email_verifications WHERE user_id = $1 AND token = $2`, [\n\t\t\t\t\t\tctx.user!.id,\n\t\t\t\t\t\tpin,\n\t\t\t\t\t]),\n\t\t\t\t]);\n\t\t\t});\n\n\t\t\treturn setApiResponse(HTTP.OK, 'VERIFIED', 'Email verified successfully.', {\n\t\t\t\tverified: true,\n\t\t\t\temail: ctx.user!.email,\n\t\t\t});\n\t\t},\n\n\t\tsendVerification: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\t// Same pending-token rule as verify(): admitted only for the phone-OTP\n\t\t\t// flow (resending the code), never for an email MFA-pending login.\n\t\t\tif (ctx.user!.mfaPending && ctx.user!.loginMethod !== 'phone') {\n\t\t\t\treturn setApiResponse(HTTP.FORBIDDEN, 'MFA_REQUIRED', 'Complete MFA verification to continue');\n\t\t\t}\n\n\t\t\tconst resolved = { ...config, ...config.resolve?.(ctx) };\n\t\t\tconst cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;\n\n\t\t\tif (ctx.user!.loginMethod === 'phone') {\n\t\t\t\tconst phone = ctx.user!.phone;\n\t\t\t\tif (!phone) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t\t'NO_PHONE_ON_ACCOUNT',\n\t\t\t\t\t\t'No phone number associated with this account',\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tconst remaining = checkCooldown(await phoneVerif.findLastSentAt(ctx.user!.id), cooldown);\n\t\t\t\tif (remaining > 0) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.TOO_MANY_REQUESTS,\n\t\t\t\t\t\t'VERIFICATION_COOLDOWN',\n\t\t\t\t\t\t'Please wait before requesting a new code.',\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tretryAfter: Math.ceil(remaining / 1000),\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tconst otp = randomInt(100000, 1000000).toString();\n\t\t\t\tconst expiresAt = new Date(Date.now() + OTP_TTL_MS);\n\t\t\t\tawait phoneVerif.upsert(ctx.user!.id, phone, otp, expiresAt);\n\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\t\ttype: MESSAGE_KEYS.phoneOtp,\n\t\t\t\t\t\tlocale: ctx.user!.locale,\n\t\t\t\t\t\tdata: { otp },\n\t\t\t\t\t\trecipient: { email: null, phone, deviceToken: null },\n\t\t\t\t\t} satisfies ICourierMessage));\n\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t'VERIFICATION_SENT',\n\t\t\t\t\t'A verification code has been sent to your phone.',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (!ctx.user!.email) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'NO_EMAIL_ON_ACCOUNT',\n\t\t\t\t\t'No email address associated with this account',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (ctx.user!.emailVerifiedAt) {\n\t\t\t\treturn setApiResponse(HTTP.OK, 'EMAIL_VERIFIED', 'Email already verified.', {\n\t\t\t\t\tverified: true,\n\t\t\t\t\temail: ctx.user!.email,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst remaining = checkCooldown(await emailVerif.findLastSentAt(ctx.user!.id), cooldown);\n\t\t\tif (remaining > 0) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.TOO_MANY_REQUESTS,\n\t\t\t\t\t'VERIFICATION_COOLDOWN',\n\t\t\t\t\t'Please wait before requesting a new code.',\n\t\t\t\t\t{\n\t\t\t\t\t\tretryAfter: Math.ceil(remaining / 1000),\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst pin = randomInt(100000, 1000000).toString();\n\t\t\tconst expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24);\n\t\t\tawait emailVerif.replace(ctx.user!.id, pin, expiresAt);\n\n\t\t\tawait background(bus\n\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\ttype: MESSAGE_KEYS.emailVerification,\n\t\t\t\t\tlocale: ctx.user!.locale,\n\t\t\t\t\trecipient: { email: ctx.user!.email, phone: null, deviceToken: null },\n\t\t\t\t\tdata: { pin },\n\t\t\t\t} satisfies ICourierMessage));\n\n\t\t\treturn setApiResponse(HTTP.OK, 'VERIFICATION_SENT', 'Verification email sent.', {\n\t\t\t\temail: ctx.user!.email,\n\t\t\t});\n\t\t},\n\t};\n}\n","export function checkCooldown(lastSentAt: Date | null, cooldownMs: number): number {\n\tif (!lastSentAt) return 0;\n\treturn Math.max(0, cooldownMs - (Date.now() - lastSentAt.getTime()));\n}\n","export function normalizeEmail(email: string): string {\n\tif (typeof email !== 'string' || email.length === 0) {\n\t\tthrow new Error('Invalid email');\n\t}\n\n\tconst lower = email.trim().toLowerCase();\n\n\tif (lower.length === 0) {\n\t\tthrow new Error('Email cannot be empty');\n\t}\n\n\tconst atIndex = lower.indexOf('@');\n\tif (atIndex === -1 || lower.lastIndexOf('@') !== atIndex) {\n\t\tthrow new Error('Invalid email format');\n\t}\n\n\tconst local = lower.substring(0, atIndex);\n\tconst domain = lower.substring(atIndex + 1);\n\n\tif (!local || !domain) {\n\t\tthrow new Error('Invalid email format');\n\t}\n\n\tconst plusIndex = local.indexOf('+');\n\tconst normalizedLocal = plusIndex === -1 ? local : local.substring(0, plusIndex);\n\n\tif (!normalizedLocal) {\n\t\tthrow new Error('Invalid email format');\n\t}\n\n\treturn `${normalizedLocal}@${domain}`;\n}\n\nexport function normalizeEmailSafe(email: string): string | null {\n\ttry {\n\t\treturn normalizeEmail(email);\n\t} catch {\n\t\treturn null;\n\t}\n}\n","import { createHash } from 'node:crypto';\n\nimport type { IStoreAdapter } from '@fonderie/store';\n\n// Store only a hash of the reset credentials — never the plaintext. A DB read\n// (SQLi elsewhere, a backup/log leak) then yields nothing directly usable\n// within the 1h window. SHA-256 is sufficient: the token carries 256 bits of\n// entropy (unguessable), and the 6-digit pin's real protection is the route\n// rate-limiter + short TTL + all-session-revoke, not the hash. Deterministic\n// (unsalted) so the lookup is a single indexed equality on the hash.\nconst hashSecret = (value: string): string => createHash('sha256').update(value).digest('hex');\n\nexport class PasswordResetModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync create(userId: string, pin: string, token: string, expiresAt: Date): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_password_resets (user_id, pin, token, expires_at, created_at)\n\t\t\tVALUES ($1, $2, $3, $4, now())\n\t\t\tON CONFLICT (user_id) DO UPDATE\n\t\t\tSET pin = $2, token = $3, expires_at = $4, created_at = now()`,\n\t\t\t[userId, hashSecret(pin), hashSecret(token), expiresAt],\n\t\t);\n\t}\n\n\tasync findLastSentAt(userId: string): Promise<Date | null> {\n\t\tconst [row] = await this.store.query<{ created_at: Date }>(\n\t\t\t`SELECT created_at FROM fonderie_password_resets WHERE user_id = $1`,\n\t\t\t[userId],\n\t\t);\n\t\tif (!row) return null;\n\t\treturn new Date(row.created_at);\n\t}\n\n\tasync findByPin(pin: string): Promise<{ userId: string; expiresAt: Date } | null> {\n\t\tconst [row] = await this.store.query<{ user_id: string; expires_at: Date }>(\n\t\t\t`SELECT user_id, expires_at FROM fonderie_password_resets WHERE pin = $1`,\n\t\t\t[hashSecret(pin)],\n\t\t);\n\t\tif (!row) return null;\n\t\treturn { userId: row.user_id, expiresAt: new Date(row.expires_at) };\n\t}\n\n\t// Lookup by the high-entropy reset token. Unlike the 6-digit pin this is\n\t// not brute-forceable, so the token path needs no rate limit. Empty/short\n\t// values are refused before the query so a NULL/blank token column can\n\t// never be matched.\n\tasync findByToken(token: string): Promise<{ userId: string; expiresAt: Date } | null> {\n\t\tif (typeof token !== 'string' || token.length < 32) return null;\n\t\tconst [row] = await this.store.query<{ user_id: string; expires_at: Date }>(\n\t\t\t`SELECT user_id, expires_at FROM fonderie_password_resets WHERE token = $1`,\n\t\t\t[hashSecret(token)],\n\t\t);\n\t\tif (!row) return null;\n\t\treturn { userId: row.user_id, expiresAt: new Date(row.expires_at) };\n\t}\n\n\tasync deleteByUser(userId: string): Promise<void> {\n\t\tawait this.store.query(`DELETE FROM fonderie_password_resets WHERE user_id = $1`, [userId]);\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nexport class EmailVerificationModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync create(userId: string, pin: string, expiresAt: Date): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_email_verifications (user_id, token, expires_at)\n\t\t\tVALUES ($1, $2, $3)\n\t\t\tON CONFLICT (user_id) DO UPDATE SET token = $2, expires_at = $3`,\n\t\t\t[userId, pin, expiresAt],\n\t\t);\n\t}\n\n\tasync find(pin: string): Promise<{ userId: string; expiresAt: Date } | null> {\n\t\tconst [row] = await this.store.query<{ user_id: string; expires_at: Date }>(\n\t\t\t`SELECT user_id, expires_at FROM fonderie_email_verifications WHERE token = $1`,\n\t\t\t[pin],\n\t\t);\n\t\tif (!row) return null;\n\t\treturn { userId: row.user_id, expiresAt: new Date(row.expires_at) };\n\t}\n\n\tasync delete(pin: string): Promise<void> {\n\t\tawait this.store.query(`DELETE FROM fonderie_email_verifications WHERE token = $1`, [pin]);\n\t}\n\n\tasync findByUser(userId: string, pin: string): Promise<{ expiresAt: Date } | null> {\n\t\tconst [row] = await this.store.query<{ expires_at: Date }>(\n\t\t\t`SELECT expires_at FROM fonderie_email_verifications WHERE user_id = $1 AND token = $2`,\n\t\t\t[userId, pin],\n\t\t);\n\t\tif (!row) return null;\n\t\treturn { expiresAt: new Date(row.expires_at) };\n\t}\n\n\tasync findLastSentAt(userId: string): Promise<Date | null> {\n\t\tconst [row] = await this.store.query<{ created_at: Date }>(\n\t\t\t`SELECT created_at FROM fonderie_email_verifications WHERE user_id = $1`,\n\t\t\t[userId],\n\t\t);\n\t\tif (!row) return null;\n\t\treturn new Date(row.created_at);\n\t}\n\n\tasync replace(userId: string, pin: string, expiresAt: Date): Promise<void> {\n\t\tawait this.store.transaction(async (tx) => {\n\t\t\tawait tx.query(`DELETE FROM fonderie_email_verifications WHERE user_id = $1`, [userId]);\n\t\t\tawait tx.query(\n\t\t\t\t`INSERT INTO fonderie_email_verifications (token, user_id, expires_at)\n\t\t\t\tVALUES ($1, $2, $3)`,\n\t\t\t\t[pin, userId, expiresAt],\n\t\t\t);\n\t\t});\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nexport class PhoneVerificationModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync upsert(userId: string, phone: string, otp: string, expiresAt: Date): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_phone_verifications (phone, user_id, otp, expires_at)\n\t\t\tVALUES ($1, $2, $3, $4)\n\t\t\tON CONFLICT (phone) DO UPDATE\n\t\t\tSET user_id = $2, otp = $3, expires_at = $4, created_at = now()`,\n\t\t\t[phone, userId, otp, expiresAt],\n\t\t);\n\t}\n\n\tasync findByUser(\n\t\tuserId: string,\n\t\totp: string,\n\t): Promise<{ phone: string; expiresAt: Date } | null> {\n\t\tconst [row] = await this.store.query<{ phone: string; expires_at: Date }>(\n\t\t\t`SELECT phone, expires_at FROM fonderie_phone_verifications WHERE user_id = $1 AND otp = $2`,\n\t\t\t[userId, otp],\n\t\t);\n\t\tif (!row) return null;\n\t\treturn { phone: row.phone, expiresAt: new Date(row.expires_at) };\n\t}\n\n\tasync findLastSentAt(userId: string): Promise<Date | null> {\n\t\tconst [row] = await this.store.query<{ created_at: Date }>(\n\t\t\t`SELECT created_at FROM fonderie_phone_verifications WHERE user_id = $1`,\n\t\t\t[userId],\n\t\t);\n\t\tif (!row) return null;\n\t\treturn new Date(row.created_at);\n\t}\n\n\tasync deleteByUser(userId: string): Promise<void> {\n\t\tawait this.store.query(`DELETE FROM fonderie_phone_verifications WHERE user_id = $1`, [userId]);\n\t}\n}\n","import { clearedTokenCookies, cookieHeaders } from '../services/cookies';\nimport { randomInt } from 'node:crypto';\n\nimport { setApiResponse, HTTP, dateOrEmpty, background } from '@fonderie/core';\nimport type { IFonderieContext, ICourierMessage } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport type { IAuthConfig } from '../config';\n\nimport type { EventBus } from '@fonderie/events';\nimport { NOTIFICATION_EVENT } from '@fonderie/events';\n\nimport { MESSAGE_KEYS, EVENT_KEYS } from '../config';\nimport { toUserDTO } from '../dtos/user';\nimport {\n\ttoLoginHistoryPageDTO,\n\tdecodeLoginCursor,\n\ttoSessionDTO,\n} from '../dtos/login-activity';\nimport type { IUser } from '../types';\nimport { UserModel } from '../models/user.model';\nimport { SessionModel } from '../models/session.model';\nimport { LoginEventModel } from '../models/login-event.model';\nimport type { LoginOutcome } from '../models/login-event.model';\nimport { EmailVerificationModel } from '../models/email-verification.model';\nimport { PhoneVerificationModel } from '../models/phone-verification.model';\nimport { normalizeEmailSafe } from '../services/email';\n\nfunction normalizePhone(phone: string): string {\n\treturn phone.trim().replace(/[\\s()\\-\\.]/g, '');\n}\n\nfunction isValidPhone(phone: unknown): phone is string {\n\treturn typeof phone === 'string' && /^\\+?[1-9]\\d{6,14}$/.test(normalizePhone(phone));\n}\n\nexport function userController(store: IStoreAdapter, config: IAuthConfig, bus?: EventBus) {\n\tconst users = new UserModel(store);\n\tconst sessions = new SessionModel(store, config.location);\n\tconst loginEvents = new LoginEventModel(store, config.location);\n\tconst emailVerif = new EmailVerificationModel(store);\n\tconst phoneVerif = new PhoneVerificationModel(store);\n\n\treturn {\n\t\tme: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst user = await users.findById(ctx.user!.id);\n\t\t\tif (!user) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'User not found');\n\t\t\t}\n\n\t\t\treturn setApiResponse(HTTP.OK, 'USER_ACCOUNT_FETCHED', 'User account fetched successful.', {\n\t\t\t\tuser: toUserDTO(user, ctx.user!.phoneVerified),\n\t\t\t});\n\t\t},\n\n\t\t// ── Login history (append-only; the caller's own attempts) ────────\n\t\tloginHistory: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst params = new URL(ctx.request.url).searchParams;\n\t\t\tconst rawLimit = Number(params.get('limit') ?? 50);\n\t\t\tconst limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;\n\n\t\t\tconst outcomeParam = params.get('outcome');\n\t\t\tif (outcomeParam !== null && outcomeParam !== 'success' && outcomeParam !== 'failed') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', \"outcome must be 'success' or 'failed'\");\n\t\t\t}\n\n\t\t\tconst cursorParam = params.get('cursor');\n\t\t\tlet cursor: { createdAt: string; id: string } | null = null;\n\t\t\tif (cursorParam) {\n\t\t\t\tcursor = decodeLoginCursor(cursorParam);\n\t\t\t\tif (!cursor) {\n\t\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid cursor');\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst query: Parameters<LoginEventModel['listByUser']>[0] = { userId: ctx.user!.id, limit };\n\t\t\tif (outcomeParam) query.outcome = outcomeParam as LoginOutcome;\n\t\t\tif (cursor) query.cursor = cursor;\n\t\t\tconst from = params.get('from');\n\t\t\tconst to = params.get('to');\n\t\t\tif (from) query.from = new Date(from);\n\t\t\tif (to) query.to = new Date(to);\n\n\t\t\tconst page = await loginEvents.listByUser(query);\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.OK,\n\t\t\t\t'LOGIN_HISTORY_FETCHED',\n\t\t\t\t'Login history retrieved.',\n\t\t\t\ttoLoginHistoryPageDTO(page),\n\t\t\t);\n\t\t},\n\n\t\t// ── Active sessions (live only; the caller's own) ─────────────────\n\t\tlistSessions: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst rows = await sessions.listLiveByUser(ctx.user!.id);\n\t\t\tconst currentSid = (ctx.user as { sid?: string | null }).sid ?? null;\n\t\t\treturn setApiResponse(HTTP.OK, 'SESSIONS_FETCHED', 'Active sessions retrieved.', {\n\t\t\t\tsessions: rows.map((r) => toSessionDTO(r, currentSid)),\n\t\t\t});\n\t\t},\n\n\t\tterminateSession: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst id = (ctx.meta['params'] as Record<string, string> | undefined)?.['id'];\n\t\t\tif (!id) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Session not found');\n\t\t\t}\n\t\t\tconst removed = await sessions.terminateById(ctx.user!.id, id);\n\t\t\tif (!removed) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Session not found');\n\t\t\t}\n\t\t\treturn setApiResponse(HTTP.OK, 'SESSION_TERMINATED', 'Session terminated.', { id });\n\t\t},\n\n\t\tterminateOtherSessions: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst currentSid = (ctx.user as { sid?: string | null }).sid ?? null;\n\t\t\tif (!currentSid) {\n\t\t\t\t// Without a resolvable current session there is nothing to spare, so\n\t\t\t\t// refuse rather than nuke every session (which would include this one).\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'NO_CURRENT_SESSION',\n\t\t\t\t\t'Cannot terminate others without a current session context',\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst count = await sessions.terminateOthers(ctx.user!.id, currentSid);\n\t\t\treturn setApiResponse(HTTP.OK, 'SESSIONS_TERMINATED', 'Other sessions terminated.', { count });\n\t\t},\n\n\t\tupdateProfile: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\n\t\t\tconst allowed = ['firstName', 'lastName', 'avatarUrl'] as const;\n\t\t\tconst fields: Record<string, unknown> = {};\n\t\t\tfor (const key of allowed) {\n\t\t\t\tif (body?.[key] !== undefined) fields[key] = body[key];\n\t\t\t}\n\n\t\t\tif (Object.keys(fields).length === 0) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'Provide at least one of: firstName, lastName, avatarUrl',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst row = await users.update(ctx.user!.id, fields);\n\t\t\tif (!row) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'User not found');\n\t\t\t}\n\n\t\t\tconst updated = await users.findById(ctx.user!.id);\n\t\t\treturn setApiResponse(HTTP.OK, 'PROFILE_UPDATED', 'Profile updated.', {\n\t\t\t\tuser: toUserDTO(updated as IUser, ctx.user!.phoneVerified),\n\t\t\t});\n\t\t},\n\n\t\tupdatePreferences: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\n\t\t\tconst fields: Parameters<typeof users.updatePreferences>[1] = {};\n\n\t\t\tif (typeof body?.['locale'] === 'string') fields.locale = body['locale'];\n\t\t\tif (typeof body?.['timezone'] === 'string') fields.timezone = body['timezone'];\n\n\t\t\tconst prefKeys = ['notifications', 'emailDigest', 'dateFormat', 'timeFormat'] as const;\n\t\t\tconst patch: Record<string, unknown> = {};\n\t\t\tfor (const key of prefKeys) {\n\t\t\t\tif (body?.[key] !== undefined) patch[key] = body[key];\n\t\t\t}\n\t\t\tif (Object.keys(patch).length > 0) fields.patch = patch;\n\n\t\t\tconst row = await users.updatePreferences(ctx.user!.id, fields);\n\t\t\tif (!row) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'Provide at least one preference field',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst updated = await users.findById(ctx.user!.id);\n\t\t\treturn setApiResponse(HTTP.OK, 'PREFERENCES_UPDATED', 'Preferences updated.', {\n\t\t\t\tuser: toUserDTO(updated as IUser, ctx.user!.phoneVerified),\n\t\t\t});\n\t\t},\n\n\t\tupdateEmail: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst newEmail = body?.['email'];\n\n\t\t\tconst normalised = typeof newEmail === 'string' ? normalizeEmailSafe(newEmail) : null;\n\t\t\tif (!normalised) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'A valid email address is required',\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst oldEmail = ctx.user!.email;\n\n\t\t\tif (normalised === oldEmail) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'New email must differ from current email',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst existing = await users.findByEmail(normalised);\n\t\t\tif (existing) {\n\t\t\t\treturn setApiResponse(HTTP.CONFLICT, 'EMAIL_IN_USE', 'Email already in use');\n\t\t\t}\n\n\t\t\tconst pin = randomInt(100000, 1000000).toString();\n\t\t\tconst expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24);\n\t\t\tawait emailVerif.replace(ctx.user!.id, pin, expiresAt);\n\t\t\tawait users.updateEmail(ctx.user!.id, normalised);\n\n\t\t\tawait background(bus\n\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\ttype: MESSAGE_KEYS.emailVerification,\n\t\t\t\t\tlocale: ctx.user!.locale,\n\t\t\t\t\tdata: { pin },\n\t\t\t\t\trecipient: { email: normalised, phone: null, deviceToken: null },\n\t\t\t\t} satisfies ICourierMessage));\n\t\t\tif (oldEmail) {\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\t\ttype: MESSAGE_KEYS.emailChanged,\n\t\t\t\t\t\tlocale: ctx.user!.locale,\n\t\t\t\t\t\tdata: { newEmail: normalised },\n\t\t\t\t\t\trecipient: { email: oldEmail, phone: null, deviceToken: null },\n\t\t\t\t\t} satisfies ICourierMessage));\n\t\t\t}\n\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.OK,\n\t\t\t\t'EMAIL_UPDATED',\n\t\t\t\t'Email updated. A verification code has been sent to your new address.',\n\t\t\t\t{\n\t\t\t\t\temail: normalised,\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\tupdatePhone: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst newPhone = body?.['phone'];\n\n\t\t\tif (!isValidPhone(newPhone)) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'A valid phone number is required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst normalised = normalizePhone(newPhone);\n\n\t\t\tconst existing = await users.findByPhone(normalised);\n\t\t\tif (existing) {\n\t\t\t\treturn setApiResponse(HTTP.CONFLICT, 'PHONE_IN_USE', 'Phone number already in use');\n\t\t\t}\n\n\t\t\tconst otp = randomInt(100000, 1000000).toString();\n\t\t\tconst expiresAt = new Date(Date.now() + 10 * 60 * 1000);\n\t\t\tawait phoneVerif.upsert(ctx.user!.id, normalised, otp, expiresAt);\n\t\t\tawait users.updatePhone(ctx.user!.id, normalised);\n\n\t\t\tawait background(bus\n\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\ttype: MESSAGE_KEYS.phoneOtp,\n\t\t\t\t\tlocale: ctx.user!.locale,\n\t\t\t\t\tdata: { otp },\n\t\t\t\t\trecipient: { email: null, phone: normalised, deviceToken: null },\n\t\t\t\t} satisfies ICourierMessage));\n\t\t\tif (ctx.user!.email) {\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\t\ttype: MESSAGE_KEYS.phoneChanged,\n\t\t\t\t\t\tlocale: ctx.user!.locale,\n\t\t\t\t\t\tdata: {},\n\t\t\t\t\t\trecipient: { email: ctx.user!.email, phone: null, deviceToken: null },\n\t\t\t\t\t} satisfies ICourierMessage));\n\t\t\t}\n\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.OK,\n\t\t\t\t'PHONE_UPDATED',\n\t\t\t\t'Phone number updated. A verification code has been sent to your new number.',\n\t\t\t\t{\n\t\t\t\t\tphone: normalised,\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\tchangePassword: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst currentPassword = body?.['currentPassword'];\n\t\t\tconst newPassword = body?.['newPassword'];\n\n\t\t\tif (typeof currentPassword !== 'string' || !currentPassword) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'currentPassword is required');\n\t\t\t}\n\t\t\tif (typeof newPassword !== 'string' || newPassword.length < 8) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'newPassword must be at least 8 characters');\n\t\t\t}\n\n\t\t\tconst user = await users.findById(ctx.user!.id);\n\t\t\tif (!user) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'User not found');\n\n\t\t\tconst { hashPassword, verifyPassword } = await import('../services/password');\n\t\t\tconst valid = await verifyPassword(currentPassword, user.passwordHash ?? '');\n\t\t\tif (!valid) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_CREDENTIAL', 'Current password is incorrect');\n\t\t\t}\n\n\t\t\tconst hash = await hashPassword(newPassword);\n\t\t\tawait users.updatePassword(ctx.user!.id, hash);\n\n\t\t\t// Revoke all sessions so a changed password invalidates any tokens an\n\t\t\t// attacker (or the user's old device) may still hold. The client must\n\t\t\t// re-authenticate after changing its password.\n\t\t\tawait sessions.deleteByUser(ctx.user!.id);\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PASSWORD_CHANGED', 'Password updated successfully.');\n\t\t},\n\n\t\t/**\n\t\t * Remove an OAuth provider from the account.\n\t\t *\n\t\t * Refuses when the account has no password, because unlinking would\n\t\t * then leave NO way to sign in — that is account deletion, not a\n\t\t * settings toggle, and a user clicking \"disconnect Google\" is not\n\t\t * asking for it. The check is inside the UPDATE's WHERE clause, so a\n\t\t * password cannot disappear between the check and the write.\n\t\t *\n\t\t * Notifies the account's email either way: changing how an account can\n\t\t * be signed into is a security event for its owner, who is not\n\t\t * necessarily the person doing it.\n\t\t */\n\t\tunlinkOauth: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst provider = String((ctx.meta['params'] as Record<string, string>)?.['provider'] ?? '');\n\t\t\tconst current = await users.findById(ctx.user!.id);\n\t\t\tif (!current?.provider) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_LINKED', 'No sign-in provider is linked.');\n\t\t\t}\n\t\t\tif (provider && provider.toLowerCase() !== current.provider.toLowerCase()) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.NOT_FOUND,\n\t\t\t\t\t'NOT_LINKED',\n\t\t\t\t\t`This account is not linked to ${provider}.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst removed = await users.clearProvider(ctx.user!.id);\n\t\t\tif (!removed) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.CONFLICT,\n\t\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t\t'Set a password before disconnecting this provider — it is currently the only way to sign in.',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (ctx.user!.email) {\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\t\ttype: MESSAGE_KEYS.oauthUnlinked,\n\t\t\t\t\t\tlocale: ctx.user!.locale,\n\t\t\t\t\t\tdata: { provider: current.provider },\n\t\t\t\t\t\trecipient: { email: ctx.user!.email, phone: null, deviceToken: null },\n\t\t\t\t\t} satisfies ICourierMessage));\n\t\t\t}\n\t\t\treturn setApiResponse(HTTP.OK, 'OAUTH_UNLINKED', 'Sign-in provider removed.');\n\t\t},\n\n\t\tdeleteMe: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst userId = ctx.user!.id;\n\t\t\tawait users.softDelete(userId);\n\n\t\t\tconst reqId = ctx.meta['requestId'] as string | undefined;\n\t\t\tawait background(bus\n\t\t\t\t?.emit(\n\t\t\t\t\tEVENT_KEYS.userDeleted,\n\t\t\t\t\t{ userId },\n\t\t\t\t\treqId !== undefined ? { requestId: reqId } : undefined,\n\t\t\t\t));\n\n\t\t\treturn Response.json(\n\t\t\t\t{ reason: 'ACCOUNT_DELETED', explanation: 'Account successfully deleted.' },\n\t\t\t\t{\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\theaders: cookieHeaders(clearedTokenCookies(config)),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\t// Subject Access Request — the authenticated user's own data as a portable\n\t\t// JSON bundle. Only auth-owned data, and only safe fields: no password hash,\n\t\t// no MFA secret, no session tokens (session metadata only).\n\t\texportMe: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst user = await users.findById(ctx.user!.id);\n\t\t\tif (!user) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'User not found');\n\n\t\t\tconst sessionRows = await sessions.listByUser(ctx.user!.id);\n\t\t\tconst bundle = {\n\t\t\t\texportedAt: new Date().toISOString(),\n\t\t\t\tprofile: toUserDTO(user, ctx.user!.phoneVerified),\n\t\t\t\tsessions: sessionRows.map((s) => ({\n\t\t\t\t\tid: s.id,\n\t\t\t\t\tuserAgent: s.userAgent,\n\t\t\t\t\tipAddress: s.ipAddress,\n\t\t\t\t\tcreatedAt: dateOrEmpty(s.createdAt),\n\t\t\t\t\texpiresAt: dateOrEmpty(s.expiresAt),\n\t\t\t\t})),\n\t\t\t\tsecurity: {\n\t\t\t\t\tmfaEnabled: user.mfaEnabled === true,\n\t\t\t\t\temailVerified: user.emailVerifiedAt !== null,\n\t\t\t\t},\n\t\t\t};\n\n\t\t\t// SAR completeness: other modules contribute the caller's data they own\n\t\t\t// (workspaces memberships, billing, …) without auth importing them — the\n\t\t\t// app wires collectors via config.dataExportContributors.\n\t\t\tconst contributors = config.dataExportContributors ?? [];\n\t\t\tif (contributors.length > 0) {\n\t\t\t\tconst modules: Record<string, unknown> = {};\n\t\t\t\tfor (const c of contributors) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tmodules[c.name] = await c.collect(ctx.user!.id);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tmodules[c.name] = null; // a failing contributor never blocks the export\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t(bundle as Record<string, unknown>)['modules'] = modules;\n\t\t\t}\n\n\t\t\treturn setApiResponse(HTTP.OK, 'DATA_EXPORT', 'Your account data.', bundle);\n\t\t},\n\t};\n}\n","import { encodeKeysetCursor, decodeKeysetCursor } from '@fonderie/core';\n\nimport type { ILoginEventRow, ILoginEventPage } from '../models/login-event.model';\nimport { type IRequestLocation, sanitizeLocation } from '../services/request-location';\n\n// ── Login history ────────────────────────────────────────────────\n\nexport interface ILoginEventDTO {\n\tid: string;\n\tmethod: string;\n\toutcome: string;\n\tfailureReason: string | null;\n\tipAddress: string | null;\n\tuserAgent: string | null;\n\t/** Where the attempt came from — present only when the app configures\n\t * `location`. Country is reliable; region/city are approximate. */\n\tlocation: IRequestLocation | null;\n\tcreatedAt: string;\n}\n\nexport interface ILoginHistoryPageDTO {\n\tevents: ILoginEventDTO[];\n\tnextCursor: string | null;\n}\n\nfunction toLoginEventDTO(row: ILoginEventRow): ILoginEventDTO {\n\treturn {\n\t\tid: row.id,\n\t\tmethod: row.method,\n\t\toutcome: row.outcome,\n\t\tfailureReason: row.failureReason,\n\t\tipAddress: row.ipAddress,\n\t\tuserAgent: row.userAgent,\n\t\t// Re-sanitized on read: the column is JSONB and could have been written\n\t\t// by anything with database access.\n\t\tlocation: sanitizeLocation(row.location),\n\t\tcreatedAt: row.createdAt instanceof Date ? row.createdAt.toISOString() : String(row.createdAt),\n\t};\n}\n\nexport function toLoginHistoryPageDTO(page: ILoginEventPage): ILoginHistoryPageDTO {\n\tconst last = page.events[page.events.length - 1];\n\t// Cursor carries created_at at full microsecond precision (createdAtRaw) so\n\t// same-microsecond rows can't be skipped between pages.\n\tconst nextCursor =\n\t\tpage.hasMore && last ? encodeKeysetCursor(last.createdAtRaw ?? last.createdAt.toISOString(), last.id) : null;\n\treturn { events: page.events.map(toLoginEventDTO), nextCursor };\n}\n\n// Shared cursor decode (range-checked in core; a crafted cursor → null → the\n// caller's 422, never a Postgres cast 500).\nexport const decodeLoginCursor = decodeKeysetCursor;\n\n// ── Active sessions ──────────────────────────────────────────────\n\nexport interface ISessionDTO {\n\tid: string;\n\tcurrent: boolean;\n\tipAddress: string | null;\n\tuserAgent: string | null;\n\t/** Where the session was opened from, when `location` is configured. */\n\tlocation: IRequestLocation | null;\n\tcreatedAt: string;\n\texpiresAt: string;\n}\n\nexport function toSessionDTO(\n\trow: {\n\t\tid: string;\n\t\tsid: string | null;\n\t\tuserAgent: string | null;\n\t\tipAddress: string | null;\n\t\tlocation?: unknown;\n\t\tcreatedAt: Date;\n\t\texpiresAt: Date;\n\t},\n\tcurrentSid: string | null,\n): ISessionDTO {\n\treturn {\n\t\tid: row.id,\n\t\tcurrent: currentSid !== null && row.sid === currentSid,\n\t\tipAddress: row.ipAddress,\n\t\tuserAgent: row.userAgent,\n\t\tlocation: sanitizeLocation(row.location),\n\t\tcreatedAt: row.createdAt instanceof Date ? row.createdAt.toISOString() : String(row.createdAt),\n\t\texpiresAt: row.expiresAt instanceof Date ? row.expiresAt.toISOString() : String(row.expiresAt),\n\t};\n}\n","import { randomBytes, createPublicKey, createHash, type KeyObject } from 'node:crypto';\nimport jwt from 'jsonwebtoken';\n\nimport { tokenPairCookies, cookieHeaders } from '../services/cookies';\nimport { setApiResponse, HTTP, constantTimeEqual, background } from '@fonderie/core';\nimport type { IFonderieContext, ICourierMessage } from '@fonderie/core';\nimport type { EventBus } from '@fonderie/events';\nimport { NOTIFICATION_EVENT } from '@fonderie/events';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAuthConfig } from '../config';\nimport { MESSAGE_KEYS, EVENT_KEYS } from '../config';\nimport { issueTokenPair, refreshTokenExpiry } from '../services/jwt';\nimport { toUserDTO } from '../dtos/user';\nimport { UserModel } from '../models/user.model';\nimport { SessionModel } from '../models/session.model';\nimport { LoginEventModel } from '../models/login-event.model';\nimport { ConsumedTokenModel } from '../models/consumed-token.model';\nimport { requestMeta } from '../services/request-meta';\nimport { normalizeEmailSafe } from '../services/email';\n\n// ── Sign in with Apple ───────────────────────────────────────────────────────\n// Apple differs from Google in three ways handled here: (1) the client secret\n// is a short-lived ES256 JWT minted from the .p8 key, not a static string;\n// (2) the web callback is a cross-site POST (response_mode=form_post), so its\n// CSRF cookie must be SameSite=None; (3) the NATIVE identityToken (from the iOS\n// sheet) did not come from our own TLS exchange, so its signature MUST be\n// verified against Apple's published JWKS.\n\nconst APPLE_AUTH_URL = 'https://appleid.apple.com/auth/authorize';\nconst APPLE_TOKEN_URL = 'https://appleid.apple.com/auth/token';\nconst APPLE_KEYS_URL = 'https://appleid.apple.com/auth/keys';\nconst APPLE_ISSUER = 'https://appleid.apple.com';\n\ntype AppleConfig = NonNullable<IAuthConfig['apple']>;\n\ninterface AppleClaims {\n\temail?: string;\n\tsub?: string;\n\t// Apple sends email_verified as a boolean OR the string \"true\".\n\temail_verified?: boolean | string;\n\tnonce?: string;\n\t// Seconds since epoch — the token's own expiry, used as the TTL of the\n\t// single-use replay record.\n\texp?: number;\n}\n\n// Mint the client_secret: a ≤5-min ES256 JWT signed with the .p8 key. Minted\n// per exchange and never stored (Apple allows up to 6 months; short is safer).\nexport function mintAppleClientSecret(apple: AppleConfig): string {\n\tconst now = Math.floor(Date.now() / 1000);\n\treturn jwt.sign(\n\t\t{ iss: apple.teamId, iat: now, exp: now + 300, aud: APPLE_ISSUER, sub: apple.clientId },\n\t\tapple.privateKey,\n\t\t{ algorithm: 'ES256', keyid: apple.keyId },\n\t);\n}\n\n// Apple's signing keys, cached for an hour. Module-level cache is safe: the keys\n// are public. Hardened against unknown-kid abuse: an attacker fully controls the\n// (unverified) kid on POST /auth/apple/native, and a naive \"refetch on unknown\n// kid\" turns every forged token into an outbound call to Apple. So refetches are\n// (a) single-flighted — concurrent callers share one fetch, (b) rate-limited to\n// at most one attempt per minute regardless of how many unknown kids arrive, and\n// (c) bounded by a request timeout so a slow/hung Apple never ties up handlers.\ninterface AppleJwk { kid: string; kty: string; n: string; e: string; alg: string; use?: string }\nlet appleKeyCache: { keys: AppleJwk[]; fetchedAt: number } | null = null;\nlet appleKeyInflight: Promise<void> | null = null;\nlet appleKeyLastAttempt = 0;\nconst APPLE_KEYS_TTL_MS = 60 * 60 * 1000;\nconst APPLE_KEYS_MIN_REFETCH_MS = 60 * 1000;\nconst APPLE_KEYS_FETCH_TIMEOUT_MS = 5000;\n\nfunction refreshAppleKeys(): Promise<void> {\n\t// Single-flight: one in-flight fetch is shared by all concurrent callers.\n\tif (appleKeyInflight) return appleKeyInflight;\n\tappleKeyLastAttempt = Date.now();\n\tappleKeyInflight = (async () => {\n\t\ttry {\n\t\t\tconst res = await fetch(APPLE_KEYS_URL, { signal: AbortSignal.timeout(APPLE_KEYS_FETCH_TIMEOUT_MS) });\n\t\t\tconst data = (await res.json()) as { keys?: AppleJwk[] };\n\t\t\tif (Array.isArray(data.keys)) appleKeyCache = { keys: data.keys, fetchedAt: Date.now() };\n\t\t} catch {\n\t\t\t// Keep any stale cache rather than failing outright.\n\t\t} finally {\n\t\t\tappleKeyInflight = null;\n\t\t}\n\t})();\n\treturn appleKeyInflight;\n}\n\nasync function getApplePublicKey(kid: string): Promise<KeyObject | null> {\n\tconst find = () => appleKeyCache?.keys.find((k) => k.kid === kid) ?? null;\n\tconst cacheFresh = () => !!appleKeyCache && Date.now() - appleKeyCache.fetchedAt < APPLE_KEYS_TTL_MS;\n\n\tlet jwk = find();\n\t// Refetch only when the kid is missing or the cache is stale — AND not more\n\t// than once per minute, so a flood of attacker-chosen kids can trigger at\n\t// most one outbound fetch per minute, never one per request.\n\tif ((!jwk || !cacheFresh()) && Date.now() - appleKeyLastAttempt >= APPLE_KEYS_MIN_REFETCH_MS) {\n\t\tawait refreshAppleKeys();\n\t\tjwk = find();\n\t}\n\tif (!jwk) return null;\n\ttry {\n\t\treturn createPublicKey({ key: jwk as unknown as JsonWebKey, format: 'jwk' });\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n// Verify an Apple id_token: signature against the JWKS (by kid), issuer, and\n// audience (Services ID for web; allow-listed bundle ids for native). exp is\n// enforced by jwt.verify. Returns the claims, or null on any failure.\nexport async function verifyAppleIdToken(\n\tidToken: string,\n\topts: { audiences: string[]; nonce?: string | undefined },\n): Promise<AppleClaims | null> {\n\tif (opts.audiences.length === 0) return null;\n\tconst decoded = jwt.decode(idToken, { complete: true });\n\tif (!decoded || typeof decoded === 'string') return null;\n\tconst kid = decoded.header.kid;\n\tif (!kid) return null;\n\tconst key = await getApplePublicKey(kid);\n\tif (!key) return null;\n\ttry {\n\t\tconst payload = jwt.verify(idToken, key, {\n\t\t\talgorithms: ['RS256'],\n\t\t\tissuer: APPLE_ISSUER,\n\t\t\t// jsonwebtoken types `audience` as a non-empty tuple; runtime accepts a\n\t\t\t// list and matches the token's aud against ANY entry (guarded non-empty\n\t\t\t// above).\n\t\t\taudience: opts.audiences as [string, ...string[]],\n\t\t}) as AppleClaims;\n\t\t// Optional session-binding: if the app bound a nonce to the native\n\t\t// request, the token's nonce claim must match it (defends against a token\n\t\t// substituted from a different session). This is NOT replay protection —\n\t\t// verbatim replay is stopped by the single-use guard in appleNative.\n\t\tif (opts.nonce !== undefined && payload.nonce !== opts.nonce) return null;\n\t\treturn payload;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nconst appleEmailVerified = (c: AppleClaims): boolean =>\n\tc.email_verified === true || c.email_verified === 'true';\n\n// Test-only: reset the module-level JWKS cache/rate-limit state so each test can\n// exercise a fresh fetch. Never called in production code paths.\nexport function __resetAppleKeysForTests(): void {\n\tappleKeyCache = null;\n\tappleKeyInflight = null;\n\tappleKeyLastAttempt = 0;\n}\n\nexport function oauthController(store: IStoreAdapter, config: IAuthConfig, bus?: EventBus) {\n\tconst users = new UserModel(store);\n\tconst sessions = new SessionModel(store, config.location);\n\tconst loginEvents = new LoginEventModel(store, config.location);\n\tconst consumedTokens = new ConsumedTokenModel(store);\n\n\t/**\n\t * Announce what an OAuth upsert actually did.\n\t *\n\t * A signing-in user hits this code path on EVERY login, so the default is\n\t * silence; only a change in what the account is gets announced.\n\t *\n\t * • new account → `userRegistered`, the same domain event a password\n\t * registration emits. This is the one that was missing: subscribers\n\t * provision on it (workspaces creates the personal workspace), so an\n\t * OAuth signup previously produced a user with none — invisible until\n\t * something later asked for a workspace that was never created.\n\t * • provider added or changed → a security notice to the address on file,\n\t * because the account gained a new way to be signed into and the owner\n\t * is not necessarily the person who did it.\n\t * • same provider as before → nothing. Emailing here would notify the\n\t * user on every sign-in.\n\t */\n\tconst announceOAuthUpsert = async (\n\t\tctx: IFonderieContext,\n\t\tupserted: {\n\t\t\tid: string;\n\t\t\tinserted: boolean;\n\t\t\tpreviousProvider: string | null;\n\t\t\tclearedUnverifiedPassword?: boolean;\n\t\t},\n\t\tprovider: string,\n\t\tuser: { email: string | null; firstName: string | null; lastName: string | null; locale: string },\n\t): Promise<void> => {\n\t\tif (!bus) return;\n\t\tconst reqId = ctx.meta['requestId'] as string | undefined;\n\t\tconst reqOpts = reqId !== undefined ? { requestId: reqId } : undefined;\n\t\tconst label = provider.charAt(0).toUpperCase() + provider.slice(1);\n\t\tconst recipient = { email: user.email, phone: null, deviceToken: null };\n\n\t\t// Announced regardless of which branch follows, and first: a password\n\t\t// that silently stops working is the one change a user cannot diagnose.\n\t\t// The recipient is now PROVEN to own this mailbox — that is what the\n\t\t// provider just established — so this reaches the right person.\n\t\tif (upserted.clearedUnverifiedPassword && user.email) {\n\t\t\tawait background(bus.emit(\n\t\t\t\tNOTIFICATION_EVENT,\n\t\t\t\t{\n\t\t\t\t\ttype: MESSAGE_KEYS.passwordRevoked,\n\t\t\t\t\tlocale: user.locale,\n\t\t\t\t\tdata: { provider: label },\n\t\t\t\t\trecipient,\n\t\t\t\t} satisfies ICourierMessage,\n\t\t\t\treqOpts,\n\t\t\t));\n\t\t}\n\n\t\tif (upserted.inserted) {\n\t\t\tawait background(bus.emit(\n\t\t\t\tEVENT_KEYS.userRegistered,\n\t\t\t\t{\n\t\t\t\t\tuserId: upserted.id,\n\t\t\t\t\temail: user.email,\n\t\t\t\t\tfirstName: user.firstName,\n\t\t\t\t\tlastName: user.lastName,\n\t\t\t\t\tloginMethod: provider,\n\t\t\t\t},\n\t\t\t\treqOpts,\n\t\t\t));\n\t\t\tif (user.email) {\n\t\t\t\tawait background(bus.emit(\n\t\t\t\t\tNOTIFICATION_EVENT,\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: MESSAGE_KEYS.oauthRegistration,\n\t\t\t\t\t\tlocale: user.locale,\n\t\t\t\t\t\tdata: { provider: label, appName: config.appName ?? 'Fonderie' },\n\t\t\t\t\t\trecipient,\n\t\t\t\t\t} satisfies ICourierMessage,\n\t\t\t\t\treqOpts,\n\t\t\t\t));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (upserted.previousProvider !== provider && user.email) {\n\t\t\tawait background(bus.emit(\n\t\t\t\tNOTIFICATION_EVENT,\n\t\t\t\t{\n\t\t\t\t\ttype: MESSAGE_KEYS.oauthLinked,\n\t\t\t\t\tlocale: user.locale,\n\t\t\t\t\tdata: { provider: label },\n\t\t\t\t\trecipient,\n\t\t\t\t} satisfies ICourierMessage,\n\t\t\t\treqOpts,\n\t\t\t));\n\t\t}\n\t};\n\n\t// Shared tail for both Apple flows (web callback + native token): given\n\t// verified claims, upsert the account by email, open a session, record the\n\t// login event, and return the same token/user envelope the Google flow does.\n\tconst completeAppleLogin = async (\n\t\tctx: IFonderieContext,\n\t\tclaims: { email?: string | undefined; sub?: string | undefined; emailVerified: boolean },\n\t): Promise<Response> => {\n\t\tconst meta = requestMeta(ctx);\n\t\tif (!claims.email) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'APPLE_AUTH_FAILED', 'No email in Apple identity token');\n\t\t}\n\t\t// Linking is BY EMAIL — an unverified address would let its holder take\n\t\t// over an account registered with it. Apple verifies both real and\n\t\t// private-relay addresses; require it.\n\t\tif (!claims.emailVerified) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'APPLE_AUTH_FAILED', 'Apple account email is not verified');\n\t\t}\n\t\tconst normalizedEmail = normalizeEmailSafe(claims.email);\n\t\tif (!normalizedEmail) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'APPLE_AUTH_FAILED', 'Invalid email in Apple identity token');\n\t\t}\n\n\t\tconst upserted = await users.upsertByProvider(normalizedEmail, 'apple', claims.sub ?? '');\n\t\tif (!upserted) {\n\t\t\tawait loginEvents.recordSafe({\n\t\t\t\tuserId: null,\n\t\t\t\temailAttempted: normalizedEmail,\n\t\t\t\tmethod: 'oauth-apple',\n\t\t\t\toutcome: 'failed',\n\t\t\t\tfailureReason: 'provider_upsert_failed',\n\t\t\t\t...meta,\n\t\t\t});\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Apple login failed');\n\t\t}\n\n\t\tconst fullUser = await users.findById(upserted.id);\n\t\tif (!fullUser) {\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Apple login failed');\n\t\t}\n\n\t\tawait announceOAuthUpsert(ctx, upserted, 'apple', fullUser);\n\n\t\tconst { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, { loginMethod: 'apple' });\n\t\tawait sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);\n\t\tawait loginEvents.recordSafe({\n\t\t\tuserId: upserted.id,\n\t\t\temailAttempted: normalizedEmail,\n\t\t\tmethod: 'oauth-apple',\n\t\t\toutcome: 'success',\n\t\t\t...meta,\n\t\t});\n\n\t\treturn Response.json(\n\t\t\t{\n\t\t\t\treason: 'APPLE_AUTH_SUCCESS',\n\t\t\t\texplanation: 'Apple authentication successful.',\n\t\t\t\tresult: {\n\t\t\t\t\ttokens: { access: accessToken, refresh: refreshToken },\n\t\t\t\t\tuser: toUserDTO(fullUser),\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tstatus: 200,\n\t\t\t\theaders: cookieHeaders([\n\t\t\t\t\t...tokenPairCookies(accessToken, refreshToken, config),\n\t\t\t\t\t// One-time value — clear the (SameSite=None) state cookie.\n\t\t\t\t\t'oauth_state=; HttpOnly; SameSite=None; Secure; Path=/; Max-Age=0',\n\t\t\t\t]),\n\t\t\t},\n\t\t);\n\t};\n\n\treturn {\n\t\tgoogleInit: async (_ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst google = config.google;\n\t\t\tif (!google) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.NOT_IMPLEMENTED,\n\t\t\t\t\t'NOT_CONFIGURED',\n\t\t\t\t\t'Google OAuth not configured',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// CSRF `state`: a random value that must round-trip — bound to THIS\n\t\t\t// browser via a short-lived cookie and echoed back by Google in the\n\t\t\t// callback query. Without it, an attacker can complete the callback\n\t\t\t// with a code from THEIR OWN Google account and silently log the\n\t\t\t// victim's browser into the attacker's account (login CSRF).\n\t\t\tconst state = randomBytes(16).toString('hex');\n\n\t\t\tconst params = new URLSearchParams({\n\t\t\t\tclient_id: google.clientId,\n\t\t\t\tredirect_uri: google.redirectUri,\n\t\t\t\tresponse_type: 'code',\n\t\t\t\tscope: 'openid email profile',\n\t\t\t\tstate,\n\t\t\t});\n\n\t\t\tconst url = `https://accounts.google.com/o/oauth2/v2/auth?${params}`;\n\n\t\t\tconst secure = (config.secureCookies ?? process.env['NODE_ENV'] === 'production') ? '; Secure' : '';\n\t\t\treturn Response.json(\n\t\t\t\t{\n\t\t\t\t\treason: 'GOOGLE_AUTH_URL',\n\t\t\t\t\texplanation: 'Redirect the user to the returned URL to begin Google OAuth.',\n\t\t\t\t\tresult: { url },\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\t// SameSite=Lax (not Strict): the cookie must be sent on the\n\t\t\t\t\t// top-level cross-site redirect back from Google.\n\t\t\t\t\theaders: cookieHeaders([\n\t\t\t\t\t\t`oauth_state=${state}; HttpOnly; SameSite=Lax; Path=/; Max-Age=600${secure}`,\n\t\t\t\t\t]),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\tgoogleCallback: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst google = config.google;\n\t\t\tif (!google) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.NOT_IMPLEMENTED,\n\t\t\t\t\t'NOT_CONFIGURED',\n\t\t\t\t\t'Google OAuth not configured',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst url = new URL(ctx.request.url);\n\t\t\tconst code = url.searchParams.get('code');\n\n\t\t\tif (!code) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_PARAMETER', 'Missing code');\n\t\t\t}\n\n\t\t\t// CSRF check: the state Google echoed back must equal the value we\n\t\t\t// bound to this browser in googleInit's cookie.\n\t\t\tconst returnedState = url.searchParams.get('state') ?? '';\n\t\t\tconst cookieHeader = ctx.request.headers.get('cookie') ?? '';\n\t\t\tconst expectedState = cookieHeader.match(/(?:^|;\\s*)oauth_state=([^;]+)/)?.[1] ?? '';\n\t\t\tif (\n\t\t\t\t!returnedState ||\n\t\t\t\t!expectedState ||\n\t\t\t\t!constantTimeEqual(Buffer.from(returnedState), Buffer.from(expectedState))\n\t\t\t) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'GOOGLE_AUTH_FAILED',\n\t\t\t\t\t'OAuth state mismatch — restart the sign-in flow',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst tokenRes = await fetch('https://oauth2.googleapis.com/token', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: { 'content-type': 'application/x-www-form-urlencoded' },\n\t\t\t\tbody: new URLSearchParams({\n\t\t\t\t\tcode,\n\t\t\t\t\tclient_id: google.clientId,\n\t\t\t\t\tclient_secret: google.clientSecret,\n\t\t\t\t\tredirect_uri: google.redirectUri,\n\t\t\t\t\tgrant_type: 'authorization_code',\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\tconst tokenData = (await tokenRes.json()) as { id_token?: string };\n\t\t\tif (!tokenData.id_token) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'GOOGLE_AUTH_FAILED',\n\t\t\t\t\t'OAuth token exchange failed',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// The id_token arrives DIRECTLY from Google's token endpoint over TLS\n\t\t\t// in a confidential-client code exchange, so per OIDC Core §3.1.3.7\n\t\t\t// signature verification may be skipped — but the claims still must\n\t\t\t// be checked before trusting them.\n\t\t\tconst payload = JSON.parse(\n\t\t\t\tBuffer.from(tokenData.id_token.split('.')[1] ?? '', 'base64url').toString(),\n\t\t\t) as { email?: string; sub?: string; aud?: string; iss?: string; exp?: number; email_verified?: boolean };\n\n\t\t\tif (payload.aud !== google.clientId) {\n\t\t\t\t// A token minted for a DIFFERENT client must never log anyone in here.\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'GOOGLE_AUTH_FAILED', 'OAuth token audience mismatch');\n\t\t\t}\n\t\t\tif (payload.iss !== 'https://accounts.google.com' && payload.iss !== 'accounts.google.com') {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'GOOGLE_AUTH_FAILED', 'OAuth token issuer mismatch');\n\t\t\t}\n\t\t\tif (typeof payload.exp !== 'number' || payload.exp * 1000 < Date.now()) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'GOOGLE_AUTH_FAILED', 'OAuth token expired');\n\t\t\t}\n\n\t\t\tif (!payload.email) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'GOOGLE_AUTH_FAILED', 'No email in OAuth response');\n\t\t\t}\n\n\t\t\t// Account linking is BY EMAIL (upsertByProvider), so an unverified\n\t\t\t// Google email would let its holder take over an existing account\n\t\t\t// that registered with that address. Require Google's verification.\n\t\t\tif (payload.email_verified !== true) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'GOOGLE_AUTH_FAILED',\n\t\t\t\t\t'Google account email is not verified',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst normalizedEmail = normalizeEmailSafe(payload.email);\n\t\t\tif (!normalizedEmail) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'GOOGLE_AUTH_FAILED', 'Invalid email in OAuth response');\n\t\t\t}\n\n\t\t\tconst meta = requestMeta(ctx);\n\n\t\t\tconst upserted = await users.upsertByProvider(normalizedEmail, 'google', payload.sub ?? '');\n\t\t\tif (!upserted) {\n\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\tuserId: null,\n\t\t\t\t\temailAttempted: normalizedEmail,\n\t\t\t\t\tmethod: 'oauth-google',\n\t\t\t\t\toutcome: 'failed',\n\t\t\t\t\tfailureReason: 'provider_upsert_failed',\n\t\t\t\t\t...meta,\n\t\t\t\t});\n\t\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'OAuth login failed');\n\t\t\t}\n\n\t\t\tconst fullUser = await users.findById(upserted.id);\n\t\t\tif (!fullUser) {\n\t\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'OAuth login failed');\n\t\t\t}\n\n\t\t\tawait announceOAuthUpsert(ctx, upserted, 'google', fullUser);\n\n\t\t\tconst { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, {\n\t\t\t\tloginMethod: 'google',\n\t\t\t});\n\t\t\tawait sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);\n\t\t\tawait loginEvents.recordSafe({\n\t\t\t\tuserId: upserted.id,\n\t\t\t\temailAttempted: normalizedEmail,\n\t\t\t\tmethod: 'oauth-google',\n\t\t\t\toutcome: 'success',\n\t\t\t\t...meta,\n\t\t\t});\n\n\t\t\treturn Response.json(\n\t\t\t\t{\n\t\t\t\t\treason: 'GOOGLE_AUTH_SUCCESS',\n\t\t\t\t\texplanation: 'Google authentication successful.',\n\t\t\t\t\tresult: {\n\t\t\t\t\t\ttokens: { access: accessToken, refresh: refreshToken },\n\t\t\t\t\t\tuser: toUserDTO(fullUser),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\theaders: cookieHeaders([\n\t\t\t\t\t\t...tokenPairCookies(accessToken, refreshToken, config),\n\t\t\t\t\t\t// One-time value — clear it once the flow completes.\n\t\t\t\t\t\t'oauth_state=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0',\n\t\t\t\t\t]),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\tappleInit: async (_ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst apple = config.apple;\n\t\t\tif (!apple) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_IMPLEMENTED, 'NOT_CONFIGURED', 'Apple OAuth not configured');\n\t\t\t}\n\n\t\t\tconst state = randomBytes(16).toString('hex');\n\t\t\tconst params = new URLSearchParams({\n\t\t\t\tclient_id: apple.clientId,\n\t\t\t\tredirect_uri: apple.redirectUri,\n\t\t\t\tresponse_type: 'code',\n\t\t\t\t// form_post is REQUIRED to receive name/email; Apple then POSTs the\n\t\t\t\t// callback cross-site.\n\t\t\t\tresponse_mode: 'form_post',\n\t\t\t\tscope: 'name email',\n\t\t\t\tstate,\n\t\t\t});\n\t\t\tconst url = `${APPLE_AUTH_URL}?${params}`;\n\n\t\t\t// SameSite=None; Secure (NOT Lax like Google): the state cookie must\n\t\t\t// survive Apple's cross-site POST back to the callback. Requires HTTPS,\n\t\t\t// which Sign in with Apple mandates for redirect URIs anyway.\n\t\t\treturn Response.json(\n\t\t\t\t{\n\t\t\t\t\treason: 'APPLE_AUTH_URL',\n\t\t\t\t\texplanation: 'Redirect the user to the returned URL to begin Apple OAuth.',\n\t\t\t\t\tresult: { url },\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\theaders: cookieHeaders([\n\t\t\t\t\t\t`oauth_state=${state}; HttpOnly; SameSite=None; Secure; Path=/; Max-Age=600`,\n\t\t\t\t\t]),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\tappleCallback: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst apple = config.apple;\n\t\t\tif (!apple) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_IMPLEMENTED, 'NOT_CONFIGURED', 'Apple OAuth not configured');\n\t\t\t}\n\n\t\t\t// Apple posts application/x-www-form-urlencoded (response_mode=form_post).\n\t\t\t// Prefer a pre-parsed body if the pipeline produced one, else read raw.\n\t\t\tconst pre = ctx.meta['body'];\n\t\t\tconst form =\n\t\t\t\tpre && typeof pre === 'object'\n\t\t\t\t\t? new URLSearchParams(pre as Record<string, string>)\n\t\t\t\t\t: new URLSearchParams(await ctx.request.text());\n\n\t\t\tconst code = form.get('code');\n\t\t\tif (!code) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_PARAMETER', 'Missing code');\n\t\t\t}\n\n\t\t\t// CSRF: the state Apple echoes must equal the value appleInit bound to\n\t\t\t// this browser via cookie.\n\t\t\tconst returnedState = form.get('state') ?? '';\n\t\t\tconst cookieHeader = ctx.request.headers.get('cookie') ?? '';\n\t\t\tconst expectedState = cookieHeader.match(/(?:^|;\\s*)oauth_state=([^;]+)/)?.[1] ?? '';\n\t\t\tif (\n\t\t\t\t!returnedState ||\n\t\t\t\t!expectedState ||\n\t\t\t\t!constantTimeEqual(Buffer.from(returnedState), Buffer.from(expectedState))\n\t\t\t) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'APPLE_AUTH_FAILED',\n\t\t\t\t\t'OAuth state mismatch — restart the sign-in flow',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst tokenRes = await fetch(APPLE_TOKEN_URL, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: { 'content-type': 'application/x-www-form-urlencoded' },\n\t\t\t\tbody: new URLSearchParams({\n\t\t\t\t\tcode,\n\t\t\t\t\tclient_id: apple.clientId,\n\t\t\t\t\tclient_secret: mintAppleClientSecret(apple),\n\t\t\t\t\tredirect_uri: apple.redirectUri,\n\t\t\t\t\tgrant_type: 'authorization_code',\n\t\t\t\t}),\n\t\t\t});\n\t\t\tconst tokenData = (await tokenRes.json()) as { id_token?: string };\n\t\t\tif (!tokenData.id_token) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'APPLE_AUTH_FAILED', 'OAuth token exchange failed');\n\t\t\t}\n\n\t\t\t// Web audience is the Services ID. Full JWKS signature check (one shared\n\t\t\t// verifier for both flows; stronger than skipping it).\n\t\t\tconst claims = await verifyAppleIdToken(tokenData.id_token, { audiences: [apple.clientId] });\n\t\t\tif (!claims) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'APPLE_AUTH_FAILED', 'Invalid Apple identity token');\n\t\t\t}\n\t\t\treturn completeAppleLogin(ctx, {\n\t\t\t\temail: claims.email,\n\t\t\t\tsub: claims.sub,\n\t\t\t\temailVerified: appleEmailVerified(claims),\n\t\t\t});\n\t\t},\n\n\t\tappleNative: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst apple = config.apple;\n\t\t\tif (!apple) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_IMPLEMENTED, 'NOT_CONFIGURED', 'Apple OAuth not configured');\n\t\t\t}\n\t\t\tconst body = ctx.meta['body'] as { identityToken?: string; nonce?: string } | undefined;\n\t\t\tconst identityToken = body?.identityToken;\n\t\t\tif (!identityToken) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_PARAMETER', 'Missing identityToken');\n\t\t\t}\n\n\t\t\t// Native identityTokens carry aud = the app's bundle id (allow-listed\n\t\t\t// via nativeClientIds), not the Services ID, and did NOT come from our\n\t\t\t// TLS exchange — so verifyAppleIdToken's JWKS signature check is the\n\t\t\t// only thing between a forged token and a login.\n\t\t\tconst audiences =\n\t\t\t\tapple.nativeClientIds && apple.nativeClientIds.length\n\t\t\t\t\t? apple.nativeClientIds\n\t\t\t\t\t: [apple.clientId];\n\t\t\tconst claims = await verifyAppleIdToken(identityToken, { audiences, nonce: body?.nonce });\n\t\t\tif (!claims) {\n\t\t\t\tconst meta = requestMeta(ctx);\n\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\tuserId: null,\n\t\t\t\t\temailAttempted: null,\n\t\t\t\t\tmethod: 'oauth-apple',\n\t\t\t\t\toutcome: 'failed',\n\t\t\t\t\tfailureReason: 'invalid_identity_token',\n\t\t\t\t\t...meta,\n\t\t\t\t});\n\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'APPLE_AUTH_FAILED', 'Invalid Apple identity token');\n\t\t\t}\n\n\t\t\t// Single-use guard: a verified native identityToken may be redeemed\n\t\t\t// exactly once. Apple does not one-time it for us (unlike the web `code`\n\t\t\t// exchange), so without this a captured token could be replayed until\n\t\t\t// its exp. Consume by hash with TTL = the token's exp; a replay is a\n\t\t\t// recorded security event, never a login.\n\t\t\tconst tokenHash = createHash('sha256').update(identityToken).digest('hex');\n\t\t\tconst expMs = typeof claims.exp === 'number' ? claims.exp * 1000 : Date.now() + 5 * 60 * 1000;\n\t\t\tconst firstUse = await consumedTokens.consumeOnce(tokenHash, new Date(expMs));\n\t\t\tif (!firstUse) {\n\t\t\t\tconst meta = requestMeta(ctx);\n\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\tuserId: null,\n\t\t\t\t\temailAttempted: claims.email ?? null,\n\t\t\t\t\tmethod: 'oauth-apple',\n\t\t\t\t\toutcome: 'failed',\n\t\t\t\t\tfailureReason: 'token_replayed',\n\t\t\t\t\t...meta,\n\t\t\t\t});\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNAUTHORIZED,\n\t\t\t\t\t'APPLE_AUTH_FAILED',\n\t\t\t\t\t'This Apple identity token has already been used',\n\t\t\t\t);\n\t\t\t}\n\t\t\tconsumedTokens.sweepExpiredSafe();\n\n\t\t\treturn completeAppleLogin(ctx, {\n\t\t\t\temail: claims.email,\n\t\t\t\tsub: claims.sub,\n\t\t\t\temailVerified: appleEmailVerified(claims),\n\t\t\t});\n\t\t},\n\t};\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\n// Single-use guard for native OAuth identity tokens. Unlike the web `code`\n// exchange (which Apple/Google make one-time), a native identityToken is\n// presented directly by the client, so without this it could be replayed until\n// its exp. consumeOnce() records a hash of the token and reports whether THIS\n// call was the first to consume it — an atomic INSERT ... ON CONFLICT DO\n// NOTHING, so concurrent replays race safely (exactly one wins). Only a hash is\n// stored, never the token.\nexport class ConsumedTokenModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\t// True if the token had not been consumed before (caller may proceed); false\n\t// if this is a replay. `expiresAt` is the token's own exp — its TTL here.\n\tasync consumeOnce(tokenHash: string, expiresAt: Date): Promise<boolean> {\n\t\tconst rows = await this.store.query<{ token_hash: string }>(\n\t\t\t`INSERT INTO fonderie_consumed_tokens (token_hash, expires_at)\n\t\t\t VALUES ($1, $2)\n\t\t\t ON CONFLICT (token_hash) DO NOTHING\n\t\t\t RETURNING token_hash`,\n\t\t\t[tokenHash, expiresAt],\n\t\t);\n\t\treturn rows.length > 0;\n\t}\n\n\t// Opportunistic, fire-and-forget cleanup of expired rows (index-backed).\n\t// Recording history must never block or fail a login.\n\tsweepExpiredSafe(): void {\n\t\tvoid this.store\n\t\t\t.query(`DELETE FROM fonderie_consumed_tokens WHERE expires_at < now()`)\n\t\t\t.catch(() => {});\n\t}\n}\n","import type { IReadinessProblem } from '@fonderie/core';\nimport { MIN_SECRET_LENGTH, PLACEHOLDER_SECRET, secretStrengthProblem } from '@fonderie/core';\nimport type { IAuthConfig } from '../config';\n\nconst MODULE = '@fonderie/auth';\n\n// Pure production-readiness assessment (no side effects) — shared by the\n// boot-time guard and `AuthModule.checkReadiness`. Evaluates \"is this ready for\n// production?\" regardless of the current NODE_ENV. Secret-strength rule\n// (min length + placeholder denylist) is the shared @fonderie/core one.\nexport function collectAuthConfigProblems(config: IAuthConfig): IReadinessProblem[] {\n\tconst problems: IReadinessProblem[] = [];\n\tconst secret = config.jwtSecret ?? '';\n\n\tconst secretProblem = secretStrengthProblem(secret);\n\tif (secretProblem === 'too-short') {\n\t\tproblems.push({\n\t\t\tmodule: MODULE,\n\t\t\tseverity: 'error',\n\t\t\tmessage: `jwtSecret must be at least ${MIN_SECRET_LENGTH} characters (got ${secret.length})`,\n\t\t});\n\t} else if (secretProblem === 'placeholder') {\n\t\tproblems.push({\n\t\t\tmodule: MODULE,\n\t\t\tseverity: 'error',\n\t\t\tmessage: 'jwtSecret looks like a placeholder or dev-default value',\n\t\t});\n\t}\n\n\t// Explicitly disabling Secure cookies ships session cookies over plaintext —\n\t// an error in production (fails the boot gate), a warning elsewhere.\n\tif (config.secureCookies === false) {\n\t\tproblems.push({\n\t\t\tmodule: MODULE,\n\t\t\tseverity: process.env['NODE_ENV'] === 'production' ? 'error' : 'warning',\n\t\t\tmessage: 'secureCookies is false — auth cookies may be sent over non-HTTPS connections in production',\n\t\t});\n\t}\n\n\t// Google OAuth secrets: if the provider is wired up, its clientSecret is a\n\t// bearer credential to Google — a placeholder or blank value is as unsafe as\n\t// a weak jwtSecret, so it's a boot-blocking error in production.\n\tif (config.google) {\n\t\tconst clientSecret = config.google.clientSecret ?? '';\n\t\tif (!clientSecret || !config.google.clientId || !config.google.redirectUri) {\n\t\t\tproblems.push({\n\t\t\t\tmodule: MODULE,\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: 'google OAuth is configured but clientId, clientSecret, or redirectUri is missing',\n\t\t\t});\n\t\t} else if (PLACEHOLDER_SECRET.test(clientSecret)) {\n\t\t\tproblems.push({\n\t\t\t\tmodule: MODULE,\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: 'google.clientSecret looks like a placeholder or dev-default value',\n\t\t\t});\n\t\t}\n\t}\n\n\t// Sign in with Apple: the .p8 private key is a bearer credential to Apple. If\n\t// the provider is wired, its required fields must be present and the key must\n\t// look like a real PEM — a missing field or placeholder is a boot-blocking\n\t// error in production, same posture as google.clientSecret.\n\tif (config.apple) {\n\t\tconst a = config.apple;\n\t\tif (!a.clientId || !a.teamId || !a.keyId || !a.privateKey || !a.redirectUri) {\n\t\t\tproblems.push({\n\t\t\t\tmodule: MODULE,\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: 'apple OAuth is configured but clientId, teamId, keyId, privateKey, or redirectUri is missing',\n\t\t\t});\n\t\t} else if (!/-----BEGIN [A-Z ]*PRIVATE KEY-----/.test(a.privateKey)) {\n\t\t\tproblems.push({\n\t\t\t\tmodule: MODULE,\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: 'apple.privateKey does not look like a PEM .p8 key (expected `-----BEGIN PRIVATE KEY-----`)',\n\t\t\t});\n\t\t}\n\t\t// nativeClientIds is the audience allow-list for POST /auth/apple/native —\n\t\t// a native identityToken is accepted iff its aud is one of these. An empty\n\t\t// or wildcard entry would accept tokens minted for OTHER Apple apps, so\n\t\t// it's a boot-blocking error (only exact bundle ids belong here).\n\t\tif (a.nativeClientIds?.some((id) => !id || id.trim() === '' || id.includes('*'))) {\n\t\t\tproblems.push({\n\t\t\t\tmodule: MODULE,\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: 'apple.nativeClientIds must be exact bundle ids — empty or wildcard entries would accept identity tokens minted for other apps',\n\t\t\t});\n\t\t}\n\t}\n\n\t// MFA is on but TOTP secrets have no at-rest encryption key — they'd be\n\t// stored plaintext. An error in production (fails the boot gate); a warning\n\t// elsewhere so dev/test with backward-compatible defaults still run.\n\tif (config.mfa && !config.mfaSecretKey) {\n\t\tproblems.push({\n\t\t\tmodule: MODULE,\n\t\t\tseverity: process.env['NODE_ENV'] === 'production' ? 'error' : 'warning',\n\t\t\tmessage: 'mfa is enabled without mfaSecretKey — TOTP secrets are stored plaintext at rest; set a 32-byte key (openssl rand -hex 32)',\n\t\t});\n\t}\n\n\t// A malformed key can never decrypt: catch it at boot rather than on the\n\t// first MFA request. 32 bytes = 64 hex chars.\n\tif (config.mfaSecretKey && !/^[0-9a-fA-F]{64}$/.test(config.mfaSecretKey)) {\n\t\tproblems.push({\n\t\t\tmodule: MODULE,\n\t\t\tseverity: 'error',\n\t\t\tmessage: 'mfaSecretKey must be 64 hex characters (32 bytes, e.g. `openssl rand -hex 32`)',\n\t\t});\n\t}\n\n\treturn problems;\n}\n\n// Boot-time guard, run automatically when an `AuthModule` is constructed\n// (fail-fast, before boot). In production a weak `jwtSecret` is fatal — a\n// forgeable token is an auth bypass — so we refuse to boot; outside production\n// the same errors are a loud warning so dev/test still run. `secureCookies`\n// warnings are only surfaced in production (dev intentionally uses non-secure\n// cookies over localhost).\nexport function validateAuthConfig(config: IAuthConfig): void {\n\tconst isProduction = process.env['NODE_ENV'] === 'production';\n\tconst problems = collectAuthConfigProblems(config);\n\tconst errors = problems.filter((p) => p.severity === 'error');\n\n\tif (isProduction && errors.length > 0) {\n\t\tthrow new Error(\n\t\t\t`[auth] insecure config — ${errors.map((e) => e.message).join('; ')}. ` +\n\t\t\t\t'Set a long, random jwtSecret (e.g. `openssl rand -base64 32`). ' +\n\t\t\t\t'Refusing to boot in production.',\n\t\t);\n\t}\n\n\tif (isProduction) {\n\t\tfor (const p of problems) console.warn(`[auth] ${p.message}`);\n\t} else {\n\t\tfor (const p of errors) {\n\t\t\tconsole.warn(`[auth] ${p.message} (insecure — would refuse to boot in production)`);\n\t\t}\n\t}\n}\n","import type { Middleware } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAuthConfig } from '../config';\nimport { verifyToken } from '../services/jwt';\nimport type { IAccessPayload } from '../services/jwt';\nimport { UserModel } from '../models/user.model';\nimport { SessionModel } from '../models/session.model';\n\n// Reads the Bearer token or session cookie, populates ctx.user\n// Does NOT reject — anonymous requests pass through\nexport function withSession(store: IStoreAdapter, config: IAuthConfig): Middleware {\n\tconst users = new UserModel(store);\n\tconst sessions = new SessionModel(store);\n\n\treturn async (ctx, next) => {\n\t\tconst token = extractToken(ctx.request);\n\t\tif (!token) {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst payload = verifyToken(token, config);\n\t\tif (!payload || payload.type !== 'access') {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Revocation: access tokens are bound to their refresh session via the\n\t\t// sid claim. Logout, rotation, and password change delete that row, so\n\t\t// the access token dies with it instead of living out its JWT expiry.\n\t\t// Tokens without a sid pass through: short-lived mfaPending tokens\n\t\t// (no session exists yet) and legacy tokens from before session\n\t\t// binding, which age out within one accessTokenDuration of deploy.\n\t\tif (payload.sid && !(await sessions.aliveBySid(payload.sid))) {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst user = await users.findById(payload.sub);\n\t\tif (!user || user.suspended || user.deletedAt) {\n\t\t\treturn next();\n\t\t}\n\n\t\tObject.assign(ctx, {\n\t\t\tuser: {\n\t\t\t\t...user,\n\t\t\t\tloginMethod: payload.loginMethod ?? 'email',\n\t\t\t\tphoneVerified: payload.phoneVerified ?? false,\n\t\t\t\tmfaPending: (payload as IAccessPayload).mfaPending ?? false,\n\t\t\t\t// The session this request is authenticated on, so the sessions\n\t\t\t\t// API can flag the current row and \"terminate others\" can spare it.\n\t\t\t\tsid: payload.sid ?? null,\n\t\t\t},\n\t\t});\n\n\t\treturn next();\n\t};\n}\n\nfunction extractToken(request: Request): string | null {\n\tconst auth = request.headers.get('authorization');\n\tif (auth?.startsWith('Bearer ')) {\n\t\treturn auth.slice(7);\n\t}\n\n\tconst cookie = request.headers.get('cookie') ?? '';\n\tconst match = cookie.match(/(?:^|;\\s*)access_token=([^;]+)/);\n\treturn match?.[1] ?? null;\n}\n","import type { IAdminRoute, IFonderieContext, Middleware } from '@fonderie/core';\nimport { HTTP, encodeKeysetCursor, setApiResponse } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { decodeLoginCursor, toLoginHistoryPageDTO, toSessionDTO } from './dtos/login-activity';\nimport { toUserDTO } from './dtos/user';\nimport type { IUserDTO } from './dtos/user';\nimport { LoginEventModel } from './models/login-event.model';\nimport { SessionModel } from './models/session.model';\nimport { UserModel } from './models/user.model';\nimport type { IUserPage } from './models/user.model';\nimport type { IUser } from './types';\n\n// What the operator sees: the app's own user shape plus the fields support\n// asks about first. Never the password hash, never the MFA secret.\nexport interface IAdminUserDTO extends IUserDTO {\n\tsuspended: boolean;\n\tdeletedAt: string | null;\n\tcreatedAt: string;\n}\n\nexport function toAdminUserDTO(user: IUser): IAdminUserDTO {\n\treturn {\n\t\t...toUserDTO(user),\n\t\tsuspended: Boolean(user.suspended),\n\t\tdeletedAt: user.deletedAt ? user.deletedAt.toISOString() : null,\n\t\tcreatedAt: user.createdAt.toISOString(),\n\t};\n}\n\nexport interface IAdminUserPageDTO {\n\tusers: IAdminUserDTO[];\n\tnextCursor: string | null;\n}\n\nexport function toAdminUserPageDTO(page: IUserPage): IAdminUserPageDTO {\n\tconst last = page.users[page.users.length - 1];\n\t// Full microsecond precision (createdAtRaw) so same-microsecond rows are\n\t// not skipped between pages.\n\tconst nextCursor =\n\t\tpage.hasMore && last\n\t\t\t? encodeKeysetCursor(last.createdAtRaw ?? last.createdAt.toISOString(), last.id)\n\t\t\t: null;\n\treturn { users: page.users.map(toAdminUserDTO), nextCursor };\n}\n\nconst ADMIN_PREFIX = '/_admin';\nconst idOf = (ctx: IFonderieContext) => ctx.meta.params?.['id'] ?? '';\nconst NOT_FOUND = () => setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such user');\n\n// Declared at the default admin path so the route table reads literally;\n// @fonderie/admin re-bases them under its prefix. There is no standalone\n// surface: these exist only through composition.\nfunction adminRouteTable(store: IStoreAdapter): Array<[string, string, Middleware]> {\n\tconst users = new UserModel(store);\n\tconst sessions = new SessionModel(store);\n\tconst events = new LoginEventModel(store);\n\n\tconst setSuspended =\n\t\t(suspended: boolean): Middleware =>\n\t\tasync (ctx) => {\n\t\t\tconst ok = await users.setSuspended(idOf(ctx), suspended);\n\t\t\tif (!ok) return NOT_FOUND();\n\t\t\tconst user = await users.findById(idOf(ctx));\n\t\t\treturn user\n\t\t\t\t? setApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\tsuspended ? 'USER_SUSPENDED' : 'USER_UNSUSPENDED',\n\t\t\t\t\t\tsuspended ? 'User suspended' : 'User unsuspended',\n\t\t\t\t\t\ttoAdminUserDTO(user),\n\t\t\t\t\t)\n\t\t\t\t: NOT_FOUND();\n\t\t};\n\n\treturn [\n\t\t[\n\t\t\t'GET',\n\t\t\t'/_admin/users',\n\t\t\t// With an email, the exact lookup support reaches for. Without one, a\n\t\t\t// page of users — an operator who has to know a name before they can\n\t\t\t// see anything cannot discover who signed up this morning.\n\t\t\tasync (ctx) => {\n\t\t\t\tconst params = new URL(ctx.request.url).searchParams;\n\t\t\t\tconst email = params.get('email')?.trim().toLowerCase();\n\t\t\t\tif (email) {\n\t\t\t\t\tconst user = await users.findByEmail(email);\n\t\t\t\t\treturn user ? setApiResponse(HTTP.OK, 'USER', 'User', toAdminUserDTO(user)) : NOT_FOUND();\n\t\t\t\t}\n\n\t\t\t\tconst rawLimit = Number(params.get('limit') ?? 50);\n\t\t\t\tconst limit = Number.isFinite(rawLimit)\n\t\t\t\t\t? Math.min(Math.max(Math.trunc(rawLimit), 1), 200)\n\t\t\t\t\t: 50;\n\t\t\t\tconst cursorParam = params.get('cursor');\n\t\t\t\tconst cursor = cursorParam ? decodeLoginCursor(cursorParam) : null;\n\t\t\t\tif (cursorParam && !cursor)\n\t\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid cursor');\n\t\t\t\tconst deleted = params.get('deleted') === '1' || params.get('deleted') === 'true';\n\t\t\t\tconst page = await users.list({\n\t\t\t\t\tlimit,\n\t\t\t\t\t...(cursor ? { cursor } : {}),\n\t\t\t\t\t...(deleted ? { deleted } : {}),\n\t\t\t\t});\n\t\t\t\treturn setApiResponse(HTTP.OK, 'USERS', 'Users', toAdminUserPageDTO(page));\n\t\t\t},\n\t\t],\n\t\t[\n\t\t\t'GET',\n\t\t\t'/_admin/users/:id',\n\t\t\tasync (ctx) => {\n\t\t\t\t// Read-only view: a soft-deleted account resolves (deletedAt set), so\n\t\t\t\t// an operator arriving from billing sees what happened to it.\n\t\t\t\tconst user = await users.findByIdIncludingDeleted(idOf(ctx));\n\t\t\t\treturn user ? setApiResponse(HTTP.OK, 'USER', 'User', toAdminUserDTO(user)) : NOT_FOUND();\n\t\t\t},\n\t\t],\n\t\t[\n\t\t\t'GET',\n\t\t\t'/_admin/users/:id/sessions',\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!(await users.findById(idOf(ctx)))) return NOT_FOUND();\n\t\t\t\tconst rows = await sessions.listLiveByUser(idOf(ctx));\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t'SESSIONS',\n\t\t\t\t\t'Live sessions',\n\t\t\t\t\trows.map((r) => toSessionDTO(r, null)),\n\t\t\t\t);\n\t\t\t},\n\t\t],\n\t\t// Signs the user out everywhere. The one write support reaches for first.\n\t\t[\n\t\t\t'DELETE',\n\t\t\t'/_admin/users/:id/sessions',\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!(await users.findById(idOf(ctx)))) return NOT_FOUND();\n\t\t\t\tawait sessions.deleteByUser(idOf(ctx));\n\t\t\t\treturn setApiResponse(HTTP.OK, 'SESSIONS_REVOKED', 'All sessions revoked');\n\t\t\t},\n\t\t],\n\t\t[\n\t\t\t'GET',\n\t\t\t'/_admin/users/:id/login-history',\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!(await users.findById(idOf(ctx)))) return NOT_FOUND();\n\t\t\t\tconst params = new URL(ctx.request.url).searchParams;\n\t\t\t\tconst rawLimit = Number(params.get('limit') ?? 50);\n\t\t\t\tconst limit = Number.isFinite(rawLimit)\n\t\t\t\t\t? Math.min(Math.max(Math.trunc(rawLimit), 1), 200)\n\t\t\t\t\t: 50;\n\t\t\t\tconst cursorParam = params.get('cursor');\n\t\t\t\tconst cursor = cursorParam ? decodeLoginCursor(cursorParam) : null;\n\t\t\t\tif (cursorParam && !cursor)\n\t\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid cursor');\n\t\t\t\tconst page = await events.listByUser({\n\t\t\t\t\tuserId: idOf(ctx),\n\t\t\t\t\tlimit,\n\t\t\t\t\t...(cursor ? { cursor } : {}),\n\t\t\t\t});\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t'LOGIN_HISTORY',\n\t\t\t\t\t'Login history',\n\t\t\t\t\ttoLoginHistoryPageDTO(page),\n\t\t\t\t);\n\t\t\t},\n\t\t],\n\t\t['POST', '/_admin/users/:id/suspend', setSuspended(true)],\n\t\t['POST', '/_admin/users/:id/unsuspend', setSuspended(false)],\n\t];\n}\n\nexport function describeAuthAdminRoutes(store: IStoreAdapter): IAdminRoute[] {\n\treturn adminRouteTable(store).map(([method, path, h]) => ({\n\t\tmethod,\n\t\tpath: path.slice(ADMIN_PREFIX.length),\n\t\thandlers: [h],\n\t}));\n}\n","import type { IAdminDescription, IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport type { EventBus } from '@fonderie/events';\n\nimport type { IReadinessProblem } from '@fonderie/core';\nimport { buildAuthRoutes } from './routes';\nimport type { IAuthConfig } from './config';\nimport { validateAuthConfig, collectAuthConfigProblems } from './services/config-guard';\nimport { withSession } from './middlewares/session';\nimport { describeAuthAdminRoutes } from './admin';\n\nexport class AuthModule implements IFonderieModule {\n\treadonly name = '@fonderie/auth';\n\t// Baked in at build time by tsup.base, so the operator's Modules page can\n\t// say what is actually deployed rather than 'not reported'.\n\treadonly version = process.env['FONDERIE_PKG_VERSION'] ?? '0.0.0-dev';\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate config: IAuthConfig,\n\t\tprivate bus?: EventBus,\n\t) {\n\t\t// Fail fast on an insecure jwtSecret (fatal in production) before boot.\n\t\tvalidateAuthConfig(config);\n\t}\n\n\t// Users, sessions, login history, suspend — only through @fonderie/admin.\n\tdescribeAdmin(): IAdminDescription {\n\t\treturn { routes: describeAuthAdminRoutes(this.store) };\n\t}\n\n\t// Report config problems for app.checkProductionReadiness() (data, not throw).\n\tcheckReadiness(): IReadinessProblem[] {\n\t\treturn collectAuthConfigProblems(this.config);\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\tapp.use(withSession(this.store, this.config));\n\n\t\tconst routes = buildAuthRoutes(this.store, this.config, this.bus);\n\t\tfor (const [method, path, ...handlers] of routes) {\n\t\t\tapp.addRoute(method, path, ...handlers);\n\t\t}\n\t}\n}\n","import type { IDefaultTemplate } from '@fonderie/core';\n\nimport { MESSAGE_KEYS, type AuthMessageKey } from './config';\n\n// Built-in default copy for every @fonderie/auth notification, shipped so the\n// emails render out of the box — no per-app template authoring, and never the\n// raw-JSON fallback. `html` values are BODY FRAGMENTS: courier injects them into\n// its branded layout shell (an app rebrands via its own `_layout`), then\n// interpolates {{vars}}. An app overrides any single key by shipping its own\n// template (DB row / FS file) for that key.\n//\n// The `satisfies Record<AuthMessageKey, IDefaultTemplate>` is the completeness\n// guarantee: add a key to MESSAGE_KEYS without a default here and it will not\n// compile. `phone-otp` is SMS — text only, no subject/html.\nexport const DEFAULT_TEMPLATES = {\n\t[MESSAGE_KEYS.emailRegistration]: {\n\t\tsubject: 'Confirm your account',\n\t\thtml: `<h1>Welcome aboard</h1>\n<p>Hi {{firstName}},</p>\n<p>Thanks for signing up. Confirm your account with this code:</p>\n<p><span class=\"pin-code\">{{pin}}</span></p>\n<p class=\"muted\">This code expires in 24 hours. If you didn’t create an account, you can safely ignore this email.</p>`,\n\t\ttext: `Welcome aboard\n\nHi {{firstName}},\n\nThanks for signing up. Confirm your account with this code: {{pin}}\n\nThis code expires in 24 hours. If you didn't create an account, you can safely ignore this email.`,\n\t},\n\n\t[MESSAGE_KEYS.emailVerification]: {\n\t\tsubject: 'Your verification code',\n\t\thtml: `<h1>Verify your email</h1>\n<p>Hi {{firstName}},</p>\n<p>Use this code to finish setting up your account:</p>\n<p><span class=\"pin-code\">{{pin}}</span></p>\n<p class=\"muted\">This code expires in 24 hours. If you didn’t create an account, you can safely ignore this email.</p>`,\n\t\ttext: `Verify your email\n\nHi {{firstName}},\n\nUse this code to finish setting up your account: {{pin}}\n\nThis code expires in 24 hours. If you didn't create an account, you can safely ignore this email.`,\n\t},\n\n\t[MESSAGE_KEYS.passwordReset]: {\n\t\tsubject: 'Reset your password',\n\t\thtml: `<h1>Reset your password</h1>\n<p>We received a request to reset your password. Use this code to continue:</p>\n<p><span class=\"pin-code\">{{pin}}</span></p>\n<p class=\"muted\">This code expires soon. If you didn’t request a reset, no action is needed — your password stays the same.</p>`,\n\t\ttext: `Reset your password\n\nWe received a request to reset your password. Use this code to continue: {{pin}}\n\nThis code expires soon. If you didn't request a reset, no action is needed — your password stays the same.`,\n\t},\n\n\t// SMS — text only, no subject/html.\n\t[MESSAGE_KEYS.phoneOtp]: {\n\t\ttext: '{{otp}} is your verification code. It expires in 10 minutes.',\n\t},\n\n\t[MESSAGE_KEYS.mfaEnabled]: {\n\t\tsubject: 'Two-factor authentication is on',\n\t\thtml: `<h1>Two-factor authentication enabled</h1>\n<p>Two-factor authentication was just turned on for your account. From now on you’ll enter a code from your authenticator app when you sign in.</p>\n<p class=\"muted\">If you didn’t do this, contact support right away — someone may have access to your account.</p>`,\n\t\ttext: `Two-factor authentication enabled\n\nTwo-factor authentication was just turned on for your account. From now on you'll enter a code from your authenticator app when you sign in.\n\nIf you didn't do this, contact support right away — someone may have access to your account.`,\n\t},\n\n\t[MESSAGE_KEYS.mfaDisabled]: {\n\t\tsubject: 'Two-factor authentication is off',\n\t\thtml: `<h1>Two-factor authentication disabled</h1>\n<p>Two-factor authentication was just turned off for your account. Your account is now protected by your password alone.</p>\n<p class=\"muted\">If you didn’t do this, contact support right away and re-enable two-factor authentication — someone may have access to your account.</p>`,\n\t\ttext: `Two-factor authentication disabled\n\nTwo-factor authentication was just turned off for your account. Your account is now protected by your password alone.\n\nIf you didn't do this, contact support right away and re-enable two-factor authentication — someone may have access to your account.`,\n\t},\n\n\t[MESSAGE_KEYS.mfaBackupCodesRegenerated]: {\n\t\tsubject: 'Your backup codes were regenerated',\n\t\thtml: `<h1>New backup codes generated</h1>\n<p>A new set of two-factor backup codes was just generated for your account. Your previous backup codes no longer work.</p>\n<p class=\"muted\">If you didn’t do this, contact support right away — someone may have access to your account.</p>`,\n\t\ttext: `New backup codes generated\n\nA new set of two-factor backup codes was just generated for your account. Your previous backup codes no longer work.\n\nIf you didn't do this, contact support right away — someone may have access to your account.`,\n\t},\n\n\t[MESSAGE_KEYS.emailChanged]: {\n\t\tsubject: 'Your email address was changed',\n\t\thtml: `<h1>Your email was changed</h1>\n<p>The email address on your account was just changed to <strong>{{newEmail}}</strong>.</p>\n<p class=\"muted\">If you made this change, you’re all set. If not, contact support right away — someone may have access to your account.</p>`,\n\t\ttext: `Your email was changed\n\nThe email address on your account was just changed to {{newEmail}}.\n\nIf you made this change, you're all set. If not, contact support right away — someone may have access to your account.`,\n\t},\n\n\t[MESSAGE_KEYS.phoneChanged]: {\n\t\tsubject: 'Your phone number was changed',\n\t\thtml: `<h1>Your phone number was changed</h1>\n<p>The phone number on your account was just updated.</p>\n<p class=\"muted\">If you made this change, you’re all set. If not, contact support right away — someone may have access to your account.</p>`,\n\t\ttext: `Your phone number was changed\n\nThe phone number on your account was just updated.\n\nIf you made this change, you're all set. If not, contact support right away — someone may have access to your account.`,\n\t},\n\n\t[MESSAGE_KEYS.passwordRevoked]: {\n\t\tsubject: 'The password on your account was removed',\n\t\thtml: `<h1>Your password was removed</h1>\n<p>You just signed in with <strong>{{provider}}</strong>. A password had been set on this account, but this email address had never been confirmed — so we could not tell who set it, and it has been removed.</p>\n<p>Signing in with {{provider}} works as before. If you want a password as well, set one from your account settings.</p>\n<p class=\"muted\">If you never set a password here, someone else may have tried to register with your address. Nothing further is needed — the account is yours.</p>`,\n\t\ttext: `Your password was removed\n\nYou just signed in with {{provider}}. A password had been set on this account,\nbut this email address had never been confirmed — so we could not tell who set\nit, and it has been removed.\n\nSigning in with {{provider}} works as before. If you want a password as well,\nset one from your account settings.\n\nIf you never set a password here, someone else may have tried to register with\nyour address. Nothing further is needed — the account is yours.`,\n\t},\n\n\t[MESSAGE_KEYS.oauthRegistration]: {\n\t\tsubject: 'Welcome to {{appName}}',\n\t\thtml: `<h1>Welcome to {{appName}}</h1>\n<p>Your account was created using <strong>{{provider}}</strong>. Sign in any time with the same {{provider}} account — there is no password to remember.</p>\n<p class=\"muted\">If you did not create this account, contact support.</p>`,\n\t\ttext: `Welcome\n\nYour account was created using {{provider}}. Sign in any time with the same\n{{provider}} account — there is no password to remember.\n\nIf you did not create this account, contact support.`,\n\t},\n\n\t[MESSAGE_KEYS.oauthLinked]: {\n\t\tsubject: 'A new sign-in method was added to your account',\n\t\thtml: `<h1>{{provider}} sign-in was added</h1>\n<p>Your account can now also be signed into with <strong>{{provider}}</strong>.</p>\n<p class=\"muted\">If you did this, nothing more is needed. If not, contact support right away and change your password — someone else may be able to sign in as you.</p>`,\n\t\ttext: `{{provider}} sign-in was added\n\nYour account can now also be signed into with {{provider}}.\n\nIf you did this, nothing more is needed. If not, contact support right away and\nchange your password — someone else may be able to sign in as you.`,\n\t},\n\n\t[MESSAGE_KEYS.oauthUnlinked]: {\n\t\tsubject: 'A sign-in method was removed from your account',\n\t\thtml: `<h1>{{provider}} sign-in was removed</h1>\n<p><strong>{{provider}}</strong> can no longer be used to sign in. Your email and password still work.</p>\n<p class=\"muted\">If you did this, you’re all set. If not, contact support right away.</p>`,\n\t\ttext: `{{provider}} sign-in was removed\n\n{{provider}} can no longer be used to sign in. Your email and password still work.\n\nIf you did this, you're all set. If not, contact support right away.`,\n\t},\n} satisfies Record<AuthMessageKey, IDefaultTemplate>;\n\n// Representative payloads for the coverage test: every {{var}} a default uses\n// must appear here. Security-notice keys carry no variables ({}). The\n// registration sample uses an empty firstName to prove the blank-name path\n// renders cleanly (the emitter may pass firstName: '').\nexport const SAMPLE_PAYLOADS: Record<AuthMessageKey, Record<string, unknown>> = {\n\t[MESSAGE_KEYS.passwordRevoked]: { provider: 'Google' },\n\t[MESSAGE_KEYS.oauthRegistration]: { provider: 'Google', appName: 'Fonderie' },\n\t[MESSAGE_KEYS.oauthLinked]: { provider: 'Google' },\n\t[MESSAGE_KEYS.oauthUnlinked]: { provider: 'Google' },\n\t[MESSAGE_KEYS.emailRegistration]: { firstName: '', pin: '123456' },\n\t[MESSAGE_KEYS.emailVerification]: { firstName: 'Ada', pin: '123456' },\n\t[MESSAGE_KEYS.passwordReset]: {\n\t\tpin: '123456',\n\t\ttoken: '0'.repeat(64),\n\t\tresetUrl: 'https://app.example.com/reset?token=' + '0'.repeat(64),\n\t},\n\t[MESSAGE_KEYS.phoneOtp]: { otp: '123456' },\n\t[MESSAGE_KEYS.mfaEnabled]: {},\n\t[MESSAGE_KEYS.mfaDisabled]: {},\n\t[MESSAGE_KEYS.mfaBackupCodesRegenerated]: {},\n\t[MESSAGE_KEYS.emailChanged]: { newEmail: 'new@example.com' },\n\t[MESSAGE_KEYS.phoneChanged]: {},\n};\n","export { requireAuth, requireAnyAuth } from '@fonderie/core/middlewares';\n","import type { IStoreAdapter } from '@fonderie/store';\n\n// Migrating an existing user base onto Fonderie auth. `UserModel.create` is for\n// fresh sign-ups — it generates a new id and sets `created_at`/verification to\n// their defaults. A migration needs the opposite: preserve each user's original\n// identity so foreign keys still resolve and history isn't rewritten. Pair this\n// with the `legacyVerify` config option: import the legacy password hash as-is,\n// and Fonderie upgrades it to bcrypt on the user's first login (rehash-on-login).\nexport interface IImportUser {\n\t// Preserve the legacy id so existing foreign keys keep pointing at this user.\n\t// Must be a UUID (the column type); omit only if you are remapping ids and\n\t// will supply the freshly-generated one. Omitting lets Postgres generate one.\n\tid?: string;\n\temail: string;\n\t// The user's existing password hash, in whatever format the old system used.\n\t// Left untouched here; `legacyVerify` validates it on login, then it's\n\t// re-stored as bcrypt. Pass null for accounts with no password (e.g. OAuth).\n\tpasswordHash?: string | null;\n\tfirstName?: string | null;\n\tlastName?: string | null;\n\tphone?: string | null;\n\tprofileImageUrl?: string | null;\n\tlocale?: string;\n\ttimezone?: string;\n\t// Preserve verification state — don't force already-verified users to\n\t// re-verify. A Date marks the account verified; null/omitted leaves it\n\t// unverified.\n\temailVerifiedAt?: Date | null;\n\tmfaEnabled?: boolean;\n\t// Preserve the original signup time; omitted defaults to now().\n\tcreatedAt?: Date;\n}\n\n// Insert one migrated user, preserving whichever identity fields are supplied\n// and letting the table defaults fill the rest. Returns the row's id (the\n// preserved one, or the generated one). Not idempotent by itself — run against a\n// fresh target, or manage replay (truncate-and-reload) at the migration level.\nexport async function importUser(\n\tstore: IStoreAdapter,\n\tuser: IImportUser,\n): Promise<{ id: string }> {\n\tconst cols: string[] = [];\n\tconst vals: unknown[] = [];\n\tconst placeholders: string[] = [];\n\tconst add = (col: string, val: unknown) => {\n\t\tcols.push(col);\n\t\tvals.push(val);\n\t\tplaceholders.push(`$${vals.length}`);\n\t};\n\n\tif (user.id !== undefined) add('id', user.id);\n\tadd('email', user.email.toLowerCase().trim());\n\tif (user.passwordHash !== undefined) add('password_hash', user.passwordHash);\n\tif (user.firstName !== undefined) add('first_name', user.firstName);\n\tif (user.lastName !== undefined) add('last_name', user.lastName);\n\tif (user.phone !== undefined) add('phone', user.phone);\n\tif (user.profileImageUrl !== undefined) add('profile_image_url', user.profileImageUrl);\n\tif (user.locale !== undefined) add('locale', user.locale);\n\tif (user.timezone !== undefined) add('timezone', user.timezone);\n\tif (user.emailVerifiedAt !== undefined) add('email_verified_at', user.emailVerifiedAt);\n\tif (user.mfaEnabled !== undefined) add('mfa_enabled', user.mfaEnabled);\n\tif (user.createdAt !== undefined) add('created_at', user.createdAt);\n\n\tconst [row] = await store.query<{ id: string }>(\n\t\t`INSERT INTO fonderie_users (${cols.join(', ')})\n\t\t VALUES (${placeholders.join(', ')})\n\t\t RETURNING id`,\n\t\tvals,\n\t);\n\tif (!row) throw new Error('[auth] importUser: insert returned no row');\n\treturn row;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\n// Data-retention / right-to-erasure support. `deleteMe` soft-deletes a user\n// (sets deleted_at) so the row stops resolving but history is preserved. A\n// retention policy then hard-deletes those rows once they age past the window —\n// call this from a scheduled job (e.g. daily). Related auth rows (sessions,\n// resets, verifications, MFA, backup codes) are removed by ON DELETE CASCADE.\n\nexport interface IPurgeOptions {\n\t// Hard-delete users whose deleted_at is older than this many days.\n\tolderThanDays: number;\n\t// Given ⇒ `fonderie.user.purged` { userId } is emitted for each removed\n\t// account, so other bricks can erase what they hold about it (billing\n\t// deletes the payment provider's customer record). Emitted after the delete.\n\tbus?: { emit(type: string, payload: unknown): Promise<void> } | undefined;\n}\n\nexport async function purgeSoftDeletedUsers(\n\tstore: IStoreAdapter,\n\t{ olderThanDays, bus }: IPurgeOptions,\n): Promise<number> {\n\tif (!Number.isFinite(olderThanDays) || olderThanDays < 0) {\n\t\tthrow new Error('[auth] purgeSoftDeletedUsers: olderThanDays must be a non-negative number');\n\t}\n\tconst rows = await store.query<{ id: string }>(\n\t\t`DELETE FROM fonderie_users\n\t\t WHERE deleted_at IS NOT NULL\n\t\t AND deleted_at < now() - make_interval(days => $1)\n\t\t RETURNING id`,\n\t\t[olderThanDays],\n\t);\n\tif (bus) {\n\t\tfor (const { id } of rows) await bus.emit('fonderie.user.purged', { userId: id });\n\t}\n\treturn rows.length;\n}\n\n// Scheduled hard-deletion of aged soft-deleted users (SOC 2 C1/P4, right to\n// erasure follow-through). Runs once immediately, then every intervalMs.\n// Non-blocking (timer unref'd). .stop() cancels.\nexport interface IUserRetentionScheduleOptions extends IPurgeOptions {\n\tintervalMs?: number; // default 24h\n\tonPurge?: (deleted: number) => void;\n}\n\nexport function startUserRetention(\n\tstore: IStoreAdapter,\n\toptions: IUserRetentionScheduleOptions,\n): { stop: () => void } {\n\tconst intervalMs = options.intervalMs ?? 24 * 60 * 60 * 1000;\n\tlet stopped = false;\n\tconst run = async () => {\n\t\tif (stopped) return;\n\t\ttry {\n\t\t\tconst deleted = await purgeSoftDeletedUsers(store, {\n\t\t\t\tolderThanDays: options.olderThanDays,\n\t\t\t\t...(options.bus ? { bus: options.bus } : {}),\n\t\t\t});\n\t\t\toptions.onPurge?.(deleted);\n\t\t} catch (err) {\n\t\t\tconsole.error('[auth] scheduled user retention purge failed:', err);\n\t\t}\n\t};\n\tconst timer = setInterval(run, intervalMs);\n\tif (typeof (timer as { unref?: () => void }).unref === 'function') (timer as { unref: () => void }).unref();\n\tvoid run();\n\treturn { stop: () => { stopped = true; clearInterval(timer); } };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAIA,eAAsB,aAAa,OAAgC;AAClE,SAAO,gBAAAA,QAAO,KAAK,OAAO,MAAM;AACjC;AAEA,eAAsB,eAAe,OAAe,MAAgC;AACnF,SAAO,gBAAAA,QAAO,QAAQ,OAAO,IAAI;AAClC;AAgBA,eAAsB,uBACrB,OACA,MACA,cACyB;AACzB,MAAI,MAAM,eAAe,OAAO,IAAI,EAAG,QAAO,EAAE,OAAO,MAAM,aAAa,MAAM;AAEhF,MAAI,cAAc;AACjB,QAAI,WAAW;AACf,QAAI;AACH,iBAAW,MAAM,aAAa,OAAO,IAAI;AAAA,IAC1C,QAAQ;AACP,iBAAW;AAAA,IACZ;AACA,QAAI,SAAU,QAAO,EAAE,OAAO,MAAM,aAAa,KAAK;AAAA,EACvD;AAEA,SAAO,EAAE,OAAO,OAAO,aAAa,MAAM;AAC3C;AA5CA,qBAEM;AAFN;AAAA;AAAA;AAAA,sBAAmB;AAEnB,IAAM,SAAS;AAAA;AAAA;;;ACFf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,IAAAC,gBAAqC;AAIrC,IAAAC,sBAA6D;;;ACN7D,kBAAqC;AAG9B,IAAM,oBAAgC,OAAO,KAAK,SAAS;AACjE,MAAI,IAAI,KAAM,gBAAgB,SAAS;AACtC,eAAO;AAAA,MACN,iBAAK;AAAA,MACL;AAAA,MACA;AAAA,IACD;AAAA,EACD;AACA,SAAO,KAAK;AACb;;;ACTA,yBAAyB;;;ACDzB,wBAOO;AAuBP,IAAM,MAAM,CAAC,MAAc,IAAI;AAC/B,IAAM,WAGF;AAAA;AAAA;AAAA,EAGH,OAAO;AAAA,IACN,IAAI,EAAE,UAAU,IAAI,cAAc,KAAK,IAAI,EAAE,EAAE;AAAA,IAC/C,SAAS,EAAE,UAAU,GAAG,cAAc,IAAI,IAAI,EAAE,EAAE;AAAA,IAClD,cAAc;AAAA,EACf;AAAA;AAAA,EAEA,UAAU,EAAE,IAAI,EAAE,UAAU,GAAG,cAAc,IAAI,IAAI,EAAE,EAAE,EAAE;AAAA;AAAA,EAE3D,QAAQ;AAAA,IACP,IAAI,EAAE,UAAU,GAAG,cAAc,IAAI,IAAI,EAAE,EAAE;AAAA,IAC7C,SAAS,EAAE,UAAU,GAAG,cAAc,IAAI,IAAI,EAAE,EAAE;AAAA,IAClD,cAAc;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,EAAE,IAAI,EAAE,UAAU,IAAI,cAAc,KAAK,IAAI,EAAE,EAAE,EAAE;AAAA;AAAA;AAAA,EAG1D,QAAQ,EAAE,IAAI,EAAE,UAAU,IAAI,cAAc,KAAK,IAAI,EAAE,EAAE,EAAE;AAAA;AAAA,EAE3D,WAAW,EAAE,IAAI,EAAE,UAAU,IAAI,cAAc,KAAK,IAAI,EAAE,EAAE,EAAE;AAC/D;AAEA,SAAS,QACR,OACA,OACA,QAKQ;AACR,MAAI,WAAW,MAAO,QAAO;AAC7B,QAAM,WAAW,QAAQ,QAAQ,KAAK;AACtC,MAAI,aAAa,MAAO,QAAO;AAE/B,QAAM,UAAU,QAAQ,SAAS,IAAI,oCAAkB,KAAK;AAC5D,QAAM,MAAM,SAAS,KAAK;AAC1B,SAAO;AAAA,IACN,OAAO;AAAA,IACP,QAAQ,YAAY,IAAI;AAAA,IACxB,GAAI,IAAI,WAAW,IAAI,eACpB,EAAE,SAAS,EAAE,MAAM,IAAI,SAAS,OAAO,IAAI,aAAa,EAAE,IAC1D,CAAC;AAAA,EACL;AACD;AAGO,SAAS,mBACf,OACA,OACA,QACoB;AACpB,QAAM,IAAI,QAAQ,OAAO,OAAO,MAAM;AACtC,MAAI,CAAC,EAAG,QAAO;AACf,aAAO,6BAAU,EAAE,OAAO,EAAE,OAAO,MAAM,EAAE,QAAQ,SAAK,wBAAK,QAAQ,KAAK,EAAE,EAAE,CAAC;AAChF;AAGO,SAAS,wBACf,OACA,OACA,QACoB;AACpB,QAAM,IAAI,QAAQ,OAAO,OAAO,MAAM;AACtC,MAAI,CAAC,KAAK,CAAC,EAAE,QAAS,QAAO;AAC7B,aAAO,6BAAU;AAAA,IAChB,OAAO,EAAE;AAAA,IACT,MAAM,EAAE,QAAQ;AAAA,IAChB,SAAK,+BAAY,QAAQ,KAAK,IAAI,EAAE,QAAQ,KAAK;AAAA,EAClD,CAAC;AACF;;;ACjHA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAAkB;AASlB,IAAM,QAAQ,aAAE,OAAO,EAAE,KAAK,EAAE,KAAK,aAAE,MAAM,CAAC;AAC9C,IAAM,WAAW,aAAE,OAAO,EAAE,IAAI,GAAG,wCAAwC,EAAE,IAAI,GAAG;AAEpF,IAAM,QAAQ,aACZ,OAAO,EACP,OAAO,CAAC,MAAM,qBAAqB,KAAK,EAAE,QAAQ,aAAa,EAAE,CAAC,GAAG,sBAAsB;AAC7F,IAAM,cAAc,aAClB,OAAO,EACP,KAAK,EACL,MAAM,WAAW,wBAAwB;AAEpC,IAAM,iBAAiB,aAAE,MAAM;AAAA,EACrC,aAAE,OAAO;AAAA,IACR;AAAA,IACA;AAAA,IACA,WAAW,aAAE,OAAO,EAAE,IAAI,GAAG,EAAE,QAAQ;AAAA,IACvC,UAAU,aAAE,OAAO,EAAE,IAAI,GAAG,EAAE,QAAQ;AAAA,EACvC,CAAC;AAAA,EACD,aAAE,OAAO,EAAE,MAAM,CAAC;AACnB,CAAC;AAEM,IAAM,cAAc,aAAE,MAAM;AAAA,EAClC,aAAE,OAAO,EAAE,OAAO,UAAU,aAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,CAAC;AAAA,EACxD,aAAE,OAAO,EAAE,MAAM,CAAC;AACnB,CAAC;AAIM,IAAM,oBAAoB,aAAE,OAAO;AAAA,EACzC,eAAe,aAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,IAAI;AAAA;AAAA;AAAA,EAGzC,OAAO,aAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;AACrC,CAAC;AAGM,IAAM,gBAAgB,aAAE,OAAO,EAAE,cAAc,aAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC;AAE7E,IAAM,uBAAuB,aAAE,OAAO,EAAE,MAAM,CAAC;AAK/C,IAAM,sBAAsB,aAAE,MAAM;AAAA,EAC1C,aAAE,OAAO,EAAE,KAAK,aAAa,SAAS,CAAC;AAAA,EACvC,aAAE,OAAO,EAAE,OAAO,aAAE,OAAO,EAAE,KAAK,EAAE,IAAI,IAAI,qBAAqB,GAAG,SAAS,CAAC;AAC/E,CAAC;AAEM,IAAM,eAAe,aAAE,OAAO,EAAE,OAAO,YAAY,CAAC;AAEpD,IAAM,sBAAsB,aACjC,OAAO;AAAA,EACP,WAAW,aAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AAAA,EACnD,UAAU,aAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AAAA,EAClD,WAAW,aAAE,OAAO,EAAE,KAAK,EAAE,KAAK,aAAE,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS;AAChE,CAAC,EACA;AAAA,EACA,CAAC,MAAM,OAAO,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,MAAM,MAAS;AAAA,EACnD;AACD;AAEM,IAAM,0BAA0B,aACrC,OAAO;AAAA,EACP,QAAQ,aAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EACpC,UAAU,aAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA;AAAA;AAAA;AAAA,EAItC,eAAe,aACb,OAAO;AAAA,IACP,OAAO,aAAE,QAAQ,EAAE,SAAS;AAAA,IAC5B,OAAO,aAAE,QAAQ,EAAE,SAAS;AAAA,IAC5B,KAAK,aAAE,QAAQ,EAAE,SAAS;AAAA,IAC1B,MAAM,aAAE,QAAQ,EAAE,SAAS;AAAA,EAC5B,CAAC,EACA,SAAS;AAAA,EACX,aAAa,aAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EACzC,YAAY,aAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EACxC,YAAY,aAAE,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS;AACzC,CAAC,EACA;AAAA,EACA,CAAC,MAAM,OAAO,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,MAAM,MAAS;AAAA,EACnD;AACD;AAEM,IAAM,oBAAoB,aAAE,OAAO,EAAE,MAAM,CAAC;AAE5C,IAAM,oBAAoB,aAAE,OAAO,EAAE,MAAM,CAAC;AAE5C,IAAM,uBAAuB,aAAE,OAAO;AAAA,EAC5C,iBAAiB,aAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EAC1C,aAAa;AACd,CAAC;AAGM,IAAM,iBAAiB,aAAE,OAAO,EAAE,OAAO,aAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC;;;AC9FlF,SAAS,WAAW,QAA6B;AAChD,QAAM,SAAS,OAAO,iBAAiB,QAAQ,IAAI,UAAU,MAAM;AACnE,SAAO,SAAS,aAAa;AAC9B;AAKO,SAAS,iBACf,aACA,cACA,QACW;AACX,QAAM,SAAS,WAAW,MAAM;AAChC,SAAO;AAAA,IACN,gBAAgB,WAAW,sCAAsC,MAAM;AAAA,IACvE,iBAAiB,YAAY,kDAAkD,MAAM;AAAA,EACtF;AACD;AAEO,SAAS,oBAAoB,QAA+B;AAClE,QAAM,SAAS,WAAW,MAAM;AAChC,SAAO;AAAA,IACN,8DAA8D,MAAM;AAAA,IACpE,2EAA2E,MAAM;AAAA,EAClF;AACD;AAMO,SAAS,cAAc,SAA4B;AACzD,QAAM,UAAU,IAAI,QAAQ;AAC5B,aAAW,KAAK,QAAS,SAAQ,OAAO,cAAc,CAAC;AACvD,SAAO;AACR;;;AC7CA,oBAAmB;AAEnB,IAAAC,eAAiD;AAIjD,oBAAmC;;;ACL5B,IAAM,gCAAgC,IAAI,KAAK;AA+C/C,IAAM,mBAA6D;AAAA,EACzE,iBAAqB;AAAA,EACrB,sBAAsB;AAAA,EACtB,KAAqB;AAAA,EACrB,qBAAqB;AACtB;AAEO,IAAM,eAAe;AAAA,EAC3B,mBAAmB;AAAA,EACnB,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,2BAA2B;AAAA,EAC3B,cAAc;AAAA,EACd,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQd,iBAAiB;AAAA;AAAA;AAAA;AAAA,EAIjB,mBAAmB;AAAA,EACnB,aAAa;AAAA,EACb,eAAe;AAChB;AAIO,IAAM,aAAa;AAAA,EACzB,gBAAgB;AAAA,EAChB,aAAa;AAAA;AAAA,EAEb,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,iBAAiB;AAClB;;;AC3FA,yBAA2B;AAC3B,0BAAsC;AAqC/B,SAAS,qBACf,QACA,QACA,aACS;AACT,SAAO,oBAAAC,QAAI;AAAA,IACV;AAAA,MACC,KAAK;AAAA,MACL,MAAM;AAAA,MACN;AAAA,MACA,eAAe;AAAA,MACf,YAAY;AAAA,IACb;AAAA,IACA,OAAO;AAAA,IACP,EAAE,WAAW,KAAK;AAAA,EACnB;AACD;AAEO,SAAS,eACf,QACA,QACA,SACY;AACZ,QAAM,WAAW,OAAO,mBAAmB;AAC3C,QAAM,iBAAiB,OAAO,uBAAuB;AACrD,QAAM,cAAc,QAAQ;AAC5B,QAAM,gBAAgB,QAAQ,iBAAiB;AAC/C,QAAM,UAAM,+BAAW;AAEvB,QAAM,cAAc,oBAAAA,QAAI;AAAA,IACvB,EAAE,KAAK,QAAQ,MAAM,UAAU,aAAa,eAAe,IAAI;AAAA,IAC/D,OAAO;AAAA,IACP,EAAE,WAAW,eAAe;AAAA,EAC7B;AAEA,QAAM,eAAe,oBAAAA,QAAI;AAAA,IACxB,EAAE,KAAK,QAAQ,MAAM,WAAW,aAAa,eAAe,IAAI;AAAA,IAChE,OAAO;AAAA,IACP,EAAE,WAAW,SAAS;AAAA,EACvB;AAEA,SAAO,EAAE,aAAa,cAAc,IAAI;AACzC;AAEO,SAAS,mBAAmB,OAAqB;AACvD,QAAM,UAAU,oBAAAA,QAAI,OAAO,KAAK;AAChC,SAAO,SAAS,MACb,IAAI,KAAK,QAAQ,MAAM,GAAI,IAC3B,IAAI,KAAK,KAAK,IAAI,IAAI,IAAI,KAAK,KAAK,KAAK,GAAI;AACjD;AAEO,SAAS,YACf,OACA,QAC0C;AAC1C,MAAI;AACH,WAAO,oBAAAA,QAAI,OAAO,OAAO,OAAO,SAAS;AAAA,EAC1C,QAAQ;AACP,WAAO;AAAA,EACR;AACD;;;AClGA,IAAAC,sBAAwC;AAExC,IAAAC,eAAkC;AAIlC,IAAM,OAAO;AACb,IAAM,QAAQ;AACd,IAAM,SAAS;AAEf,SAAS,KAAK,QAAgB,SAAyB;AACtD,QAAM,MAAM,OAAO,MAAM,CAAC;AAC1B,MAAI,IAAI;AAER,WAAS,IAAI,GAAG,KAAK,GAAG,KAAK;AAC5B,QAAI,CAAC,IAAI,IAAI;AACb,UAAM;AAAA,EACP;AAEA,QAAM,MAAM,OAAO,KAAK,aAAa,MAAM,CAAC;AAC5C,QAAM,WAAO,gCAAW,QAAQ,GAAG,EAAE,OAAO,GAAG,EAAE,OAAO;AACxD,QAAM,UAAU,KAAK,EAAE,KAAK,KAAK;AACjC,QAAM,UACD,KAAK,MAAM,KAAK,KAAK,QAAS,OAC9B,KAAK,SAAS,CAAC,KAAK,KAAK,QAAS,OAClC,KAAK,SAAS,CAAC,KAAK,KAAK,QAAS,KACnC,KAAK,SAAS,CAAC,KAAK,KAAK,OAC5B,KAAK,IAAI,IAAI,MAAM;AAEpB,SAAO,KAAK,SAAS,EAAE,SAAS,QAAQ,GAAG;AAC5C;AAEA,SAAS,cAAsB;AAC9B,SAAO,KAAK,MAAM,KAAK,IAAI,IAAI,MAAO,IAAI;AAC3C;AAGA,SAAS,aAAa,OAAuB;AAC5C,QAAM,WAAW;AACjB,QAAM,QAAQ,MAAM,YAAY,EAAE,QAAQ,OAAO,EAAE;AACnD,MAAI,OAAO;AACX,MAAI,QAAQ;AAEZ,QAAM,SAAmB,CAAC;AAC1B,aAAW,QAAQ,OAAO;AACzB,UAAM,MAAM,SAAS,QAAQ,IAAI;AACjC,QAAI,QAAQ,IAAI;AACf;AAAA,IACD;AAEA,YAAS,SAAS,IAAK;AACvB,YAAQ;AAER,QAAI,QAAQ,GAAG;AACd,aAAO,KAAM,UAAW,OAAO,IAAM,GAAI;AACzC,cAAQ;AAAA,IACT;AAAA,EACD;AAEA,SAAO,OAAO,KAAK,MAAM;AAC1B;AAEA,SAAS,aAAa,KAAqB;AAC1C,QAAM,WAAW;AACjB,MAAI,OAAO;AACX,MAAI,QAAQ;AACZ,MAAI,SAAS;AAEb,aAAW,QAAQ,KAAK;AACvB,YAAS,SAAS,IAAK;AACvB,YAAQ;AACR,WAAO,QAAQ,GAAG;AACjB,gBAAU,SAAU,UAAW,OAAO,IAAM,EAAI;AAChD,cAAQ;AAAA,IACT;AAAA,EACD;AAEA,MAAI,OAAO,GAAG;AACb,cAAU,SAAU,SAAU,IAAI,OAAS,EAAI;AAAA,EAChD;AAEA,SAAO;AACR;AAIO,SAAS,qBAA6B;AAC5C,SAAO,iBAAa,iCAAY,EAAE,CAAC;AACpC;AAEO,SAAS,gBAAgBC,QAAe,QAAgB,QAAwB;AACtF,QAAM,SAAS,IAAI,gBAAgB;AAAA,IAClC;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,QAAQ,OAAO,MAAM;AAAA,IACrB,QAAQ,OAAO,IAAI;AAAA,EACpB,CAAC;AAED,SAAO,kBAAkB,mBAAmB,MAAM,CAAC,IAAI,mBAAmBA,MAAK,CAAC,IAAI,MAAM;AAC3F;AAEO,SAAS,gBAAgB,OAAe,QAAyB;AACvE,QAAM,IAAI,YAAY;AACtB,WAAS,IAAI,CAAC,OAAO,KAAK,OAAO,KAAK;AAGrC,YAAI,gCAAkB,KAAK,QAAQ,IAAI,CAAC,GAAG,KAAK,GAAG;AAClD,aAAO;AAAA,IACR;AAAA,EACD;AAEA,SAAO;AACR;AAMO,SAAS,oBAAoB,QAAQ,GAAa;AACxD,SAAO,MAAM,KAAK,EAAE,QAAQ,MAAM,GAAG,UAAM,iCAAY,CAAC,EAAE,SAAS,KAAK,EAAE,YAAY,CAAC;AACxF;;;AHvGA;;;AIlBA,IAAAC,sBAA8D;AAc9D,IAAM,SAAS;AAOR,SAAS,cAAc,QAA6B;AAC1D,MAAI,CAAC,QAAQ;AAKZ,WAAO,EAAE,SAAS,CAAC,UAAU,OAAO,SAAS,CAAC,WAAW,OAAO;AAAA,EACjE;AAEA,QAAM,MAAM,OAAO,KAAK,QAAQ,KAAK;AACrC,MAAI,IAAI,WAAW,IAAI;AACtB,UAAM,IAAI,MAAM,kFAAkF;AAAA,EACnG;AAEA,SAAO;AAAA,IACN,QAAQ,OAAO;AACd,YAAM,SAAK,iCAAY,EAAE;AACzB,YAAM,aAAS,oCAAe,eAAe,KAAK,EAAE;AACpD,YAAM,MAAM,OAAO,OAAO,CAAC,OAAO,OAAO,OAAO,MAAM,GAAG,OAAO,MAAM,CAAC,CAAC;AACxE,YAAM,MAAM,OAAO,WAAW;AAC9B,aAAO,GAAG,MAAM,GAAG,GAAG,SAAS,KAAK,CAAC,IAAI,IAAI,SAAS,KAAK,CAAC,IAAI,IAAI,SAAS,KAAK,CAAC;AAAA,IACpF;AAAA,IACA,QAAQ,QAAQ;AACf,UAAI,CAAC,OAAO,WAAW,MAAM,EAAG,QAAO;AACvC,YAAM,CAAC,OAAO,QAAQ,OAAO,IAAI,OAAO,MAAM,OAAO,MAAM,EAAE,MAAM,GAAG;AACtE,UAAI,CAAC,SAAS,CAAC,UAAU,CAAC,SAAS;AAClC,cAAM,IAAI,MAAM,wCAAwC;AAAA,MACzD;AACA,YAAM,eAAW,sCAAiB,eAAe,KAAK,OAAO,KAAK,OAAO,KAAK,CAAC;AAC/E,eAAS,WAAW,OAAO,KAAK,QAAQ,KAAK,CAAC;AAC9C,aAAO,OAAO,OAAO;AAAA,QACpB,SAAS,OAAO,OAAO,KAAK,SAAS,KAAK,CAAC;AAAA,QAC3C,SAAS,MAAM;AAAA,MAChB,CAAC,EAAE,SAAS,MAAM;AAAA,IACnB;AAAA,EACD;AACD;;;ACzDA,IAAAC,eAA2D;AAgC3D,IAAM,sBAAwC;AAAA,EAC7C,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,eAAe,EAAE,OAAO,MAAM,OAAO,MAAM,KAAK,OAAO,MAAM,MAAM;AAAA,EACnE,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,YAAY;AACb;AAMA,SAAS,iBAAiB,OAAoD;AAC7E,QAAM,MAAiC,CAAC;AACxC,MAAI,OAAO,MAAM,WAAW,SAAU,KAAI,SAAS,MAAM;AACzD,MAAI,OAAO,MAAM,aAAa,SAAU,KAAI,WAAW,MAAM;AAC7D,MAAI,OAAO,MAAM,gBAAgB,SAAU,KAAI,cAAc,MAAM;AACnE,MAAI,OAAO,MAAM,eAAe,SAAU,KAAI,aAAa,MAAM;AACjE,MAAI,OAAO,MAAM,eAAe,SAAU,KAAI,aAAa,MAAM;AACjE,SAAO;AACR;AAEA,SAAS,mBAAmB,OAA4D;AACvF,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO,CAAC;AACjD,QAAM,MAAkD,CAAC;AACzD,aAAW,OAAO,CAAC,SAAS,SAAS,OAAO,MAAM,GAAY;AAC7D,UAAM,IAAK,MAAkC,GAAG;AAChD,QAAI,OAAO,MAAM,UAAW,KAAI,GAAG,IAAI;AAAA,EACxC;AACA,SAAO;AACR;AAEO,SAAS,UAAU,MAAa,gBAAgB,OAAiB;AACvE,QAAM,QAAQ,KAAK,eAAgB,CAAC;AACpC,QAAM,UAAU,iBAAiB,KAAK;AACtC,SAAO;AAAA,IACN,QAAI,4BAAc,KAAK,EAAE;AAAA,IACzB,WAAO,4BAAc,KAAK,KAAK;AAAA,IAC/B,eAAW,4BAAc,KAAK,SAAS;AAAA,IACvC,cAAU,4BAAc,KAAK,QAAQ;AAAA,IACrC,WAAO,4BAAc,KAAK,KAAK;AAAA,IAC/B,qBAAiB,4BAAc,KAAK,eAAe;AAAA,IACnD,UAAU,OAAO,KAAK,aAAa,YAAY,KAAK,WAAW;AAAA,IAC/D,eAAW,0BAAY,KAAK,SAAS;AAAA,IACrC,aAAa;AAAA,MACZ,GAAG;AAAA,MACH,GAAG;AAAA,MACH,eAAe;AAAA,QACd,GAAG,oBAAoB;AAAA,QACvB,GAAG,mBAAmB,MAAM,aAAa;AAAA,MAC1C;AAAA,MACA,QAAQ,KAAK,UAAU,QAAQ,UAAU;AAAA,MACzC,UAAU,KAAK,YAAY,QAAQ,YAAY;AAAA,IAChD;AAAA,IACA,iBAAiB,KAAK,oBAAoB;AAAA,IAC1C,iBAAiB;AAAA,IACjB,gBAAY,6BAAe,KAAK,UAAU;AAAA,IAC1C,cAAU,4BAAc,KAAK,QAAQ;AAAA,IACrC,aAAa,KAAK,iBAAiB,QAAQ,KAAK,iBAAiB;AAAA,IACjE,eAAW,6BAAe,KAAK,SAAS;AAAA,IACxC,eAAW,6BAAe,KAAK,SAAS;AAAA,IACxC,aAAa,MAAM,QAAQ,KAAK,WAAW,IAAI,KAAK,cAAc,CAAC;AAAA,IACnE,eAAW,0BAAY,KAAK,SAAS;AAAA,IACrC,eAAW,0BAAY,KAAK,SAAS;AAAA,EACtC;AACD;;;ACrFA,IAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwBd,IAAM,YAAN,MAAgB;AAAA,EACtB,YAAoB,OAAsB;AAAtB;AAAA,EAAuB;AAAA,EAAvB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMpB,MAAM,KAAK,QAAwB,CAAC,GAAuB;AAC1D,UAAM,QAAQ,KAAK,IAAI,MAAM,SAAS,IAAI,cAAc;AACxD,UAAM,SAAoB,CAAC;AAC3B,UAAM,QAAkB,CAAC,MAAM,UAAU,2BAA2B,oBAAoB;AAExF,QAAI,MAAM,QAAQ;AACjB,aAAO,KAAK,MAAM,OAAO,WAAW,MAAM,OAAO,EAAE;AACnD,YAAM,KAAK,wBAAwB,OAAO,SAAS,CAAC,mBAAmB,OAAO,MAAM,SAAS;AAAA,IAC9F;AAEA,WAAO,KAAK,QAAQ,CAAC;AACrB,UAAM,OAAO,MAAM,KAAK,MAAM;AAAA,MAC7B,UAAU,YAAY;AAAA;AAAA,aAEZ,MAAM,KAAK,OAAO,CAAC;AAAA;AAAA,cAElB,OAAO,MAAM;AAAA,MACxB;AAAA,IACD;AACA,WAAO,EAAE,OAAO,KAAK,MAAM,GAAG,KAAK,GAAG,SAAS,KAAK,SAAS,MAAM;AAAA,EACpE;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,yBAAyB,IAAmC;AACjE,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B,UAAU,YAAY;AAAA,MACtB,CAAC,EAAE;AAAA,IACJ;AACA,WAAO,OAAO;AAAA,EACf;AAAA,EAEA,MAAM,SAAS,IAAmC;AACjD,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B,UAAU,YAAY;AAAA,MACtB,CAAC,EAAE;AAAA,IACJ;AACA,WAAO,OAAO;AAAA,EACf;AAAA,EAEA,MAAM,YAAYC,QAAsC;AACvD,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B,UAAU,YAAY;AAAA,MACtB,CAACA,MAAK;AAAA,IACP;AACA,WAAO,OAAO;AAAA,EACf;AAAA,EAEA,MAAM,YAAYC,QAAsC;AACvD,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B,UAAU,YAAY;AAAA,MACtB,CAACA,MAAK;AAAA,IACP;AACA,WAAO,OAAO;AAAA,EACf;AAAA,EAEA,MAAM,oBACLA,QACA,YAA2B,MAC3B,WAA0B,MACA;AAC1B,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAOA,CAACA,QAAO,WAAW,QAAQ;AAAA,IAC5B;AACA,WAAO;AAAA,EACR;AAAA,EAEA,MAAM,OACLD,QACA,cACA,WACA,UACiC;AACjC,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B;AAAA;AAAA;AAAA,MAGA,CAACA,OAAM,YAAY,EAAE,KAAK,GAAG,cAAc,WAAW,QAAQ;AAAA,IAC/D;AACA,WAAO,OAAO;AAAA,EACf;AAAA,EAEA,MAAM,OAAO,IAAY,QAA2D;AACnF,UAAM,YAAoC;AAAA,MACzC,WAAW;AAAA,MACX,UAAU;AAAA,MACV,aAAa;AAAA,MACb,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,aAAa;AAAA,IACd;AAEA,UAAM,OAAiB,CAAC;AACxB,UAAM,SAAoB,CAAC;AAE3B,eAAW,CAAC,KAAK,GAAG,KAAK,OAAO,QAAQ,SAAS,GAAG;AACnD,UAAK,OAAmC,GAAG,MAAM,QAAW;AAC3D,eAAO,KAAM,OAAmC,GAAG,CAAC;AACpD,aAAK,KAAK,GAAG,GAAG,OAAO,OAAO,MAAM,EAAE;AAAA,MACvC;AAAA,IACD;AAEA,WAAO,KAAK,EAAE;AACd,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B,6BAA6B,KAAK,KAAK,IAAI,CAAC,oCAAoC,OAAO,MAAM;AAAA,MAC7F;AAAA,IACD;AACA,WAAO,OAAO;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,MAAM,cAAc,IAA8B;AACjD,UAAM,OAAO,MAAM,KAAK,MAAM;AAAA,MAC7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMA,CAAC,EAAE;AAAA,IACJ;AACA,WAAO,KAAK,SAAS;AAAA,EACtB;AAAA,EAEA,MAAM,eAAe,IAAY,cAAqC;AACrE,UAAM,KAAK,MAAM,MAAM,8DAA8D;AAAA,MACpF;AAAA,MACA;AAAA,IACD,CAAC;AAAA,EACF;AAAA,EAEA,MAAM,kBAAkB,IAA2B;AAClD,UAAM,KAAK,MAAM;AAAA,MAChB;AAAA,MACA,CAAC,EAAE;AAAA,IACJ;AAAA,EACD;AAAA;AAAA;AAAA,EAIA,MAAM,aAAa,IAAY,WAAsC;AACpE,UAAM,OAAO,MAAM,KAAK,MAAM;AAAA,MAC7B;AAAA,MACA,CAAC,IAAI,SAAS;AAAA,IACf;AACA,WAAO,KAAK,SAAS;AAAA,EACtB;AAAA,EAEA,MAAM,WAAW,IAA2B;AAC3C,UAAM,KAAK,MAAM;AAAA,MAChB;AAAA,MACA,CAAC,EAAE;AAAA,IACJ;AAAA,EACD;AAAA,EAEA,MAAM,cAAc,IAAY,QAA+B;AAC9D,UAAM,KAAK,MAAM,MAAM,2DAA2D,CAAC,QAAQ,EAAE,CAAC;AAAA,EAC/F;AAAA,EAEA,MAAM,qBAAqB,IAAY,QAA+B;AACrE,UAAM,KAAK,MAAM;AAAA,MAChB;AAAA;AAAA;AAAA,MAGA,CAAC,QAAQ,EAAE;AAAA,IACZ;AAAA,EACD;AAAA,EAEA,MAAM,oBAAoB,IAAoC;AAC7D,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B;AAAA;AAAA,MAEA,CAAC,EAAE;AAAA,IACJ;AACA,WAAO,KAAK,sBAAsB;AAAA,EACnC;AAAA,EAEA,MAAM,iBAAiB,IAA2B;AACjD,UAAM,KAAK,MAAM;AAAA,MAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAOA,CAAC,EAAE;AAAA,IACJ;AAAA,EACD;AAAA,EAEA,MAAM,UAAU,IAA2B;AAC1C,UAAM,KAAK,MAAM,MAAM,8DAA8D,CAAC,EAAE,CAAC;AAAA,EAC1F;AAAA,EAEA,MAAM,WAAW,IAA2B;AAC3C,UAAM,KAAK,MAAM;AAAA,MAChB;AAAA,MACA,CAAC,EAAE;AAAA,IACJ;AAAA,EACD;AAAA,EAEA,MAAM,YAAY,IAAYA,QAA8B;AAC3D,UAAM,KAAK,MAAM;AAAA,MAChB;AAAA,MACA,CAACA,OAAM,YAAY,EAAE,KAAK,GAAG,EAAE;AAAA,IAChC;AAAA,EACD;AAAA,EAEA,MAAM,YAAY,IAAYC,QAA8B;AAC3D,UAAM,KAAK,MAAM;AAAA,MAChB;AAAA,MACA,CAACA,QAAO,EAAE;AAAA,IACX;AAAA,EACD;AAAA,EAEA,MAAM,kBACL,IACA,QAKiC;AACjC,UAAM,OAAiB,CAAC;AACxB,UAAM,SAAoB,CAAC;AAE3B,QAAI,OAAO,WAAW,QAAW;AAChC,aAAO,KAAK,OAAO,MAAM;AACzB,WAAK,KAAK,aAAa,OAAO,MAAM,EAAE;AAAA,IACvC;AACA,QAAI,OAAO,aAAa,QAAW;AAClC,aAAO,KAAK,OAAO,QAAQ;AAC3B,WAAK,KAAK,eAAe,OAAO,MAAM,EAAE;AAAA,IACzC;AACA,QAAI,OAAO,SAAS,OAAO,KAAK,OAAO,KAAK,EAAE,SAAS,GAAG;AACzD,aAAO,KAAK,KAAK,UAAU,OAAO,KAAK,CAAC;AACxC,WAAK,KAAK,iCAAiC,OAAO,MAAM,SAAS;AAAA,IAClE;AAEA,QAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,WAAO,KAAK,EAAE;AACd,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B,6BAA6B,KAAK,KAAK,IAAI,CAAC,oCAAoC,OAAO,MAAM;AAAA,MAC7F;AAAA,IACD;AACA,WAAO,OAAO;AAAA,EACf;AAAA,EAEA,MAAM,aAAa,IAAoC;AACtD,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B;AAAA,MACA,CAAC,EAAE;AAAA,IACJ;AACA,WAAO,KAAK,cAAc;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmCA,MAAM,iBACLD,QACA,UACA,YAMS;AACT,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAM9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MA8BA,CAACA,QAAO,UAAU,UAAU;AAAA,IAC7B;AACA,WAAO,OAAO;AAAA,EACf;AACD;AAEA,IAAM,iBAAiB;;;ACxVvB,IAAM,sBAAsB;AAE5B,IAAM,WAAW;AAEjB,SAAS,IAAI,GAAY,IAA4B;AACpD,MAAI,OAAO,MAAM,SAAU,QAAO;AAClC,QAAM,IAAI,EAAE,KAAK;AACjB,MAAI,EAAE,WAAW,KAAK,EAAE,SAAS,SAAU,QAAO;AAClD,SAAO,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,OAAO;AACnC;AACA,SAAS,UAAU,GAAY,IAA2B;AACzD,QAAM,IAAI,IAAI,GAAG,EAAE;AACnB,SAAO,IAAI,EAAE,YAAY,IAAI;AAC9B;AACA,SAAS,KAAK,GAA4B;AACzC,SAAO,OAAO,MAAM,YAAY,IAAI;AACrC;AAGA,SAAS,MAAM,GAAY,OAA8B;AACxD,MAAI,OAAO,MAAM,YAAY,CAAC,OAAO,SAAS,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,MAAO,QAAO;AAChF,SAAO,KAAK,MAAM,IAAI,GAAG,IAAI;AAC9B;AAIA,SAAS,OAAO,GAA2B;AAC1C,MAAI,OAAO,MAAM,YAAY,CAAC,OAAO,SAAS,CAAC,KAAK,KAAK,KAAK,IAAI,IAAQ,QAAO;AACjF,SAAO,KAAK,MAAM,CAAC;AACpB;AAGA,SAAS,QAAQ,GAA2B;AAC3C,QAAM,IAAI,OAAO,MAAM,YAAY,aAAa,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI;AACtE,SAAO,OAAO,MAAM,YAAY,OAAO,UAAU,CAAC,KAAK,IAAI,KAAK,KAAK,aAAgB,IAAI;AAC1F;AAGO,SAAS,iBAAiB,OAAyC;AACzE,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,IAAI;AACV,QAAM,MAAwB;AAAA,IAC7B,SAAS,UAAU,EAAE,SAAS,GAAG,eAAe;AAAA,IAChD,aAAa,IAAI,EAAE,aAAa,CAAC;AAAA,IACjC,aAAa,UAAU,EAAE,aAAa,GAAG,oBAAoB;AAAA,IAC7D,iBAAiB,IAAI,EAAE,iBAAiB,CAAC;AAAA,IACzC,MAAM,IAAI,EAAE,MAAM,CAAC;AAAA,IACnB,YAAY,UAAU,EAAE,YAAY,GAAG,kCAAkC;AAAA,IACzE,WAAW,UAAU,EAAE,WAAW,GAAG,eAAe;AAAA,IACpD,UAAU,IAAI,EAAE,UAAU,GAAG,wCAAwC;AAAA,IACrE,UAAU,MAAM,EAAE,UAAU,GAAG,EAAE;AAAA,IACjC,WAAW,MAAM,EAAE,WAAW,GAAG,GAAG;AAAA,IACpC,gBAAgB,OAAO,EAAE,gBAAgB,CAAC;AAAA,IAC1C,WAAW,QAAQ,EAAE,WAAW,CAAC;AAAA,IACjC,KAAK,IAAI,EAAE,KAAK,CAAC;AAAA,IACjB,KAAK,IAAI,EAAE,KAAK,CAAC;AAAA,IACjB,KAAK,UAAU,EAAE,KAAK,GAAG,eAAe;AAAA,IACxC,QAAQ,KAAK,EAAE,QAAQ,CAAC;AAAA,IACxB,OAAO,KAAK,EAAE,OAAO,CAAC;AAAA,IACtB,SAAS,KAAK,EAAE,SAAS,CAAC;AAAA,EAC3B;AAEA,QAAM,QAAQ,OAAO,YAAY,OAAO,QAAQ,GAAG,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,MAAM,IAAI,CAAC;AAClF,SAAO,OAAO,KAAK,KAAK,EAAE,SAAS,IAAI,QAAQ;AAChD;AAOA,IAAM,aAAa,oBAAI,QAA2F;AAE3G,SAAS,gBACf,UACA,KACA,YAAY,qBACuB;AACnC,MAAI,CAAC,SAAU,QAAO,QAAQ,QAAQ,IAAI;AAC1C,QAAM,SAAS,WAAW,IAAI,IAAI,OAAO;AACzC,MAAI,UAAU,OAAO,aAAa,SAAU,QAAO,OAAO;AAC1D,QAAM,SAAS,YAAY,UAAU,KAAK,SAAS;AACnD,aAAW,IAAI,IAAI,SAAS,EAAE,UAAU,OAAO,CAAC;AAChD,SAAO;AACR;AAEA,eAAe,YACd,UACA,KACA,WACmC;AACnC,MAAI;AACJ,MAAI;AACH,UAAM,SAAS,MAAM,QAAQ,KAAK;AAAA,MACjC,QAAQ,QAAQ,EAAE,KAAK,MAAM,SAAS,GAAG,CAAC;AAAA,MAC1C,IAAI,QAAc,CAACE,aAAY;AAC9B,gBAAQ,WAAW,MAAMA,SAAQ,IAAI,GAAG,SAAS;AAAA,MAClD,CAAC;AAAA,IACF,CAAC;AACD,WAAO,iBAAiB,MAAM;AAAA,EAC/B,SAAS,KAAK;AACb,YAAQ,KAAK,kEAAkE,GAAG;AAClF,WAAO;AAAA,EACR,UAAE;AACD,QAAI,MAAO,cAAa,KAAK;AAAA,EAC9B;AACD;;;ACpKO,IAAM,eAAN,MAAmB;AAAA;AAAA,EAEzB,YACS,OACA,QACP;AAFO;AACA;AAAA,EACN;AAAA,EAFM;AAAA,EACA;AAAA,EAGT,MAAM,OACL,QACA,OACA,WACA,KACA,MACgB;AAChB,UAAM,WACL,KAAK,UAAU,MAAM,UAClB,MAAM,gBAAgB,KAAK,QAAQ,EAAE,IAAI,KAAK,WAAW,SAAS,KAAK,QAAQ,CAAC,IAChF;AACJ,UAAM,KAAK,MAAM;AAAA,MAChB;AAAA;AAAA;AAAA,MAGA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA,OAAO;AAAA,QACP,MAAM,aAAa;AAAA,QACnB,MAAM,aAAa;AAAA,QACnB,WAAW,KAAK,UAAU,QAAQ,IAAI;AAAA,MACvC;AAAA,IACD;AAAA,EACD;AAAA,EAEA,MAAM,OAAO,OAA8B;AAC1C,UAAM,KAAK,MAAM,MAAM,kDAAkD,CAAC,KAAK,CAAC;AAAA,EACjF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAY,KAA4B;AAC7C,UAAM,KAAK,MAAM,MAAM,gDAAgD,CAAC,GAAG,CAAC;AAAA,EAC7E;AAAA;AAAA;AAAA,EAIA,MAAM,aAAa,QAA+B;AACjD,UAAM,KAAK,MAAM,MAAM,oDAAoD,CAAC,MAAM,CAAC;AAAA,EACpF;AAAA;AAAA,EAGA,MAAM,WAAW,QAEf;AACD,WAAO,KAAK,MAAM;AAAA,MACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAQA,CAAC,MAAM;AAAA,IACR;AAAA,EACD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,eAAe,QAUnB;AACD,WAAO,KAAK,MAAM;AAAA,MACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MASA,CAAC,MAAM;AAAA,IACR;AAAA,EACD;AAAA;AAAA;AAAA,EAIA,MAAM,cAAc,QAAgB,IAA8B;AACjE,UAAM,OAAO,MAAM,KAAK,MAAM;AAAA,MAC7B;AAAA,MACA,CAAC,IAAI,MAAM;AAAA,IACZ;AACA,WAAO,KAAK,SAAS;AAAA,EACtB;AAAA;AAAA;AAAA,EAIA,MAAM,gBAAgB,QAAgB,SAAkC;AACvE,UAAM,OAAO,MAAM,KAAK,MAAM;AAAA,MAC7B;AAAA;AAAA;AAAA,MAGA,CAAC,QAAQ,OAAO;AAAA,IACjB;AACA,WAAO,KAAK;AAAA,EACb;AAAA,EAEA,MAAM,OAAO,OAAiC;AAC7C,UAAM,OAAO,MAAM,KAAK,MAAM;AAAA,MAC7B;AAAA,MACA,CAAC,KAAK;AAAA,IACP;AACA,WAAO,KAAK,SAAS;AAAA,EACtB;AAAA;AAAA,EAGA,MAAM,WAAW,KAA+B;AAC/C,UAAM,OAAO,MAAM,KAAK,MAAM;AAAA,MAC7B;AAAA,MACA,CAAC,GAAG;AAAA,IACL;AACA,WAAO,KAAK,SAAS;AAAA,EACtB;AACD;;;ACzIO,IAAM,kBAAN,MAAsB;AAAA,EAC5B,YAAoB,OAAsB;AAAtB;AAAA,EAAuB;AAAA,EAAvB;AAAA,EAEpB,MAAM,QAAQ,QAAgB,YAAqC;AAClE,UAAM,KAAK,MAAM,YAAY,OAAO,OAAO;AAC1C,YAAM,GAAG,MAAM,4DAA4D,CAAC,MAAM,CAAC;AACnF,UAAI,WAAW,WAAW,EAAG;AAC7B,YAAM,eAAe,WAAW,IAAI,CAAC,GAAG,MAAM,SAAS,IAAI,CAAC,GAAG,EAAE,KAAK,IAAI;AAC1E,YAAM,GAAG;AAAA,QACR,qEAAqE,YAAY;AAAA,QACjF,CAAC,QAAQ,GAAG,UAAU;AAAA,MACvB;AAAA,IACD,CAAC;AAAA,EACF;AAAA,EAEA,MAAM,WAAW,QAA6D;AAC7E,UAAM,OAAO,MAAM,KAAK,MAAM;AAAA,MAC7B;AAAA;AAAA,MAEA,CAAC,MAAM;AAAA,IACR;AACA,WAAO,KAAK,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,UAAU,EAAE,UAAU,EAAE;AAAA,EAC7D;AAAA,EAEA,MAAM,QAAQ,IAA2B;AACxC,UAAM,KAAK,MAAM,MAAM,sEAAsE;AAAA,MAC5F;AAAA,IACD,CAAC;AAAA,EACF;AAAA,EAEA,MAAM,aAAa,QAA+B;AACjD,UAAM,KAAK,MAAM,MAAM,4DAA4D,CAAC,MAAM,CAAC;AAAA,EAC5F;AACD;;;AClCA,IAAAC,eAA2B;AA8BpB,IAAM,kBAAN,MAAsB;AAAA;AAAA;AAAA,EAG5B,YACS,OACA,QACP;AAFO;AACA;AAAA,EACN;AAAA,EAFM;AAAA,EACA;AAAA,EAGT,MAAM,OAAO,GAAoC;AAChD,UAAM,WAAW,KAAK,SACnB,MAAM,gBAAgB,KAAK,QAAQ,EAAE,IAAI,EAAE,WAAW,SAAS,EAAE,WAAW,IAAI,QAAQ,EAAE,CAAC,IAC3F;AACH,UAAM,KAAK,MAAM;AAAA,MAChB;AAAA;AAAA;AAAA,MAGA;AAAA,QACC,EAAE;AAAA,QACF,EAAE,kBAAkB;AAAA,QACpB,EAAE;AAAA,QACF,EAAE;AAAA,QACF,EAAE,iBAAiB;AAAA,QACnB,EAAE;AAAA,QACF,EAAE;AAAA,QACF,WAAW,KAAK,UAAU,QAAQ,IAAI;AAAA,MACvC;AAAA,IACD;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,WAAW,GAAoC;AACpD,cAAM,yBAAW,KAAK,OAAO,CAAC,CAAC;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,WAAW,OAAmD;AACnE,UAAM,QAAQ,KAAK,IAAI,MAAM,SAAS,IAAI,iBAAiB;AAC3D,UAAM,SAAoB,CAAC,MAAM,MAAM;AACvC,UAAM,QAAkB,CAAC,cAAc;AAEvC,QAAI,MAAM,SAAS;AAClB,aAAO,KAAK,MAAM,OAAO;AACzB,YAAM,KAAK,cAAc,OAAO,MAAM,EAAE;AAAA,IACzC;AACA,QAAI,MAAM,MAAM;AACf,aAAO,KAAK,MAAM,IAAI;AACtB,YAAM,KAAK,kBAAkB,OAAO,MAAM,EAAE;AAAA,IAC7C;AACA,QAAI,MAAM,IAAI;AACb,aAAO,KAAK,MAAM,EAAE;AACpB,YAAM,KAAK,kBAAkB,OAAO,MAAM,EAAE;AAAA,IAC7C;AACA,QAAI,MAAM,QAAQ;AACjB,aAAO,KAAK,MAAM,OAAO,WAAW,MAAM,OAAO,EAAE;AACnD,YAAM,KAAK,wBAAwB,OAAO,SAAS,CAAC,mBAAmB,OAAO,MAAM,SAAS;AAAA,IAC9F;AAEA,WAAO,KAAK,QAAQ,CAAC;AACrB,UAAM,OAAO,MAAM,KAAK,MAAM;AAAA,MAC7B;AAAA;AAAA;AAAA;AAAA,aAIU,MAAM,KAAK,OAAO,CAAC;AAAA;AAAA,cAElB,OAAO,MAAM;AAAA,MACxB;AAAA,IACD;AACA,WAAO,EAAE,QAAQ,KAAK,MAAM,GAAG,KAAK,GAAG,SAAS,KAAK,SAAS,MAAM;AAAA,EACrE;AACD;AAEA,IAAM,oBAAoB;;;AC/F1B,IAAM,SAAS;AAIf,IAAI,6BAA6B;AAqBjC,SAAS,mBAAmB,KAA6B;AACxD,MAAI,2BAA4B;AAChC,+BAA6B;AAC7B,UAAQ;AAAA,IACP,6EACI,IAAI,QAAQ,MAAM,IAAI,IAAI,IAAI,IAAI,QAAQ,GAAG,EAAE,QAAQ;AAAA,EAO5D;AACD;AAEO,SAAS,YAAY,KAAqC;AAChE,QAAM,KAAK,IAAI,KAAK,UAAU;AAC9B,QAAM,KAAK,IAAI,QAAQ,QAAQ,IAAI,YAAY;AAC/C,QAAM,OAAqB;AAAA,IAC1B,WAAW,OAAO,OAAO,YAAY,GAAG,SAAS,IAAI,KAAK;AAAA,IAC1D,WAAW,MAAM,GAAG,SAAS,IAAI,GAAG,MAAM,GAAG,MAAM,IAAI;AAAA,IACvD,SAAS,IAAI,QAAQ;AAAA,EACtB;AACA,MAAI,KAAK,cAAc,QAAQ,KAAK,cAAc,KAAM,oBAAmB,GAAG;AAC9E,SAAO;AACR;;;AXtCO,SAAS,cACf,OACA,QACA,QACA,KACC;AACD,QAAM,QAAQ,IAAI,UAAU,KAAK;AACjC,QAAM,WAAW,IAAI,aAAa,OAAO,OAAO,QAAQ;AACxD,QAAM,cAAc,IAAI,gBAAgB,KAAK;AAC7C,QAAM,cAAc,IAAI,gBAAgB,OAAO,OAAO,QAAQ;AAE9D,QAAM,YAAY,cAAc,OAAO,YAAY;AAEnD,SAAO;AAAA;AAAA,IAEN,OAAO,OAAO,QAA6C;AAC1D,YAAM,SAAS,mBAAmB;AAClC,YAAM,MAAM,gBAAgB,IAAI,KAAM,SAAS,IAAI,KAAM,IAAI,QAAQ,MAAM;AAC3E,YAAM,aAAa,oBAAoB;AACvC,YAAM,aAAa,MAAM,QAAQ,IAAI,WAAW,IAAI,CAAC,MAAM,aAAa,CAAC,CAAC,CAAC;AAE3E,YAAM,KAAK,MAAM,cAAAC,QAAO,UAAU,GAAG;AAErC,YAAM,QAAQ,IAAI;AAAA,QACjB,MAAM,qBAAqB,IAAI,KAAM,IAAI,UAAU,QAAQ,MAAM,CAAC;AAAA,QAClE,YAAY,QAAQ,IAAI,KAAM,IAAI,UAAU;AAAA,MAC7C,CAAC;AAED,iBAAO;AAAA,QACN,kBAAK;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,UACC;AAAA;AAAA;AAAA,UAGA,aAAa;AAAA,QACd;AAAA,MACD;AAAA,IACD;AAAA;AAAA,IAGA,QAAQ,OAAO,QAA6C;AAC3D,YAAM,OAAO,IAAI,KAAK,MAAM;AAC5B,YAAM,QAAQ,OAAO,OAAO;AAG5B,YAAM,oBAAoB,IAAI,KAAM,eAAe;AACnD,YAAM,OAAO,YAAY,GAAG;AAC5B,YAAM,YAAY,YACjB,MAAM,YAAY,WAAW;AAAA,QAC5B,QAAQ,IAAI,KAAM;AAAA,QAClB,gBAAgB,IAAI,KAAM;AAAA,QAC1B,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,eAAe;AAAA,QACf,GAAG;AAAA,MACJ,CAAC;AAEF,UAAI,OAAO,UAAU,UAAU;AAC9B,mBAAO,6BAAe,kBAAK,eAAe,qBAAqB,mBAAmB;AAAA,MACnF;AAEA,YAAM,gBAAgB,MAAM,MAAM,oBAAoB,IAAI,KAAM,EAAE;AAClE,YAAM,gBAAgB,gBAAgB,UAAU,QAAQ,aAAa,IAAI;AAEzE,UAAI,eAAe;AAElB,YAAI,CAAC,gBAAgB,OAAO,aAAa,GAAG;AAC3C,qBAAO,6BAAe,kBAAK,cAAc,gBAAgB,mBAAmB;AAAA,QAC7E;AACA,cAAM,MAAM,iBAAiB,IAAI,KAAM,EAAE;AAEzC,kBAAM,yBAAW,KACd,KAAK,kCAAoB;AAAA,UAC1B,MAAM,aAAa;AAAA,UACnB,QAAQ,IAAI,KAAM;AAAA,UAClB,MAAM,CAAC;AAAA,UACP,WAAW,EAAE,OAAO,IAAI,KAAM,OAAO,OAAO,MAAM,aAAa,KAAK;AAAA,QACrE,CAA2B,CAAC;AAE7B,mBAAO,6BAAe,kBAAK,IAAI,gBAAgB,8BAA8B;AAAA,UAC5E,YAAY;AAAA,QACb,CAAC;AAAA,MACF,WAAW,iBAAiB,KAAK,KAAK,GAAG;AAExC,YAAI,CAAC,IAAI,KAAM,YAAY;AAC1B,qBAAO;AAAA,YACN,kBAAK;AAAA,YACL;AAAA,YACA;AAAA,UACD;AAAA,QACD;AACA,YAAI,CAAC,IAAI,KAAM,YAAY;AAC1B,qBAAO,6BAAe,kBAAK,aAAa,sBAAsB,oBAAoB;AAAA,QACnF;AAEA,cAAM,SAAS,MAAM,YAAY,WAAW,IAAI,KAAM,EAAE;AACxD,cAAM,SAAS,MAAM,QAAQ;AAAA,UAC5B,OAAO,IAAI,OAAO,SAAS;AAAA,YAC1B,IAAI,IAAI;AAAA,YACR,OAAO,MAAM,eAAe,MAAM,YAAY,GAAG,IAAI,QAAQ;AAAA,UAC9D,EAAE;AAAA,QACH;AACA,cAAM,UAAU,OAAO,KAAK,CAAC,MAAM,EAAE,KAAK;AAE1C,YAAI,CAAC,SAAS;AACb,cAAI,kBAAmB,OAAM,UAAU;AACvC,qBAAO,6BAAe,kBAAK,cAAc,gBAAgB,qBAAqB;AAAA,QAC/E;AAEA,cAAM,YAAY,QAAQ,QAAQ,EAAE;AAAA,MACrC,OAAO;AAEN,YAAI,CAAC,IAAI,KAAM,YAAY;AAC1B,qBAAO;AAAA,YACN,kBAAK;AAAA,YACL;AAAA,YACA;AAAA,UACD;AAAA,QACD;AACA,cAAM,eAAe,MAAM,MAAM,aAAa,IAAI,KAAM,EAAE;AAC1D,YAAI,CAAC,cAAc;AAClB,qBAAO,6BAAe,kBAAK,aAAa,sBAAsB,oBAAoB;AAAA,QACnF;AACA,cAAM,SAAS,UAAU,QAAQ,YAAY;AAC7C,YAAI,CAAC,gBAAgB,OAAO,MAAM,GAAG;AACpC,cAAI,kBAAmB,OAAM,UAAU;AACvC,qBAAO,6BAAe,kBAAK,cAAc,gBAAgB,mBAAmB;AAAA,QAC7E;AACA,YAAI,CAAC,IAAI,KAAM,YAAY;AAC1B,gBAAM,MAAM,UAAU,IAAI,KAAM,EAAE;AAAA,QACnC;AAAA,MACD;AAEA,YAAM,EAAE,aAAa,cAAc,IAAI,IAAI,eAAe,IAAI,KAAM,IAAI,QAAQ;AAAA,QAC/E,aAAa,IAAI,KAAM;AAAA,QACvB,eAAe,IAAI,KAAM;AAAA,MAC1B,CAAC;AACD,YAAM,SAAS,OAAO,IAAI,KAAM,IAAI,cAAc,mBAAmB,YAAY,GAAG,KAAK,IAAI;AAC7F,UAAI,mBAAmB;AACtB,cAAM,YAAY,WAAW;AAAA,UAC5B,QAAQ,IAAI,KAAM;AAAA,UAClB,gBAAgB,IAAI,KAAM;AAAA,UAC1B,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,GAAG;AAAA,QACJ,CAAC;AAAA,MACF;AAEA,YAAM,WAAW,MAAM,MAAM,SAAS,IAAI,KAAM,EAAE;AAClD,UAAI,CAAC,UAAU;AACd,mBAAO,6BAAe,kBAAK,cAAc,gBAAgB,iCAAiC;AAAA,MAC3F;AAEA,aAAO,SAAS;AAAA,QACf;AAAA,UACC,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ;AAAA,YACP,QAAQ,EAAE,QAAQ,aAAa,SAAS,aAAa;AAAA,YACrD,MAAM,UAAU,UAAU,IAAI,KAAM,aAAa;AAAA,UAClD;AAAA,QACD;AAAA,QACA;AAAA,UACC,QAAQ;AAAA,UACR,SAAS,cAAc,iBAAiB,aAAa,cAAc,MAAM,CAAC;AAAA,QAC3E;AAAA,MACD;AAAA,IACD;AAAA;AAAA,IAGA,uBAAuB,OAAO,QAA6C;AAC1E,YAAM,OAAO,IAAI,KAAK,MAAM;AAC5B,YAAM,QAAQ,OAAO,OAAO;AAE5B,UAAI,OAAO,UAAU,UAAU;AAC9B,mBAAO,6BAAe,kBAAK,eAAe,qBAAqB,wBAAwB;AAAA,MACxF;AAEA,UAAI,CAAC,IAAI,KAAM,YAAY;AAC1B,mBAAO,6BAAe,kBAAK,aAAa,mBAAmB,oBAAoB;AAAA,MAChF;AAEA,YAAM,eAAe,MAAM,MAAM,aAAa,IAAI,KAAM,EAAE;AAC1D,YAAM,SAAS,eAAe,UAAU,QAAQ,YAAY,IAAI;AAChE,UAAI,CAAC,UAAU,CAAC,gBAAgB,OAAO,MAAM,GAAG;AAC/C,mBAAO,6BAAe,kBAAK,cAAc,gBAAgB,mBAAmB;AAAA,MAC7E;AAEA,YAAM,aAAa,oBAAoB;AACvC,YAAM,aAAa,MAAM,QAAQ,IAAI,WAAW,IAAI,CAAC,MAAM,aAAa,CAAC,CAAC,CAAC;AAC3E,YAAM,YAAY,QAAQ,IAAI,KAAM,IAAI,UAAU;AAElD,gBAAM,yBAAW,KACd,KAAK,kCAAoB;AAAA,QAC1B,MAAM,aAAa;AAAA,QACnB,QAAQ,IAAI,KAAM;AAAA,QAClB,MAAM,CAAC;AAAA,QACP,WAAW,EAAE,OAAO,IAAI,KAAM,OAAO,OAAO,MAAM,aAAa,KAAK;AAAA,MACrE,CAA2B,CAAC;AAE7B,iBAAO,6BAAe,kBAAK,IAAI,4BAA4B,6BAA6B;AAAA,QACvF,aAAa;AAAA,MACd,CAAC;AAAA,IACF;AAAA;AAAA,IAGA,SAAS,OAAO,QAA6C;AAC5D,YAAM,OAAO,IAAI,KAAK,MAAM;AAC5B,YAAM,QAAQ,OAAO,OAAO;AAE5B,UAAI,OAAO,UAAU,UAAU;AAC9B,mBAAO,6BAAe,kBAAK,eAAe,qBAAqB,wBAAwB;AAAA,MACxF;AAEA,YAAM,OAAO,MAAM,MAAM,SAAS,IAAI,KAAM,EAAE;AAC9C,UAAI,CAAC,QAAQ,CAAC,KAAK,YAAY;AAC9B,mBAAO,6BAAe,kBAAK,aAAa,mBAAmB,oBAAoB;AAAA,MAChF;AAIA,YAAM,eAAe,MAAM,MAAM,aAAa,IAAI,KAAM,EAAE;AAC1D,YAAM,SAAS,eAAe,UAAU,QAAQ,YAAY,IAAI;AAChE,UAAI,CAAC,UAAU,CAAC,gBAAgB,OAAO,MAAM,GAAG;AAC/C,mBAAO,6BAAe,kBAAK,cAAc,gBAAgB,mBAAmB;AAAA,MAC7E;AAEA,YAAM,QAAQ,IAAI,CAAC,MAAM,WAAW,IAAI,KAAM,EAAE,GAAG,YAAY,aAAa,IAAI,KAAM,EAAE,CAAC,CAAC;AAE1F,gBAAM,yBAAW,KACd,KAAK,kCAAoB;AAAA,QAC1B,MAAM,aAAa;AAAA,QACnB,QAAQ,KAAK;AAAA,QACb,MAAM,CAAC;AAAA,QACP,WAAW,EAAE,OAAO,KAAK,OAAO,OAAO,MAAM,aAAa,KAAK;AAAA,MAChE,CAA2B,CAAC;AAE7B,iBAAO,6BAAe,kBAAK,IAAI,gBAAgB,4BAA4B;AAAA,IAC5E;AAAA,EACD;AACD;;;AY5QA,IAAAC,sBAAuC;AAIvC,IAAAC,iBAAmC;AACnC,IAAAC,eAAiD;;;ACN1C,SAAS,cAAc,YAAyB,YAA4B;AAClF,MAAI,CAAC,WAAY,QAAO;AACxB,SAAO,KAAK,IAAI,GAAG,cAAc,KAAK,IAAI,IAAI,WAAW,QAAQ,EAAE;AACpE;;;ADcA;;;AEjBO,SAAS,eAAeC,QAAuB;AACrD,MAAI,OAAOA,WAAU,YAAYA,OAAM,WAAW,GAAG;AACpD,UAAM,IAAI,MAAM,eAAe;AAAA,EAChC;AAEA,QAAM,QAAQA,OAAM,KAAK,EAAE,YAAY;AAEvC,MAAI,MAAM,WAAW,GAAG;AACvB,UAAM,IAAI,MAAM,uBAAuB;AAAA,EACxC;AAEA,QAAM,UAAU,MAAM,QAAQ,GAAG;AACjC,MAAI,YAAY,MAAM,MAAM,YAAY,GAAG,MAAM,SAAS;AACzD,UAAM,IAAI,MAAM,sBAAsB;AAAA,EACvC;AAEA,QAAM,QAAS,MAAM,UAAU,GAAG,OAAO;AACzC,QAAM,SAAS,MAAM,UAAU,UAAU,CAAC;AAE1C,MAAI,CAAC,SAAS,CAAC,QAAQ;AACtB,UAAM,IAAI,MAAM,sBAAsB;AAAA,EACvC;AAEA,QAAM,YAAY,MAAM,QAAQ,GAAG;AACnC,QAAM,kBAAkB,cAAc,KAAK,QAAQ,MAAM,UAAU,GAAG,SAAS;AAE/E,MAAI,CAAC,iBAAiB;AACrB,UAAM,IAAI,MAAM,sBAAsB;AAAA,EACvC;AAEA,SAAO,GAAG,eAAe,IAAI,MAAM;AACpC;AAEO,SAAS,mBAAmBA,QAA8B;AAChE,MAAI;AACH,WAAO,eAAeA,MAAK;AAAA,EAC5B,QAAQ;AACP,WAAO;AAAA,EACR;AACD;;;ACvCA,IAAAC,sBAA2B;AAU3B,IAAM,aAAa,CAAC,cAA0B,gCAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AAEtF,IAAM,qBAAN,MAAyB;AAAA,EAC/B,YAAoB,OAAsB;AAAtB;AAAA,EAAuB;AAAA,EAAvB;AAAA,EAEpB,MAAM,OAAO,QAAgB,KAAa,OAAe,WAAgC;AACxF,UAAM,KAAK,MAAM;AAAA,MAChB;AAAA;AAAA;AAAA;AAAA,MAIA,CAAC,QAAQ,WAAW,GAAG,GAAG,WAAW,KAAK,GAAG,SAAS;AAAA,IACvD;AAAA,EACD;AAAA,EAEA,MAAM,eAAe,QAAsC;AAC1D,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B;AAAA,MACA,CAAC,MAAM;AAAA,IACR;AACA,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO,IAAI,KAAK,IAAI,UAAU;AAAA,EAC/B;AAAA,EAEA,MAAM,UAAU,KAAkE;AACjF,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B;AAAA,MACA,CAAC,WAAW,GAAG,CAAC;AAAA,IACjB;AACA,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO,EAAE,QAAQ,IAAI,SAAS,WAAW,IAAI,KAAK,IAAI,UAAU,EAAE;AAAA,EACnE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,YAAY,OAAoE;AACrF,QAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAAI,QAAO;AAC3D,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B;AAAA,MACA,CAAC,WAAW,KAAK,CAAC;AAAA,IACnB;AACA,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO,EAAE,QAAQ,IAAI,SAAS,WAAW,IAAI,KAAK,IAAI,UAAU,EAAE;AAAA,EACnE;AAAA,EAEA,MAAM,aAAa,QAA+B;AACjD,UAAM,KAAK,MAAM,MAAM,2DAA2D,CAAC,MAAM,CAAC;AAAA,EAC3F;AACD;;;AC1DO,IAAM,yBAAN,MAA6B;AAAA,EACnC,YAAoB,OAAsB;AAAtB;AAAA,EAAuB;AAAA,EAAvB;AAAA,EAEpB,MAAM,OAAO,QAAgB,KAAa,WAAgC;AACzE,UAAM,KAAK,MAAM;AAAA,MAChB;AAAA;AAAA;AAAA,MAGA,CAAC,QAAQ,KAAK,SAAS;AAAA,IACxB;AAAA,EACD;AAAA,EAEA,MAAM,KAAK,KAAkE;AAC5E,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B;AAAA,MACA,CAAC,GAAG;AAAA,IACL;AACA,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO,EAAE,QAAQ,IAAI,SAAS,WAAW,IAAI,KAAK,IAAI,UAAU,EAAE;AAAA,EACnE;AAAA,EAEA,MAAM,OAAO,KAA4B;AACxC,UAAM,KAAK,MAAM,MAAM,6DAA6D,CAAC,GAAG,CAAC;AAAA,EAC1F;AAAA,EAEA,MAAM,WAAW,QAAgB,KAAkD;AAClF,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B;AAAA,MACA,CAAC,QAAQ,GAAG;AAAA,IACb;AACA,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO,EAAE,WAAW,IAAI,KAAK,IAAI,UAAU,EAAE;AAAA,EAC9C;AAAA,EAEA,MAAM,eAAe,QAAsC;AAC1D,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B;AAAA,MACA,CAAC,MAAM;AAAA,IACR;AACA,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO,IAAI,KAAK,IAAI,UAAU;AAAA,EAC/B;AAAA,EAEA,MAAM,QAAQ,QAAgB,KAAa,WAAgC;AAC1E,UAAM,KAAK,MAAM,YAAY,OAAO,OAAO;AAC1C,YAAM,GAAG,MAAM,+DAA+D,CAAC,MAAM,CAAC;AACtF,YAAM,GAAG;AAAA,QACR;AAAA;AAAA,QAEA,CAAC,KAAK,QAAQ,SAAS;AAAA,MACxB;AAAA,IACD,CAAC;AAAA,EACF;AACD;;;ACrDO,IAAM,yBAAN,MAA6B;AAAA,EACnC,YAAoB,OAAsB;AAAtB;AAAA,EAAuB;AAAA,EAAvB;AAAA,EAEpB,MAAM,OAAO,QAAgBC,QAAe,KAAa,WAAgC;AACxF,UAAM,KAAK,MAAM;AAAA,MAChB;AAAA;AAAA;AAAA;AAAA,MAIA,CAACA,QAAO,QAAQ,KAAK,SAAS;AAAA,IAC/B;AAAA,EACD;AAAA,EAEA,MAAM,WACL,QACA,KACqD;AACrD,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B;AAAA,MACA,CAAC,QAAQ,GAAG;AAAA,IACb;AACA,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO,EAAE,OAAO,IAAI,OAAO,WAAW,IAAI,KAAK,IAAI,UAAU,EAAE;AAAA,EAChE;AAAA,EAEA,MAAM,eAAe,QAAsC;AAC1D,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B;AAAA,MACA,CAAC,MAAM;AAAA,IACR;AACA,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO,IAAI,KAAK,IAAI,UAAU;AAAA,EAC/B;AAAA,EAEA,MAAM,aAAa,QAA+B;AACjD,UAAM,KAAK,MAAM,MAAM,+DAA+D,CAAC,MAAM,CAAC;AAAA,EAC/F;AACD;;;ALTA,SAAS,eAAeC,QAAuB;AAC9C,SAAOA,OAAM,KAAK,EAAE,QAAQ,eAAe,EAAE;AAC9C;AAEA,SAAS,aAAaA,QAAiC;AACtD,SAAO,OAAOA,WAAU,YAAY,qBAAqB,KAAK,eAAeA,MAAK,CAAC;AACpF;AAEA,SAAS,oBAAoB,KAAsC;AAClE,QAAM,OAAO,IAAI,KAAK,MAAM;AAC5B,MAAI,OAAO,OAAO,cAAc,MAAM,UAAU;AAC/C,WAAO,KAAK,cAAc;AAAA,EAC3B;AAEA,QAAM,SAAS,IAAI,QAAQ,QAAQ,IAAI,QAAQ,KAAK;AACpD,QAAM,QAAQ,OAAO,MAAM,iCAAiC;AAE5D,SAAO,QAAQ,CAAC,KAAK;AACtB;AAEO,SAAS,eAAe,OAAsB,QAAqB,KAAgB;AACzF,QAAM,QAAQ,IAAI,UAAU,KAAK;AACjC,QAAM,WAAW,IAAI,aAAa,OAAO,OAAO,QAAQ;AACxD,QAAM,cAAc,IAAI,gBAAgB,OAAO,OAAO,QAAQ;AAC9D,QAAM,gBAAgB,IAAI,mBAAmB,KAAK;AAClD,QAAM,aAAa,IAAI,uBAAuB,KAAK;AACnD,QAAM,aAAa,IAAI,uBAAuB,KAAK;AAEnD,QAAM,aAAa,KAAK,KAAK;AAE7B,SAAO;AAAA,IACN,UAAU,OAAO,QAA6C;AAC7D,YAAM,OAAO,IAAI,KAAK,MAAM;AAC5B,YAAM,EAAE,OAAAC,QAAO,UAAAC,WAAU,OAAAF,QAAO,YAAY,MAAM,WAAW,KAAK,IAAI,QAAQ,CAAC;AAG/E,UAAI,OAAOC,WAAU,YAAY,OAAOC,cAAa,UAAU;AAC9D,cAAM,kBAAkB,mBAAmBD,MAAK;AAChD,YAAI,CAAC,iBAAiB;AACrB,qBAAO,6BAAe,kBAAK,eAAe,qBAAqB,uBAAuB;AAAA,QACvF;AAEA,YAAIC,UAAS,SAAS,GAAG;AACxB,qBAAO;AAAA,YACN,kBAAK;AAAA,YACL;AAAA,YACA;AAAA,UACD;AAAA,QACD;AAEA,cAAM,WAAW,MAAM,MAAM,YAAY,eAAe;AACxD,YAAI,UAAU;AACb,qBAAO,6BAAe,kBAAK,UAAU,uBAAuB,0BAA0B;AAAA,QACvF;AAEA,cAAM,eAAe,MAAM,aAAaA,SAAQ;AAChD,cAAM,MAAM,MAAM,MAAM;AAAA,UACvB;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACD;AAEA,YAAI,CAAC,KAAK;AACT,qBAAO,6BAAe,kBAAK,cAAc,gBAAgB,qBAAqB;AAAA,QAC/E;AAEA,cAAM,UAAM,+BAAU,KAAQ,GAAO,EAAE,SAAS;AAChD,cAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,MAAO,KAAK,KAAK,EAAE;AAC3D,cAAM,WAAW,OAAO,IAAI,IAAI,KAAK,SAAS;AAE9C,cAAM,OAAO,MAAM,MAAM,SAAS,IAAI,EAAE;AACxC,YAAI,CAAC,MAAM;AACV,qBAAO,6BAAe,kBAAK,cAAc,gBAAgB,qBAAqB;AAAA,QAC/E;AAEA,cAAM,QAAQ,IAAI,KAAK,WAAW;AAClC,cAAM,UAAU,UAAU,SAAY,EAAE,WAAW,MAAM,IAAI;AAC7D,kBAAM,yBAAW,KACd;AAAA,UACD;AAAA,UACA;AAAA,YACC,MAAM,aAAa;AAAA,YACnB,QAAQ,KAAK;AAAA,YACb,MAAM,EAAE,KAAK,WAAW,aAAa,GAAG;AAAA,YACxC,WAAW,EAAE,OAAO,iBAAiB,OAAO,MAAM,aAAa,KAAK;AAAA,UACrE;AAAA,UACA;AAAA,QACD,CAAC;AACF,kBAAM,yBAAW,KACd;AAAA,UACD,WAAW;AAAA,UACX;AAAA,YACC,QAAQ,KAAK;AAAA,YACb,OAAO,KAAK;AAAA,YACZ,WAAW,KAAK;AAAA,YAChB,UAAU,KAAK;AAAA,YACf,aAAa;AAAA,UACd;AAAA,UACA;AAAA,QACD,CAAC;AAEF,cAAM,EAAE,aAAa,cAAc,IAAI,IAAI,eAAe,KAAK,IAAI,QAAQ;AAAA,UAC1E,aAAa;AAAA,QACd,CAAC;AACD,cAAM,eAAe,YAAY,GAAG;AACpC,cAAM,SAAS,OAAO,KAAK,IAAI,cAAc,mBAAmB,YAAY,GAAG,KAAK,YAAY;AAIhG,cAAM,YAAY,WAAW;AAAA,UAC5B,QAAQ,KAAK;AAAA,UACb,gBAAgB;AAAA,UAChB,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,GAAG;AAAA,QACJ,CAAC;AAED,cAAM,mBAAmB,EAAE,GAAG,QAAQ,GAAG,OAAO,UAAU,GAAG,EAAE;AAC/D,cAAM,uBAAuB,CAAC,CAAE,iBAAiB,uBAAwB,CAAC,KAAK;AAE/E,eAAO,SAAS;AAAA,UACf;AAAA,YACC,QAAQ;AAAA,YACR,aAAa;AAAA,YACb,QAAQ;AAAA,cACP,QAAQ,EAAE,QAAQ,aAAa,SAAS,aAAa;AAAA,cACrD,MAAM,UAAU,IAAI;AAAA,cACpB;AAAA,YACD;AAAA,UACD;AAAA,UACA;AAAA,YACC,QAAQ;AAAA,YACR,SAAS,cAAc,iBAAiB,aAAa,cAAc,MAAM,CAAC;AAAA,UAC3E;AAAA,QACD;AAAA,MACD;AAGA,UAAI,aAAaF,MAAK,GAAG;AACxB,cAAM,WAAW,MAAM,MAAM,YAAY,eAAeA,MAAK,CAAC;AAC9D,YAAI,UAAU;AACb,qBAAO,6BAAe,kBAAK,UAAU,uBAAuB,0BAA0B;AAAA,QACvF;AAEA,cAAM,EAAE,GAAG,IAAI,MAAM,MAAM;AAAA,UAC1B,eAAeA,MAAK;AAAA,UACnB,aAA+B;AAAA,UAC/B,YAA8B;AAAA,QAChC;AAEA,cAAM,UAAM,+BAAU,KAAQ,GAAO,EAAE,SAAS;AAChD,cAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,UAAU;AAClD,cAAM,WAAW,OAAO,IAAI,eAAeA,MAAK,GAAG,KAAK,SAAS;AAEjE,cAAM,OAAO,MAAM,MAAM,SAAS,EAAE;AACpC,YAAI,CAAC,MAAM;AACV,qBAAO,6BAAe,kBAAK,cAAc,gBAAgB,qBAAqB;AAAA,QAC/E;AAEA,cAAM,SAAS,IAAI,KAAK,WAAW;AACnC,cAAM,WAAW,WAAW,SAAY,EAAE,WAAW,OAAO,IAAI;AAChE,kBAAM,yBAAW,KACd;AAAA,UACD;AAAA,UACA;AAAA,YACC,MAAM,aAAa;AAAA,YACnB,QAAQ,KAAK;AAAA,YACb,MAAM,EAAE,IAAI;AAAA,YACZ,WAAW,EAAE,OAAO,MAAM,OAAO,eAAeA,MAAK,GAAG,aAAa,KAAK;AAAA,UAC3E;AAAA,UACA;AAAA,QACD,CAAC;AACF,kBAAM,yBAAW,KACd;AAAA,UACD,WAAW;AAAA,UACX;AAAA,YACC,QAAQ,KAAK;AAAA,YACb,OAAO,KAAK;AAAA,YACZ,WAAW,KAAK;AAAA,YAChB,UAAU,KAAK;AAAA,YACf,aAAa;AAAA,UACd;AAAA,UACA;AAAA,QACD,CAAC;AAQF,cAAM,WAAW,qBAAqB,KAAK,IAAI,QAAQ,OAAO;AAE9D,eAAO,SAAS;AAAA,UACf;AAAA,YACC,QAAQ;AAAA,YACR,aAAa;AAAA,YACb,QAAQ;AAAA,cACP;AAAA,cACA,MAAM,UAAU,IAAI;AAAA,YACrB;AAAA,UACD;AAAA,UACA,EAAE,QAAQ,IAAI;AAAA,QACf;AAAA,MACD;AAEA,iBAAO;AAAA,QACN,kBAAK;AAAA,QACL;AAAA,QACA;AAAA,MACD;AAAA,IACD;AAAA,IAEA,OAAO,OAAO,QAA6C;AAC1D,YAAM,OAAO,IAAI,KAAK,MAAM;AAC5B,YAAM,OAAO,YAAY,GAAG;AAG5B,UAAI,OAAO,OAAO,OAAO,MAAM,YAAY,OAAO,OAAO,UAAU,MAAM,UAAU;AAClF,cAAM,EAAE,OAAO,UAAU,UAAAE,UAAS,IAAI;AACtC,cAAMD,SAAQ,mBAAmB,QAAQ;AACzC,YAAI,CAACA,QAAO;AACX,qBAAO,6BAAe,kBAAK,eAAe,qBAAqB,uBAAuB;AAAA,QACvF;AAEA,cAAM,OAAO,MAAM,MAAM,YAAYA,MAAK;AAC1C,YAAI,CAAC,QAAQ,CAAC,KAAK,cAAc;AAChC,gBAAM,YAAY,WAAW;AAAA,YAC5B,QAAQ;AAAA,YACR,gBAAgBA;AAAA,YAChB,QAAQ;AAAA,YACR,SAAS;AAAA,YACT,eAAe;AAAA,YACf,GAAG;AAAA,UACJ,CAAC;AACD,qBAAO,6BAAe,kBAAK,cAAc,uBAAuB,qBAAqB;AAAA,QACtF;AAEA,cAAM,EAAE,OAAO,YAAY,IAAI,MAAM;AAAA,UACpCC;AAAA,UACA,KAAK;AAAA,UACL,OAAO;AAAA,QACR;AACA,YAAI,CAAC,OAAO;AACX,gBAAM,YAAY,WAAW;AAAA,YAC5B,QAAQ,KAAK;AAAA,YACb,gBAAgBD;AAAA,YAChB,QAAQ;AAAA,YACR,SAAS;AAAA,YACT,eAAe;AAAA,YACf,GAAG;AAAA,UACJ,CAAC;AACD,qBAAO,6BAAe,kBAAK,cAAc,uBAAuB,qBAAqB;AAAA,QACtF;AAIA,YAAI,aAAa;AAChB,gBAAM,MAAM,eAAe,KAAK,IAAI,MAAM,aAAaC,SAAQ,CAAC;AAAA,QACjE;AAEA,YAAI,KAAK,WAAW;AACnB,qBAAO;AAAA,YACN,kBAAK;AAAA,YACL;AAAA,YACA;AAAA,UACD;AAAA,QACD;AAEA,YAAI,KAAK,YAAY;AAIpB,gBAAM,WAAW,qBAAqB,KAAK,IAAI,QAAQ,OAAO;AAC9D,qBAAO,6BAAe,kBAAK,IAAI,gBAAgB,wCAAwC;AAAA,YACtF;AAAA,UACD,CAAC;AAAA,QACF;AAEA,cAAM,EAAE,aAAa,cAAc,IAAI,IAAI,eAAe,KAAK,IAAI,QAAQ;AAAA,UAC1E,aAAa;AAAA,QACd,CAAC;AACD,cAAM,SAAS,OAAO,KAAK,IAAI,cAAc,mBAAmB,YAAY,GAAG,KAAK,IAAI;AACxF,cAAM,YAAY,WAAW;AAAA,UAC5B,QAAQ,KAAK;AAAA,UACb,gBAAgBD;AAAA,UAChB,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,GAAG;AAAA,QACJ,CAAC;AAED,cAAM,gBAAgB,EAAE,GAAG,QAAQ,GAAG,OAAO,UAAU,GAAG,EAAE;AAC5D,cAAM,uBAAuB,CAAC,CAAE,cAAc,uBAAwB,CAAC,KAAK;AAE5E,eAAO,SAAS;AAAA,UACf;AAAA,YACC,QAAQ;AAAA,YACR,aAAa;AAAA,YACb,QAAQ;AAAA,cACP,QAAQ,EAAE,QAAQ,aAAa,SAAS,aAAa;AAAA,cACrD,MAAM,UAAU,IAAI;AAAA,cACpB;AAAA,YACD;AAAA,UACD;AAAA,UACA;AAAA,YACC,QAAQ;AAAA,YACR,SAAS,cAAc,iBAAiB,aAAa,cAAc,MAAM,CAAC;AAAA,UAC3E;AAAA,QACD;AAAA,MACD;AAGA,YAAMD,SAAQ,OAAO,OAAO;AAC5B,UAAI,aAAaA,MAAK,GAAG;AACxB,cAAM,OAAO,MAAM,MAAM,YAAY,eAAeA,MAAK,CAAC;AAC1D,YAAI,CAAC,MAAM;AACV,qBAAO,6BAAe,kBAAK,cAAc,uBAAuB,qBAAqB;AAAA,QACtF;AACA,YAAI,KAAK,WAAW;AACnB,qBAAO;AAAA,YACN,kBAAK;AAAA,YACL;AAAA,YACA;AAAA,UACD;AAAA,QACD;AAEA,cAAM,UAAM,+BAAU,KAAQ,GAAO,EAAE,SAAS;AAChD,cAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,UAAU;AAClD,cAAM,WAAW,OAAO,KAAK,IAAI,eAAeA,MAAK,GAAG,KAAK,SAAS;AAEtE,kBAAM,yBAAW,KACd,KAAK,mCAAoB;AAAA,UAC1B,MAAM,aAAa;AAAA,UACnB,QAAQ,KAAK;AAAA,UACb,MAAM,EAAE,IAAI;AAAA,UACZ,WAAW,EAAE,OAAO,MAAM,OAAO,eAAeA,MAAK,GAAG,aAAa,KAAK;AAAA,QAC3E,CAA2B,CAAC;AAO7B,cAAM,WAAW,qBAAqB,KAAK,IAAI,QAAQ,OAAO;AAE9D,eAAO,SAAS;AAAA,UACf;AAAA,YACC,QAAQ;AAAA,YACR,aAAa;AAAA,YACb,QAAQ,EAAE,SAAS;AAAA,UACpB;AAAA,UACA,EAAE,QAAQ,IAAI;AAAA,QACf;AAAA,MACD;AAEA,iBAAO;AAAA,QACN,kBAAK;AAAA,QACL;AAAA,QACA;AAAA,MACD;AAAA,IACD;AAAA,IAEA,QAAQ,OAAO,QAA6C;AAM3D,YAAM,MAAO,IAAI,MAAyC;AAC1D,UAAI,KAAK;AACR,cAAM,SAAS,YAAY,GAAG,EAAE,MAAM,MAAM,MAAS;AAAA,MACtD;AAGA,YAAM,QAAQ,oBAAoB,GAAG;AACrC,UAAI,OAAO;AACV,cAAM,SAAS,OAAO,KAAK,EAAE,MAAM,MAAM,MAAS;AAAA,MACnD;AAEA,aAAO,SAAS;AAAA,QACf,EAAE,QAAQ,eAAe,aAAa,2BAA2B;AAAA,QACjE;AAAA,UACC,QAAQ;AAAA,UACR,SAAS,cAAc,oBAAoB,MAAM,CAAC;AAAA,QACnD;AAAA,MACD;AAAA,IACD;AAAA,IAEA,SAAS,OAAO,QAA6C;AAC5D,YAAM,QAAQ,oBAAoB,GAAG;AACrC,UAAI,CAAC,OAAO;AACX,mBAAO,6BAAe,kBAAK,cAAc,qBAAqB,2BAA2B;AAAA,MAC1F;AAEA,YAAM,UAAU,YAAY,OAAO,MAAM;AACzC,UAAI,CAAC,WAAW,QAAQ,SAAS,WAAW;AAC3C,mBAAO,6BAAe,kBAAK,cAAc,wBAAwB,uBAAuB;AAAA,MACzF;AAEA,YAAM,QAAQ,MAAM,SAAS,OAAO,KAAK;AACzC,UAAI,CAAC,OAAO;AACX,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,YAAM,OAAO,MAAM,MAAM,SAAS,QAAQ,GAAG;AAC7C,UAAI,CAAC,QAAQ,KAAK,aAAa,KAAK,WAAW;AAC9C,mBAAO,6BAAe,kBAAK,cAAc,gBAAgB,cAAc;AAAA,MACxE;AAEA,YAAM,SAAS,OAAO,KAAK;AAC3B,YAAM,EAAE,aAAa,cAAc,IAAI,IAAI,eAAe,KAAK,IAAI,QAAQ;AAAA,QAC1E,aAAa,QAAQ,eAAe;AAAA,QACpC,eAAe,QAAQ,iBAAiB;AAAA,MACzC,CAAC;AACD,YAAM,SAAS,OAAO,KAAK,IAAI,cAAc,mBAAmB,YAAY,GAAG,KAAK,YAAY,GAAG,CAAC;AAEpG,aAAO,SAAS;AAAA,QACf;AAAA,UACC,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ,EAAE,QAAQ,EAAE,QAAQ,aAAa,SAAS,aAAa,EAAE;AAAA,QAClE;AAAA,QACA;AAAA,UACC,QAAQ;AAAA,UACR,SAAS,cAAc,iBAAiB,aAAa,cAAc,MAAM,CAAC;AAAA,QAC3E;AAAA,MACD;AAAA,IACD;AAAA,IAEA,gBAAgB,OAAO,QAA6C;AACnE,YAAM,OAAO,IAAI,KAAK,MAAM;AAC5B,YAAM,WAAW,OAAO,OAAO;AAE/B,UAAI,OAAO,aAAa,UAAU;AACjC,mBAAO,6BAAe,kBAAK,eAAe,qBAAqB,mBAAmB;AAAA,MACnF;AAEA,YAAMC,SAAQ,mBAAmB,QAAQ;AACzC,UAAI,CAACA,QAAO;AACX,mBAAO,6BAAe,kBAAK,eAAe,qBAAqB,uBAAuB;AAAA,MACvF;AAEA,YAAM,OAAO,MAAM,MAAM,YAAYA,MAAK;AAC1C,UAAI,CAAC,MAAM;AACV,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,YAAM,WAAW,EAAE,GAAG,QAAQ,GAAG,OAAO,UAAU,GAAG,EAAE;AACvD,YAAM,WAAW,SAAS,wBAAwB;AAClD,YAAM,YAAY,cAAc,MAAM,cAAc,eAAe,KAAK,EAAE,GAAG,QAAQ;AACrF,UAAI,YAAY,GAAG;AAIlB,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,YAAM,UAAM,+BAAU,KAAQ,GAAO,EAAE,SAAS;AAGhD,YAAM,YAAQ,iCAAY,EAAE,EAAE,SAAS,KAAK;AAC5C,YAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,MAAO,KAAK,EAAE;AACtD,YAAM,cAAc,OAAO,KAAK,IAAI,KAAK,OAAO,SAAS;AAIzD,YAAM,OAAO,SAAS;AACtB,YAAM,WAAW,OAAO,GAAG,IAAI,GAAG,KAAK,SAAS,GAAG,IAAI,MAAM,GAAG,SAAS,KAAK,KAAK;AAEnF,gBAAM,yBAAW,KACd,KAAK,mCAAoB;AAAA,QAC1B,MAAM,aAAa;AAAA,QACnB,QAAQ,KAAK;AAAA,QACb,WAAW,EAAE,OAAAA,QAAO,OAAO,MAAM,aAAa,KAAK;AAAA,QACnD,MAAM,EAAE,KAAK,OAAO,SAAS;AAAA,MAC9B,CAA2B,CAAC;AAE7B,iBAAO;AAAA,QACN,kBAAK;AAAA,QACL;AAAA,QACA;AAAA,MACD;AAAA,IACD;AAAA,IAEA,eAAe,OAAO,QAA6C;AAClE,YAAM,OAAO,IAAI,KAAK,MAAM;AAC5B,YAAM,MAAM,OAAO,KAAK;AACxB,YAAMC,YAAW,OAAO,UAAU;AAElC,YAAM,WAAW,OAAO,OAAO;AAE/B,UAAI,OAAOA,cAAa,UAAU;AACjC,mBAAO,6BAAe,kBAAK,eAAe,qBAAqB,sBAAsB;AAAA,MACtF;AACA,UAAIA,UAAS,SAAS,GAAG;AACxB,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAKA,UAAI;AACJ,UAAI,OAAO,aAAa,YAAY,SAAS,KAAK,EAAE,UAAU,IAAI;AACjE,cAAM,MAAM,cAAc,YAAY,SAAS,KAAK,CAAC;AAAA,MACtD,OAAO;AACN,YAAI,OAAO,QAAQ,YAAY,CAAC,UAAU,KAAK,IAAI,KAAK,CAAC,GAAG;AAC3D,qBAAO;AAAA,YACN,kBAAK;AAAA,YACL;AAAA,YACA;AAAA,UACD;AAAA,QACD;AACA,cAAM,MAAM,cAAc,UAAU,IAAI,KAAK,CAAC;AAAA,MAC/C;AAEA,UAAI,CAAC,OAAO,oBAAI,KAAK,IAAI,IAAI,WAAW;AACvC,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,YAAM,eAAe,MAAM,aAAaA,SAAQ;AAChD,YAAM,MAAM,YAAY,OAAO,OAAO;AACrC,cAAM,QAAQ,IAAI;AAAA,UACjB,GAAG,MAAM,8DAA8D;AAAA,YACtE;AAAA,YACA,IAAI;AAAA,UACL,CAAC;AAAA,UACD,GAAG,MAAM,2DAA2D,CAAC,IAAI,MAAM,CAAC;AAAA;AAAA;AAAA,UAGhF,GAAG,MAAM,oDAAoD,CAAC,IAAI,MAAM,CAAC;AAAA,QAC1E,CAAC;AAAA,MACF,CAAC;AAED,iBAAO,6BAAe,kBAAK,IAAI,6BAA6B,8BAA8B;AAAA,IAC3F;AAAA,IAEA,QAAQ,OAAO,QAA6C;AAK3D,UAAI,IAAI,KAAM,cAAc,IAAI,KAAM,gBAAgB,SAAS;AAC9D,mBAAO,6BAAe,kBAAK,WAAW,gBAAgB,uCAAuC;AAAA,MAC9F;AAGA,UAAI,IAAI,KAAM,gBAAgB,WAAW,IAAI,KAAM,iBAAiB;AACnE,mBAAO,6BAAe,kBAAK,IAAI,YAAY,gCAAgC;AAAA,UAC1E,UAAU;AAAA,UACV,OAAO,IAAI,KAAM;AAAA,QAClB,CAAC;AAAA,MACF;AAEA,YAAM,OAAO,IAAI,KAAK,MAAM;AAC5B,YAAM,MAAM,OAAO,OAAO;AAE1B,UAAI,OAAO,QAAQ,YAAY,CAAC,UAAU,KAAK,IAAI,KAAK,CAAC,GAAG;AAC3D,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,YAAM,MAAM,IAAI,KAAK;AAGrB,UAAI,IAAI,KAAM,gBAAgB,SAAS;AAMtC,cAAM,YAAY,YAAY,GAAG;AACjC,cAAM,SAAS,MAAM,WAAW,WAAW,IAAI,KAAM,IAAI,GAAG;AAC5D,YAAI,CAAC,QAAQ;AACZ,gBAAM,YAAY,WAAW;AAAA,YAC5B,QAAQ,IAAI,KAAM;AAAA,YAClB,gBAAgB;AAAA,YAChB,QAAQ;AAAA,YACR,SAAS;AAAA,YACT,eAAe;AAAA,YACf,GAAG;AAAA,UACJ,CAAC;AACD,qBAAO,6BAAe,kBAAK,aAAa,uBAAuB,wBAAwB;AAAA,QACxF;AACA,YAAI,oBAAI,KAAK,IAAI,OAAO,WAAW;AAClC,gBAAM,WAAW,aAAa,IAAI,KAAM,EAAE;AAC1C,gBAAM,YAAY,WAAW;AAAA,YAC5B,QAAQ,IAAI,KAAM;AAAA,YAClB,gBAAgB;AAAA,YAChB,QAAQ;AAAA,YACR,SAAS;AAAA,YACT,eAAe;AAAA,YACf,GAAG;AAAA,UACJ,CAAC;AACD,qBAAO;AAAA,YACN,kBAAK;AAAA,YACL;AAAA,YACA;AAAA,UACD;AAAA,QACD;AACA,cAAM,WAAW,aAAa,IAAI,KAAM,EAAE;AAE1C,cAAM,EAAE,aAAa,cAAc,IAAI,IAAI,eAAe,IAAI,KAAM,IAAI,QAAQ;AAAA,UAC/E,aAAa;AAAA,UACb,eAAe;AAAA,QAChB,CAAC;AACD,cAAM,SAAS,OAAO,IAAI,KAAM,IAAI,cAAc,mBAAmB,YAAY,GAAG,KAAK,SAAS;AAClG,cAAM,YAAY,WAAW;AAAA,UAC5B,QAAQ,IAAI,KAAM;AAAA,UAClB,gBAAgB;AAAA,UAChB,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,GAAG;AAAA,QACJ,CAAC;AAED,cAAM,eAAe,MAAM,MAAM,SAAS,IAAI,KAAM,EAAE;AACtD,YAAI,CAAC,cAAc;AAClB,qBAAO,6BAAe,kBAAK,WAAW,aAAa,gBAAgB;AAAA,QACpE;AACA,YAAI,aAAa,WAAW;AAC3B,qBAAO;AAAA,YACN,kBAAK;AAAA,YACL;AAAA,YACA;AAAA,UACD;AAAA,QACD;AAEA,eAAO,SAAS;AAAA,UACf;AAAA,YACC,QAAQ;AAAA,YACR,aAAa;AAAA,YACb,QAAQ;AAAA,cACP,QAAQ,EAAE,QAAQ,aAAa,SAAS,aAAa;AAAA,cACrD,MAAM,UAAU,cAAc,IAAI;AAAA,YACnC;AAAA,UACD;AAAA,UACA;AAAA,YACC,QAAQ;AAAA,YACR,SAAS,cAAc,iBAAiB,aAAa,cAAc,MAAM,CAAC;AAAA,UAC3E;AAAA,QACD;AAAA,MACD;AAGA,YAAM,MAAM,MAAM,WAAW,WAAW,IAAI,KAAM,IAAI,GAAG;AACzD,UAAI,CAAC,KAAK;AACT,mBAAO,6BAAe,kBAAK,aAAa,uBAAuB,wBAAwB;AAAA,MACxF;AACA,UAAI,oBAAI,KAAK,IAAI,IAAI,WAAW;AAC/B,mBAAO,6BAAe,kBAAK,aAAa,uBAAuB,aAAa;AAAA,MAC7E;AAEA,YAAM,MAAM,YAAY,OAAO,OAAO;AACrC,cAAM,QAAQ,IAAI;AAAA,UACjB,GAAG;AAAA,YACF;AAAA,YACA,CAAC,IAAI,KAAM,EAAE;AAAA,UACd;AAAA,UACA,GAAG,MAAM,8EAA8E;AAAA,YACtF,IAAI,KAAM;AAAA,YACV;AAAA,UACD,CAAC;AAAA,QACF,CAAC;AAAA,MACF,CAAC;AAED,iBAAO,6BAAe,kBAAK,IAAI,YAAY,gCAAgC;AAAA,QAC1E,UAAU;AAAA,QACV,OAAO,IAAI,KAAM;AAAA,MAClB,CAAC;AAAA,IACF;AAAA,IAEA,kBAAkB,OAAO,QAA6C;AAGrE,UAAI,IAAI,KAAM,cAAc,IAAI,KAAM,gBAAgB,SAAS;AAC9D,mBAAO,6BAAe,kBAAK,WAAW,gBAAgB,uCAAuC;AAAA,MAC9F;AAEA,YAAM,WAAW,EAAE,GAAG,QAAQ,GAAG,OAAO,UAAU,GAAG,EAAE;AACvD,YAAM,WAAW,SAAS,wBAAwB;AAElD,UAAI,IAAI,KAAM,gBAAgB,SAAS;AACtC,cAAMF,SAAQ,IAAI,KAAM;AACxB,YAAI,CAACA,QAAO;AACX,qBAAO;AAAA,YACN,kBAAK;AAAA,YACL;AAAA,YACA;AAAA,UACD;AAAA,QACD;AAEA,cAAMG,aAAY,cAAc,MAAM,WAAW,eAAe,IAAI,KAAM,EAAE,GAAG,QAAQ;AACvF,YAAIA,aAAY,GAAG;AAClB,qBAAO;AAAA,YACN,kBAAK;AAAA,YACL;AAAA,YACA;AAAA,YACA;AAAA,cACC,YAAY,KAAK,KAAKA,aAAY,GAAI;AAAA,YACvC;AAAA,UACD;AAAA,QACD;AAEA,cAAM,UAAM,+BAAU,KAAQ,GAAO,EAAE,SAAS;AAChD,cAAMC,aAAY,IAAI,KAAK,KAAK,IAAI,IAAI,UAAU;AAClD,cAAM,WAAW,OAAO,IAAI,KAAM,IAAIJ,QAAO,KAAKI,UAAS;AAE3D,kBAAM,yBAAW,KACd,KAAK,mCAAoB;AAAA,UAC1B,MAAM,aAAa;AAAA,UACnB,QAAQ,IAAI,KAAM;AAAA,UAClB,MAAM,EAAE,IAAI;AAAA,UACZ,WAAW,EAAE,OAAO,MAAM,OAAAJ,QAAO,aAAa,KAAK;AAAA,QACpD,CAA2B,CAAC;AAE7B,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,UAAI,CAAC,IAAI,KAAM,OAAO;AACrB,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,UAAI,IAAI,KAAM,iBAAiB;AAC9B,mBAAO,6BAAe,kBAAK,IAAI,kBAAkB,2BAA2B;AAAA,UAC3E,UAAU;AAAA,UACV,OAAO,IAAI,KAAM;AAAA,QAClB,CAAC;AAAA,MACF;AAEA,YAAM,YAAY,cAAc,MAAM,WAAW,eAAe,IAAI,KAAM,EAAE,GAAG,QAAQ;AACvF,UAAI,YAAY,GAAG;AAClB,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA;AAAA,YACC,YAAY,KAAK,KAAK,YAAY,GAAI;AAAA,UACvC;AAAA,QACD;AAAA,MACD;AAEA,YAAM,UAAM,+BAAU,KAAQ,GAAO,EAAE,SAAS;AAChD,YAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,MAAO,KAAK,KAAK,EAAE;AAC3D,YAAM,WAAW,QAAQ,IAAI,KAAM,IAAI,KAAK,SAAS;AAErD,gBAAM,yBAAW,KACd,KAAK,mCAAoB;AAAA,QAC1B,MAAM,aAAa;AAAA,QACnB,QAAQ,IAAI,KAAM;AAAA,QAClB,WAAW,EAAE,OAAO,IAAI,KAAM,OAAO,OAAO,MAAM,aAAa,KAAK;AAAA,QACpE,MAAM,EAAE,IAAI;AAAA,MACb,CAA2B,CAAC;AAE7B,iBAAO,6BAAe,kBAAK,IAAI,qBAAqB,4BAA4B;AAAA,QAC/E,OAAO,IAAI,KAAM;AAAA,MAClB,CAAC;AAAA,IACF;AAAA,EACD;AACD;;;AMlzBA,IAAAK,sBAA0B;AAE1B,IAAAC,eAA8D;AAM9D,IAAAC,iBAAmC;;;ACTnC,IAAAC,eAAuD;AAyBvD,SAAS,gBAAgB,KAAqC;AAC7D,SAAO;AAAA,IACN,IAAI,IAAI;AAAA,IACR,QAAQ,IAAI;AAAA,IACZ,SAAS,IAAI;AAAA,IACb,eAAe,IAAI;AAAA,IACnB,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA;AAAA;AAAA,IAGf,UAAU,iBAAiB,IAAI,QAAQ;AAAA,IACvC,WAAW,IAAI,qBAAqB,OAAO,IAAI,UAAU,YAAY,IAAI,OAAO,IAAI,SAAS;AAAA,EAC9F;AACD;AAEO,SAAS,sBAAsB,MAA6C;AAClF,QAAM,OAAO,KAAK,OAAO,KAAK,OAAO,SAAS,CAAC;AAG/C,QAAM,aACL,KAAK,WAAW,WAAO,iCAAmB,KAAK,gBAAgB,KAAK,UAAU,YAAY,GAAG,KAAK,EAAE,IAAI;AACzG,SAAO,EAAE,QAAQ,KAAK,OAAO,IAAI,eAAe,GAAG,WAAW;AAC/D;AAIO,IAAM,oBAAoB;AAe1B,SAAS,aACf,KASA,YACc;AACd,SAAO;AAAA,IACN,IAAI,IAAI;AAAA,IACR,SAAS,eAAe,QAAQ,IAAI,QAAQ;AAAA,IAC5C,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,IACf,UAAU,iBAAiB,IAAI,QAAQ;AAAA,IACvC,WAAW,IAAI,qBAAqB,OAAO,IAAI,UAAU,YAAY,IAAI,OAAO,IAAI,SAAS;AAAA,IAC7F,WAAW,IAAI,qBAAqB,OAAO,IAAI,UAAU,YAAY,IAAI,OAAO,IAAI,SAAS;AAAA,EAC9F;AACD;;;AD5DA,SAASC,gBAAeC,QAAuB;AAC9C,SAAOA,OAAM,KAAK,EAAE,QAAQ,eAAe,EAAE;AAC9C;AAEA,SAASC,cAAaD,QAAiC;AACtD,SAAO,OAAOA,WAAU,YAAY,qBAAqB,KAAKD,gBAAeC,MAAK,CAAC;AACpF;AAEO,SAAS,eAAe,OAAsB,QAAqB,KAAgB;AACzF,QAAM,QAAQ,IAAI,UAAU,KAAK;AACjC,QAAM,WAAW,IAAI,aAAa,OAAO,OAAO,QAAQ;AACxD,QAAM,cAAc,IAAI,gBAAgB,OAAO,OAAO,QAAQ;AAC9D,QAAM,aAAa,IAAI,uBAAuB,KAAK;AACnD,QAAM,aAAa,IAAI,uBAAuB,KAAK;AAEnD,SAAO;AAAA,IACN,IAAI,OAAO,QAA6C;AACvD,YAAM,OAAO,MAAM,MAAM,SAAS,IAAI,KAAM,EAAE;AAC9C,UAAI,CAAC,MAAM;AACV,mBAAO,6BAAe,kBAAK,WAAW,aAAa,gBAAgB;AAAA,MACpE;AAEA,iBAAO,6BAAe,kBAAK,IAAI,wBAAwB,oCAAoC;AAAA,QAC1F,MAAM,UAAU,MAAM,IAAI,KAAM,aAAa;AAAA,MAC9C,CAAC;AAAA,IACF;AAAA;AAAA,IAGA,cAAc,OAAO,QAA6C;AACjE,YAAM,SAAS,IAAI,IAAI,IAAI,QAAQ,GAAG,EAAE;AACxC,YAAM,WAAW,OAAO,OAAO,IAAI,OAAO,KAAK,EAAE;AACjD,YAAM,QAAQ,OAAO,SAAS,QAAQ,IAAI,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,QAAQ,GAAG,CAAC,GAAG,GAAG,IAAI;AAE7F,YAAM,eAAe,OAAO,IAAI,SAAS;AACzC,UAAI,iBAAiB,QAAQ,iBAAiB,aAAa,iBAAiB,UAAU;AACrF,mBAAO,6BAAe,kBAAK,eAAe,qBAAqB,uCAAuC;AAAA,MACvG;AAEA,YAAM,cAAc,OAAO,IAAI,QAAQ;AACvC,UAAI,SAAmD;AACvD,UAAI,aAAa;AAChB,iBAAS,kBAAkB,WAAW;AACtC,YAAI,CAAC,QAAQ;AACZ,qBAAO,6BAAe,kBAAK,eAAe,qBAAqB,gBAAgB;AAAA,QAChF;AAAA,MACD;AAEA,YAAM,QAAsD,EAAE,QAAQ,IAAI,KAAM,IAAI,MAAM;AAC1F,UAAI,aAAc,OAAM,UAAU;AAClC,UAAI,OAAQ,OAAM,SAAS;AAC3B,YAAM,OAAO,OAAO,IAAI,MAAM;AAC9B,YAAM,KAAK,OAAO,IAAI,IAAI;AAC1B,UAAI,KAAM,OAAM,OAAO,IAAI,KAAK,IAAI;AACpC,UAAI,GAAI,OAAM,KAAK,IAAI,KAAK,EAAE;AAE9B,YAAM,OAAO,MAAM,YAAY,WAAW,KAAK;AAC/C,iBAAO;AAAA,QACN,kBAAK;AAAA,QACL;AAAA,QACA;AAAA,QACA,sBAAsB,IAAI;AAAA,MAC3B;AAAA,IACD;AAAA;AAAA,IAGA,cAAc,OAAO,QAA6C;AACjE,YAAM,OAAO,MAAM,SAAS,eAAe,IAAI,KAAM,EAAE;AACvD,YAAM,aAAc,IAAI,KAAiC,OAAO;AAChE,iBAAO,6BAAe,kBAAK,IAAI,oBAAoB,8BAA8B;AAAA,QAChF,UAAU,KAAK,IAAI,CAAC,MAAM,aAAa,GAAG,UAAU,CAAC;AAAA,MACtD,CAAC;AAAA,IACF;AAAA,IAEA,kBAAkB,OAAO,QAA6C;AACrE,YAAM,KAAM,IAAI,KAAK,QAAQ,IAA2C,IAAI;AAC5E,UAAI,CAAC,IAAI;AACR,mBAAO,6BAAe,kBAAK,WAAW,aAAa,mBAAmB;AAAA,MACvE;AACA,YAAM,UAAU,MAAM,SAAS,cAAc,IAAI,KAAM,IAAI,EAAE;AAC7D,UAAI,CAAC,SAAS;AACb,mBAAO,6BAAe,kBAAK,WAAW,aAAa,mBAAmB;AAAA,MACvE;AACA,iBAAO,6BAAe,kBAAK,IAAI,sBAAsB,uBAAuB,EAAE,GAAG,CAAC;AAAA,IACnF;AAAA,IAEA,wBAAwB,OAAO,QAA6C;AAC3E,YAAM,aAAc,IAAI,KAAiC,OAAO;AAChE,UAAI,CAAC,YAAY;AAGhB,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AACA,YAAM,QAAQ,MAAM,SAAS,gBAAgB,IAAI,KAAM,IAAI,UAAU;AACrE,iBAAO,6BAAe,kBAAK,IAAI,uBAAuB,8BAA8B,EAAE,MAAM,CAAC;AAAA,IAC9F;AAAA,IAEA,eAAe,OAAO,QAA6C;AAClE,YAAM,OAAO,IAAI,KAAK,MAAM;AAE5B,YAAM,UAAU,CAAC,aAAa,YAAY,WAAW;AACrD,YAAM,SAAkC,CAAC;AACzC,iBAAW,OAAO,SAAS;AAC1B,YAAI,OAAO,GAAG,MAAM,OAAW,QAAO,GAAG,IAAI,KAAK,GAAG;AAAA,MACtD;AAEA,UAAI,OAAO,KAAK,MAAM,EAAE,WAAW,GAAG;AACrC,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,YAAM,MAAM,MAAM,MAAM,OAAO,IAAI,KAAM,IAAI,MAAM;AACnD,UAAI,CAAC,KAAK;AACT,mBAAO,6BAAe,kBAAK,WAAW,aAAa,gBAAgB;AAAA,MACpE;AAEA,YAAM,UAAU,MAAM,MAAM,SAAS,IAAI,KAAM,EAAE;AACjD,iBAAO,6BAAe,kBAAK,IAAI,mBAAmB,oBAAoB;AAAA,QACrE,MAAM,UAAU,SAAkB,IAAI,KAAM,aAAa;AAAA,MAC1D,CAAC;AAAA,IACF;AAAA,IAEA,mBAAmB,OAAO,QAA6C;AACtE,YAAM,OAAO,IAAI,KAAK,MAAM;AAE5B,YAAM,SAAwD,CAAC;AAE/D,UAAI,OAAO,OAAO,QAAQ,MAAM,SAAU,QAAO,SAAS,KAAK,QAAQ;AACvE,UAAI,OAAO,OAAO,UAAU,MAAM,SAAU,QAAO,WAAW,KAAK,UAAU;AAE7E,YAAM,WAAW,CAAC,iBAAiB,eAAe,cAAc,YAAY;AAC5E,YAAM,QAAiC,CAAC;AACxC,iBAAW,OAAO,UAAU;AAC3B,YAAI,OAAO,GAAG,MAAM,OAAW,OAAM,GAAG,IAAI,KAAK,GAAG;AAAA,MACrD;AACA,UAAI,OAAO,KAAK,KAAK,EAAE,SAAS,EAAG,QAAO,QAAQ;AAElD,YAAM,MAAM,MAAM,MAAM,kBAAkB,IAAI,KAAM,IAAI,MAAM;AAC9D,UAAI,CAAC,KAAK;AACT,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,YAAM,UAAU,MAAM,MAAM,SAAS,IAAI,KAAM,EAAE;AACjD,iBAAO,6BAAe,kBAAK,IAAI,uBAAuB,wBAAwB;AAAA,QAC7E,MAAM,UAAU,SAAkB,IAAI,KAAM,aAAa;AAAA,MAC1D,CAAC;AAAA,IACF;AAAA,IAEA,aAAa,OAAO,QAA6C;AAChE,YAAM,OAAO,IAAI,KAAK,MAAM;AAC5B,YAAM,WAAW,OAAO,OAAO;AAE/B,YAAM,aAAa,OAAO,aAAa,WAAW,mBAAmB,QAAQ,IAAI;AACjF,UAAI,CAAC,YAAY;AAChB,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AACA,YAAM,WAAW,IAAI,KAAM;AAE3B,UAAI,eAAe,UAAU;AAC5B,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,YAAM,WAAW,MAAM,MAAM,YAAY,UAAU;AACnD,UAAI,UAAU;AACb,mBAAO,6BAAe,kBAAK,UAAU,gBAAgB,sBAAsB;AAAA,MAC5E;AAEA,YAAM,UAAM,+BAAU,KAAQ,GAAO,EAAE,SAAS;AAChD,YAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,MAAO,KAAK,KAAK,EAAE;AAC3D,YAAM,WAAW,QAAQ,IAAI,KAAM,IAAI,KAAK,SAAS;AACrD,YAAM,MAAM,YAAY,IAAI,KAAM,IAAI,UAAU;AAEhD,gBAAM,yBAAW,KACd,KAAK,mCAAoB;AAAA,QAC1B,MAAM,aAAa;AAAA,QACnB,QAAQ,IAAI,KAAM;AAAA,QAClB,MAAM,EAAE,IAAI;AAAA,QACZ,WAAW,EAAE,OAAO,YAAY,OAAO,MAAM,aAAa,KAAK;AAAA,MAChE,CAA2B,CAAC;AAC7B,UAAI,UAAU;AACb,kBAAM,yBAAW,KACd,KAAK,mCAAoB;AAAA,UAC1B,MAAM,aAAa;AAAA,UACnB,QAAQ,IAAI,KAAM;AAAA,UAClB,MAAM,EAAE,UAAU,WAAW;AAAA,UAC7B,WAAW,EAAE,OAAO,UAAU,OAAO,MAAM,aAAa,KAAK;AAAA,QAC9D,CAA2B,CAAC;AAAA,MAC9B;AAEA,iBAAO;AAAA,QACN,kBAAK;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,UACC,OAAO;AAAA,QACR;AAAA,MACD;AAAA,IACD;AAAA,IAEA,aAAa,OAAO,QAA6C;AAChE,YAAM,OAAO,IAAI,KAAK,MAAM;AAC5B,YAAM,WAAW,OAAO,OAAO;AAE/B,UAAI,CAACC,cAAa,QAAQ,GAAG;AAC5B,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,YAAM,aAAaF,gBAAe,QAAQ;AAE1C,YAAM,WAAW,MAAM,MAAM,YAAY,UAAU;AACnD,UAAI,UAAU;AACb,mBAAO,6BAAe,kBAAK,UAAU,gBAAgB,6BAA6B;AAAA,MACnF;AAEA,YAAM,UAAM,+BAAU,KAAQ,GAAO,EAAE,SAAS;AAChD,YAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,GAAI;AACtD,YAAM,WAAW,OAAO,IAAI,KAAM,IAAI,YAAY,KAAK,SAAS;AAChE,YAAM,MAAM,YAAY,IAAI,KAAM,IAAI,UAAU;AAEhD,gBAAM,yBAAW,KACd,KAAK,mCAAoB;AAAA,QAC1B,MAAM,aAAa;AAAA,QACnB,QAAQ,IAAI,KAAM;AAAA,QAClB,MAAM,EAAE,IAAI;AAAA,QACZ,WAAW,EAAE,OAAO,MAAM,OAAO,YAAY,aAAa,KAAK;AAAA,MAChE,CAA2B,CAAC;AAC7B,UAAI,IAAI,KAAM,OAAO;AACpB,kBAAM,yBAAW,KACd,KAAK,mCAAoB;AAAA,UAC1B,MAAM,aAAa;AAAA,UACnB,QAAQ,IAAI,KAAM;AAAA,UAClB,MAAM,CAAC;AAAA,UACP,WAAW,EAAE,OAAO,IAAI,KAAM,OAAO,OAAO,MAAM,aAAa,KAAK;AAAA,QACrE,CAA2B,CAAC;AAAA,MAC9B;AAEA,iBAAO;AAAA,QACN,kBAAK;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,UACC,OAAO;AAAA,QACR;AAAA,MACD;AAAA,IACD;AAAA,IAEA,gBAAgB,OAAO,QAA6C;AACnE,YAAM,OAAO,IAAI,KAAK,MAAM;AAC5B,YAAM,kBAAkB,OAAO,iBAAiB;AAChD,YAAM,cAAkB,OAAO,aAAa;AAE5C,UAAI,OAAO,oBAAoB,YAAY,CAAC,iBAAiB;AAC5D,mBAAO,6BAAe,kBAAK,eAAe,qBAAqB,6BAA6B;AAAA,MAC7F;AACA,UAAI,OAAO,gBAAgB,YAAY,YAAY,SAAS,GAAG;AAC9D,mBAAO,6BAAe,kBAAK,eAAe,qBAAqB,2CAA2C;AAAA,MAC3G;AAEA,YAAM,OAAO,MAAM,MAAM,SAAS,IAAI,KAAM,EAAE;AAC9C,UAAI,CAAC,KAAM,YAAO,6BAAe,kBAAK,WAAW,aAAa,gBAAgB;AAE9E,YAAM,EAAE,cAAAG,eAAc,gBAAAC,gBAAe,IAAI,MAAM;AAC/C,YAAM,QAAQ,MAAMA,gBAAe,iBAAiB,KAAK,gBAAgB,EAAE;AAC3E,UAAI,CAAC,OAAO;AACX,mBAAO,6BAAe,kBAAK,eAAe,sBAAsB,+BAA+B;AAAA,MAChG;AAEA,YAAM,OAAO,MAAMD,cAAa,WAAW;AAC3C,YAAM,MAAM,eAAe,IAAI,KAAM,IAAI,IAAI;AAK7C,YAAM,SAAS,aAAa,IAAI,KAAM,EAAE;AAExC,iBAAO,6BAAe,kBAAK,IAAI,oBAAoB,gCAAgC;AAAA,IACpF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAeA,aAAa,OAAO,QAA6C;AAChE,YAAM,WAAW,OAAQ,IAAI,KAAK,QAAQ,IAA+B,UAAU,KAAK,EAAE;AAC1F,YAAM,UAAU,MAAM,MAAM,SAAS,IAAI,KAAM,EAAE;AACjD,UAAI,CAAC,SAAS,UAAU;AACvB,mBAAO,6BAAe,kBAAK,WAAW,cAAc,gCAAgC;AAAA,MACrF;AACA,UAAI,YAAY,SAAS,YAAY,MAAM,QAAQ,SAAS,YAAY,GAAG;AAC1E,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA,iCAAiC,QAAQ;AAAA,QAC1C;AAAA,MACD;AAEA,YAAM,UAAU,MAAM,MAAM,cAAc,IAAI,KAAM,EAAE;AACtD,UAAI,CAAC,SAAS;AACb,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,UAAI,IAAI,KAAM,OAAO;AACpB,kBAAM,yBAAW,KACd,KAAK,mCAAoB;AAAA,UAC1B,MAAM,aAAa;AAAA,UACnB,QAAQ,IAAI,KAAM;AAAA,UAClB,MAAM,EAAE,UAAU,QAAQ,SAAS;AAAA,UACnC,WAAW,EAAE,OAAO,IAAI,KAAM,OAAO,OAAO,MAAM,aAAa,KAAK;AAAA,QACrE,CAA2B,CAAC;AAAA,MAC9B;AACA,iBAAO,6BAAe,kBAAK,IAAI,kBAAkB,2BAA2B;AAAA,IAC7E;AAAA,IAEA,UAAU,OAAO,QAA6C;AAC7D,YAAM,SAAS,IAAI,KAAM;AACzB,YAAM,MAAM,WAAW,MAAM;AAE7B,YAAM,QAAQ,IAAI,KAAK,WAAW;AAClC,gBAAM,yBAAW,KACd;AAAA,QACD,WAAW;AAAA,QACX,EAAE,OAAO;AAAA,QACT,UAAU,SAAY,EAAE,WAAW,MAAM,IAAI;AAAA,MAC9C,CAAC;AAEF,aAAO,SAAS;AAAA,QACf,EAAE,QAAQ,mBAAmB,aAAa,gCAAgC;AAAA,QAC1E;AAAA,UACC,QAAQ;AAAA,UACR,SAAS,cAAc,oBAAoB,MAAM,CAAC;AAAA,QACnD;AAAA,MACD;AAAA,IACD;AAAA;AAAA;AAAA;AAAA,IAKA,UAAU,OAAO,QAA6C;AAC7D,YAAM,OAAO,MAAM,MAAM,SAAS,IAAI,KAAM,EAAE;AAC9C,UAAI,CAAC,KAAM,YAAO,6BAAe,kBAAK,WAAW,aAAa,gBAAgB;AAE9E,YAAM,cAAc,MAAM,SAAS,WAAW,IAAI,KAAM,EAAE;AAC1D,YAAM,SAAS;AAAA,QACd,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,QACnC,SAAS,UAAU,MAAM,IAAI,KAAM,aAAa;AAAA,QAChD,UAAU,YAAY,IAAI,CAAC,OAAO;AAAA,UACjC,IAAI,EAAE;AAAA,UACN,WAAW,EAAE;AAAA,UACb,WAAW,EAAE;AAAA,UACb,eAAW,0BAAY,EAAE,SAAS;AAAA,UAClC,eAAW,0BAAY,EAAE,SAAS;AAAA,QACnC,EAAE;AAAA,QACF,UAAU;AAAA,UACT,YAAY,KAAK,eAAe;AAAA,UAChC,eAAe,KAAK,oBAAoB;AAAA,QACzC;AAAA,MACD;AAKA,YAAM,eAAe,OAAO,0BAA0B,CAAC;AACvD,UAAI,aAAa,SAAS,GAAG;AAC5B,cAAM,UAAmC,CAAC;AAC1C,mBAAW,KAAK,cAAc;AAC7B,cAAI;AACH,oBAAQ,EAAE,IAAI,IAAI,MAAM,EAAE,QAAQ,IAAI,KAAM,EAAE;AAAA,UAC/C,QAAQ;AACP,oBAAQ,EAAE,IAAI,IAAI;AAAA,UACnB;AAAA,QACD;AACA,QAAC,OAAmC,SAAS,IAAI;AAAA,MAClD;AAEA,iBAAO,6BAAe,kBAAK,IAAI,eAAe,sBAAsB,MAAM;AAAA,IAC3E;AAAA,EACD;AACD;;;AEvbA,IAAAE,sBAAyE;AACzE,IAAAC,uBAAgB;AAGhB,IAAAC,eAAoE;AAGpE,IAAAC,iBAAmC;;;ACE5B,IAAM,qBAAN,MAAyB;AAAA,EAC/B,YAAoB,OAAsB;AAAtB;AAAA,EAAuB;AAAA,EAAvB;AAAA;AAAA;AAAA,EAIpB,MAAM,YAAY,WAAmB,WAAmC;AACvE,UAAM,OAAO,MAAM,KAAK,MAAM;AAAA,MAC7B;AAAA;AAAA;AAAA;AAAA,MAIA,CAAC,WAAW,SAAS;AAAA,IACtB;AACA,WAAO,KAAK,SAAS;AAAA,EACtB;AAAA;AAAA;AAAA,EAIA,mBAAyB;AACxB,SAAK,KAAK,MACR,MAAM,+DAA+D,EACrE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EACjB;AACD;;;ADHA,IAAM,iBAAiB;AACvB,IAAM,kBAAkB;AACxB,IAAM,iBAAiB;AACvB,IAAM,eAAe;AAiBd,SAAS,sBAAsB,OAA4B;AACjE,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,SAAO,qBAAAC,QAAI;AAAA,IACV,EAAE,KAAK,MAAM,QAAQ,KAAK,KAAK,KAAK,MAAM,KAAK,KAAK,cAAc,KAAK,MAAM,SAAS;AAAA,IACtF,MAAM;AAAA,IACN,EAAE,WAAW,SAAS,OAAO,MAAM,MAAM;AAAA,EAC1C;AACD;AAUA,IAAI,gBAAgE;AACpE,IAAI,mBAAyC;AAC7C,IAAI,sBAAsB;AAC1B,IAAM,oBAAoB,KAAK,KAAK;AACpC,IAAM,4BAA4B,KAAK;AACvC,IAAM,8BAA8B;AAEpC,SAAS,mBAAkC;AAE1C,MAAI,iBAAkB,QAAO;AAC7B,wBAAsB,KAAK,IAAI;AAC/B,sBAAoB,YAAY;AAC/B,QAAI;AACH,YAAM,MAAM,MAAM,MAAM,gBAAgB,EAAE,QAAQ,YAAY,QAAQ,2BAA2B,EAAE,CAAC;AACpG,YAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,UAAI,MAAM,QAAQ,KAAK,IAAI,EAAG,iBAAgB,EAAE,MAAM,KAAK,MAAM,WAAW,KAAK,IAAI,EAAE;AAAA,IACxF,QAAQ;AAAA,IAER,UAAE;AACD,yBAAmB;AAAA,IACpB;AAAA,EACD,GAAG;AACH,SAAO;AACR;AAEA,eAAe,kBAAkB,KAAwC;AACxE,QAAM,OAAO,MAAM,eAAe,KAAK,KAAK,CAAC,MAAM,EAAE,QAAQ,GAAG,KAAK;AACrE,QAAM,aAAa,MAAM,CAAC,CAAC,iBAAiB,KAAK,IAAI,IAAI,cAAc,YAAY;AAEnF,MAAI,MAAM,KAAK;AAIf,OAAK,CAAC,OAAO,CAAC,WAAW,MAAM,KAAK,IAAI,IAAI,uBAAuB,2BAA2B;AAC7F,UAAM,iBAAiB;AACvB,UAAM,KAAK;AAAA,EACZ;AACA,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AACH,eAAO,qCAAgB,EAAE,KAAK,KAA8B,QAAQ,MAAM,CAAC;AAAA,EAC5E,QAAQ;AACP,WAAO;AAAA,EACR;AACD;AAKA,eAAsB,mBACrB,SACA,MAC8B;AAC9B,MAAI,KAAK,UAAU,WAAW,EAAG,QAAO;AACxC,QAAM,UAAU,qBAAAA,QAAI,OAAO,SAAS,EAAE,UAAU,KAAK,CAAC;AACtD,MAAI,CAAC,WAAW,OAAO,YAAY,SAAU,QAAO;AACpD,QAAM,MAAM,QAAQ,OAAO;AAC3B,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,MAAM,MAAM,kBAAkB,GAAG;AACvC,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AACH,UAAM,UAAU,qBAAAA,QAAI,OAAO,SAAS,KAAK;AAAA,MACxC,YAAY,CAAC,OAAO;AAAA,MACpB,QAAQ;AAAA;AAAA;AAAA;AAAA,MAIR,UAAU,KAAK;AAAA,IAChB,CAAC;AAKD,QAAI,KAAK,UAAU,UAAa,QAAQ,UAAU,KAAK,MAAO,QAAO;AACrE,WAAO;AAAA,EACR,QAAQ;AACP,WAAO;AAAA,EACR;AACD;AAEA,IAAM,qBAAqB,CAAC,MAC3B,EAAE,mBAAmB,QAAQ,EAAE,mBAAmB;AAU5C,SAAS,gBAAgB,OAAsB,QAAqB,KAAgB;AAC1F,QAAM,QAAQ,IAAI,UAAU,KAAK;AACjC,QAAM,WAAW,IAAI,aAAa,OAAO,OAAO,QAAQ;AACxD,QAAM,cAAc,IAAI,gBAAgB,OAAO,OAAO,QAAQ;AAC9D,QAAM,iBAAiB,IAAI,mBAAmB,KAAK;AAmBnD,QAAM,sBAAsB,OAC3B,KACA,UAMA,UACA,SACmB;AACnB,QAAI,CAAC,IAAK;AACV,UAAM,QAAQ,IAAI,KAAK,WAAW;AAClC,UAAM,UAAU,UAAU,SAAY,EAAE,WAAW,MAAM,IAAI;AAC7D,UAAM,QAAQ,SAAS,OAAO,CAAC,EAAE,YAAY,IAAI,SAAS,MAAM,CAAC;AACjE,UAAM,YAAY,EAAE,OAAO,KAAK,OAAO,OAAO,MAAM,aAAa,KAAK;AAMtE,QAAI,SAAS,6BAA6B,KAAK,OAAO;AACrD,gBAAM,yBAAW,IAAI;AAAA,QACpB;AAAA,QACA;AAAA,UACC,MAAM,aAAa;AAAA,UACnB,QAAQ,KAAK;AAAA,UACb,MAAM,EAAE,UAAU,MAAM;AAAA,UACxB;AAAA,QACD;AAAA,QACA;AAAA,MACD,CAAC;AAAA,IACF;AAEA,QAAI,SAAS,UAAU;AACtB,gBAAM,yBAAW,IAAI;AAAA,QACpB,WAAW;AAAA,QACX;AAAA,UACC,QAAQ,SAAS;AAAA,UACjB,OAAO,KAAK;AAAA,UACZ,WAAW,KAAK;AAAA,UAChB,UAAU,KAAK;AAAA,UACf,aAAa;AAAA,QACd;AAAA,QACA;AAAA,MACD,CAAC;AACD,UAAI,KAAK,OAAO;AACf,kBAAM,yBAAW,IAAI;AAAA,UACpB;AAAA,UACA;AAAA,YACC,MAAM,aAAa;AAAA,YACnB,QAAQ,KAAK;AAAA,YACb,MAAM,EAAE,UAAU,OAAO,SAAS,OAAO,WAAW,WAAW;AAAA,YAC/D;AAAA,UACD;AAAA,UACA;AAAA,QACD,CAAC;AAAA,MACF;AACA;AAAA,IACD;AAEA,QAAI,SAAS,qBAAqB,YAAY,KAAK,OAAO;AACzD,gBAAM,yBAAW,IAAI;AAAA,QACpB;AAAA,QACA;AAAA,UACC,MAAM,aAAa;AAAA,UACnB,QAAQ,KAAK;AAAA,UACb,MAAM,EAAE,UAAU,MAAM;AAAA,UACxB;AAAA,QACD;AAAA,QACA;AAAA,MACD,CAAC;AAAA,IACF;AAAA,EACD;AAKA,QAAM,qBAAqB,OAC1B,KACA,WACuB;AACvB,UAAM,OAAO,YAAY,GAAG;AAC5B,QAAI,CAAC,OAAO,OAAO;AAClB,iBAAO,6BAAe,kBAAK,aAAa,qBAAqB,kCAAkC;AAAA,IAChG;AAIA,QAAI,CAAC,OAAO,eAAe;AAC1B,iBAAO,6BAAe,kBAAK,aAAa,qBAAqB,qCAAqC;AAAA,IACnG;AACA,UAAM,kBAAkB,mBAAmB,OAAO,KAAK;AACvD,QAAI,CAAC,iBAAiB;AACrB,iBAAO,6BAAe,kBAAK,aAAa,qBAAqB,uCAAuC;AAAA,IACrG;AAEA,UAAM,WAAW,MAAM,MAAM,iBAAiB,iBAAiB,SAAS,OAAO,OAAO,EAAE;AACxF,QAAI,CAAC,UAAU;AACd,YAAM,YAAY,WAAW;AAAA,QAC5B,QAAQ;AAAA,QACR,gBAAgB;AAAA,QAChB,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,eAAe;AAAA,QACf,GAAG;AAAA,MACJ,CAAC;AACD,iBAAO,6BAAe,kBAAK,cAAc,gBAAgB,oBAAoB;AAAA,IAC9E;AAEA,UAAM,WAAW,MAAM,MAAM,SAAS,SAAS,EAAE;AACjD,QAAI,CAAC,UAAU;AACd,iBAAO,6BAAe,kBAAK,cAAc,gBAAgB,oBAAoB;AAAA,IAC9E;AAEA,UAAM,oBAAoB,KAAK,UAAU,SAAS,QAAQ;AAE1D,UAAM,EAAE,aAAa,cAAc,IAAI,IAAI,eAAe,SAAS,IAAI,QAAQ,EAAE,aAAa,QAAQ,CAAC;AACvG,UAAM,SAAS,OAAO,SAAS,IAAI,cAAc,mBAAmB,YAAY,GAAG,KAAK,IAAI;AAC5F,UAAM,YAAY,WAAW;AAAA,MAC5B,QAAQ,SAAS;AAAA,MACjB,gBAAgB;AAAA,MAChB,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,GAAG;AAAA,IACJ,CAAC;AAED,WAAO,SAAS;AAAA,MACf;AAAA,QACC,QAAQ;AAAA,QACR,aAAa;AAAA,QACb,QAAQ;AAAA,UACP,QAAQ,EAAE,QAAQ,aAAa,SAAS,aAAa;AAAA,UACrD,MAAM,UAAU,QAAQ;AAAA,QACzB;AAAA,MACD;AAAA,MACA;AAAA,QACC,QAAQ;AAAA,QACR,SAAS,cAAc;AAAA,UACtB,GAAG,iBAAiB,aAAa,cAAc,MAAM;AAAA;AAAA,UAErD;AAAA,QACD,CAAC;AAAA,MACF;AAAA,IACD;AAAA,EACD;AAEA,SAAO;AAAA,IACN,YAAY,OAAO,SAA8C;AAChE,YAAM,SAAS,OAAO;AACtB,UAAI,CAAC,QAAQ;AACZ,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAOA,YAAM,YAAQ,iCAAY,EAAE,EAAE,SAAS,KAAK;AAE5C,YAAM,SAAS,IAAI,gBAAgB;AAAA,QAClC,WAAW,OAAO;AAAA,QAClB,cAAc,OAAO;AAAA,QACrB,eAAe;AAAA,QACf,OAAO;AAAA,QACP;AAAA,MACD,CAAC;AAED,YAAM,MAAM,gDAAgD,MAAM;AAElE,YAAM,SAAU,OAAO,iBAAiB,QAAQ,IAAI,UAAU,MAAM,eAAgB,aAAa;AACjG,aAAO,SAAS;AAAA,QACf;AAAA,UACC,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ,EAAE,IAAI;AAAA,QACf;AAAA,QACA;AAAA,UACC,QAAQ;AAAA;AAAA;AAAA,UAGR,SAAS,cAAc;AAAA,YACtB,eAAe,KAAK,gDAAgD,MAAM;AAAA,UAC3E,CAAC;AAAA,QACF;AAAA,MACD;AAAA,IACD;AAAA,IAEA,gBAAgB,OAAO,QAA6C;AACnE,YAAM,SAAS,OAAO;AACtB,UAAI,CAAC,QAAQ;AACZ,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,YAAM,MAAM,IAAI,IAAI,IAAI,QAAQ,GAAG;AACnC,YAAM,OAAO,IAAI,aAAa,IAAI,MAAM;AAExC,UAAI,CAAC,MAAM;AACV,mBAAO,6BAAe,kBAAK,aAAa,qBAAqB,cAAc;AAAA,MAC5E;AAIA,YAAM,gBAAgB,IAAI,aAAa,IAAI,OAAO,KAAK;AACvD,YAAM,eAAe,IAAI,QAAQ,QAAQ,IAAI,QAAQ,KAAK;AAC1D,YAAM,gBAAgB,aAAa,MAAM,+BAA+B,IAAI,CAAC,KAAK;AAClF,UACC,CAAC,iBACD,CAAC,iBACD,KAAC,gCAAkB,OAAO,KAAK,aAAa,GAAG,OAAO,KAAK,aAAa,CAAC,GACxE;AACD,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,YAAM,WAAW,MAAM,MAAM,uCAAuC;AAAA,QACnE,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,QAC/D,MAAM,IAAI,gBAAgB;AAAA,UACzB;AAAA,UACA,WAAW,OAAO;AAAA,UAClB,eAAe,OAAO;AAAA,UACtB,cAAc,OAAO;AAAA,UACrB,YAAY;AAAA,QACb,CAAC;AAAA,MACF,CAAC;AAED,YAAM,YAAa,MAAM,SAAS,KAAK;AACvC,UAAI,CAAC,UAAU,UAAU;AACxB,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAMA,YAAM,UAAU,KAAK;AAAA,QACpB,OAAO,KAAK,UAAU,SAAS,MAAM,GAAG,EAAE,CAAC,KAAK,IAAI,WAAW,EAAE,SAAS;AAAA,MAC3E;AAEA,UAAI,QAAQ,QAAQ,OAAO,UAAU;AAEpC,mBAAO,6BAAe,kBAAK,aAAa,sBAAsB,+BAA+B;AAAA,MAC9F;AACA,UAAI,QAAQ,QAAQ,iCAAiC,QAAQ,QAAQ,uBAAuB;AAC3F,mBAAO,6BAAe,kBAAK,aAAa,sBAAsB,6BAA6B;AAAA,MAC5F;AACA,UAAI,OAAO,QAAQ,QAAQ,YAAY,QAAQ,MAAM,MAAO,KAAK,IAAI,GAAG;AACvE,mBAAO,6BAAe,kBAAK,aAAa,sBAAsB,qBAAqB;AAAA,MACpF;AAEA,UAAI,CAAC,QAAQ,OAAO;AACnB,mBAAO,6BAAe,kBAAK,aAAa,sBAAsB,4BAA4B;AAAA,MAC3F;AAKA,UAAI,QAAQ,mBAAmB,MAAM;AACpC,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,YAAM,kBAAkB,mBAAmB,QAAQ,KAAK;AACxD,UAAI,CAAC,iBAAiB;AACrB,mBAAO,6BAAe,kBAAK,aAAa,sBAAsB,iCAAiC;AAAA,MAChG;AAEA,YAAM,OAAO,YAAY,GAAG;AAE5B,YAAM,WAAW,MAAM,MAAM,iBAAiB,iBAAiB,UAAU,QAAQ,OAAO,EAAE;AAC1F,UAAI,CAAC,UAAU;AACd,cAAM,YAAY,WAAW;AAAA,UAC5B,QAAQ;AAAA,UACR,gBAAgB;AAAA,UAChB,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,eAAe;AAAA,UACf,GAAG;AAAA,QACJ,CAAC;AACD,mBAAO,6BAAe,kBAAK,cAAc,gBAAgB,oBAAoB;AAAA,MAC9E;AAEA,YAAM,WAAW,MAAM,MAAM,SAAS,SAAS,EAAE;AACjD,UAAI,CAAC,UAAU;AACd,mBAAO,6BAAe,kBAAK,cAAc,gBAAgB,oBAAoB;AAAA,MAC9E;AAEA,YAAM,oBAAoB,KAAK,UAAU,UAAU,QAAQ;AAE3D,YAAM,EAAE,aAAa,cAAc,IAAI,IAAI,eAAe,SAAS,IAAI,QAAQ;AAAA,QAC9E,aAAa;AAAA,MACd,CAAC;AACD,YAAM,SAAS,OAAO,SAAS,IAAI,cAAc,mBAAmB,YAAY,GAAG,KAAK,IAAI;AAC5F,YAAM,YAAY,WAAW;AAAA,QAC5B,QAAQ,SAAS;AAAA,QACjB,gBAAgB;AAAA,QAChB,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,GAAG;AAAA,MACJ,CAAC;AAED,aAAO,SAAS;AAAA,QACf;AAAA,UACC,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ;AAAA,YACP,QAAQ,EAAE,QAAQ,aAAa,SAAS,aAAa;AAAA,YACrD,MAAM,UAAU,QAAQ;AAAA,UACzB;AAAA,QACD;AAAA,QACA;AAAA,UACC,QAAQ;AAAA,UACR,SAAS,cAAc;AAAA,YACtB,GAAG,iBAAiB,aAAa,cAAc,MAAM;AAAA;AAAA,YAErD;AAAA,UACD,CAAC;AAAA,QACF;AAAA,MACD;AAAA,IACD;AAAA,IAEA,WAAW,OAAO,SAA8C;AAC/D,YAAM,QAAQ,OAAO;AACrB,UAAI,CAAC,OAAO;AACX,mBAAO,6BAAe,kBAAK,iBAAiB,kBAAkB,4BAA4B;AAAA,MAC3F;AAEA,YAAM,YAAQ,iCAAY,EAAE,EAAE,SAAS,KAAK;AAC5C,YAAM,SAAS,IAAI,gBAAgB;AAAA,QAClC,WAAW,MAAM;AAAA,QACjB,cAAc,MAAM;AAAA,QACpB,eAAe;AAAA;AAAA;AAAA,QAGf,eAAe;AAAA,QACf,OAAO;AAAA,QACP;AAAA,MACD,CAAC;AACD,YAAM,MAAM,GAAG,cAAc,IAAI,MAAM;AAKvC,aAAO,SAAS;AAAA,QACf;AAAA,UACC,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ,EAAE,IAAI;AAAA,QACf;AAAA,QACA;AAAA,UACC,QAAQ;AAAA,UACR,SAAS,cAAc;AAAA,YACtB,eAAe,KAAK;AAAA,UACrB,CAAC;AAAA,QACF;AAAA,MACD;AAAA,IACD;AAAA,IAEA,eAAe,OAAO,QAA6C;AAClE,YAAM,QAAQ,OAAO;AACrB,UAAI,CAAC,OAAO;AACX,mBAAO,6BAAe,kBAAK,iBAAiB,kBAAkB,4BAA4B;AAAA,MAC3F;AAIA,YAAM,MAAM,IAAI,KAAK,MAAM;AAC3B,YAAM,OACL,OAAO,OAAO,QAAQ,WACnB,IAAI,gBAAgB,GAA6B,IACjD,IAAI,gBAAgB,MAAM,IAAI,QAAQ,KAAK,CAAC;AAEhD,YAAM,OAAO,KAAK,IAAI,MAAM;AAC5B,UAAI,CAAC,MAAM;AACV,mBAAO,6BAAe,kBAAK,aAAa,qBAAqB,cAAc;AAAA,MAC5E;AAIA,YAAM,gBAAgB,KAAK,IAAI,OAAO,KAAK;AAC3C,YAAM,eAAe,IAAI,QAAQ,QAAQ,IAAI,QAAQ,KAAK;AAC1D,YAAM,gBAAgB,aAAa,MAAM,+BAA+B,IAAI,CAAC,KAAK;AAClF,UACC,CAAC,iBACD,CAAC,iBACD,KAAC,gCAAkB,OAAO,KAAK,aAAa,GAAG,OAAO,KAAK,aAAa,CAAC,GACxE;AACD,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,YAAM,WAAW,MAAM,MAAM,iBAAiB;AAAA,QAC7C,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,QAC/D,MAAM,IAAI,gBAAgB;AAAA,UACzB;AAAA,UACA,WAAW,MAAM;AAAA,UACjB,eAAe,sBAAsB,KAAK;AAAA,UAC1C,cAAc,MAAM;AAAA,UACpB,YAAY;AAAA,QACb,CAAC;AAAA,MACF,CAAC;AACD,YAAM,YAAa,MAAM,SAAS,KAAK;AACvC,UAAI,CAAC,UAAU,UAAU;AACxB,mBAAO,6BAAe,kBAAK,aAAa,qBAAqB,6BAA6B;AAAA,MAC3F;AAIA,YAAM,SAAS,MAAM,mBAAmB,UAAU,UAAU,EAAE,WAAW,CAAC,MAAM,QAAQ,EAAE,CAAC;AAC3F,UAAI,CAAC,QAAQ;AACZ,mBAAO,6BAAe,kBAAK,aAAa,qBAAqB,8BAA8B;AAAA,MAC5F;AACA,aAAO,mBAAmB,KAAK;AAAA,QAC9B,OAAO,OAAO;AAAA,QACd,KAAK,OAAO;AAAA,QACZ,eAAe,mBAAmB,MAAM;AAAA,MACzC,CAAC;AAAA,IACF;AAAA,IAEA,aAAa,OAAO,QAA6C;AAChE,YAAM,QAAQ,OAAO;AACrB,UAAI,CAAC,OAAO;AACX,mBAAO,6BAAe,kBAAK,iBAAiB,kBAAkB,4BAA4B;AAAA,MAC3F;AACA,YAAM,OAAO,IAAI,KAAK,MAAM;AAC5B,YAAM,gBAAgB,MAAM;AAC5B,UAAI,CAAC,eAAe;AACnB,mBAAO,6BAAe,kBAAK,aAAa,qBAAqB,uBAAuB;AAAA,MACrF;AAMA,YAAM,YACL,MAAM,mBAAmB,MAAM,gBAAgB,SAC5C,MAAM,kBACN,CAAC,MAAM,QAAQ;AACnB,YAAM,SAAS,MAAM,mBAAmB,eAAe,EAAE,WAAW,OAAO,MAAM,MAAM,CAAC;AACxF,UAAI,CAAC,QAAQ;AACZ,cAAM,OAAO,YAAY,GAAG;AAC5B,cAAM,YAAY,WAAW;AAAA,UAC5B,QAAQ;AAAA,UACR,gBAAgB;AAAA,UAChB,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,eAAe;AAAA,UACf,GAAG;AAAA,QACJ,CAAC;AACD,mBAAO,6BAAe,kBAAK,cAAc,qBAAqB,8BAA8B;AAAA,MAC7F;AAOA,YAAM,gBAAY,gCAAW,QAAQ,EAAE,OAAO,aAAa,EAAE,OAAO,KAAK;AACzE,YAAM,QAAQ,OAAO,OAAO,QAAQ,WAAW,OAAO,MAAM,MAAO,KAAK,IAAI,IAAI,IAAI,KAAK;AACzF,YAAM,WAAW,MAAM,eAAe,YAAY,WAAW,IAAI,KAAK,KAAK,CAAC;AAC5E,UAAI,CAAC,UAAU;AACd,cAAM,OAAO,YAAY,GAAG;AAC5B,cAAM,YAAY,WAAW;AAAA,UAC5B,QAAQ;AAAA,UACR,gBAAgB,OAAO,SAAS;AAAA,UAChC,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,eAAe;AAAA,UACf,GAAG;AAAA,QACJ,CAAC;AACD,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AACA,qBAAe,iBAAiB;AAEhC,aAAO,mBAAmB,KAAK;AAAA,QAC9B,OAAO,OAAO;AAAA,QACd,KAAK,OAAO;AAAA,QACZ,eAAe,mBAAmB,MAAM;AAAA,MACzC,CAAC;AAAA,IACF;AAAA,EACD;AACD;;;A1B/oBO,SAAS,gBACf,OACA,QACA,KACoB;AACpB,QAAM,OAAO,eAAe,OAAO,QAAQ,GAAG;AAC9C,QAAM,OAAO,eAAe,OAAO,QAAQ,GAAG;AAC9C,QAAM,QAAQ,gBAAgB,OAAO,QAAQ,GAAG;AAChD,QAAM,MAAM,cAAc,OAAO,QAAQ,OAAO,WAAW,YAAY,GAAG;AAK1E,QAAM,aAAyB,OAAO,sBACnC,sCACA,CAAC,MAAM,SAAS,KAAK;AAIxB,QAAM,cAA0B,CAAC,MAAM,SAAS,KAAK;AAGrD,QAAM,UAAU,CAAC,UAChB,mBAAmB,OAAO,OAAO,OAAO,SAAS,KAAK;AACvD,QAAM,YAAY,CAAC,UAClB,wBAAwB,OAAO,OAAO,OAAO,SAAS,KAAK;AAI5D,QAAM,IAAI,CAAC,IAAiB,QAAgB,SAAiB,aAA4C;AACxG,UAAM,IAAI,OAAO,SAAS,EAAE;AAC5B,QAAI,CAAC,EAAG,QAAO,CAAC,QAAQ,MAAM,GAAG,QAAQ;AACzC,QAAI,OAAO,MAAM,SAAU,QAAO,CAAC,QAAQ,GAAG,GAAG,QAAQ;AACzD,WAAO,CAAC,EAAE,UAAU,QAAQ,EAAE,QAAQ,MAAM,GAAG,QAAQ;AAAA,EACxD;AAEA,QAAM,SAA4B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAcjC;AAAA,MAAE;AAAA,MAAa;AAAA,MAAO;AAAA,MAAmB,gBACxC,8BAAe,mBAAK,IAAI,kBAAkB,kCAAkC;AAAA,QAC3E,WAAW,CAAC,GAAG,OAAO,SAAS;AAAA,MAChC,CAAC;AAAA,IACF;AAAA;AAAA,IAGA,EAAE,YAAY,QAAQ,kBAAkB,QAAQ,UAAU,OAAG,6BAAS,cAAc,GAAG,KAAK,QAAQ;AAAA,IACpG,EAAE,SAAS,QAAQ,eAAe,QAAQ,OAAO,OAAG,6BAAS,WAAW,GAAG,UAAU,OAAO,GAAG,KAAK,KAAK;AAAA;AAAA,IAGzG,EAAE,WAAW,QAAQ,qBAAiB,6BAAS,aAAa,GAAG,KAAK,OAAO;AAAA;AAAA,IAG3E,EAAE,kBAAkB,QAAQ,sBAAsB,QAAQ,QAAQ,OAAG,6BAAS,oBAAoB,GAAG,UAAU,QAAQ,GAAG,KAAK,cAAc;AAAA,IAC7I,EAAE,iBAAiB,QAAQ,qBAAqB,QAAQ,OAAO,OAAG,6BAAS,mBAAmB,GAAG,KAAK,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOnH,EAAE,eAAe,QAAQ,gBAAgB,QAAQ,QAAQ,GAAG,wCAAgB,6BAAS,YAAY,GAAG,KAAK,MAAM;AAAA,IAC/G,EAAE,oBAAoB,OAAO,2BAA2B,oCAAgB,KAAK,gBAAgB;AAAA;AAAA,IAG7F,EAAE,UAAU,QAAQ,gBAAgB,qCAAa,6BAAS,aAAa,GAAG,KAAK,MAAM;AAAA;AAAA,IAGrF,EAAE,MAAM,OAAO,UAAU,iCAAa,KAAK,EAAE;AAAA,IAC7C,EAAE,iBAAiB,OAAO,kBAAkB,iCAAa,gBAAY,6BAAS,mBAAmB,GAAG,KAAK,aAAa;AAAA,IACtH,EAAE,qBAAqB,OAAO,sBAAsB,iCAAa,gBAAY,6BAAS,uBAAuB,GAAG,KAAK,iBAAiB;AAAA,IACtI,EAAE,eAAe,OAAO,gBAAgB,iCAAa,gBAAY,6BAAS,iBAAiB,GAAG,KAAK,WAAW;AAAA,IAC9G,EAAE,eAAe,OAAO,gBAAgB,iCAAa,gBAAY,6BAAS,iBAAiB,GAAG,KAAK,WAAW;AAAA,IAC9G,EAAE,kBAAkB,OAAO,mBAAmB,qCAAa,6BAAS,oBAAoB,GAAG,KAAK,cAAc;AAAA;AAAA;AAAA,IAG9G,EAAE,eAAe,UAAU,yBAAyB,iCAAa,KAAK,WAAW;AAAA,IAEjF,EAAE,YAAY,UAAU,UAAU,iCAAa,YAAY,KAAK,QAAQ;AAAA,IACxE,EAAE,YAAY,OAAO,iBAAiB,iCAAa,KAAK,QAAQ;AAAA;AAAA;AAAA;AAAA,IAKhE,EAAE,gBAAgB,OAAO,uBAAuB,iCAAa,KAAK,YAAY;AAAA,IAC9E,EAAE,gBAAgB,OAAO,kBAAkB,iCAAa,KAAK,YAAY;AAAA,IACzE,EAAE,0BAA0B,UAAU,yBAAyB,iCAAa,KAAK,sBAAsB;AAAA,IACvG,EAAE,oBAAoB,UAAU,sBAAsB,iCAAa,KAAK,gBAAgB;AAAA;AAAA;AAAA,IAIxF,EAAE,YAAY,QAAQ,mBAAmB,iCAAa,mBAAmB,qCAAiB,IAAI,KAAK;AAAA;AAAA;AAAA,IAGnG,EAAE,aAAa,QAAQ,oBAAoB,QAAQ,WAAW,GAAG,oCAAgB,mBAAmB,yCAAiB,6BAAS,cAAc,GAAG,IAAI,MAAM;AAAA,IACzJ,EAAE,cAAc,QAAQ,qBAAqB,iCAAa,mBAAmB,yCAAiB,6BAAS,cAAc,GAAG,IAAI,OAAO;AAAA,IACnI,EAAE,kBAAkB,QAAQ,0BAA0B,iCAAa,mBAAmB,yCAAiB,6BAAS,cAAc,GAAG,IAAI,qBAAqB;AAAA,EAC3J;AAEA,MAAI,OAAO,UAAU,SAAS,QAAQ,GAAG;AACxC,WAAO;AAAA,MACN,CAAC,OAAO,gBAAgB,MAAM,UAAU;AAAA;AAAA;AAAA,MAGxC,CAAC,OAAO,yBAAyB,QAAQ,OAAO,GAAG,MAAM,cAAc;AAAA,IACxE;AAAA,EACD;AAEA,MAAI,OAAO,UAAU,SAAS,OAAO,GAAG;AACvC,WAAO;AAAA;AAAA;AAAA;AAAA,MAIN,CAAC,OAAO,eAAe,MAAM,SAAS;AAAA,MACtC,CAAC,QAAQ,wBAAwB,QAAQ,OAAO,GAAG,MAAM,aAAa;AAAA;AAAA;AAAA,MAGtE,CAAC,QAAQ,sBAAsB,QAAQ,OAAO,OAAG,6BAAS,iBAAiB,GAAG,MAAM,WAAW;AAAA,IAChG;AAAA,EACD;AAEA,SAAO;AACR;;;A4BrKA,IAAAC,gBAA6E;AAG7E,IAAM,SAAS;AAMR,SAAS,0BAA0B,QAA0C;AACnF,QAAM,WAAgC,CAAC;AACvC,QAAM,SAAS,OAAO,aAAa;AAEnC,QAAM,oBAAgB,qCAAsB,MAAM;AAClD,MAAI,kBAAkB,aAAa;AAClC,aAAS,KAAK;AAAA,MACb,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,SAAS,8BAA8B,+BAAiB,oBAAoB,OAAO,MAAM;AAAA,IAC1F,CAAC;AAAA,EACF,WAAW,kBAAkB,eAAe;AAC3C,aAAS,KAAK;AAAA,MACb,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,SAAS;AAAA,IACV,CAAC;AAAA,EACF;AAIA,MAAI,OAAO,kBAAkB,OAAO;AACnC,aAAS,KAAK;AAAA,MACb,QAAQ;AAAA,MACR,UAAU,QAAQ,IAAI,UAAU,MAAM,eAAe,UAAU;AAAA,MAC/D,SAAS;AAAA,IACV,CAAC;AAAA,EACF;AAKA,MAAI,OAAO,QAAQ;AAClB,UAAM,eAAe,OAAO,OAAO,gBAAgB;AACnD,QAAI,CAAC,gBAAgB,CAAC,OAAO,OAAO,YAAY,CAAC,OAAO,OAAO,aAAa;AAC3E,eAAS,KAAK;AAAA,QACb,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,SAAS;AAAA,MACV,CAAC;AAAA,IACF,WAAW,iCAAmB,KAAK,YAAY,GAAG;AACjD,eAAS,KAAK;AAAA,QACb,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,SAAS;AAAA,MACV,CAAC;AAAA,IACF;AAAA,EACD;AAMA,MAAI,OAAO,OAAO;AACjB,UAAM,IAAI,OAAO;AACjB,QAAI,CAAC,EAAE,YAAY,CAAC,EAAE,UAAU,CAAC,EAAE,SAAS,CAAC,EAAE,cAAc,CAAC,EAAE,aAAa;AAC5E,eAAS,KAAK;AAAA,QACb,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,SAAS;AAAA,MACV,CAAC;AAAA,IACF,WAAW,CAAC,qCAAqC,KAAK,EAAE,UAAU,GAAG;AACpE,eAAS,KAAK;AAAA,QACb,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,SAAS;AAAA,MACV,CAAC;AAAA,IACF;AAKA,QAAI,EAAE,iBAAiB,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,KAAK,MAAM,MAAM,GAAG,SAAS,GAAG,CAAC,GAAG;AACjF,eAAS,KAAK;AAAA,QACb,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,SAAS;AAAA,MACV,CAAC;AAAA,IACF;AAAA,EACD;AAKA,MAAI,OAAO,OAAO,CAAC,OAAO,cAAc;AACvC,aAAS,KAAK;AAAA,MACb,QAAQ;AAAA,MACR,UAAU,QAAQ,IAAI,UAAU,MAAM,eAAe,UAAU;AAAA,MAC/D,SAAS;AAAA,IACV,CAAC;AAAA,EACF;AAIA,MAAI,OAAO,gBAAgB,CAAC,oBAAoB,KAAK,OAAO,YAAY,GAAG;AAC1E,aAAS,KAAK;AAAA,MACb,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,SAAS;AAAA,IACV,CAAC;AAAA,EACF;AAEA,SAAO;AACR;AAQO,SAAS,mBAAmB,QAA2B;AAC7D,QAAM,eAAe,QAAQ,IAAI,UAAU,MAAM;AACjD,QAAM,WAAW,0BAA0B,MAAM;AACjD,QAAM,SAAS,SAAS,OAAO,CAAC,MAAM,EAAE,aAAa,OAAO;AAE5D,MAAI,gBAAgB,OAAO,SAAS,GAAG;AACtC,UAAM,IAAI;AAAA,MACT,iCAA4B,OAAO,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,IAGpE;AAAA,EACD;AAEA,MAAI,cAAc;AACjB,eAAW,KAAK,SAAU,SAAQ,KAAK,UAAU,EAAE,OAAO,EAAE;AAAA,EAC7D,OAAO;AACN,eAAW,KAAK,QAAQ;AACvB,cAAQ,KAAK,UAAU,EAAE,OAAO,uDAAkD;AAAA,IACnF;AAAA,EACD;AACD;;;AClIO,SAAS,YAAY,OAAsB,QAAiC;AAClF,QAAM,QAAQ,IAAI,UAAU,KAAK;AACjC,QAAM,WAAW,IAAI,aAAa,KAAK;AAEvC,SAAO,OAAO,KAAK,SAAS;AAC3B,UAAM,QAAQ,aAAa,IAAI,OAAO;AACtC,QAAI,CAAC,OAAO;AACX,aAAO,KAAK;AAAA,IACb;AAEA,UAAM,UAAU,YAAY,OAAO,MAAM;AACzC,QAAI,CAAC,WAAW,QAAQ,SAAS,UAAU;AAC1C,aAAO,KAAK;AAAA,IACb;AAQA,QAAI,QAAQ,OAAO,CAAE,MAAM,SAAS,WAAW,QAAQ,GAAG,GAAI;AAC7D,aAAO,KAAK;AAAA,IACb;AAEA,UAAM,OAAO,MAAM,MAAM,SAAS,QAAQ,GAAG;AAC7C,QAAI,CAAC,QAAQ,KAAK,aAAa,KAAK,WAAW;AAC9C,aAAO,KAAK;AAAA,IACb;AAEA,WAAO,OAAO,KAAK;AAAA,MAClB,MAAM;AAAA,QACL,GAAG;AAAA,QACH,aAAa,QAAQ,eAAe;AAAA,QACpC,eAAe,QAAQ,iBAAiB;AAAA,QACxC,YAAa,QAA2B,cAAc;AAAA;AAAA;AAAA,QAGtD,KAAK,QAAQ,OAAO;AAAA,MACrB;AAAA,IACD,CAAC;AAED,WAAO,KAAK;AAAA,EACb;AACD;AAEA,SAAS,aAAa,SAAiC;AACtD,QAAM,OAAO,QAAQ,QAAQ,IAAI,eAAe;AAChD,MAAI,MAAM,WAAW,SAAS,GAAG;AAChC,WAAO,KAAK,MAAM,CAAC;AAAA,EACpB;AAEA,QAAM,SAAS,QAAQ,QAAQ,IAAI,QAAQ,KAAK;AAChD,QAAM,QAAQ,OAAO,MAAM,gCAAgC;AAC3D,SAAO,QAAQ,CAAC,KAAK;AACtB;;;ACjEA,IAAAC,gBAAyD;AAoBlD,SAAS,eAAe,MAA4B;AAC1D,SAAO;AAAA,IACN,GAAG,UAAU,IAAI;AAAA,IACjB,WAAW,QAAQ,KAAK,SAAS;AAAA,IACjC,WAAW,KAAK,YAAY,KAAK,UAAU,YAAY,IAAI;AAAA,IAC3D,WAAW,KAAK,UAAU,YAAY;AAAA,EACvC;AACD;AAOO,SAAS,mBAAmB,MAAoC;AACtE,QAAM,OAAO,KAAK,MAAM,KAAK,MAAM,SAAS,CAAC;AAG7C,QAAM,aACL,KAAK,WAAW,WACb,kCAAmB,KAAK,gBAAgB,KAAK,UAAU,YAAY,GAAG,KAAK,EAAE,IAC7E;AACJ,SAAO,EAAE,OAAO,KAAK,MAAM,IAAI,cAAc,GAAG,WAAW;AAC5D;AAEA,IAAM,eAAe;AACrB,IAAM,OAAO,CAAC,QAA0B,IAAI,KAAK,SAAS,IAAI,KAAK;AACnE,IAAM,YAAY,UAAM,8BAAe,mBAAK,WAAW,aAAa,cAAc;AAKlF,SAAS,gBAAgB,OAA2D;AACnF,QAAM,QAAQ,IAAI,UAAU,KAAK;AACjC,QAAM,WAAW,IAAI,aAAa,KAAK;AACvC,QAAM,SAAS,IAAI,gBAAgB,KAAK;AAExC,QAAM,eACL,CAAC,cACD,OAAO,QAAQ;AACd,UAAM,KAAK,MAAM,MAAM,aAAa,KAAK,GAAG,GAAG,SAAS;AACxD,QAAI,CAAC,GAAI,QAAO,UAAU;AAC1B,UAAM,OAAO,MAAM,MAAM,SAAS,KAAK,GAAG,CAAC;AAC3C,WAAO,WACJ;AAAA,MACA,mBAAK;AAAA,MACL,YAAY,mBAAmB;AAAA,MAC/B,YAAY,mBAAmB;AAAA,MAC/B,eAAe,IAAI;AAAA,IACpB,IACC,UAAU;AAAA,EACd;AAED,SAAO;AAAA,IACN;AAAA,MACC;AAAA,MACA;AAAA;AAAA;AAAA;AAAA,MAIA,OAAO,QAAQ;AACd,cAAM,SAAS,IAAI,IAAI,IAAI,QAAQ,GAAG,EAAE;AACxC,cAAMC,SAAQ,OAAO,IAAI,OAAO,GAAG,KAAK,EAAE,YAAY;AACtD,YAAIA,QAAO;AACV,gBAAM,OAAO,MAAM,MAAM,YAAYA,MAAK;AAC1C,iBAAO,WAAO,8BAAe,mBAAK,IAAI,QAAQ,QAAQ,eAAe,IAAI,CAAC,IAAI,UAAU;AAAA,QACzF;AAEA,cAAM,WAAW,OAAO,OAAO,IAAI,OAAO,KAAK,EAAE;AACjD,cAAM,QAAQ,OAAO,SAAS,QAAQ,IACnC,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,QAAQ,GAAG,CAAC,GAAG,GAAG,IAC/C;AACH,cAAM,cAAc,OAAO,IAAI,QAAQ;AACvC,cAAM,SAAS,cAAc,kBAAkB,WAAW,IAAI;AAC9D,YAAI,eAAe,CAAC;AACnB,qBAAO,8BAAe,mBAAK,eAAe,qBAAqB,gBAAgB;AAChF,cAAM,UAAU,OAAO,IAAI,SAAS,MAAM,OAAO,OAAO,IAAI,SAAS,MAAM;AAC3E,cAAM,OAAO,MAAM,MAAM,KAAK;AAAA,UAC7B;AAAA,UACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,UAC3B,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,QAC9B,CAAC;AACD,mBAAO,8BAAe,mBAAK,IAAI,SAAS,SAAS,mBAAmB,IAAI,CAAC;AAAA,MAC1E;AAAA,IACD;AAAA,IACA;AAAA,MACC;AAAA,MACA;AAAA,MACA,OAAO,QAAQ;AAGd,cAAM,OAAO,MAAM,MAAM,yBAAyB,KAAK,GAAG,CAAC;AAC3D,eAAO,WAAO,8BAAe,mBAAK,IAAI,QAAQ,QAAQ,eAAe,IAAI,CAAC,IAAI,UAAU;AAAA,MACzF;AAAA,IACD;AAAA,IACA;AAAA,MACC;AAAA,MACA;AAAA,MACA,OAAO,QAAQ;AACd,YAAI,CAAE,MAAM,MAAM,SAAS,KAAK,GAAG,CAAC,EAAI,QAAO,UAAU;AACzD,cAAM,OAAO,MAAM,SAAS,eAAe,KAAK,GAAG,CAAC;AACpD,mBAAO;AAAA,UACN,mBAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA,KAAK,IAAI,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC;AAAA,QACtC;AAAA,MACD;AAAA,IACD;AAAA;AAAA,IAEA;AAAA,MACC;AAAA,MACA;AAAA,MACA,OAAO,QAAQ;AACd,YAAI,CAAE,MAAM,MAAM,SAAS,KAAK,GAAG,CAAC,EAAI,QAAO,UAAU;AACzD,cAAM,SAAS,aAAa,KAAK,GAAG,CAAC;AACrC,mBAAO,8BAAe,mBAAK,IAAI,oBAAoB,sBAAsB;AAAA,MAC1E;AAAA,IACD;AAAA,IACA;AAAA,MACC;AAAA,MACA;AAAA,MACA,OAAO,QAAQ;AACd,YAAI,CAAE,MAAM,MAAM,SAAS,KAAK,GAAG,CAAC,EAAI,QAAO,UAAU;AACzD,cAAM,SAAS,IAAI,IAAI,IAAI,QAAQ,GAAG,EAAE;AACxC,cAAM,WAAW,OAAO,OAAO,IAAI,OAAO,KAAK,EAAE;AACjD,cAAM,QAAQ,OAAO,SAAS,QAAQ,IACnC,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,QAAQ,GAAG,CAAC,GAAG,GAAG,IAC/C;AACH,cAAM,cAAc,OAAO,IAAI,QAAQ;AACvC,cAAM,SAAS,cAAc,kBAAkB,WAAW,IAAI;AAC9D,YAAI,eAAe,CAAC;AACnB,qBAAO,8BAAe,mBAAK,eAAe,qBAAqB,gBAAgB;AAChF,cAAM,OAAO,MAAM,OAAO,WAAW;AAAA,UACpC,QAAQ,KAAK,GAAG;AAAA,UAChB;AAAA,UACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,QAC5B,CAAC;AACD,mBAAO;AAAA,UACN,mBAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA,sBAAsB,IAAI;AAAA,QAC3B;AAAA,MACD;AAAA,IACD;AAAA,IACA,CAAC,QAAQ,6BAA6B,aAAa,IAAI,CAAC;AAAA,IACxD,CAAC,QAAQ,+BAA+B,aAAa,KAAK,CAAC;AAAA,EAC5D;AACD;AAEO,SAAS,wBAAwB,OAAqC;AAC5E,SAAO,gBAAgB,KAAK,EAAE,IAAI,CAAC,CAAC,QAAQ,MAAM,CAAC,OAAO;AAAA,IACzD;AAAA,IACA,MAAM,KAAK,MAAM,aAAa,MAAM;AAAA,IACpC,UAAU,CAAC,CAAC;AAAA,EACb,EAAE;AACH;;;ACvKO,IAAM,aAAN,MAA4C;AAAA,EAMlD,YACS,OACA,QACA,KACP;AAHO;AACA;AACA;AAGR,uBAAmB,MAAM;AAAA,EAC1B;AAAA,EANS;AAAA,EACA;AAAA,EACA;AAAA,EARA,OAAO;AAAA;AAAA;AAAA,EAGP,UAAU;AAAA;AAAA,EAYnB,gBAAmC;AAClC,WAAO,EAAE,QAAQ,wBAAwB,KAAK,KAAK,EAAE;AAAA,EACtD;AAAA;AAAA,EAGA,iBAAsC;AACrC,WAAO,0BAA0B,KAAK,MAAM;AAAA,EAC7C;AAAA,EAEA,QAAQ,KAAyB;AAChC,QAAI,IAAI,YAAY,KAAK,OAAO,KAAK,MAAM,CAAC;AAE5C,UAAM,SAAS,gBAAgB,KAAK,OAAO,KAAK,QAAQ,KAAK,GAAG;AAChE,eAAW,CAAC,QAAQ,MAAM,GAAG,QAAQ,KAAK,QAAQ;AACjD,UAAI,SAAS,QAAQ,MAAM,GAAG,QAAQ;AAAA,IACvC;AAAA,EACD;AACD;;;AC9BO,IAAM,oBAAoB;AAAA,EAChC,CAAC,aAAa,iBAAiB,GAAG;AAAA,IACjC,SAAS;AAAA,IACT,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,IAKN,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOP;AAAA,EAEA,CAAC,aAAa,iBAAiB,GAAG;AAAA,IACjC,SAAS;AAAA,IACT,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,IAKN,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOP;AAAA,EAEA,CAAC,aAAa,aAAa,GAAG;AAAA,IAC7B,SAAS;AAAA,IACT,MAAM;AAAA;AAAA;AAAA;AAAA,IAIN,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,EAKP;AAAA;AAAA,EAGA,CAAC,aAAa,QAAQ,GAAG;AAAA,IACxB,MAAM;AAAA,EACP;AAAA,EAEA,CAAC,aAAa,UAAU,GAAG;AAAA,IAC1B,SAAS;AAAA,IACT,MAAM;AAAA;AAAA;AAAA,IAGN,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,EAKP;AAAA,EAEA,CAAC,aAAa,WAAW,GAAG;AAAA,IAC3B,SAAS;AAAA,IACT,MAAM;AAAA;AAAA;AAAA,IAGN,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,EAKP;AAAA,EAEA,CAAC,aAAa,yBAAyB,GAAG;AAAA,IACzC,SAAS;AAAA,IACT,MAAM;AAAA;AAAA;AAAA,IAGN,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,EAKP;AAAA,EAEA,CAAC,aAAa,YAAY,GAAG;AAAA,IAC5B,SAAS;AAAA,IACT,MAAM;AAAA;AAAA;AAAA,IAGN,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,EAKP;AAAA,EAEA,CAAC,aAAa,YAAY,GAAG;AAAA,IAC5B,SAAS;AAAA,IACT,MAAM;AAAA;AAAA;AAAA,IAGN,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,EAKP;AAAA,EAEA,CAAC,aAAa,eAAe,GAAG;AAAA,IAC/B,SAAS;AAAA,IACT,MAAM;AAAA;AAAA;AAAA;AAAA,IAIN,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWP;AAAA,EAEA,CAAC,aAAa,iBAAiB,GAAG;AAAA,IACjC,SAAS;AAAA,IACT,MAAM;AAAA;AAAA;AAAA,IAGN,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMP;AAAA,EAEA,CAAC,aAAa,WAAW,GAAG;AAAA,IAC3B,SAAS;AAAA,IACT,MAAM;AAAA;AAAA;AAAA,IAGN,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMP;AAAA,EAEA,CAAC,aAAa,aAAa,GAAG;AAAA,IAC7B,SAAS;AAAA,IACT,MAAM;AAAA;AAAA;AAAA,IAGN,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,EAKP;AACD;AAMO,IAAM,kBAAmE;AAAA,EAC/E,CAAC,aAAa,eAAe,GAAG,EAAE,UAAU,SAAS;AAAA,EACrD,CAAC,aAAa,iBAAiB,GAAG,EAAE,UAAU,UAAU,SAAS,WAAW;AAAA,EAC5E,CAAC,aAAa,WAAW,GAAG,EAAE,UAAU,SAAS;AAAA,EACjD,CAAC,aAAa,aAAa,GAAG,EAAE,UAAU,SAAS;AAAA,EACnD,CAAC,aAAa,iBAAiB,GAAG,EAAE,WAAW,IAAI,KAAK,SAAS;AAAA,EACjE,CAAC,aAAa,iBAAiB,GAAG,EAAE,WAAW,OAAO,KAAK,SAAS;AAAA,EACpE,CAAC,aAAa,aAAa,GAAG;AAAA,IAC7B,KAAK;AAAA,IACL,OAAO,IAAI,OAAO,EAAE;AAAA,IACpB,UAAU,yCAAyC,IAAI,OAAO,EAAE;AAAA,EACjE;AAAA,EACA,CAAC,aAAa,QAAQ,GAAG,EAAE,KAAK,SAAS;AAAA,EACzC,CAAC,aAAa,UAAU,GAAG,CAAC;AAAA,EAC5B,CAAC,aAAa,WAAW,GAAG,CAAC;AAAA,EAC7B,CAAC,aAAa,yBAAyB,GAAG,CAAC;AAAA,EAC3C,CAAC,aAAa,YAAY,GAAG,EAAE,UAAU,kBAAkB;AAAA,EAC3D,CAAC,aAAa,YAAY,GAAG,CAAC;AAC/B;;;AC7MA,IAAAC,sBAA4C;;;ACqC5C,eAAsB,WACrB,OACA,MAC0B;AAC1B,QAAM,OAAiB,CAAC;AACxB,QAAM,OAAkB,CAAC;AACzB,QAAM,eAAyB,CAAC;AAChC,QAAM,MAAM,CAAC,KAAa,QAAiB;AAC1C,SAAK,KAAK,GAAG;AACb,SAAK,KAAK,GAAG;AACb,iBAAa,KAAK,IAAI,KAAK,MAAM,EAAE;AAAA,EACpC;AAEA,MAAI,KAAK,OAAO,OAAW,KAAI,MAAM,KAAK,EAAE;AAC5C,MAAI,SAAS,KAAK,MAAM,YAAY,EAAE,KAAK,CAAC;AAC5C,MAAI,KAAK,iBAAiB,OAAW,KAAI,iBAAiB,KAAK,YAAY;AAC3E,MAAI,KAAK,cAAc,OAAW,KAAI,cAAc,KAAK,SAAS;AAClE,MAAI,KAAK,aAAa,OAAW,KAAI,aAAa,KAAK,QAAQ;AAC/D,MAAI,KAAK,UAAU,OAAW,KAAI,SAAS,KAAK,KAAK;AACrD,MAAI,KAAK,oBAAoB,OAAW,KAAI,qBAAqB,KAAK,eAAe;AACrF,MAAI,KAAK,WAAW,OAAW,KAAI,UAAU,KAAK,MAAM;AACxD,MAAI,KAAK,aAAa,OAAW,KAAI,YAAY,KAAK,QAAQ;AAC9D,MAAI,KAAK,oBAAoB,OAAW,KAAI,qBAAqB,KAAK,eAAe;AACrF,MAAI,KAAK,eAAe,OAAW,KAAI,eAAe,KAAK,UAAU;AACrE,MAAI,KAAK,cAAc,OAAW,KAAI,cAAc,KAAK,SAAS;AAElE,QAAM,CAAC,GAAG,IAAI,MAAM,MAAM;AAAA,IACzB,+BAA+B,KAAK,KAAK,IAAI,CAAC;AAAA,aACnC,aAAa,KAAK,IAAI,CAAC;AAAA;AAAA,IAElC;AAAA,EACD;AACA,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,2CAA2C;AACrE,SAAO;AACR;;;ACtDA,eAAsB,sBACrB,OACA,EAAE,eAAe,IAAI,GACH;AAClB,MAAI,CAAC,OAAO,SAAS,aAAa,KAAK,gBAAgB,GAAG;AACzD,UAAM,IAAI,MAAM,2EAA2E;AAAA,EAC5F;AACA,QAAM,OAAO,MAAM,MAAM;AAAA,IACxB;AAAA;AAAA;AAAA;AAAA,IAIA,CAAC,aAAa;AAAA,EACf;AACA,MAAI,KAAK;AACR,eAAW,EAAE,GAAG,KAAK,KAAM,OAAM,IAAI,KAAK,wBAAwB,EAAE,QAAQ,GAAG,CAAC;AAAA,EACjF;AACA,SAAO,KAAK;AACb;AAUO,SAAS,mBACf,OACA,SACuB;AACvB,QAAM,aAAa,QAAQ,cAAc,KAAK,KAAK,KAAK;AACxD,MAAI,UAAU;AACd,QAAM,MAAM,YAAY;AACvB,QAAI,QAAS;AACb,QAAI;AACH,YAAM,UAAU,MAAM,sBAAsB,OAAO;AAAA,QAClD,eAAe,QAAQ;AAAA,QACvB,GAAI,QAAQ,MAAM,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC;AAAA,MAC3C,CAAC;AACD,cAAQ,UAAU,OAAO;AAAA,IAC1B,SAAS,KAAK;AACb,cAAQ,MAAM,iDAAiD,GAAG;AAAA,IACnE;AAAA,EACD;AACA,QAAM,QAAQ,YAAY,KAAK,UAAU;AACzC,MAAI,OAAQ,MAAiC,UAAU,WAAY,CAAC,MAAgC,MAAM;AAC1G,OAAK,IAAI;AACT,SAAO,EAAE,MAAM,MAAM;AAAE,cAAU;AAAM,kBAAc,KAAK;AAAA,EAAG,EAAE;AAChE;","names":["bcrypt","import_core","import_middlewares","import_core","jwt","import_node_crypto","import_core","email","import_node_crypto","import_core","email","phone","resolve","import_core","QRCode","import_node_crypto","import_events","import_core","email","import_node_crypto","phone","phone","email","password","remaining","expiresAt","import_node_crypto","import_core","import_events","import_core","normalizePhone","phone","isValidPhone","hashPassword","verifyPassword","import_node_crypto","import_jsonwebtoken","import_core","import_events","jwt","import_core","import_core","email","import_middlewares"]}
|
|
1
|
+
{"version":3,"sources":["../src/services/password.ts","../src/index.ts","../src/routes.ts","../src/middlewares/require-email-login.ts","../src/middlewares/validate.ts","../src/services/rate-limit.ts","../src/schemas.ts","../src/services/cookies.ts","../src/controllers/mfa.controller.ts","../src/config.ts","../src/services/jwt.ts","../src/services/mfa.ts","../src/services/mfa-crypto.ts","../src/dtos/user.ts","../src/models/user.model.ts","../src/services/request-location.ts","../src/models/session.model.ts","../src/models/backup-code.model.ts","../src/models/login-event.model.ts","../src/services/request-meta.ts","../src/controllers/auth.controller.ts","../src/services/cooldown.ts","../src/services/email.ts","../src/models/password-reset.model.ts","../src/models/email-verification.model.ts","../src/models/phone-verification.model.ts","../src/controllers/user.controller.ts","../src/dtos/login-activity.ts","../src/controllers/oauth.controller.ts","../src/models/consumed-token.model.ts","../src/services/config-guard.ts","../src/middlewares/session.ts","../src/admin.ts","../src/module.ts","../src/templates.ts","../src/templates.es.ts","../src/templates.fr.ts","../src/middlewares/require-auth.ts","../src/migrate.ts","../src/services/retention.ts"],"sourcesContent":["import bcrypt from 'bcryptjs';\n\nconst ROUNDS = 12;\n\nexport async function hashPassword(plain: string): Promise<string> {\n\treturn bcrypt.hash(plain, ROUNDS);\n}\n\nexport async function verifyPassword(plain: string, hash: string): Promise<boolean> {\n\treturn bcrypt.compare(plain, hash);\n}\n\nexport interface IVerifyResult {\n\tvalid: boolean;\n\t// True when the password validated only via the legacy verifier — the caller\n\t// should re-store it as a bcrypt hash (rehash-on-login).\n\tneedsRehash: boolean;\n}\n\n// The login-path verifier: `verifyPassword` (the bcrypt primitive) PLUS an\n// optional foreign-hash fallback for apps migrating onto Fonderie auth. Tries\n// bcrypt first; if that fails and a `legacyVerify` is provided, falls back to it\n// and flags `needsRehash` so the caller can re-store the password as bcrypt\n// (rehash-on-login). `bcrypt.compare` returns false rather than throwing on a\n// non-bcrypt hash, so the fallback is reached cleanly; the legacy verifier is\n// wrapped defensively in case a custom implementation throws.\nexport async function verifyPasswordForLogin(\n\tplain: string,\n\thash: string,\n\tlegacyVerify?: (plain: string, hash: string) => boolean | Promise<boolean>,\n): Promise<IVerifyResult> {\n\tif (await verifyPassword(plain, hash)) return { valid: true, needsRehash: false };\n\n\tif (legacyVerify) {\n\t\tlet legacyOk = false;\n\t\ttry {\n\t\t\tlegacyOk = await legacyVerify(plain, hash);\n\t\t} catch {\n\t\t\tlegacyOk = false;\n\t\t}\n\t\tif (legacyOk) return { valid: true, needsRehash: true };\n\t}\n\n\treturn { valid: false, needsRehash: false };\n}\n","// ── Public API ───────────────────────────────────────────────────\nexport type { IUser, ISession, IMfaChallenge } from './types';\nexport { AuthModule } from './module';\nexport type { IAuthConfig, IAuthSecrets, IAuthRuntimeConfig, IDataExportContributor } from './config';\nexport { AUTH_CONFIG_KEYS, MESSAGE_KEYS } from './config';\nexport type { AuthMessageKey } from './config';\n// Built-in default templates for auth's notifications. Pass to courier via\n// config.templates.defaults so the emails render out of the box; override any\n// key per-app with a DB row / FS file.\nexport { DEFAULT_TEMPLATES } from './templates';\n\n// DTOs\nexport type { IUserDTO } from './dtos/user';\nexport { toUserDTO } from './dtos/user';\n\nexport type {\n\tILoginEventDTO,\n\tILoginHistoryPageDTO,\n\tISessionDTO,\n} from './dtos/login-activity';\nexport {\n\tsanitizeLocation,\n\tresolveLocation,\n} from './services/request-location';\nexport type {\n\tIRequestLocation,\n\tILocationRequest,\n\tLocationResolver,\n} from './services/request-location';\n\n// Request validation — schemas are the enforced contract for every\n// body-taking route; exported for docs generation and typed clients.\nexport { validate } from './middlewares/validate';\nexport * as schemas from './schemas';\nexport type { RegisterInput, LoginInput, ResetPasswordInput, ChangePasswordInput } from './schemas';\n\n// Guards — used by other modules and user route handlers\nexport { withSession } from './middlewares/session';\nexport { requireAuth } from './middlewares/require-auth';\n\n// Utilities\nexport { normalizeEmail, normalizeEmailSafe } from './services/email';\n\n// Migration — import an existing user base (preserves identity; pairs with the\n// `legacyVerify` config option for rehash-on-login).\nexport { importUser } from './migrate';\nexport { purgeSoftDeletedUsers, startUserRetention } from './services/retention';\nexport type { IPurgeOptions, IUserRetentionScheduleOptions } from './services/retention';\nexport type { IImportUser } from './migrate';\n\n// Production-readiness — validate the auth config (fatal on a weak jwtSecret in\n// production). Runs automatically on AuthModule construction; exported for an\n// app's own preflight.\nexport { validateAuthConfig } from './services/config-guard';\n\n// Brute-force protection — on by default; see services/rate-limit.ts\nexport { buildAuthIpLimiter, buildAuthAccountLimiter } from './services/rate-limit';\nexport type { IAuthRateLimitConfig, AuthLimitedRoute } from './services/rate-limit';\nexport type { IAdminUserDTO } from './admin';\nexport { describeAuthAdminRoutes, toAdminUserDTO, toAdminUserPageDTO } from './admin';\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { ILocaleSettings, Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport type { EventBus } from '@fonderie/events';\nimport type { IAuthConfig, AuthRouteId } from './config';\n\nimport { requireAuth, requireAnyAuth, requireVerified } from '@fonderie/core/middlewares';\nimport { requireEmailLogin } from './middlewares/require-email-login';\nimport { validate } from './middlewares/validate';\nimport { buildAuthIpLimiter, buildAuthAccountLimiter } from './services/rate-limit';\nimport {\n\tloginSchema,\n\tverifySchema,\n\trefreshSchema,\n\tmfaTokenSchema,\n\tregisterSchema,\n\tupdateEmailSchema,\n\tupdatePhoneSchema,\n\tupdateProfileSchema,\n\tresetPasswordSchema,\n\tchangePasswordSchema,\n\tforgotPasswordSchema,\n\tupdatePreferencesSchema,\n\tappleNativeSchema,\n} from './schemas';\n\nimport { mfaController } from './controllers/mfa.controller';\nimport { authController } from './controllers/auth.controller';\nimport { userController } from './controllers/user.controller';\nimport { oauthController } from './controllers/oauth.controller';\n\ntype RouteDefinition = [string, string, ...Middleware[]];\n\nexport function buildAuthRoutes(\n\tstore: IStoreAdapter,\n\tconfig: IAuthConfig,\n\tbus?: EventBus,\n\tlocales?: ILocaleSettings,\n): RouteDefinition[] {\n\tconst user = userController(store, config, bus);\n\tconst auth = authController(store, config, bus, locales);\n\tconst oauth = oauthController(store, config, bus);\n\tconst mfa = mfaController(store, config, config.appName ?? 'Fonderie', bus);\n\n\t// Opt-in verification gate: only enforces email/phone verification when\n\t// requireVerification: true is set in config. Defaults to false so that\n\t// auth can be used without a courier/email provider.\n\tconst verifyGate: Middleware = config.requireVerification\n\t\t? requireVerified\n\t\t: (_ctx, next) => next();\n\n\t// Brute-force protection — on by default, backed by the module's own\n\t// store; see services/rate-limit.ts. Null when disabled via config.\n\tconst passthrough: Middleware = (_ctx, next) => next();\n\t// IP limit runs before validate(); account limit runs after (keys on the\n\t// validated, bounded email — never raw attacker input).\n\tconst ipLimit = (route: Parameters<typeof buildAuthIpLimiter>[0]): Middleware =>\n\t\tbuildAuthIpLimiter(route, store, config.rateLimit) ?? passthrough;\n\tconst acctLimit = (route: Parameters<typeof buildAuthAccountLimiter>[0]): Middleware =>\n\t\tbuildAuthAccountLimiter(route, store, config.rateLimit) ?? passthrough;\n\n\t// Apply an optional per-route method/path override (config.routes) keyed by a\n\t// stable id, so an app can match an existing frontend's contract without a shim.\n\tconst R = (id: AuthRouteId, method: string, path: string, ...handlers: Middleware[]): RouteDefinition => {\n\t\tconst o = config.routes?.[id];\n\t\tif (!o) return [method, path, ...handlers];\n\t\tif (typeof o === 'string') return [method, o, ...handlers];\n\t\treturn [o.method ?? method, o.path ?? path, ...handlers];\n\t};\n\n\tconst routes: RouteDefinition[] = [\n\t\t// Which sign-in methods this deployment can actually honour (Public).\n\t\t//\n\t\t// A frontend has to decide which buttons to draw, and the only honest\n\t\t// source is the side holding the credentials. The alternative — a\n\t\t// build-time flag in the app — stores the same fact twice and lets the\n\t\t// two disagree; the symptom is a user clicking a provider the server\n\t\t// cannot complete and landing on the provider's error page, which the\n\t\t// app cannot explain.\n\t\t//\n\t\t// Public and unauthenticated on purpose: the login screen needs it\n\t\t// before anyone has signed in. It discloses nothing a visitor could not\n\t\t// learn by looking at the buttons, and deliberately nothing else — no\n\t\t// client ids, no redirect URIs, no module inventory.\n\t\tR('providers', 'GET', '/auth/providers', async () =>\n\t\t\tsetApiResponse(HTTP.OK, 'AUTH_PROVIDERS', 'Sign-in methods available here', {\n\t\t\t\tproviders: [...config.providers],\n\t\t\t}),\n\t\t),\n\n\t\t// Registration & Login (Public)\n\t\tR('register', 'POST', '/auth/register', ipLimit('register'), validate(registerSchema), auth.register),\n\t\tR('login', 'POST', '/auth/login', ipLimit('login'), validate(loginSchema), acctLimit('login'), auth.login),\n\n\t\t// Token Management (Public)\n\t\tR('refresh', 'POST', '/auth/refresh', validate(refreshSchema), auth.refresh),\n\n\t\t// Email — Password Recovery (Public)\n\t\tR('forgotPassword', 'POST', '/auth/email/forgot', ipLimit('forgot'), validate(forgotPasswordSchema), acctLimit('forgot'), auth.forgotPassword),\n\t\tR('resetPassword', 'POST', '/auth/email/reset', ipLimit('reset'), validate(resetPasswordSchema), auth.resetPassword),\n\n\t\t// Verification (email or phone, determined by loginMethod). requireAnyAuth,\n\t\t// not requireAuth: the phone-OTP flow reaches these with a short-lived\n\t\t// pending token (login/register no longer issue real tokens before the OTP\n\t\t// round-trip); the controllers reject pending tokens for any other flow.\n\t\t// ipLimit guards the 6-digit OTP against brute force (like mfaVerify).\n\t\tR('verifyEmail', 'POST', '/auth/verify', ipLimit('verify'), requireAnyAuth, validate(verifySchema), auth.verify),\n\t\tR('sendVerification', 'GET', '/auth/send-verification', requireAnyAuth, auth.sendVerification),\n\n\t\t// Account Management (Protected)\n\t\tR('logout', 'POST', '/auth/logout', requireAuth, validate(refreshSchema), auth.logout),\n\n\t\t// User Profile (Protected; writes also gate on requireVerification)\n\t\tR('me', 'GET', '/users', requireAuth, user.me),\n\t\tR('updateProfile', 'PUT', '/users/profile', requireAuth, verifyGate, validate(updateProfileSchema), user.updateProfile),\n\t\tR('updatePreferences', 'PUT', '/users/preferences', requireAuth, verifyGate, validate(updatePreferencesSchema), user.updatePreferences),\n\t\tR('updateEmail', 'PUT', '/users/email', requireAuth, verifyGate, validate(updateEmailSchema), user.updateEmail),\n\t\tR('updatePhone', 'PUT', '/users/phone', requireAuth, verifyGate, validate(updatePhoneSchema), user.updatePhone),\n\t\tR('changePassword', 'PUT', '/users/password', requireAuth, validate(changePasswordSchema), user.changePassword),\n\t\t// Disconnect an OAuth provider. requireAuth only — the controller\n\t\t// refuses when it would leave the account with no way to sign in.\n\t\tR('unlinkOauth', 'DELETE', '/auth/oauth/:provider', requireAuth, user.unlinkOauth),\n\n\t\tR('deleteMe', 'DELETE', '/users', requireAuth, verifyGate, user.deleteMe),\n\t\tR('exportMe', 'GET', '/users/export', requireAuth, user.exportMe),\n\n\t\t// Security surfaces (Protected; the caller's own login history + sessions).\n\t\t// The literal /others route is registered before /:id so it isn't\n\t\t// captured as an id.\n\t\tR('loginHistory', 'GET', '/auth/login-history', requireAuth, user.loginHistory),\n\t\tR('listSessions', 'GET', '/auth/sessions', requireAuth, user.listSessions),\n\t\tR('terminateOtherSessions', 'DELETE', '/auth/sessions/others', requireAuth, user.terminateOtherSessions),\n\t\tR('terminateSession', 'DELETE', '/auth/sessions/:id', requireAuth, user.terminateSession),\n\n\t\t// MFA (email sessions only — requireVerified is always enforced here\n\t\t// because MFA is a security feature and email verification is meaningful)\n\t\tR('mfaSetup', 'POST', '/auth/mfa/setup', requireAuth, requireEmailLogin, requireVerified, mfa.setup),\n\t\t// /auth/mfa/verify accepts both mfaPending tokens (TOTP/backup-code login)\n\t\t// and full tokens (setup confirmation), so requireAnyAuth is used here.\n\t\tR('mfaVerify', 'POST', '/auth/mfa/verify', ipLimit('mfaVerify'), requireAnyAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.verify),\n\t\tR('mfaDisable', 'POST', '/auth/mfa/disable', requireAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.disable),\n\t\tR('mfaBackupCodes', 'POST', '/auth/mfa/backup-codes', requireAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.regenerateBackupCodes),\n\t];\n\n\tif (config.providers.includes('google')) {\n\t\troutes.push(\n\t\t\t['GET', '/auth/google', oauth.googleInit],\n\t\t\t// ipLimit: an unauthenticated caller can otherwise force one outbound\n\t\t\t// token-exchange to Google per request (the state check doesn't gate it).\n\t\t\t['GET', '/auth/google/callback', ipLimit('login'), oauth.googleCallback],\n\t\t);\n\t}\n\n\tif (config.providers.includes('apple')) {\n\t\troutes.push(\n\t\t\t// Web redirect flow. The callback is a POST (response_mode=form_post);\n\t\t\t// its urlencoded body is parsed inside the controller. ipLimit for the\n\t\t\t// same reason as Google's callback (bounds outbound token exchanges).\n\t\t\t['GET', '/auth/apple', oauth.appleInit],\n\t\t\t['POST', '/auth/apple/callback', ipLimit('login'), oauth.appleCallback],\n\t\t\t// Native (iOS) flow: the app posts the identityToken from the Apple\n\t\t\t// sheet. ipLimit guards token brute-forcing; validate bounds the body.\n\t\t\t['POST', '/auth/apple/native', ipLimit('login'), validate(appleNativeSchema), oauth.appleNative],\n\t\t);\n\t}\n\n\treturn routes;\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\n\nexport const requireEmailLogin: Middleware = async (ctx, next) => {\n\tif (ctx.user!.loginMethod !== 'email') {\n\t\treturn setApiResponse(\n\t\t\tHTTP.FORBIDDEN,\n\t\t\t'EMAIL_LOGIN_REQUIRED',\n\t\t\t'This action requires email authentication',\n\t\t);\n\t}\n\treturn next();\n};\n","// Moved to @fonderie/core/middlewares so every package shares one validation\n// middleware (same 422 envelope, same parse semantics). Re-exported here for\n// back-compat.\nexport { validate } from '@fonderie/core/middlewares';\n","import type { Middleware } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport {\n\tbyBodyField,\n\tbyIp,\n\trateLimit,\n\tStoreAdapterStore,\n\ttype IRateLimitRule,\n\ttype IRateLimitStore,\n} from '@fonderie/rate-limit';\n\n// Per-route brute-force protection for auth. Defaults are deliberately\n// conservative and, crucially, ON without the caller asking — the whole point\n// of shipping this in the module is that \"add login\" gets throttling too.\n//\n// Two phases per route, wired at different points in the chain:\n// - IP limit runs BEFORE validate() — a cheap guard that sheds floods\n// before they reach schema parsing.\n// - Account (body-field) limit runs AFTER validate() — so it keys on a\n// bounded, well-typed identifier, never raw attacker input.\n\nexport interface IAuthRateLimitConfig {\n\t// Override the backing store. Defaults to StoreAdapterStore over the\n\t// module's own IStoreAdapter — distributed-correct with zero extra infra.\n\tstore?: IRateLimitStore;\n\t// Per-route rule overrides; set a route to false to disable just that one.\n\trules?: Partial<Record<AuthLimitedRoute, IRateLimitRule | false>>;\n}\n\nexport type AuthLimitedRoute = 'login' | 'register' | 'forgot' | 'reset' | 'verify' | 'mfaVerify';\n\n// capacity = burst; refillPerSec = capacity / windowSeconds.\nconst min = (n: number) => n * 60;\nconst DEFAULTS: Record<\n\tAuthLimitedRoute,\n\t{ ip: IRateLimitRule; account?: IRateLimitRule; accountField?: string }\n> = {\n\t// login: 10/15min per IP AND 5/15min per account (credential stuffing\n\t// rotates IPs, so the per-account bucket is the one that bites).\n\tlogin: {\n\t\tip: { capacity: 10, refillPerSec: 10 / min(15) },\n\t\taccount: { capacity: 5, refillPerSec: 5 / min(15) },\n\t\taccountField: 'email',\n\t},\n\t// register: 5/hour per IP — signup abuse is IP-shaped.\n\tregister: { ip: { capacity: 5, refillPerSec: 5 / min(60) } },\n\t// forgot: 5/hour per IP AND 3/hour per account (email-bombing protection).\n\tforgot: {\n\t\tip: { capacity: 5, refillPerSec: 5 / min(60) },\n\t\taccount: { capacity: 3, refillPerSec: 3 / min(60) },\n\t\taccountField: 'email',\n\t},\n\t// reset: 10/15min per IP — the reset PIN is a 6-digit code looked up\n\t// globally, so an unthrottled endpoint is brute-forceable into an account\n\t// takeover. There's no email in the reset body to key an account bucket on,\n\t// so this IP bucket is the guard. (Moving reset to a high-entropy opaque\n\t// token is tracked as a follow-up hardening.)\n\treset: { ip: { capacity: 10, refillPerSec: 10 / min(15) } },\n\t// verify: 10/15min per IP — /auth/verify checks a 6-digit email/phone OTP,\n\t// and in the phone flow that OTP is the login credential itself.\n\tverify: { ip: { capacity: 10, refillPerSec: 10 / min(15) } },\n\t// mfaVerify: 10/15min per IP — TOTP brute-force.\n\tmfaVerify: { ip: { capacity: 10, refillPerSec: 10 / min(15) } },\n};\n\nfunction resolve(\n\troute: AuthLimitedRoute,\n\tstore: IStoreAdapter,\n\tconfig: IAuthRateLimitConfig | false | undefined,\n): {\n\tstore: IRateLimitStore;\n\tipRule: IRateLimitRule;\n\taccount?: { rule: IRateLimitRule; field: string };\n} | null {\n\tif (config === false) return null;\n\tconst override = config?.rules?.[route];\n\tif (override === false) return null;\n\n\tconst backing = config?.store ?? new StoreAdapterStore(store);\n\tconst def = DEFAULTS[route];\n\treturn {\n\t\tstore: backing,\n\t\tipRule: override ?? def.ip,\n\t\t...(def.account && def.accountField\n\t\t\t? { account: { rule: def.account, field: def.accountField } }\n\t\t\t: {}),\n\t};\n}\n\n// Phase 1: IP limit, placed BEFORE validate().\nexport function buildAuthIpLimiter(\n\troute: AuthLimitedRoute,\n\tstore: IStoreAdapter,\n\tconfig: IAuthRateLimitConfig | false | undefined,\n): Middleware | null {\n\tconst r = resolve(route, store, config);\n\tif (!r) return null;\n\treturn rateLimit({ store: r.store, rule: r.ipRule, key: byIp(`auth:${route}`) });\n}\n\n// Phase 2: account limit, placed AFTER validate() so the keyed field is clean.\nexport function buildAuthAccountLimiter(\n\troute: AuthLimitedRoute,\n\tstore: IStoreAdapter,\n\tconfig: IAuthRateLimitConfig | false | undefined,\n): Middleware | null {\n\tconst r = resolve(route, store, config);\n\tif (!r || !r.account) return null;\n\treturn rateLimit({\n\t\tstore: r.store,\n\t\trule: r.account.rule,\n\t\tkey: byBodyField(`auth:${route}`, r.account.field),\n\t});\n}\n","import { z } from 'zod';\n\n// Request schemas — the validation contract for every body-taking auth route.\n// Wired into routes.ts via validate(); exported so docs generators and typed\n// clients read the same source of truth the runtime enforces.\n//\n// Passwords are capped at 128 chars: bcrypt truncates at 72 bytes anyway, and\n// an explicit cap stops oversized inputs from reaching the hash function.\n\nconst email = z.string().trim().pipe(z.email());\nconst password = z.string().min(8, 'password must be at least 8 characters').max(128);\n// E.164-ish, tolerant of spaces/dashes/parens — controllers normalize further.\nconst phone = z\n\t.string()\n\t.refine((v) => /^\\+?[1-9]\\d{6,14}$/.test(v.replace(/[\\s\\-()]/g, '')), 'Invalid phone number');\nconst sixDigitPin = z\n\t.string()\n\t.trim()\n\t.regex(/^\\d{6}$/, 'must be a 6-digit code');\n\n// The language the person signed up in. Optional and lenient: an unreadable\n// tag falls back to the app's system locale rather than failing sign-up.\nconst signupLocale = z.string().max(35).nullish();\n\nexport const registerSchema = z.union([\n\tz.object({\n\t\temail,\n\t\tpassword,\n\t\tfirstName: z.string().max(100).nullish(),\n\t\tlastName: z.string().max(100).nullish(),\n\t\tlocale: signupLocale,\n\t}),\n\tz.object({ phone, locale: signupLocale }),\n]);\n\nexport const loginSchema = z.union([\n\tz.object({ email, password: z.string().min(1).max(128) }),\n\tz.object({ phone }),\n]);\n\n// Native Sign in with Apple: the iOS app posts the identityToken it got from\n// the native Apple sheet. Bounded length — it's a JWT, never megabytes.\nexport const appleNativeSchema = z.object({\n\tidentityToken: z.string().min(1).max(8192),\n\t// Optional nonce the app bound to the native request; when present we check\n\t// it against the token's `nonce` claim to defend against replay.\n\tnonce: z.string().max(256).optional(),\n});\n\n// refreshToken may come from the body or the refresh_token cookie.\nexport const refreshSchema = z.object({ refreshToken: z.string().min(1).optional() });\n\nexport const forgotPasswordSchema = z.object({ email });\n\n// Reset by the 6-digit pin (typed from the email; route is IP-rate-limited)\n// or by the high-entropy token (from the email's reset link; not\n// brute-forceable, so it needs no rate limit).\nexport const resetPasswordSchema = z.union([\n\tz.object({ pin: sixDigitPin, password }),\n\tz.object({ token: z.string().trim().min(32, 'invalid reset token'), password }),\n]);\n\nexport const verifySchema = z.object({ token: sixDigitPin });\n\nexport const updateProfileSchema = z\n\t.object({\n\t\tfirstName: z.string().max(100).nullable().optional(),\n\t\tlastName: z.string().max(100).nullable().optional(),\n\t\tavatarUrl: z.string().trim().pipe(z.url()).nullable().optional(),\n\t})\n\t.refine(\n\t\t(o) => Object.values(o).some((v) => v !== undefined),\n\t\t'Provide at least one of: firstName, lastName, avatarUrl',\n\t);\n\nexport const updatePreferencesSchema = z\n\t.object({\n\t\tlocale: z.string().max(35).optional(),\n\t\ttimezone: z.string().max(64).optional(),\n\t\t// Typed at last — these four accepted `unknown`, letting null and\n\t\t// arbitrary JSON into stored preferences that the user DTO then served\n\t\t// against string-typed client fields.\n\t\tnotifications: z\n\t\t\t.object({\n\t\t\t\temail: z.boolean().optional(),\n\t\t\t\tinApp: z.boolean().optional(),\n\t\t\t\tsms: z.boolean().optional(),\n\t\t\t\tpush: z.boolean().optional(),\n\t\t\t})\n\t\t\t.optional(),\n\t\temailDigest: z.string().max(20).optional(),\n\t\tdateFormat: z.string().max(30).optional(),\n\t\ttimeFormat: z.string().max(30).optional(),\n\t})\n\t.refine(\n\t\t(o) => Object.values(o).some((v) => v !== undefined),\n\t\t'Provide at least one preference field',\n\t);\n\nexport const updateEmailSchema = z.object({ email });\n\nexport const updatePhoneSchema = z.object({ phone });\n\nexport const changePasswordSchema = z.object({\n\tcurrentPassword: z.string().min(1).max(128),\n\tnewPassword: password,\n});\n\n// TOTP or backup code — verify/disable accept either; both are short strings.\nexport const mfaTokenSchema = z.object({ token: z.string().trim().min(6).max(64) });\n\nexport type RegisterInput = z.infer<typeof registerSchema>;\nexport type LoginInput = z.infer<typeof loginSchema>;\nexport type ResetPasswordInput = z.infer<typeof resetPasswordSchema>;\nexport type ChangePasswordInput = z.infer<typeof changePasswordSchema>;\n","import type { IAuthConfig } from '../config';\n\n// Single source of truth for the auth token cookie pair, so every login\n// path (email, phone, OAuth, MFA, refresh) sets identical attributes.\n//\n// Secure defaults to NODE_ENV === 'production': in production the tokens\n// must never travel over plain HTTP; local HTTP dev keeps working without\n// config. Override with secureCookies when the environment variable doesn't\n// reflect reality (e.g. HTTPS in staging, TLS-terminating proxy quirks).\n\nfunction secureAttr(config: IAuthConfig): string {\n\tconst secure = config.secureCookies ?? process.env['NODE_ENV'] === 'production';\n\treturn secure ? '; Secure' : '';\n}\n\n// Returns ONE string per cookie. Each must go out as its own `Set-Cookie`\n// header — joining them (e.g. with \", \") is invalid HTTP and mangles the\n// distinct Paths. Callers append each via `jsonWithCookies`.\nexport function tokenPairCookies(\n\taccessToken: string,\n\trefreshToken: string,\n\tconfig: IAuthConfig,\n): string[] {\n\tconst secure = secureAttr(config);\n\treturn [\n\t\t`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/${secure}`,\n\t\t`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh${secure}`,\n\t];\n}\n\nexport function clearedTokenCookies(config: IAuthConfig): string[] {\n\tconst secure = secureAttr(config);\n\treturn [\n\t\t`access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0${secure}`,\n\t\t`refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0${secure}`,\n\t];\n}\n\n// Build a Headers object with each cookie as its OWN Set-Cookie entry. Pass it\n// straight to `Response.json(body, { status, headers: cookieHeaders(...) })`. A\n// plain `{ 'Set-Cookie': [...] }` init coalesces them into one comma-joined\n// header (invalid); appending onto a Headers instance keeps them distinct.\nexport function cookieHeaders(cookies: string[]): Headers {\n\tconst headers = new Headers();\n\tfor (const c of cookies) headers.append('Set-Cookie', c);\n\treturn headers;\n}\n","import { tokenPairCookies, cookieHeaders } from '../services/cookies';\nimport QRCode from 'qrcode';\n\nimport { setApiResponse, HTTP, background } from '@fonderie/core';\nimport type { IFonderieContext, ICourierMessage } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport type { EventBus } from '@fonderie/events';\nimport { NOTIFICATION_EVENT } from '@fonderie/events';\n\nimport type { IAuthConfig } from '../config';\nimport { MESSAGE_KEYS } from '../config';\nimport { issueTokenPair, refreshTokenExpiry } from '../services/jwt';\nimport {\n\tgenerateTotpSecret,\n\tgenerateTotpUri,\n\tverifyTotpToken,\n\tgenerateBackupCodes,\n} from '../services/mfa';\nimport { hashPassword, verifyPassword } from '../services/password';\nimport { makeMfaCipher } from '../services/mfa-crypto';\nimport { toUserDTO } from '../dtos/user';\nimport { UserModel } from '../models/user.model';\nimport { SessionModel } from '../models/session.model';\nimport { BackupCodeModel } from '../models/backup-code.model';\nimport { LoginEventModel } from '../models/login-event.model';\nimport { requestMeta } from '../services/request-meta';\n\nexport function mfaController(\n\tstore: IStoreAdapter,\n\tconfig: IAuthConfig,\n\tissuer: string,\n\tbus?: EventBus,\n) {\n\tconst users = new UserModel(store);\n\tconst sessions = new SessionModel(store, config.location);\n\tconst backupCodes = new BackupCodeModel(store);\n\tconst loginEvents = new LoginEventModel(store, config.location);\n\t// Encrypts/decrypts TOTP secrets at rest. Passthrough when no key is set.\n\tconst mfaCipher = makeMfaCipher(config.mfaSecretKey);\n\n\treturn {\n\t\t// ── 1. Setup ───────────────────────────────────────────────\n\t\tsetup: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst secret = generateTotpSecret();\n\t\t\tconst uri = generateTotpUri(ctx.user!.email ?? ctx.user!.id, secret, issuer);\n\t\t\tconst plainCodes = generateBackupCodes();\n\t\t\tconst codeHashes = await Promise.all(plainCodes.map((c) => hashPassword(c)));\n\n\t\t\tconst qr = await QRCode.toDataURL(uri);\n\n\t\t\tawait Promise.all([\n\t\t\t\tusers.saveMfaPendingSecret(ctx.user!.id, mfaCipher.encrypt(secret)),\n\t\t\t\tbackupCodes.replace(ctx.user!.id, codeHashes),\n\t\t\t]);\n\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.OK,\n\t\t\t\t'MFA_SETUP_INITIATED',\n\t\t\t\t'Scan the QR code with your authenticator app.',\n\t\t\t\t{\n\t\t\t\t\tqr,\n\t\t\t\t\t// uri — expose when adding a \"manual entry\" flow in the UI (otpauth:// URI lets\n\t\t\t\t\t// users add the credential by typing the secret instead of scanning the QR code)\n\t\t\t\t\tbackupCodes: plainCodes,\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\t// ── 2. Verify (setup confirm · TOTP login · backup code login) ──\n\t\tverify: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst token = body?.['token'];\n\t\t\t// Only the login-completion path (a pending MFA challenge) belongs in\n\t\t\t// login history; enrollment confirmation is not a login attempt.\n\t\t\tconst isLoginCompletion = ctx.user!.mfaPending === true;\n\t\t\tconst meta = requestMeta(ctx);\n\t\t\tconst failedMfa = async () =>\n\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\tuserId: ctx.user!.id,\n\t\t\t\t\temailAttempted: ctx.user!.email,\n\t\t\t\t\tmethod: 'mfa',\n\t\t\t\t\toutcome: 'failed',\n\t\t\t\t\tfailureReason: 'invalid_code',\n\t\t\t\t\t...meta,\n\t\t\t\t});\n\n\t\t\tif (typeof token !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'token is required');\n\t\t\t}\n\n\t\t\tconst pendingCipher = await users.getMfaPendingSecret(ctx.user!.id);\n\t\t\tconst pendingSecret = pendingCipher ? mfaCipher.decrypt(pendingCipher) : null;\n\n\t\t\tif (pendingSecret) {\n\t\t\t\t// ── Setup confirmation (TOTP only — backup codes cannot confirm setup) ──\n\t\t\t\tif (!verifyTotpToken(token, pendingSecret)) {\n\t\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_CODE', 'Invalid MFA token');\n\t\t\t\t}\n\t\t\t\tawait users.confirmMfaSecret(ctx.user!.id);\n\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\t\ttype: MESSAGE_KEYS.mfaEnabled,\n\t\t\t\t\t\tlocale: ctx.user!.locale,\n\t\t\t\t\t\tdata: {},\n\t\t\t\t\t\trecipient: { email: ctx.user!.email, phone: null, deviceToken: null },\n\t\t\t\t\t} satisfies ICourierMessage));\n\n\t\t\t\treturn setApiResponse(HTTP.OK, 'MFA_VERIFIED', 'MFA verified successfully.', {\n\t\t\t\t\tmfaEnabled: true,\n\t\t\t\t});\n\t\t\t} else if (/^[A-Z0-9]{8}$/i.test(token)) {\n\t\t\t\t// ── Backup code consumption ────────────────────────────────────────────\n\t\t\t\tif (!ctx.user!.mfaPending) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t\t\t'MFA_NOT_PENDING',\n\t\t\t\t\t\t'Use the mfaToken from the login response',\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (!ctx.user!.mfaEnabled) {\n\t\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'MFA_NOT_CONFIGURED', 'MFA not configured');\n\t\t\t\t}\n\n\t\t\t\tconst unused = await backupCodes.findUnused(ctx.user!.id);\n\t\t\t\tconst checks = await Promise.all(\n\t\t\t\t\tunused.map(async (row) => ({\n\t\t\t\t\t\tid: row.id,\n\t\t\t\t\t\tmatch: await verifyPassword(token.toUpperCase(), row.codeHash),\n\t\t\t\t\t})),\n\t\t\t\t);\n\t\t\t\tconst matched = checks.find((r) => r.match);\n\n\t\t\t\tif (!matched) {\n\t\t\t\t\tif (isLoginCompletion) await failedMfa();\n\t\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_CODE', 'Invalid backup code');\n\t\t\t\t}\n\n\t\t\t\tawait backupCodes.consume(matched.id);\n\t\t\t} else {\n\t\t\t\t// ── TOTP login verification ───────────────────────────────────────────\n\t\t\t\tif (!ctx.user!.mfaPending) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t\t\t'MFA_NOT_PENDING',\n\t\t\t\t\t\t'Use the mfaToken from the login response',\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst storedSecret = await users.getMfaSecret(ctx.user!.id);\n\t\t\t\tif (!storedSecret) {\n\t\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'MFA_NOT_CONFIGURED', 'MFA not configured');\n\t\t\t\t}\n\t\t\t\tconst secret = mfaCipher.decrypt(storedSecret);\n\t\t\t\tif (!verifyTotpToken(token, secret)) {\n\t\t\t\t\tif (isLoginCompletion) await failedMfa();\n\t\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_CODE', 'Invalid MFA token');\n\t\t\t\t}\n\t\t\t\tif (!ctx.user!.mfaEnabled) {\n\t\t\t\t\tawait users.enableMfa(ctx.user!.id);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst { accessToken, refreshToken, sid } = issueTokenPair(ctx.user!.id, config, {\n\t\t\t\tloginMethod: ctx.user!.loginMethod,\n\t\t\t\tphoneVerified: ctx.user!.phoneVerified,\n\t\t\t});\n\t\t\tawait sessions.create(ctx.user!.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);\n\t\t\tif (isLoginCompletion) {\n\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\tuserId: ctx.user!.id,\n\t\t\t\t\temailAttempted: ctx.user!.email,\n\t\t\t\t\tmethod: 'mfa',\n\t\t\t\t\toutcome: 'success',\n\t\t\t\t\t...meta,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst fullUser = await users.findById(ctx.user!.id);\n\t\t\tif (!fullUser) {\n\t\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'User not found after MFA verify');\n\t\t\t}\n\n\t\t\treturn Response.json(\n\t\t\t\t{\n\t\t\t\t\treason: 'MFA_VERIFIED',\n\t\t\t\t\texplanation: 'MFA verified successfully.',\n\t\t\t\t\tresult: {\n\t\t\t\t\t\ttokens: { access: accessToken, refresh: refreshToken },\n\t\t\t\t\t\tuser: toUserDTO(fullUser, ctx.user!.phoneVerified),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\theaders: cookieHeaders(tokenPairCookies(accessToken, refreshToken, config)),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\t// ── 3. Regenerate backup codes ────────────────────────────\n\t\tregenerateBackupCodes: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst token = body?.['token'];\n\n\t\t\tif (typeof token !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'TOTP token is required');\n\t\t\t}\n\n\t\t\tif (!ctx.user!.mfaEnabled) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'MFA_NOT_ENABLED', 'MFA is not enabled');\n\t\t\t}\n\n\t\t\tconst storedSecret = await users.getMfaSecret(ctx.user!.id);\n\t\t\tconst secret = storedSecret ? mfaCipher.decrypt(storedSecret) : null;\n\t\t\tif (!secret || !verifyTotpToken(token, secret)) {\n\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_CODE', 'Invalid TOTP code');\n\t\t\t}\n\n\t\t\tconst plainCodes = generateBackupCodes();\n\t\t\tconst codeHashes = await Promise.all(plainCodes.map((c) => hashPassword(c)));\n\t\t\tawait backupCodes.replace(ctx.user!.id, codeHashes);\n\n\t\t\tawait background(bus\n\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\ttype: MESSAGE_KEYS.mfaBackupCodesRegenerated,\n\t\t\t\t\tlocale: ctx.user!.locale,\n\t\t\t\t\tdata: {},\n\t\t\t\t\trecipient: { email: ctx.user!.email, phone: null, deviceToken: null },\n\t\t\t\t} satisfies ICourierMessage));\n\n\t\t\treturn setApiResponse(HTTP.OK, 'BACKUP_CODES_REGENERATED', 'Backup codes regenerated.', {\n\t\t\t\tbackupCodes: plainCodes,\n\t\t\t});\n\t\t},\n\n\t\t// ── 4. Disable ─────────────────────────────────────────────\n\t\tdisable: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst token = body?.['token'];\n\n\t\t\tif (typeof token !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'TOTP token is required');\n\t\t\t}\n\n\t\t\tconst user = await users.findById(ctx.user!.id);\n\t\t\tif (!user || !user.mfaEnabled) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'MFA_NOT_ENABLED', 'MFA is not enabled');\n\t\t\t}\n\n\t\t\t// Fetched on demand like mfa.verify — the encrypted secret no longer\n\t\t\t// rides along on every user row.\n\t\t\tconst storedSecret = await users.getMfaSecret(ctx.user!.id);\n\t\t\tconst secret = storedSecret ? mfaCipher.decrypt(storedSecret) : null;\n\t\t\tif (!secret || !verifyTotpToken(token, secret)) {\n\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_CODE', 'Invalid TOTP code');\n\t\t\t}\n\n\t\t\tawait Promise.all([users.disableMfa(ctx.user!.id), backupCodes.deleteByUser(ctx.user!.id)]);\n\n\t\t\tawait background(bus\n\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\ttype: MESSAGE_KEYS.mfaDisabled,\n\t\t\t\t\tlocale: user.locale,\n\t\t\t\t\tdata: {},\n\t\t\t\t\trecipient: { email: user.email, phone: null, deviceToken: null },\n\t\t\t\t} satisfies ICourierMessage));\n\n\t\t\treturn setApiResponse(HTTP.OK, 'MFA_DISABLED', 'MFA disabled successfully.');\n\t\t},\n\t};\n}\n","import type { LocationResolver } from './services/request-location';\nimport type { IAuthRateLimitConfig } from './services/rate-limit';\nexport const DEFAULT_VERIFICATION_COOLDOWN = 5 * 60 * 1000; // 5 minutes\nexport const DEFAULT_SESSION_DURATION = '7d';\n\n// Boot-time only — never resolvable at runtime\nexport interface IAuthSecrets {\n\tjwtSecret: string;\n\t// Optional 32-byte key (64 hex chars, e.g. `openssl rand -hex 32`) that\n\t// encrypts TOTP secrets at rest (AES-256-GCM). Unset → secrets are stored\n\t// plaintext (backward-compatible). Set it in production; losing it makes\n\t// existing MFA secrets unrecoverable (users must re-enroll).\n\tmfaSecretKey?: string;\n\tgoogle?: {\n\t\tclientId: string;\n\t\tclientSecret: string;\n\t\tredirectUri: string;\n\t};\n\t// Sign in with Apple. Unlike Google, Apple's client secret is not a static\n\t// string — it's a short-lived ES256 JWT minted from a .p8 key at token\n\t// exchange, so the config carries the signing material, not a secret.\n\tapple?: {\n\t\t// Services ID (Sign in with Apple identifier) — the `aud` for the WEB\n\t\t// redirect flow and the `sub` of the client-secret JWT.\n\t\tclientId: string;\n\t\t// Apple Developer Team ID — the `iss` of the client-secret JWT.\n\t\tteamId: string;\n\t\t// Key ID of the .p8 signing key — the `kid` header of the client secret.\n\t\tkeyId: string;\n\t\t// Contents of the .p8 private key (PEM, `-----BEGIN PRIVATE KEY----- …`).\n\t\tprivateKey: string;\n\t\t// Web form_post callback URL registered with Apple.\n\t\tredirectUri: string;\n\t\t// iOS bundle identifiers whose NATIVE identityToken we accept. Native Sign\n\t\t// in with Apple mints id_tokens with `aud` = the app's bundle id (NOT the\n\t\t// Services ID), so each must be allow-listed for POST /auth/apple/native.\n\t\tnativeClientIds?: string[];\n\t};\n}\n\n// Behavioral — safe to expose to admin dashboard\nexport interface IAuthRuntimeConfig {\n\tsessionDuration?: string;\n\tverificationCooldown?: number;\n\tmfa?: boolean;\n\trequireVerification?: boolean;\n}\n\n// Type-checked: adding/renaming a field in IAuthRuntimeConfig breaks this at compile time\nexport const AUTH_CONFIG_KEYS: Record<keyof IAuthRuntimeConfig, string> = {\n\tsessionDuration: 'auth.session.duration',\n\tverificationCooldown: 'auth.verification.cooldown',\n\tmfa: 'auth.mfa.enabled',\n\trequireVerification: 'auth.verification.required',\n} as const;\n\nexport const MESSAGE_KEYS = {\n\temailRegistration: 'email-registration',\n\temailVerification: 'email-verification',\n\tpasswordReset: 'password-reset',\n\tphoneOtp: 'phone-otp',\n\tmfaEnabled: 'mfa-enabled',\n\tmfaDisabled: 'mfa-disabled',\n\tmfaBackupCodesRegenerated: 'mfa-backup-codes-regenerated',\n\temailChanged: 'email-changed',\n\tphoneChanged: 'phone-changed',\n\t// Changing HOW an account can be signed into is a security event for the\n\t// account's owner, who may not be the person doing it. Both directions are\n\t// notified for the same reason password changes are.\n\t// A password was REVOKED because the account it sat on had never been\n\t// verified and an OAuth identity proved ownership of the mailbox. The\n\t// recipient is now known to own that address, so this reaches the right\n\t// person — and it is the only way they learn a password stopped working.\n\tpasswordRevoked: 'password-revoked',\n\t// Welcome for an account CREATED by an OAuth sign-in. Distinct from\n\t// emailRegistration, which carries a verification PIN — an OAuth signup\n\t// arrives already verified by the provider and has nothing to confirm.\n\toauthRegistration: 'oauth-registration',\n\toauthLinked: 'oauth-linked',\n\toauthUnlinked: 'oauth-unlinked',\n} as const;\n\nexport type AuthMessageKey = (typeof MESSAGE_KEYS)[keyof typeof MESSAGE_KEYS];\n\nexport const EVENT_KEYS = {\n\tuserRegistered: 'fonderie.user.registered',\n\tuserDeleted: 'fonderie.user.deleted',\n\t// Emitted by purgeSoftDeletedUsers (given a bus) once the row is hard-deleted.\n\tuserPurged: 'fonderie.user.purged',\n\temailVerified: 'fonderie.user.email_verified',\n\tpasswordChanged: 'fonderie.user.password_changed',\n} as const;\n\nexport type AuthEventKey = (typeof EVENT_KEYS)[keyof typeof EVENT_KEYS];\n\nexport interface IAuthConfig extends IAuthSecrets, IAuthRuntimeConfig {\n\t// Optional resolver: where is this request from? Auth calls it when it\n\t// records an auth event — every login attempt, every registration, and every\n\t// new session — and stores the result on that row (country / region / city\n\t// / time zone, plus network facts if the resolver knows them). Called at\n\t// most once per request. Absent ⇒ no location, exactly as before. The result\n\t// is sanitized and bounded; a resolver that throws or takes longer than\n\t// 500 ms leaves the row without a location and never fails the request.\n\t// In-process resolvers never approach that; the cap exists for a DB-backed\n\t// or hosted-API resolver (richer data: ISP, ASN, proxy/VPN).\n\t// On Vercel/Cloudflare: ({ headers }) => geoFromHeaders(headers, { trust })\n\t// from @fonderie/geo — zero infrastructure.\n\tlocation?: LocationResolver;\n\t// Adds the Secure attribute to auth cookies. Defaults to\n\t// NODE_ENV === 'production'; set explicitly when that heuristic is wrong.\n\tsecureCookies?: boolean;\n\t// Brute-force protection — ON by default, backed by the module's own\n\t// store adapter (distributed-correct across instances with zero config).\n\t// Inject a store (e.g. RedisStore) for high-throughput deployments,\n\t// override individual rules, or set false to disable entirely.\n\trateLimit?: IAuthRateLimitConfig | false;\n\t// Access-token lifetime (jsonwebtoken duration string). Default '24h'.\n\t// Access tokens are session-bound and die on logout / rotation /\n\t// password change regardless of this value; shorten it to bound the\n\t// window of a stolen token whose session is still alive.\n\taccessTokenDuration?: string;\n\tproviders: ('email' | 'phone' | 'google' | 'github' | 'apple')[];\n\tappName?: string;\n\t// Base URL for the password-reset LINK. When set, forgot-password emits a\n\t// ready-built `resetUrl` (base + `token=<high-entropy token>`) in the\n\t// notification payload, so a template can offer a click-to-reset link\n\t// instead of only the 6-digit code. Unset → resetUrl is '' and only the\n\t// pin flows (unchanged behaviour). The token backs POST /auth/email/reset\n\t// with `{ token, password }`.\n\tpasswordResetUrl?: string;\n\tresolve?: (ctx: { meta: Record<string, unknown> }) => Partial<IAuthRuntimeConfig>;\n\t// Override the HTTP path (and optionally method) of any auth route, keyed by a\n\t// stable id. Lets an app match an existing frontend's contract without a\n\t// gateway/shim, e.g. `{ forgotPassword: '/auth/forgot-password', updateProfile:\n\t// { method: 'PATCH', path: '/users/me' } }`. A bare string overrides the path;\n\t// an object can also change the method. Unset routes keep their defaults.\n\troutes?: Partial<Record<AuthRouteId, AuthRouteOverride>>;\n\t// Migrating an existing app onto Fonderie auth? Fonderie stores bcrypt\n\t// hashes, so a user imported with a foreign hash (argon2, scrypt, pbkdf2,\n\t// a framework's format, …) can't log in via the built-in bcrypt check.\n\t// Provide `legacyVerify` to validate that foreign hash on login; on the\n\t// first successful login Fonderie transparently re-stores the password as\n\t// bcrypt (rehash-on-login), so the legacy verifier is only ever hit once\n\t// per migrated user. Return true iff `plain` matches `hash`. If the imported\n\t// hashes are already bcrypt, you don't need this — the built-in check\n\t// accepts them and re-stores at the current cost factor automatically.\n\tlegacyVerify?: (plain: string, hash: string) => boolean | Promise<boolean>;\n\t// SAR export contributors from other modules (see IDataExportContributor).\n\tdataExportContributors?: IDataExportContributor[];\n}\n\n// A module's contribution to the per-user data export (SAR). The app wires\n// these (e.g. from @fonderie/workspaces) so auth can aggregate data owned by\n// other packages without importing them.\nexport interface IDataExportContributor {\n\tname: string;\n\tcollect: (userId: string) => Promise<unknown> | unknown;\n}\n\n// Stable ids for every auth route, for the `routes` path/method override map.\nexport type AuthRouteId =\n\t| 'providers' | 'unlinkOauth'\n\t| 'register' | 'login' | 'refresh'\n\t| 'forgotPassword' | 'resetPassword'\n\t| 'verifyEmail' | 'sendVerification'\n\t| 'logout'\n\t| 'me' | 'updateProfile' | 'updatePreferences' | 'updateEmail' | 'updatePhone' | 'changePassword' | 'deleteMe' | 'exportMe'\n\t| 'loginHistory' | 'listSessions' | 'terminateSession' | 'terminateOtherSessions'\n\t| 'mfaSetup' | 'mfaVerify' | 'mfaDisable' | 'mfaBackupCodes';\n\nexport type AuthRouteOverride = string | { method?: string; path?: string };\n","import { randomUUID } from 'node:crypto';\nimport jwt, { type SignOptions } from 'jsonwebtoken';\n\nimport type { IAuthConfig } from '../config';\n\nexport interface TokenPair {\n\taccessToken: string;\n\trefreshToken: string;\n\t// Server-side session id both tokens are bound to. Store it on the\n\t// fonderie_sessions row — deleting that row revokes the access token\n\t// immediately, not just the refresh token.\n\tsid: string;\n}\n\nexport interface IAccessPayload {\n\tsub: string; // userId\n\ttype: 'access';\n\tloginMethod: 'email' | 'phone' | 'google' | 'apple';\n\tphoneVerified: boolean;\n\tmfaPending?: boolean;\n\t// Absent only on legacy tokens issued before session binding and on\n\t// short-lived mfaPending tokens (no session exists yet at that point).\n\tsid?: string;\n}\n\nexport interface IRefreshPayload {\n\tsub: string;\n\ttype: 'refresh';\n\tloginMethod: 'email' | 'phone' | 'google' | 'apple';\n\tphoneVerified: boolean;\n\tsid?: string;\n}\n\nexport interface ITokenOptions {\n\tloginMethod: 'email' | 'phone' | 'google' | 'apple';\n\tphoneVerified?: boolean;\n}\n\nexport function issueMfaPendingToken(\n\tuserId: string,\n\tconfig: IAuthConfig,\n\tloginMethod: 'email' | 'phone',\n): string {\n\treturn jwt.sign(\n\t\t{\n\t\t\tsub: userId,\n\t\t\ttype: 'access',\n\t\t\tloginMethod,\n\t\t\tphoneVerified: false,\n\t\t\tmfaPending: true,\n\t\t} satisfies IAccessPayload,\n\t\tconfig.jwtSecret,\n\t\t{ expiresIn: '5m' },\n\t);\n}\n\nexport function issueTokenPair(\n\tuserId: string,\n\tconfig: IAuthConfig,\n\toptions: ITokenOptions,\n): TokenPair {\n\tconst duration = config.sessionDuration ?? '7d';\n\tconst accessDuration = config.accessTokenDuration ?? '24h';\n\tconst loginMethod = options.loginMethod;\n\tconst phoneVerified = options.phoneVerified ?? false;\n\tconst sid = randomUUID();\n\n\tconst accessToken = jwt.sign(\n\t\t{ sub: userId, type: 'access', loginMethod, phoneVerified, sid } satisfies IAccessPayload,\n\t\tconfig.jwtSecret,\n\t\t{ expiresIn: accessDuration } as SignOptions,\n\t);\n\n\tconst refreshToken = jwt.sign(\n\t\t{ sub: userId, type: 'refresh', loginMethod, phoneVerified, sid } satisfies IRefreshPayload,\n\t\tconfig.jwtSecret,\n\t\t{ expiresIn: duration } as SignOptions,\n\t);\n\n\treturn { accessToken, refreshToken, sid };\n}\n\nexport function refreshTokenExpiry(token: string): Date {\n\tconst decoded = jwt.decode(token) as { exp?: number } | null;\n\treturn decoded?.exp\n\t\t? new Date(decoded.exp * 1000)\n\t\t: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);\n}\n\nexport function verifyToken(\n\ttoken: string,\n\tconfig: IAuthConfig,\n): IAccessPayload | IRefreshPayload | null {\n\ttry {\n\t\treturn jwt.verify(token, config.jwtSecret) as IAccessPayload | IRefreshPayload;\n\t} catch {\n\t\treturn null;\n\t}\n}\n","import { createHmac, randomBytes } from 'node:crypto';\n\nimport { constantTimeEqual } from '@fonderie/core';\n\n// ── TOTP (RFC 6238) — no external dependency ─────────────────────\n\nconst STEP = 30; // seconds per window\nconst DRIFT = 1; // ±1 window tolerance\nconst DIGITS = 6;\n\nfunction hotp(secret: string, counter: number): string {\n\tconst buf = Buffer.alloc(8);\n\tlet c = counter;\n\n\tfor (let i = 7; i >= 0; i--) {\n\t\tbuf[i] = c & 0xff;\n\t\tc >>= 8;\n\t}\n\n\tconst key = Buffer.from(base32Decode(secret));\n\tconst hmac = createHmac('sha1', key).update(buf).digest();\n\tconst offset = (hmac[19] ?? 0) & 0x0f;\n\tconst code =\n\t\t((((hmac[offset] ?? 0) & 0x7f) << 24) |\n\t\t\t(((hmac[offset + 1] ?? 0) & 0xff) << 16) |\n\t\t\t(((hmac[offset + 2] ?? 0) & 0xff) << 8) |\n\t\t\t((hmac[offset + 3] ?? 0) & 0xff)) %\n\t\tMath.pow(10, DIGITS);\n\n\treturn code.toString().padStart(DIGITS, '0');\n}\n\nfunction timeCounter(): number {\n\treturn Math.floor(Date.now() / 1000 / STEP);\n}\n\n// RFC 4648 base32 decode — uppercase alphabet only\nfunction base32Decode(input: string): Buffer {\n\tconst ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n\tconst clean = input.toUpperCase().replace(/=+$/, '');\n\tlet bits = 0;\n\tlet value = 0;\n\n\tconst output: number[] = [];\n\tfor (const char of clean) {\n\t\tconst idx = ALPHABET.indexOf(char);\n\t\tif (idx === -1) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tvalue = (value << 5) | idx;\n\t\tbits += 5;\n\n\t\tif (bits >= 8) {\n\t\t\toutput.push((value >>> (bits - 8)) & 0xff);\n\t\t\tbits -= 8;\n\t\t}\n\t}\n\n\treturn Buffer.from(output);\n}\n\nfunction base32Encode(buf: Buffer): string {\n\tconst ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n\tlet bits = 0;\n\tlet value = 0;\n\tlet output = '';\n\n\tfor (const byte of buf) {\n\t\tvalue = (value << 8) | byte;\n\t\tbits += 8;\n\t\twhile (bits >= 5) {\n\t\t\toutput += ALPHABET[(value >>> (bits - 5)) & 0x1f];\n\t\t\tbits -= 5;\n\t\t}\n\t}\n\n\tif (bits > 0) {\n\t\toutput += ALPHABET[(value << (5 - bits)) & 0x1f];\n\t}\n\n\treturn output;\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\nexport function generateTotpSecret(): string {\n\treturn base32Encode(randomBytes(20));\n}\n\nexport function generateTotpUri(email: string, secret: string, issuer: string): string {\n\tconst params = new URLSearchParams({\n\t\tsecret,\n\t\tissuer,\n\t\talgorithm: 'SHA1',\n\t\tdigits: String(DIGITS),\n\t\tperiod: String(STEP),\n\t});\n\n\treturn `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(email)}?${params}`;\n}\n\nexport function verifyTotpToken(token: string, secret: string): boolean {\n\tconst t = timeCounter();\n\tfor (let i = -DRIFT; i <= DRIFT; i++) {\n\t\t// Constant-time compare (shared) — a plain === leaks how many leading\n\t\t// digits matched through response timing.\n\t\tif (constantTimeEqual(hotp(secret, t + i), token)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\n\treturn false;\n}\n\nexport function generateTotpCode(secret: string): string {\n\treturn hotp(secret, timeCounter());\n}\n\nexport function generateBackupCodes(count = 8): string[] {\n\treturn Array.from({ length: count }, () => randomBytes(4).toString('hex').toUpperCase());\n}\n","import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\n\n// At-rest encryption for TOTP secrets. Unlike passwords, a TOTP secret must stay\n// reversible (the server recomputes codes from it), so it can't be hashed — it's\n// encrypted with a key the app holds instead. AES-256-GCM; output is\n// `mfa.v1:iv:tag:ciphertext`, all hex. Authenticated (tampering fails on decrypt).\n//\n// Backward compatible by design:\n// - No key configured → passthrough. Secrets are stored plaintext exactly as\n// before this change, so upgrading without a key is a no-op.\n// - With a key, `decrypt` still passes through any value lacking the `mfa.v1:`\n// prefix, so pre-existing plaintext secrets keep working and are re-encrypted\n// the next time the user re-runs MFA setup (lazy migration). A base32 TOTP\n// secret never starts with the prefix, so the two are unambiguous.\nconst PREFIX = 'mfa.v1:';\n\nexport interface IMfaCipher {\n\tencrypt(plain: string): string;\n\tdecrypt(stored: string): string;\n}\n\nexport function makeMfaCipher(keyHex?: string): IMfaCipher {\n\tif (!keyHex) {\n\t\t// Passthrough: no key, no encryption. `decrypt` is identity — a legacy\n\t\t// plaintext secret returns unchanged; a prefixed value (written when a key\n\t\t// was previously set, now removed) can't be recovered and will simply fail\n\t\t// TOTP verification, which is the correct outcome for a lost key.\n\t\treturn { encrypt: (plain) => plain, decrypt: (stored) => stored };\n\t}\n\n\tconst key = Buffer.from(keyHex, 'hex');\n\tif (key.length !== 32) {\n\t\tthrow new Error('[auth] mfaSecretKey must be 32 bytes (64 hex chars, e.g. `openssl rand -hex 32`)');\n\t}\n\n\treturn {\n\t\tencrypt(plain) {\n\t\t\tconst iv = randomBytes(12);\n\t\t\tconst cipher = createCipheriv('aes-256-gcm', key, iv);\n\t\t\tconst enc = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]);\n\t\t\tconst tag = cipher.getAuthTag();\n\t\t\treturn `${PREFIX}${iv.toString('hex')}:${tag.toString('hex')}:${enc.toString('hex')}`;\n\t\t},\n\t\tdecrypt(stored) {\n\t\t\tif (!stored.startsWith(PREFIX)) return stored; // legacy plaintext — pass through\n\t\t\tconst [ivHex, tagHex, dataHex] = stored.slice(PREFIX.length).split(':');\n\t\t\tif (!ivHex || !tagHex || !dataHex) {\n\t\t\t\tthrow new Error('[auth] malformed mfa secret ciphertext');\n\t\t\t}\n\t\t\tconst decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(ivHex, 'hex'));\n\t\t\tdecipher.setAuthTag(Buffer.from(tagHex, 'hex'));\n\t\t\treturn Buffer.concat([\n\t\t\t\tdecipher.update(Buffer.from(dataHex, 'hex')),\n\t\t\t\tdecipher.final(),\n\t\t\t]).toString('utf8');\n\t\t},\n\t};\n}\n","import { stringOrEmpty, booleanOrFalse, dateOrEmpty } from '@fonderie/core';\n\nimport type { IUser, IUserPreferences } from '../types';\n\nexport interface IUserDTO {\n\tid: string;\n\temail: string;\n\tfirstName: string;\n\tlastName: string;\n\tphone: string;\n\tprofileImageUrl: string;\n\tisActive: boolean;\n\tlastLogin: string;\n\tpreferences: IUserPreferences;\n\tisEmailVerified: boolean;\n\tisPhoneVerified: boolean;\n\tmfaEnabled: boolean;\n\t// OAuth provider linked to this account ('google', 'apple'), or '' if none.\n\tprovider: string;\n\t// Whether a password is set. Together with `provider` this is everything a\n\t// settings screen needs to render sign-in methods AND to know that\n\t// disconnecting the provider is possible — an account with no password has\n\t// no other credential, so unlinking is refused (409 PASSWORD_REQUIRED).\n\t// Exposed as a boolean precisely so the hash never can be.\n\thasPassword: boolean;\n\tsuspended: boolean;\n\twhitelist: boolean;\n\tipWhitelist: string[];\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\nconst DEFAULT_PREFERENCES: IUserPreferences = {\n\tlocale: 'en-US',\n\ttimezone: 'UTC',\n\tnotifications: { email: true, inApp: true, sms: false, push: false },\n\temailDigest: 'immediate',\n\tdateFormat: 'MM/DD/YYYY',\n\ttimeFormat: 'hh:mm A',\n};\n\n// Historical rows may hold garbage preferences (the update schema accepted\n// `unknown` for four keys until it was typed). Only well-typed values\n// survive into the DTO, and notifications deep-merge over the defaults so a\n// partial stored object can't shrink the shape the client type promises.\nfunction cleanPreferences(prefs: IUserPreferences): Partial<IUserPreferences> {\n\tconst out: Partial<IUserPreferences> = {};\n\tif (typeof prefs.locale === 'string') out.locale = prefs.locale;\n\tif (typeof prefs.timezone === 'string') out.timezone = prefs.timezone;\n\tif (typeof prefs.emailDigest === 'string') out.emailDigest = prefs.emailDigest;\n\tif (typeof prefs.dateFormat === 'string') out.dateFormat = prefs.dateFormat;\n\tif (typeof prefs.timeFormat === 'string') out.timeFormat = prefs.timeFormat;\n\treturn out;\n}\n\nfunction cleanNotifications(value: unknown): Partial<IUserPreferences['notifications']> {\n\tif (!value || typeof value !== 'object') return {};\n\tconst out: Partial<IUserPreferences['notifications']> = {};\n\tfor (const key of ['email', 'inApp', 'sms', 'push'] as const) {\n\t\tconst v = (value as Record<string, unknown>)[key];\n\t\tif (typeof v === 'boolean') out[key] = v;\n\t}\n\treturn out;\n}\n\nexport function toUserDTO(user: IUser, phoneVerified = false): IUserDTO {\n\tconst prefs = user.preferences ?? ({} as IUserPreferences);\n\tconst cleaned = cleanPreferences(prefs);\n\treturn {\n\t\tid: stringOrEmpty(user.id),\n\t\temail: stringOrEmpty(user.email),\n\t\tfirstName: stringOrEmpty(user.firstName),\n\t\tlastName: stringOrEmpty(user.lastName),\n\t\tphone: stringOrEmpty(user.phone),\n\t\tprofileImageUrl: stringOrEmpty(user.profileImageUrl),\n\t\tisActive: typeof user.isActive === 'boolean' ? user.isActive : true,\n\t\tlastLogin: dateOrEmpty(user.lastLogin),\n\t\tpreferences: {\n\t\t\t...DEFAULT_PREFERENCES,\n\t\t\t...cleaned,\n\t\t\tnotifications: {\n\t\t\t\t...DEFAULT_PREFERENCES.notifications,\n\t\t\t\t...cleanNotifications(prefs.notifications),\n\t\t\t},\n\t\t\tlocale: user.locale || cleaned.locale || 'en-US',\n\t\t\ttimezone: user.timezone || cleaned.timezone || 'UTC',\n\t\t},\n\t\tisEmailVerified: user.emailVerifiedAt !== null,\n\t\tisPhoneVerified: phoneVerified,\n\t\tmfaEnabled: booleanOrFalse(user.mfaEnabled),\n\t\tprovider: stringOrEmpty(user.provider),\n\t\thasPassword: user.passwordHash !== null && user.passwordHash !== undefined,\n\t\tsuspended: booleanOrFalse(user.suspended),\n\t\twhitelist: booleanOrFalse(user.whitelist),\n\t\tipWhitelist: Array.isArray(user.ipWhitelist) ? user.ipWhitelist : [],\n\t\tcreatedAt: dateOrEmpty(user.createdAt),\n\t\tupdatedAt: dateOrEmpty(user.updatedAt),\n\t};\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { IUser } from '../types';\n\nexport interface IUserUpdateFields {\n\tfirstName?: string;\n\tlastName?: string;\n\tphoneNumber?: string;\n\tavatarUrl?: string;\n\tlocale?: string;\n\ttimezone?: string;\n\tpreferences?: unknown;\n}\n\nconst USER_COLUMNS = `\n\tid,\n\temail,\n\tpassword_hash AS \"passwordHash\",\n\tfirst_name AS \"firstName\",\n\tlast_name AS \"lastName\",\n\tphone,\n\tprofile_image_url AS \"profileImageUrl\",\n\tlocale,\n\ttimezone,\n\tis_active AS \"isActive\",\n\tlast_login AS \"lastLogin\",\n\tpreferences,\n\tsuspended,\n\twhitelist,\n\tip_whitelist AS \"ipWhitelist\",\n\tmfa_enabled AS \"mfaEnabled\",\n\temail_verified_at AS \"emailVerifiedAt\",\n\tprovider,\n\tdeleted_at AS \"deletedAt\",\n\tcreated_at AS \"createdAt\",\n\tupdated_at AS \"updatedAt\"\n`;\n\nexport class UserModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\t// One page of users, newest first. Keyset-paginated on (created_at, id) —\n\t// the same cursor contract as login history and the audit log. The +1\n\t// over-fetch that detects a next page lives here so no outer clamp can\n\t// shave it off. Soft-deleted rows are excluded, as in every other finder.\n\tasync list(query: IUserListQuery = {}): Promise<IUserPage> {\n\t\tconst limit = Math.min(query.limit ?? 50, MAX_LIST_LIMIT);\n\t\tconst params: unknown[] = [];\n\t\tconst where: string[] = [query.deleted ? 'deleted_at IS NOT NULL' : 'deleted_at IS NULL'];\n\n\t\tif (query.cursor) {\n\t\t\tparams.push(query.cursor.createdAt, query.cursor.id);\n\t\t\twhere.push(`(created_at, id) < ($${params.length - 1}::timestamptz, $${params.length}::uuid)`);\n\t\t}\n\n\t\tparams.push(limit + 1);\n\t\tconst rows = await this.store.query<IUser & { createdAtRaw?: string }>(\n\t\t\t`SELECT ${USER_COLUMNS}, created_at::text AS \"createdAtRaw\"\n\t\t\t FROM fonderie_users\n\t\t\t WHERE ${where.join(' AND ')}\n\t\t\t ORDER BY created_at DESC, id DESC\n\t\t\t LIMIT $${params.length}`,\n\t\t\tparams,\n\t\t);\n\t\treturn { users: rows.slice(0, limit), hasMore: rows.length > limit };\n\t}\n\n\t// For operator views only: a soft-deleted account still resolves, with\n\t// deletedAt set, so the console can say \"deleted on …\" instead of \"no such\n\t// user\". Nothing that authenticates or acts on an account may use this.\n\tasync findByIdIncludingDeleted(id: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findById(id: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE id = $1 AND deleted_at IS NULL`,\n\t\t\t[id],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findByEmail(email: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE email = $1 AND deleted_at IS NULL`,\n\t\t\t[email],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findByPhone(phone: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NULL`,\n\t\t\t[phone],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findOrCreateByPhone(\n\t\tphone: string,\n\t\tfirstName: string | null = null,\n\t\tlastName: string | null = null,\n\t\t// Only on insert: an existing account keeps the locale it chose.\n\t\tlocale: string | null = null,\n\t): Promise<{ id: string }> {\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`INSERT INTO fonderie_users (phone, first_name, last_name, locale)\n\t\t\tVALUES ($1, $2, $3, COALESCE($4, 'en-US'))\n\t\t\tON CONFLICT (phone) DO UPDATE\n\t\t\tSET first_name = COALESCE(EXCLUDED.first_name, fonderie_users.first_name),\n\t\t\t last_name = COALESCE(EXCLUDED.last_name, fonderie_users.last_name),\n\t\t\t updated_at = now()\n\t\t\tRETURNING id`,\n\t\t\t[phone, firstName, lastName, locale],\n\t\t);\n\t\treturn row!;\n\t}\n\n\tasync create(\n\t\temail: string,\n\t\tpasswordHash: string,\n\t\tfirstName: string | null,\n\t\tlastName: string | null,\n\t\t// The sign-up language, else the app's system locale; null keeps the\n\t\t// column default (en-US) for callers that predate locales.\n\t\tlocale: string | null = null,\n\t): Promise<{ id: string } | null> {\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`INSERT INTO fonderie_users (email, password_hash, first_name, last_name, locale)\n\t\t\tVALUES ($1, $2, $3, $4, COALESCE($5, 'en-US'))\n\t\t\tRETURNING id`,\n\t\t\t[email.toLowerCase().trim(), passwordHash, firstName, lastName, locale],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync update(id: string, fields: IUserUpdateFields): Promise<{ id: string } | null> {\n\t\tconst columnMap: Record<string, string> = {\n\t\t\tfirstName: 'first_name',\n\t\t\tlastName: 'last_name',\n\t\t\tphoneNumber: 'phone',\n\t\t\tavatarUrl: 'profile_image_url',\n\t\t\tlocale: 'locale',\n\t\t\ttimezone: 'timezone',\n\t\t\tpreferences: 'preferences',\n\t\t};\n\n\t\tconst sets: string[] = [];\n\t\tconst values: unknown[] = [];\n\n\t\tfor (const [key, col] of Object.entries(columnMap)) {\n\t\t\tif ((fields as Record<string, unknown>)[key] !== undefined) {\n\t\t\t\tvalues.push((fields as Record<string, unknown>)[key]);\n\t\t\t\tsets.push(`${col} = $${values.length}`);\n\t\t\t}\n\t\t}\n\n\t\tvalues.push(id);\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`UPDATE fonderie_users SET ${sets.join(', ')}, updated_at = now() WHERE id = $${values.length} AND deleted_at IS NULL RETURNING id`,\n\t\t\tvalues,\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\t/**\n\t * Clear the OAuth provider, but ONLY when the account can still be signed\n\t * into afterwards.\n\t *\n\t * An account created through Google has no password. Unlinking it without\n\t * checking would not be \"removing a sign-in method\" — it would be locking\n\t * the owner out permanently, which is account deletion wearing a friendlier\n\t * label. The guard lives in the WHERE clause so the check and the write are\n\t * one atomic statement: a password cannot be removed between them.\n\t *\n\t * Returns false when the account has no password (nothing was changed) so\n\t * the caller can say why rather than reporting a silent success.\n\t */\n\tasync clearProvider(id: string): Promise<boolean> {\n\t\tconst rows = await this.store.query<{ id: string }>(\n\t\t\t`UPDATE fonderie_users\n\t\t\t SET provider = NULL, provider_id = NULL\n\t\t\t WHERE id = $1\n\t\t\t AND password_hash IS NOT NULL\n\t\t\t AND provider IS NOT NULL\n\t\t\t RETURNING id`,\n\t\t\t[id],\n\t\t);\n\t\treturn rows.length > 0;\n\t}\n\n\tasync updatePassword(id: string, passwordHash: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_users SET password_hash = $1 WHERE id = $2`, [\n\t\t\tpasswordHash,\n\t\t\tid,\n\t\t]);\n\t}\n\n\tasync markEmailVerified(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET email_verified_at = now(), updated_at = now() WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\t// The operator's lock: login, refresh and the session middleware all refuse a\n\t// suspended user. Returns false when there is no such user.\n\tasync setSuspended(id: string, suspended: boolean): Promise<boolean> {\n\t\tconst rows = await this.store.query<{ id: string }>(\n\t\t\t`UPDATE fonderie_users SET suspended = $2, updated_at = now() WHERE id = $1 AND deleted_at IS NULL RETURNING id`,\n\t\t\t[id, suspended],\n\t\t);\n\t\treturn rows.length > 0;\n\t}\n\n\tasync softDelete(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET deleted_at = now(), updated_at = now() WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync saveMfaSecret(id: string, secret: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_users SET mfa_secret = $1 WHERE id = $2`, [secret, id]);\n\t}\n\n\tasync saveMfaPendingSecret(id: string, secret: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users\n\t\t\t SET mfa_secret_pending = $1, mfa_secret_pending_expires_at = now() + interval '15 minutes'\n\t\t\t WHERE id = $2`,\n\t\t\t[secret, id],\n\t\t);\n\t}\n\n\tasync getMfaPendingSecret(id: string): Promise<string | null> {\n\t\tconst [row] = await this.store.query<{ mfa_secret_pending: string | null }>(\n\t\t\t`SELECT mfa_secret_pending FROM fonderie_users\n\t\t\t WHERE id = $1 AND mfa_secret_pending_expires_at > now()`,\n\t\t\t[id],\n\t\t);\n\t\treturn row?.mfa_secret_pending ?? null;\n\t}\n\n\tasync confirmMfaSecret(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users\n\t\t\t SET mfa_secret = mfa_secret_pending,\n\t\t\t mfa_secret_pending = NULL,\n\t\t\t mfa_secret_pending_expires_at = NULL,\n\t\t\t mfa_enabled = true,\n\t\t\t updated_at = now()\n\t\t\t WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync enableMfa(id: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_users SET mfa_enabled = true WHERE id = $1`, [id]);\n\t}\n\n\tasync disableMfa(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET mfa_enabled = false, mfa_secret = NULL, updated_at = now() WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync updateEmail(id: string, email: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET email = $1, email_verified_at = NULL, updated_at = now() WHERE id = $2`,\n\t\t\t[email.toLowerCase().trim(), id],\n\t\t);\n\t}\n\n\tasync updatePhone(id: string, phone: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET phone = $1, updated_at = now() WHERE id = $2`,\n\t\t\t[phone, id],\n\t\t);\n\t}\n\n\tasync updatePreferences(\n\t\tid: string,\n\t\tfields: {\n\t\t\tlocale?: string;\n\t\t\ttimezone?: string;\n\t\t\tpatch?: Record<string, unknown>;\n\t\t},\n\t): Promise<{ id: string } | null> {\n\t\tconst sets: string[] = [];\n\t\tconst values: unknown[] = [];\n\n\t\tif (fields.locale !== undefined) {\n\t\t\tvalues.push(fields.locale);\n\t\t\tsets.push(`locale = $${values.length}`);\n\t\t}\n\t\tif (fields.timezone !== undefined) {\n\t\t\tvalues.push(fields.timezone);\n\t\t\tsets.push(`timezone = $${values.length}`);\n\t\t}\n\t\tif (fields.patch && Object.keys(fields.patch).length > 0) {\n\t\t\tvalues.push(JSON.stringify(fields.patch));\n\t\t\tsets.push(`preferences = preferences || $${values.length}::jsonb`);\n\t\t}\n\n\t\tif (sets.length === 0) return null;\n\n\t\tvalues.push(id);\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`UPDATE fonderie_users SET ${sets.join(', ')}, updated_at = now() WHERE id = $${values.length} AND deleted_at IS NULL RETURNING id`,\n\t\t\tvalues,\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync getMfaSecret(id: string): Promise<string | null> {\n\t\tconst [row] = await this.store.query<{ mfa_secret: string | null }>(\n\t\t\t`SELECT mfa_secret FROM fonderie_users WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t\treturn row?.mfa_secret ?? null;\n\t}\n\n\t/**\n\t * Link an OAuth identity to the account owning `email`, creating it if there\n\t * is none.\n\t *\n\t * Returns what HAPPENED, not just who it happened to. Three outcomes hide\n\t * behind one upsert, and callers must tell them apart:\n\t *\n\t * • `inserted` — a brand-new account. Whoever provisions on signup (the\n\t * personal workspace, a wallet, a welcome email) must run, exactly as\n\t * it does for a password registration. This is the case that was\n\t * previously invisible: the upsert returned an id and looked identical\n\t * to a returning user signing in, so OAuth signups silently skipped\n\t * every signup side effect.\n\t * • `previousProvider` differs — an existing account gained, or switched,\n\t * a way to sign in. A security event for the owner.\n\t * • `previousProvider` is the same — an ordinary login. Nothing to do, and\n\t * notifying here would email the user on every single sign-in.\n\t *\n\t * The prior state is read in a CTE of the SAME statement, so it sees the\n\t * snapshot from before the insert. Reading it in a separate query would\n\t * race two concurrent logins into both seeing \"new\".\n\t *\n\t * SECURITY — linking is BY EMAIL, so this merges into whatever row already\n\t * holds the address. If that row was never verified, its password was never\n\t * proven to belong to the mailbox; anyone can register an address they do\n\t * not own. The provider HAS proven ownership. So an unverified password is\n\t * revoked at the moment of linking: without that, linking sets\n\t * email_verified_at and the earlier registrant is left holding a working\n\t * password on an account now treated as verified.\n\t *\n\t * The legitimate user loses nothing but a password reset — they own the\n\t * mailbox. Someone who does not own it cannot receive that mail.\n\t */\n\tasync upsertByProvider(\n\t\temail: string,\n\t\tprovider: string,\n\t\tproviderId: string,\n\t): Promise<{\n\t\tid: string;\n\t\tinserted: boolean;\n\t\tpreviousProvider: string | null;\n\t\tclearedUnverifiedPassword: boolean;\n\t} | null> {\n\t\tconst [row] = await this.store.query<{\n\t\t\tid: string;\n\t\t\tinserted: boolean;\n\t\t\tpreviousProvider: string | null;\n\t\t\tclearedUnverifiedPassword: boolean;\n\t\t}>(\n\t\t\t`WITH prior AS (\n\t\t\t\tSELECT provider, email_verified_at, password_hash IS NOT NULL AS had_password\n\t\t\t\t FROM fonderie_users WHERE email = $1\n\t\t\t), upserted AS (\n\t\t\t\tINSERT INTO fonderie_users (email, email_verified_at, provider, provider_id)\n\t\t\t\tVALUES ($1, now(), $2, $3)\n\t\t\t\tON CONFLICT (email) DO UPDATE\n\t\t\t\tSET provider = $2, provider_id = $3,\n\t\t\t\t email_verified_at = COALESCE(fonderie_users.email_verified_at, now()),\n\t\t\t\t -- A password on an UNVERIFIED account is a claim, not a\n\t\t\t\t -- credential: nobody ever proved that mailbox belongs to\n\t\t\t\t -- whoever set it. The provider has now proven it belongs to\n\t\t\t\t -- the person signing in. Drop the unproven claim rather than\n\t\t\t\t -- promote it — otherwise linking marks the account verified\n\t\t\t\t -- and silently hands the earlier registrant a working\n\t\t\t\t -- password on a now-trusted account.\n\t\t\t\t password_hash = CASE\n\t\t\t\t WHEN fonderie_users.email_verified_at IS NULL THEN NULL\n\t\t\t\t ELSE fonderie_users.password_hash\n\t\t\t\t END\n\t\t\t\tRETURNING id\n\t\t\t)\n\t\t\tSELECT upserted.id,\n\t\t\t NOT EXISTS (SELECT 1 FROM prior) AS inserted,\n\t\t\t (SELECT provider FROM prior) AS \"previousProvider\",\n\t\t\t -- True only when a password was actually revoked, so the\n\t\t\t -- caller can tell the owner their sign-in method changed.\n\t\t\t COALESCE((SELECT had_password AND email_verified_at IS NULL FROM prior), false)\n\t\t\t AS \"clearedUnverifiedPassword\"\n\t\t\tFROM upserted`,\n\t\t\t[email, provider, providerId],\n\t\t);\n\t\treturn row ?? null;\n\t}\n}\n\nconst MAX_LIST_LIMIT = 200;\n\nexport interface IUserListQuery {\n\tlimit?: number;\n\tcursor?: { createdAt: string; id: string };\n\t// true ⇒ ONLY soft-deleted accounts (an operator view; every other finder\n\t// excludes them). They stay until the retention purge hard-deletes them.\n\tdeleted?: boolean;\n}\n\nexport interface IUserPage {\n\t// Carries createdAtRaw (created_at::text) so the cursor keeps the\n\t// microsecond precision a Date round-trip would drop.\n\tusers: Array<IUser & { createdAtRaw?: string }>;\n\thasMore: boolean;\n}\n","// Optional location enrichment for auth events (login history, registrations,\n// sessions).\n//\n// Auth never resolves a location itself and imports no geo code: the app hands\n// in a resolver (IAuthConfig.location). Typical choices — the platform's\n// edge headers via @fonderie/geo's geoFromHeaders (zero infrastructure), a\n// self-hosted table, or a hosted IP API that also knows the network (ISP, ASN,\n// proxy/VPN, hosting). Field names match @fonderie/geo's GeoLocation, so its\n// result can be returned as-is.\n//\n// Whatever the resolver returns is treated as untrusted input: every field is\n// type-checked and bounded before it is stored, and a resolver that throws or\n// stalls costs the login nothing — the row is written without a location.\n\n/** Where a login attempt came from. Every field is optional; unknown is null. */\nexport interface IRequestLocation {\n\tcountry?: string | null; // ISO-3166-1 alpha-2\n\tcountryName?: string | null;\n\tsubdivision?: string | null; // ISO-3166-2 region part (QC, CA, ENG)\n\tsubdivisionName?: string | null;\n\tcity?: string | null;\n\t/** Postal / ZIP code as the provider reports it (94043, K1A 0B1, SW1A). Approximate. */\n\tpostalCode?: string | null;\n\tcontinent?: string | null; // two-letter continent code\n\ttimeZone?: string | null; // IANA name\n\tlatitude?: number | null;\n\tlongitude?: number | null;\n\t/** How far off the point may be, in kilometres (MaxMind's accuracy_radius).\n\t * Lets a UI say \"near Mountain View (±20 km)\" instead of implying precision. */\n\taccuracyRadius?: number | null;\n\t/** A stable, language-neutral place key (MaxMind/GeoNames id). Lets a reader\n\t * see the stored place in their own language later, and lets an app map it\n\t * to its own regions. Names above are a snapshot; this is the reference. */\n\tgeonameId?: number | null;\n\t/** Network facts — only resolvers backed by an IP-intelligence API know these. */\n\tisp?: string | null;\n\torg?: string | null;\n\tasn?: string | null; // e.g. \"AS15169\"\n\tmobile?: boolean | null;\n\tproxy?: boolean | null;\n\thosting?: boolean | null;\n}\n\nexport interface ILocationRequest {\n\t/** The resolved client IP (adapter-resolved, proxy-aware), or null. */\n\tip: string | null;\n\t/** The incoming request's headers — for platform geolocation headers. */\n\theaders: Headers;\n}\n\nexport type LocationResolver = (\n\treq: ILocationRequest,\n) => IRequestLocation | null | undefined | Promise<IRequestLocation | null | undefined>;\n\n// How long auth waits for the resolver before writing the row without a\n// location. Internal, deliberately not exported. It is zero-cost for in-process\n// resolvers (edge headers return in microseconds) and exists for the ones that\n// are not: @fonderie/geo's PostgresGeoProvider does a DB query that can hang\n// on an exhausted pool, and an app may choose a hosted IP-intelligence API for\n// richer data (ISP, ASN, proxy/VPN, accuracy radius). A legitimate lookup\n// answers in a few to ~150 ms; the session write that triggers resolution is\n// on the login path, so the cap bounds the worst-case delay a slow provider\n// can add to a sign-in.\nconst LOCATION_TIMEOUT_MS = 500;\n\nconst TEXT_MAX = 128;\n\nfunction str(v: unknown, re?: RegExp): string | null {\n\tif (typeof v !== 'string') return null;\n\tconst t = v.trim();\n\tif (t.length === 0 || t.length > TEXT_MAX) return null;\n\treturn re && !re.test(t) ? null : t;\n}\nfunction upperCode(v: unknown, re: RegExp): string | null {\n\tconst t = str(v, re);\n\treturn t ? t.toUpperCase() : null;\n}\nfunction bool(v: unknown): boolean | null {\n\treturn typeof v === 'boolean' ? v : null;\n}\n// Two decimals ≈ 1 km: plenty for \"which city\", and a login history should not\n// keep more precision than the city it names.\nfunction coord(v: unknown, limit: number): number | null {\n\tif (typeof v !== 'number' || !Number.isFinite(v) || Math.abs(v) > limit) return null;\n\treturn Math.round(v * 100) / 100;\n}\n\n// Kilometres, a positive whole number. MaxMind's values run 1–1000; anything\n// beyond the planet's half-circumference is not a radius.\nfunction radius(v: unknown): number | null {\n\tif (typeof v !== 'number' || !Number.isFinite(v) || v <= 0 || v > 20_000) return null;\n\treturn Math.round(v);\n}\n\n// GeoNames ids are positive integers well inside 2^31.\nfunction placeId(v: unknown): number | null {\n\tconst n = typeof v === 'string' && /^\\d{1,10}$/.test(v) ? Number(v) : v;\n\treturn typeof n === 'number' && Number.isInteger(n) && n > 0 && n <= 2_147_483_647 ? n : null;\n}\n\n/** Keep only well-formed, bounded fields. Returns null when nothing survives. */\nexport function sanitizeLocation(input: unknown): IRequestLocation | null {\n\tif (!input || typeof input !== 'object') return null;\n\tconst i = input as Record<string, unknown>;\n\tconst out: IRequestLocation = {\n\t\tcountry: upperCode(i['country'], /^[A-Za-z]{2}$/),\n\t\tcountryName: str(i['countryName']),\n\t\tsubdivision: upperCode(i['subdivision'], /^[A-Za-z0-9]{1,3}$/),\n\t\tsubdivisionName: str(i['subdivisionName']),\n\t\tcity: str(i['city']),\n\t\tpostalCode: upperCode(i['postalCode'], /^[A-Za-z0-9][A-Za-z0-9 -]{0,11}$/),\n\t\tcontinent: upperCode(i['continent'], /^[A-Za-z]{2}$/),\n\t\ttimeZone: str(i['timeZone'], /^(UTC|[A-Za-z_]+(\\/[A-Za-z0-9_+-]+)+)$/),\n\t\tlatitude: coord(i['latitude'], 90),\n\t\tlongitude: coord(i['longitude'], 180),\n\t\taccuracyRadius: radius(i['accuracyRadius']),\n\t\tgeonameId: placeId(i['geonameId']),\n\t\tisp: str(i['isp']),\n\t\torg: str(i['org']),\n\t\tasn: upperCode(i['asn'], /^AS\\d{1,10}$/i),\n\t\tmobile: bool(i['mobile']),\n\t\tproxy: bool(i['proxy']),\n\t\thosting: bool(i['hosting']),\n\t};\n\t// Drop nulls so the stored JSON carries only what is known.\n\tconst known = Object.fromEntries(Object.entries(out).filter(([, v]) => v !== null)) as IRequestLocation;\n\treturn Object.keys(known).length > 0 ? known : null;\n}\n\n/** Run the resolver bounded by a timeout; never throws. */\n// One resolution per request: a sign-in writes a login-event row AND a session\n// row, and both ask. Keyed on the request's Headers object (unique per\n// request, collected with it), so the resolver — possibly a paid API — runs\n// once.\nconst perRequest = new WeakMap<Headers, { resolver: LocationResolver; result: Promise<IRequestLocation | null> }>();\n\nexport function resolveLocation(\n\tresolver: LocationResolver | undefined,\n\treq: ILocationRequest,\n\ttimeoutMs = LOCATION_TIMEOUT_MS,\n): Promise<IRequestLocation | null> {\n\tif (!resolver) return Promise.resolve(null);\n\tconst cached = perRequest.get(req.headers);\n\tif (cached && cached.resolver === resolver) return cached.result;\n\tconst result = resolveOnce(resolver, req, timeoutMs);\n\tperRequest.set(req.headers, { resolver, result });\n\treturn result;\n}\n\nasync function resolveOnce(\n\tresolver: LocationResolver,\n\treq: ILocationRequest,\n\ttimeoutMs: number,\n): Promise<IRequestLocation | null> {\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\ttry {\n\t\tconst result = await Promise.race([\n\t\t\tPromise.resolve().then(() => resolver(req)),\n\t\t\tnew Promise<null>((resolve) => {\n\t\t\t\ttimer = setTimeout(() => resolve(null), timeoutMs);\n\t\t\t}),\n\t\t]);\n\t\treturn sanitizeLocation(result);\n\t} catch (err) {\n\t\tconsole.warn('[auth] location resolver failed; recording without a location:', err);\n\t\treturn null;\n\t} finally {\n\t\tif (timer) clearTimeout(timer);\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IRequestMeta } from '../services/request-meta';\nimport { type IRequestLocation, type LocationResolver, resolveLocation } from '../services/request-location';\n\nexport class SessionModel {\n\t// `locate` is IAuthConfig.location. Absent ⇒ sessions carry no location.\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate locate?: LocationResolver,\n\t) {}\n\n\tasync create(\n\t\tuserId: string,\n\t\ttoken: string,\n\t\texpiresAt: Date,\n\t\tsid?: string,\n\t\tmeta?: IRequestMeta,\n\t): Promise<void> {\n\t\tconst location =\n\t\t\tthis.locate && meta?.headers\n\t\t\t\t? await resolveLocation(this.locate, { ip: meta.ipAddress, headers: meta.headers })\n\t\t\t\t: null;\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid, user_agent, ip_address, location)\n\t\t\tVALUES ($1, $2, $3, $4, $5, $6, $7)\n\t\t\tON CONFLICT (token) DO NOTHING`,\n\t\t\t[\n\t\t\t\tuserId,\n\t\t\t\ttoken,\n\t\t\t\texpiresAt,\n\t\t\t\tsid ?? null,\n\t\t\t\tmeta?.userAgent ?? null,\n\t\t\t\tmeta?.ipAddress ?? null,\n\t\t\t\tlocation ? JSON.stringify(location) : null,\n\t\t\t],\n\t\t);\n\t}\n\n\tasync delete(token: string): Promise<void> {\n\t\tawait this.store.query(`DELETE FROM fonderie_sessions WHERE token = $1`, [token]);\n\t}\n\n\t// Delete the session an access token is bound to (by its sid claim). Lets\n\t// logout revoke the current session without the client resending the refresh\n\t// token — the access token already identifies its session.\n\tasync deleteBySid(sid: string): Promise<void> {\n\t\tawait this.store.query(`DELETE FROM fonderie_sessions WHERE sid = $1`, [sid]);\n\t}\n\n\t// Revoke every session for a user (e.g. on password change). Access tokens\n\t// bound to these sessions via the sid claim die on their next request.\n\tasync deleteByUser(userId: string): Promise<void> {\n\t\tawait this.store.query(`DELETE FROM fonderie_sessions WHERE user_id = $1`, [userId]);\n\t}\n\n\t// Session metadata for a user (no tokens) — for the data-export / SAR bundle.\n\tasync listByUser(userId: string): Promise<\n\t\tArray<{ id: string; userAgent: string | null; ipAddress: string | null; createdAt: Date; expiresAt: Date }>\n\t> {\n\t\treturn this.store.query(\n\t\t\t`SELECT id,\n\t\t\t user_agent AS \"userAgent\",\n\t\t\t ip_address AS \"ipAddress\",\n\t\t\t created_at AS \"createdAt\",\n\t\t\t expires_at AS \"expiresAt\"\n\t\t\t FROM fonderie_sessions\n\t\t\t WHERE user_id = $1\n\t\t\t ORDER BY created_at DESC`,\n\t\t\t[userId],\n\t\t);\n\t}\n\n\t// Live sessions for the Active Sessions screen: unexpired only, carrying the\n\t// sid so the caller can flag the current one (row.sid === the JWT's sid).\n\t// No token — that never leaves the server.\n\tasync listLiveByUser(userId: string): Promise<\n\t\tArray<{\n\t\t\tid: string;\n\t\t\tsid: string | null;\n\t\t\tuserAgent: string | null;\n\t\t\tipAddress: string | null;\n\t\t\tlocation: IRequestLocation | null;\n\t\t\tcreatedAt: Date;\n\t\t\texpiresAt: Date;\n\t\t}>\n\t> {\n\t\treturn this.store.query(\n\t\t\t`SELECT id, sid,\n\t\t\t user_agent AS \"userAgent\",\n\t\t\t ip_address AS \"ipAddress\",\n\t\t\t location,\n\t\t\t created_at AS \"createdAt\",\n\t\t\t expires_at AS \"expiresAt\"\n\t\t\t FROM fonderie_sessions\n\t\t\t WHERE user_id = $1 AND expires_at > now()\n\t\t\t ORDER BY created_at DESC`,\n\t\t\t[userId],\n\t\t);\n\t}\n\n\t// Terminate one session by row id, scoped to its owner (a user can never\n\t// delete another user's session). Returns whether a row was removed.\n\tasync terminateById(userId: string, id: string): Promise<boolean> {\n\t\tconst rows = await this.store.query<{ id: string }>(\n\t\t\t`DELETE FROM fonderie_sessions WHERE id = $1 AND user_id = $2 RETURNING id`,\n\t\t\t[id, userId],\n\t\t);\n\t\treturn rows.length > 0;\n\t}\n\n\t// Terminate all of a user's sessions except the current one (by sid).\n\t// Returns the number of sessions removed.\n\tasync terminateOthers(userId: string, keepSid: string): Promise<number> {\n\t\tconst rows = await this.store.query<{ id: string }>(\n\t\t\t`DELETE FROM fonderie_sessions\n\t\t\t WHERE user_id = $1 AND (sid IS DISTINCT FROM $2)\n\t\t\t RETURNING id`,\n\t\t\t[userId, keepSid],\n\t\t);\n\t\treturn rows.length;\n\t}\n\n\tasync exists(token: string): Promise<boolean> {\n\t\tconst rows = await this.store.query<{ id: string }>(\n\t\t\t`SELECT id FROM fonderie_sessions WHERE token = $1 AND expires_at > now()`,\n\t\t\t[token],\n\t\t);\n\t\treturn rows.length > 0;\n\t}\n\n\t// Liveness check for session-bound access tokens (by the sid claim).\n\tasync aliveBySid(sid: string): Promise<boolean> {\n\t\tconst rows = await this.store.query<{ id: string }>(\n\t\t\t`SELECT id FROM fonderie_sessions WHERE sid = $1 AND expires_at > now()`,\n\t\t\t[sid],\n\t\t);\n\t\treturn rows.length > 0;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nexport class BackupCodeModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync replace(userId: string, codeHashes: string[]): Promise<void> {\n\t\tawait this.store.transaction(async (tx) => {\n\t\t\tawait tx.query(`DELETE FROM fonderie_mfa_backup_codes WHERE user_id = $1`, [userId]);\n\t\t\tif (codeHashes.length === 0) return;\n\t\t\tconst placeholders = codeHashes.map((_, i) => `($1, $${i + 2})`).join(', ');\n\t\t\tawait tx.query(\n\t\t\t\t`INSERT INTO fonderie_mfa_backup_codes (user_id, code_hash) VALUES ${placeholders}`,\n\t\t\t\t[userId, ...codeHashes],\n\t\t\t);\n\t\t});\n\t}\n\n\tasync findUnused(userId: string): Promise<{ id: string; codeHash: string }[]> {\n\t\tconst rows = await this.store.query<{ id: string; code_hash: string }>(\n\t\t\t`SELECT id, code_hash FROM fonderie_mfa_backup_codes\n\t\t\t WHERE user_id = $1 AND used_at IS NULL`,\n\t\t\t[userId],\n\t\t);\n\t\treturn rows.map((r) => ({ id: r.id, codeHash: r.code_hash }));\n\t}\n\n\tasync consume(id: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_mfa_backup_codes SET used_at = now() WHERE id = $1`, [\n\t\t\tid,\n\t\t]);\n\t}\n\n\tasync deleteByUser(userId: string): Promise<void> {\n\t\tawait this.store.query(`DELETE FROM fonderie_mfa_backup_codes WHERE user_id = $1`, [userId]);\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport { background } from '@fonderie/core';\n\nimport {\n\ttype IRequestLocation,\n\ttype LocationResolver,\n\tresolveLocation,\n} from '../services/request-location';\n\n// One row per login ATTEMPT — success or failure — across every method.\n// Append-only: rows survive logout/expiry (unlike fonderie_sessions, which is\n// the live-session list and the single source of truth for Active Sessions).\n// user_id is nullable so unknown-email attempts still record.\n// 'registration' marks the sign-up request itself: with verification not\n// enforced the account is live from that moment, so it is the first — sometimes\n// only — record of where the user came from.\nexport type LoginMethod = 'password' | 'phone' | 'mfa' | 'oauth-google' | 'oauth-apple' | 'registration';\nexport type LoginOutcome = 'success' | 'failed';\n\nexport interface ILoginEventInput {\n\tuserId: string | null;\n\temailAttempted?: string | null;\n\tmethod: LoginMethod;\n\toutcome: LoginOutcome;\n\tfailureReason?: string | null;\n\tipAddress: string | null;\n\tuserAgent: string | null;\n\t/** Request headers, handed to the location resolver. Not stored. */\n\theaders?: Headers;\n}\n\nexport class LoginEventModel {\n\t// `locate` is IAuthConfig.location. Absent ⇒ rows carry no location,\n\t// exactly as before.\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate locate?: LocationResolver,\n\t) {}\n\n\tasync record(e: ILoginEventInput): Promise<void> {\n\t\tconst location = this.locate\n\t\t\t? await resolveLocation(this.locate, { ip: e.ipAddress, headers: e.headers ?? new Headers() })\n\t\t\t: null;\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_login_events\n\t\t\t (user_id, email_attempted, method, outcome, failure_reason, ip_address, user_agent, location)\n\t\t\t VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n\t\t\t[\n\t\t\t\te.userId,\n\t\t\t\te.emailAttempted ?? null,\n\t\t\t\te.method,\n\t\t\t\te.outcome,\n\t\t\t\te.failureReason ?? null,\n\t\t\t\te.ipAddress,\n\t\t\t\te.userAgent,\n\t\t\t\tlocation ? JSON.stringify(location) : null,\n\t\t\t],\n\t\t);\n\t}\n\n\t// Never throws — recording history must not fail a login. It IS awaited,\n\t// though: this is a security audit trail (who signed in, from where), and a\n\t// detached write is abandoned when a serverless instance freezes after the\n\t// response, losing the row entirely. background() decides whether waiting is\n\t// actually necessary for the current runtime.\n\tasync recordSafe(e: ILoginEventInput): Promise<void> {\n\t\tawait background(this.record(e));\n\t}\n\n\t// One page of a user's own login history, newest first. Keyset-paginated on\n\t// (created_at, id) — mirrors audit's event list so the two log UIs share a\n\t// cursor contract. The +1 over-fetch that detects a next page lives here so\n\t// no outer clamp can shave it off.\n\tasync listByUser(query: ILoginEventQuery): Promise<ILoginEventPage> {\n\t\tconst limit = Math.min(query.limit ?? 50, MAX_HISTORY_LIMIT);\n\t\tconst params: unknown[] = [query.userId];\n\t\tconst where: string[] = ['user_id = $1'];\n\n\t\tif (query.outcome) {\n\t\t\tparams.push(query.outcome);\n\t\t\twhere.push(`outcome = $${params.length}`);\n\t\t}\n\t\tif (query.from) {\n\t\t\tparams.push(query.from);\n\t\t\twhere.push(`created_at >= $${params.length}`);\n\t\t}\n\t\tif (query.to) {\n\t\t\tparams.push(query.to);\n\t\t\twhere.push(`created_at <= $${params.length}`);\n\t\t}\n\t\tif (query.cursor) {\n\t\t\tparams.push(query.cursor.createdAt, query.cursor.id);\n\t\t\twhere.push(`(created_at, id) < ($${params.length - 1}::timestamptz, $${params.length}::uuid)`);\n\t\t}\n\n\t\tparams.push(limit + 1);\n\t\tconst rows = await this.store.query<ILoginEventRow>(\n\t\t\t`SELECT id, method, outcome, failure_reason AS \"failureReason\",\n\t\t\t ip_address AS \"ipAddress\", user_agent AS \"userAgent\", location,\n\t\t\t created_at AS \"createdAt\", created_at::text AS \"createdAtRaw\"\n\t\t\t FROM fonderie_login_events\n\t\t\t WHERE ${where.join(' AND ')}\n\t\t\t ORDER BY created_at DESC, id DESC\n\t\t\t LIMIT $${params.length}`,\n\t\t\tparams,\n\t\t);\n\t\treturn { events: rows.slice(0, limit), hasMore: rows.length > limit };\n\t}\n}\n\nconst MAX_HISTORY_LIMIT = 200;\n\nexport interface ILoginEventQuery {\n\tuserId: string;\n\toutcome?: LoginOutcome;\n\tfrom?: Date;\n\tto?: Date;\n\tcursor?: { createdAt: string; id: string } | null;\n\tlimit?: number;\n}\n\nexport interface ILoginEventRow {\n\tid: string;\n\tmethod: LoginMethod;\n\toutcome: LoginOutcome;\n\tfailureReason: string | null;\n\tipAddress: string | null;\n\tuserAgent: string | null;\n\tlocation: IRequestLocation | null;\n\tcreatedAt: Date;\n\t// created_at::text — full microsecond precision for the keyset cursor\n\t// (node-pg parses timestamptz into a millisecond Date, which would make the\n\t// cursor skip same-millisecond rows between pages).\n\tcreatedAtRaw: string;\n}\n\nexport interface ILoginEventPage {\n\tevents: ILoginEventRow[];\n\thasMore: boolean;\n}\n","import type { IFonderieContext } from '@fonderie/core';\n\n// The request metadata auth persists for security surfaces (session rows,\n// login events): effective client IP and raw user-agent. IP comes from\n// ctx.meta['clientIp'], which the adapters resolve with explicit proxy trust\n// (resolveClientIp in @fonderie/core) — never re-derive forwarding logic here.\n// The user-agent is stored raw and parsed at display time.\nexport interface IRequestMeta {\n\tipAddress: string | null;\n\tuserAgent: string | null;\n\t/** The request's headers, for the optional location resolver. Never stored. */\n\theaders?: Headers;\n}\n\n// Cap the stored UA: real ones are <300 chars; anything longer is noise or abuse.\nconst UA_MAX = 512;\n\n// Warn ONCE per process. The condition is a property of how the app is wired,\n// not of a particular request, so repeating it per login would bury it.\nlet warnedAboutMissingIdentity = false;\n\n/**\n * Warn when a context carries no caller identity at all.\n *\n * A real HTTP request arriving through an adapter has a user-agent, and a\n * resolved client IP. BOTH being absent means the context was built by hand —\n * almost always `fonderie.handle(new Request(...))` called directly, with the\n * caller's headers dropped and no `{ meta: { clientIp } }` seed.\n *\n * The consequence is invisible until someone looks at it: login history shows\n * \"Unknown device\" and no IP for those sign-ins, and only for those, so it\n * reads as a display bug rather than missing security data. It happened on an\n * OAuth callback — the method an attacker is most likely to use, and the one\n * whose history therefore matters most.\n *\n * Only one of the two missing is NOT reported: a request can legitimately lack\n * a user-agent, and an IP can be unresolvable on some transports. Both missing\n * together is the signature of a synthesized request, which is why that is the\n * condition rather than either alone.\n */\nfunction warnIfIdentityless(ctx: IFonderieContext): void {\n\tif (warnedAboutMissingIdentity) return;\n\twarnedAboutMissingIdentity = true;\n\tconsole.warn(\n\t\t`[auth] recording a security event with no client IP and no user-agent for ` +\n\t\t\t`${ctx.request.method} ${new URL(ctx.request.url).pathname}. Login history and ` +\n\t\t\t`session rows will show \"Unknown device\" with no IP for these. This is what a ` +\n\t\t\t`hand-built request looks like: if you call fonderie.handle() directly, forward ` +\n\t\t\t`the caller's user-agent header and pass the resolved IP as ` +\n\t\t\t`\\`handle(req, { meta: { clientIp } })\\` — resolve it with resolveClientIp from ` +\n\t\t\t`@fonderie/core/middlewares rather than reading the socket, or a proxied ` +\n\t\t\t`deployment records the proxy. Reported once per process.`,\n\t);\n}\n\nexport function requestMeta(ctx: IFonderieContext): IRequestMeta {\n\tconst ip = ctx.meta['clientIp'];\n\tconst ua = ctx.request.headers.get('user-agent');\n\tconst meta: IRequestMeta = {\n\t\tipAddress: typeof ip === 'string' && ip.length > 0 ? ip : null,\n\t\tuserAgent: ua && ua.length > 0 ? ua.slice(0, UA_MAX) : null,\n\t\theaders: ctx.request.headers,\n\t};\n\tif (meta.ipAddress === null && meta.userAgent === null) warnIfIdentityless(ctx);\n\treturn meta;\n}\n\n// Test seam: the warning fires once per process by design, which would make\n// every test after the first silently pass.\nexport function __resetIdentityWarningForTests(): void {\n\twarnedAboutMissingIdentity = false;\n}\n","import { tokenPairCookies, clearedTokenCookies, cookieHeaders } from '../services/cookies';\nimport { randomBytes, randomInt } from 'node:crypto';\n\nimport type { EventBus } from '@fonderie/events';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport { NOTIFICATION_EVENT } from '@fonderie/events';\nimport { setApiResponse, HTTP, background, canonicalLocale, defineLocales } from '@fonderie/core';\nimport type { IFonderieContext, ICourierMessage, ILocaleSettings } from '@fonderie/core';\n\nimport { EVENT_KEYS } from '../config';\nimport { toUserDTO } from '../dtos/user';\nimport type { IAuthConfig } from '../config';\nimport { UserModel } from '../models/user.model';\nimport { checkCooldown } from '../services/cooldown';\nimport { SessionModel } from '../models/session.model';\nimport { LoginEventModel } from '../models/login-event.model';\nimport { requestMeta } from '../services/request-meta';\nimport { hashPassword, verifyPasswordForLogin } from '../services/password';\nimport { normalizeEmailSafe } from '../services/email';\nimport { PasswordResetModel } from '../models/password-reset.model';\nimport { DEFAULT_VERIFICATION_COOLDOWN, MESSAGE_KEYS } from '../config';\nimport { EmailVerificationModel } from '../models/email-verification.model';\nimport { PhoneVerificationModel } from '../models/phone-verification.model';\nimport {\n\tissueTokenPair,\n\tissueMfaPendingToken,\n\tverifyToken,\n\trefreshTokenExpiry,\n} from '../services/jwt';\n\nfunction normalizePhone(phone: string): string {\n\treturn phone.trim().replace(/[\\s()\\-\\.]/g, '');\n}\n\nfunction isValidPhone(phone: unknown): phone is string {\n\treturn typeof phone === 'string' && /^\\+?[1-9]\\d{6,14}$/.test(normalizePhone(phone));\n}\n\nfunction extractRefreshToken(ctx: IFonderieContext): string | null {\n\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\tif (typeof body?.['refreshToken'] === 'string') {\n\t\treturn body['refreshToken'] as string;\n\t}\n\n\tconst cookie = ctx.request.headers.get('cookie') ?? '';\n\tconst match = cookie.match(/(?:^|;\\s*)refresh_token=([^;]+)/);\n\n\treturn match?.[1] ?? null;\n}\n\nexport function authController(\n\tstore: IStoreAdapter,\n\tconfig: IAuthConfig,\n\tbus?: EventBus,\n\tlocales: ILocaleSettings = defineLocales(),\n) {\n\tconst users = new UserModel(store);\n\tconst sessions = new SessionModel(store, config.location);\n\tconst loginEvents = new LoginEventModel(store, config.location);\n\tconst passwordReset = new PasswordResetModel(store);\n\tconst emailVerif = new EmailVerificationModel(store);\n\tconst phoneVerif = new PhoneVerificationModel(store);\n\n\tconst OTP_TTL_MS = 10 * 60 * 1000;\n\n\treturn {\n\t\tregister: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst { email, password, phone, firstName = null, lastName = null } = body ?? {};\n\t\t\t// The language the person signed up in, so the very first email — the\n\t\t\t// verification code — already arrives in it. Else the app's system locale.\n\t\t\tconst locale = canonicalLocale(body?.['locale'] as string | undefined) ?? locales.default;\n\n\t\t\t// ── Email branch takes priority (cheaper than SMS) ───────\n\t\t\tif (typeof email === 'string' && typeof password === 'string') {\n\t\t\t\tconst normalizedEmail = normalizeEmailSafe(email);\n\t\t\t\tif (!normalizedEmail) {\n\t\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid email address');\n\t\t\t\t}\n\n\t\t\t\tif (password.length < 8) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t\t'password must be at least 8 characters',\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tconst existing = await users.findByEmail(normalizedEmail);\n\t\t\t\tif (existing) {\n\t\t\t\t\treturn setApiResponse(HTTP.CONFLICT, 'USER_ALREADY_EXISTS', 'Email already registered');\n\t\t\t\t}\n\n\t\t\t\tconst passwordHash = await hashPassword(password);\n\t\t\t\tconst row = await users.create(\n\t\t\t\t\tnormalizedEmail,\n\t\t\t\t\tpasswordHash,\n\t\t\t\t\tfirstName as string | null,\n\t\t\t\t\tlastName as string | null,\n\t\t\t\t\tlocale,\n\t\t\t\t);\n\n\t\t\t\tif (!row) {\n\t\t\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Registration failed');\n\t\t\t\t}\n\n\t\t\t\tconst pin = randomInt(100000, 1000000).toString();\n\t\t\t\tconst expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24);\n\t\t\t\tawait emailVerif.create(row.id, pin, expiresAt);\n\n\t\t\t\tconst user = await users.findById(row.id);\n\t\t\t\tif (!user) {\n\t\t\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Registration failed');\n\t\t\t\t}\n\n\t\t\t\tconst reqId = ctx.meta['requestId'] as string | undefined;\n\t\t\t\tconst reqOpts = reqId !== undefined ? { requestId: reqId } : undefined;\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(\n\t\t\t\t\t\tNOTIFICATION_EVENT,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: MESSAGE_KEYS.emailRegistration,\n\t\t\t\t\t\t\tlocale: user.locale,\n\t\t\t\t\t\t\tdata: { pin, firstName: firstName ?? '' },\n\t\t\t\t\t\t\trecipient: { email: normalizedEmail, phone: null, deviceToken: null },\n\t\t\t\t\t\t} satisfies ICourierMessage,\n\t\t\t\t\t\treqOpts,\n\t\t\t\t\t));\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(\n\t\t\t\t\t\tEVENT_KEYS.userRegistered,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tuserId: user.id,\n\t\t\t\t\t\t\temail: user.email,\n\t\t\t\t\t\t\tfirstName: user.firstName,\n\t\t\t\t\t\t\tlastName: user.lastName,\n\t\t\t\t\t\t\tloginMethod: 'email' as const,\n\t\t\t\t\t\t},\n\t\t\t\t\t\treqOpts,\n\t\t\t\t\t));\n\n\t\t\t\tconst { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {\n\t\t\t\t\tloginMethod: 'email',\n\t\t\t\t});\n\t\t\t\tconst registerMeta = requestMeta(ctx);\n\t\t\t\tawait sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, registerMeta);\n\t\t\t\t// Where the account was created from — the user's first entry in\n\t\t\t\t// their own history. Location resolves once for this request (the\n\t\t\t\t// session above already asked).\n\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\tuserId: user.id,\n\t\t\t\t\temailAttempted: normalizedEmail,\n\t\t\t\t\tmethod: 'registration',\n\t\t\t\t\toutcome: 'success',\n\t\t\t\t\t...registerMeta,\n\t\t\t\t});\n\n\t\t\t\tconst resolvedRegister = { ...config, ...config.resolve?.(ctx) };\n\t\t\t\tconst requiresVerification = !!(resolvedRegister.requireVerification) && !user.emailVerifiedAt;\n\n\t\t\t\treturn Response.json(\n\t\t\t\t\t{\n\t\t\t\t\t\treason: 'USER_EMAIL_REGISTERED',\n\t\t\t\t\t\texplanation: 'Account created. Check your email for a verification code.',\n\t\t\t\t\t\tresult: {\n\t\t\t\t\t\t\ttokens: { access: accessToken, refresh: refreshToken },\n\t\t\t\t\t\t\tuser: toUserDTO(user),\n\t\t\t\t\t\t\trequiresVerification,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tstatus: 201,\n\t\t\t\t\t\theaders: cookieHeaders(tokenPairCookies(accessToken, refreshToken, config)),\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// ── Phone branch ──────────────────────────────────────────\n\t\t\tif (isValidPhone(phone)) {\n\t\t\t\tconst existing = await users.findByPhone(normalizePhone(phone));\n\t\t\t\tif (existing) {\n\t\t\t\t\treturn setApiResponse(HTTP.CONFLICT, 'USER_ALREADY_EXISTS', 'Phone already registered');\n\t\t\t\t}\n\n\t\t\t\tconst { id } = await users.findOrCreateByPhone(\n\t\t\t\t\tnormalizePhone(phone),\n\t\t\t\t\t(firstName as string | null) ?? null,\n\t\t\t\t\t(lastName as string | null) ?? null,\n\t\t\t\t\tlocale,\n\t\t\t\t);\n\n\t\t\t\tconst otp = randomInt(100000, 1000000).toString();\n\t\t\t\tconst expiresAt = new Date(Date.now() + OTP_TTL_MS);\n\t\t\t\tawait phoneVerif.upsert(id, normalizePhone(phone), otp, expiresAt);\n\n\t\t\t\tconst user = await users.findById(id);\n\t\t\t\tif (!user) {\n\t\t\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Registration failed');\n\t\t\t\t}\n\n\t\t\t\tconst reqId2 = ctx.meta['requestId'] as string | undefined;\n\t\t\t\tconst reqOpts2 = reqId2 !== undefined ? { requestId: reqId2 } : undefined;\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(\n\t\t\t\t\t\tNOTIFICATION_EVENT,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: MESSAGE_KEYS.phoneOtp,\n\t\t\t\t\t\t\tlocale: user.locale,\n\t\t\t\t\t\t\tdata: { otp },\n\t\t\t\t\t\t\trecipient: { email: null, phone: normalizePhone(phone), deviceToken: null },\n\t\t\t\t\t\t} satisfies ICourierMessage,\n\t\t\t\t\t\treqOpts2,\n\t\t\t\t\t));\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(\n\t\t\t\t\t\tEVENT_KEYS.userRegistered,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tuserId: user.id,\n\t\t\t\t\t\t\temail: user.email,\n\t\t\t\t\t\t\tfirstName: user.firstName,\n\t\t\t\t\t\t\tlastName: user.lastName,\n\t\t\t\t\t\t\tloginMethod: 'phone' as const,\n\t\t\t\t\t\t},\n\t\t\t\t\t\treqOpts2,\n\t\t\t\t\t));\n\n\t\t\t\t// No session and no full tokens before the OTP round-trip: possession\n\t\t\t\t// of the phone is the ONLY credential in this flow, so issuing real\n\t\t\t\t// tokens here would authenticate anyone who typed the number. Mirror\n\t\t\t\t// the MFA flow instead — a short-lived pending token that only\n\t\t\t\t// /auth/verify and /auth/send-verification accept; the real token\n\t\t\t\t// pair is issued by verify() once the OTP matches.\n\t\t\t\tconst otpToken = issueMfaPendingToken(user.id, config, 'phone');\n\n\t\t\t\treturn Response.json(\n\t\t\t\t\t{\n\t\t\t\t\t\treason: 'USER_PHONE_REGISTERED',\n\t\t\t\t\t\texplanation: 'Account created. A verification code has been sent to your phone.',\n\t\t\t\t\t\tresult: {\n\t\t\t\t\t\t\totpToken,\n\t\t\t\t\t\t\tuser: toUserDTO(user),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{ status: 202 },\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t'Provide email + password or a valid phone number',\n\t\t\t);\n\t\t},\n\n\t\tlogin: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst meta = requestMeta(ctx);\n\n\t\t\t// ── Email branch takes priority (cheaper than SMS) ────────\n\t\t\tif (typeof body?.['email'] === 'string' && typeof body?.['password'] === 'string') {\n\t\t\t\tconst { email: rawEmail, password } = body as { email: string; password: string };\n\t\t\t\tconst email = normalizeEmailSafe(rawEmail);\n\t\t\t\tif (!email) {\n\t\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid email address');\n\t\t\t\t}\n\n\t\t\t\tconst user = await users.findByEmail(email);\n\t\t\t\tif (!user || !user.passwordHash) {\n\t\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\t\tuserId: null,\n\t\t\t\t\t\temailAttempted: email,\n\t\t\t\t\t\tmethod: 'password',\n\t\t\t\t\t\toutcome: 'failed',\n\t\t\t\t\t\tfailureReason: 'unknown_email',\n\t\t\t\t\t\t...meta,\n\t\t\t\t\t});\n\t\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_CREDENTIALS', 'Invalid credentials');\n\t\t\t\t}\n\n\t\t\t\tconst { valid, needsRehash } = await verifyPasswordForLogin(\n\t\t\t\t\tpassword,\n\t\t\t\t\tuser.passwordHash,\n\t\t\t\t\tconfig.legacyVerify,\n\t\t\t\t);\n\t\t\t\tif (!valid) {\n\t\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\t\tuserId: user.id,\n\t\t\t\t\t\temailAttempted: email,\n\t\t\t\t\t\tmethod: 'password',\n\t\t\t\t\t\toutcome: 'failed',\n\t\t\t\t\t\tfailureReason: 'bad_password',\n\t\t\t\t\t\t...meta,\n\t\t\t\t\t});\n\t\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_CREDENTIALS', 'Invalid credentials');\n\t\t\t\t}\n\n\t\t\t\t// Rehash-on-login: a password validated via the legacy verifier is\n\t\t\t\t// re-stored as bcrypt so the foreign hash is used at most once.\n\t\t\t\tif (needsRehash) {\n\t\t\t\t\tawait users.updatePassword(user.id, await hashPassword(password));\n\t\t\t\t}\n\n\t\t\t\tif (user.suspended) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t\t\t'ACCOUNT_SUSPENDED',\n\t\t\t\t\t\t'Account suspended. Please contact support.',\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tif (user.mfaEnabled) {\n\t\t\t\t\t// Password verified; MFA still pending. The completed login is\n\t\t\t\t\t// recorded on mfa/verify — recording success here would log a\n\t\t\t\t\t// login that hasn't happened yet.\n\t\t\t\t\tconst mfaToken = issueMfaPendingToken(user.id, config, 'email');\n\t\t\t\t\treturn setApiResponse(HTTP.OK, 'MFA_REQUIRED', 'Multi-factor authentication required', {\n\t\t\t\t\t\tmfaToken,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tconst { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {\n\t\t\t\t\tloginMethod: 'email',\n\t\t\t\t});\n\t\t\t\tawait sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);\n\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\tuserId: user.id,\n\t\t\t\t\temailAttempted: email,\n\t\t\t\t\tmethod: 'password',\n\t\t\t\t\toutcome: 'success',\n\t\t\t\t\t...meta,\n\t\t\t\t});\n\n\t\t\t\tconst resolvedLogin = { ...config, ...config.resolve?.(ctx) };\n\t\t\t\tconst requiresVerification = !!(resolvedLogin.requireVerification) && !user.emailVerifiedAt;\n\n\t\t\t\treturn Response.json(\n\t\t\t\t\t{\n\t\t\t\t\t\treason: 'USER_EMAIL_LOGIN',\n\t\t\t\t\t\texplanation: 'Login successful.',\n\t\t\t\t\t\tresult: {\n\t\t\t\t\t\t\ttokens: { access: accessToken, refresh: refreshToken },\n\t\t\t\t\t\t\tuser: toUserDTO(user),\n\t\t\t\t\t\t\trequiresVerification,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tstatus: 200,\n\t\t\t\t\t\theaders: cookieHeaders(tokenPairCookies(accessToken, refreshToken, config)),\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// ── Phone branch ──────────────────────────────────────────\n\t\t\tconst phone = body?.['phone'];\n\t\t\tif (isValidPhone(phone)) {\n\t\t\t\tconst user = await users.findByPhone(normalizePhone(phone));\n\t\t\t\tif (!user) {\n\t\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_CREDENTIALS', 'Invalid credentials');\n\t\t\t\t}\n\t\t\t\tif (user.suspended) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t\t\t'ACCOUNT_SUSPENDED',\n\t\t\t\t\t\t'Account suspended. Please contact support.',\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tconst otp = randomInt(100000, 1000000).toString();\n\t\t\t\tconst expiresAt = new Date(Date.now() + OTP_TTL_MS);\n\t\t\t\tawait phoneVerif.upsert(user.id, normalizePhone(phone), otp, expiresAt);\n\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\t\ttype: MESSAGE_KEYS.phoneOtp,\n\t\t\t\t\t\tlocale: user.locale,\n\t\t\t\t\t\tdata: { otp },\n\t\t\t\t\t\trecipient: { email: null, phone: normalizePhone(phone), deviceToken: null },\n\t\t\t\t\t} satisfies ICourierMessage));\n\n\t\t\t\t// Same rule as phone registration: the OTP IS the credential, so no\n\t\t\t\t// session/full tokens until verify() confirms it. Also no user DTO —\n\t\t\t\t// before proof of possession, the caller has only typed a phone\n\t\t\t\t// number, and returning profile data would leak PII to anyone who\n\t\t\t\t// knows (or guesses) a registered number.\n\t\t\t\tconst otpToken = issueMfaPendingToken(user.id, config, 'phone');\n\n\t\t\t\treturn Response.json(\n\t\t\t\t\t{\n\t\t\t\t\t\treason: 'USER_PHONE_OTP_SENT',\n\t\t\t\t\t\texplanation: 'A verification code has been sent to your phone.',\n\t\t\t\t\t\tresult: { otpToken },\n\t\t\t\t\t},\n\t\t\t\t\t{ status: 202 },\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t'Provide email + password or a valid phone number',\n\t\t\t);\n\t\t},\n\n\t\tlogout: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\t// Kill the session this request is authenticated on — by its sid claim,\n\t\t\t// which is always present under requireAuth. This is the reliable path:\n\t\t\t// clients that don't resend the refresh token (most, since only the\n\t\t\t// access token is persisted) previously left the session row alive, so\n\t\t\t// it lingered in Active Sessions and its refresh token stayed valid.\n\t\t\tconst sid = (ctx.user as { sid?: string | null } | null)?.sid;\n\t\t\tif (sid) {\n\t\t\t\tawait sessions.deleteBySid(sid).catch(() => undefined);\n\t\t\t}\n\t\t\t// Also honour an explicitly-passed refresh token (covers pre-sid tokens\n\t\t\t// and callers that log out a specific refresh session).\n\t\t\tconst token = extractRefreshToken(ctx);\n\t\t\tif (token) {\n\t\t\t\tawait sessions.delete(token).catch(() => undefined);\n\t\t\t}\n\n\t\t\treturn Response.json(\n\t\t\t\t{ reason: 'USER_LOGOUT', explanation: 'Logged out successfully.' },\n\t\t\t\t{\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\theaders: cookieHeaders(clearedTokenCookies(config)),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\trefresh: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst token = extractRefreshToken(ctx);\n\t\t\tif (!token) {\n\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_PARAMETER', 'No refresh token provided');\n\t\t\t}\n\n\t\t\tconst payload = verifyToken(token, config);\n\t\t\tif (!payload || payload.type !== 'refresh') {\n\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'TOKEN_REFRESH_FAILED', 'Invalid refresh token');\n\t\t\t}\n\n\t\t\tconst valid = await sessions.exists(token);\n\t\t\tif (!valid) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNAUTHORIZED,\n\t\t\t\t\t'TOKEN_REFRESH_FAILED',\n\t\t\t\t\t'Session expired or already revoked',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst user = await users.findById(payload.sub);\n\t\t\tif (!user || user.suspended || user.deletedAt) {\n\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t\t}\n\n\t\t\tawait sessions.delete(token);\n\t\t\tconst { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {\n\t\t\t\tloginMethod: payload.loginMethod ?? 'email',\n\t\t\t\tphoneVerified: payload.phoneVerified ?? false,\n\t\t\t});\n\t\t\tawait sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, requestMeta(ctx));\n\n\t\t\treturn Response.json(\n\t\t\t\t{\n\t\t\t\t\treason: 'TOKENS_REFRESHED',\n\t\t\t\t\texplanation: 'Tokens refreshed successfully.',\n\t\t\t\t\tresult: { tokens: { access: accessToken, refresh: refreshToken } },\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\theaders: cookieHeaders(tokenPairCookies(accessToken, refreshToken, config)),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\tforgotPassword: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst rawEmail = body?.['email'];\n\n\t\t\tif (typeof rawEmail !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'email is required');\n\t\t\t}\n\n\t\t\tconst email = normalizeEmailSafe(rawEmail);\n\t\t\tif (!email) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid email address');\n\t\t\t}\n\n\t\t\tconst user = await users.findByEmail(email);\n\t\t\tif (!user) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t'PASSWORD_RESET_EMAIL_SENT',\n\t\t\t\t\t'Password reset email sent (if account exists).',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst resolved = { ...config, ...config.resolve?.(ctx) };\n\t\t\tconst cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;\n\t\t\tconst remaining = checkCooldown(await passwordReset.findLastSentAt(user.id), cooldown);\n\t\t\tif (remaining > 0) {\n\t\t\t\t// Silently skip the send: a 429 here would fire only for existing\n\t\t\t\t// accounts, letting an attacker enumerate users by requesting twice.\n\t\t\t\t// The response must be indistinguishable from the not-found branch.\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t'PASSWORD_RESET_EMAIL_SENT',\n\t\t\t\t\t'Password reset email sent (if account exists).',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst pin = randomInt(100000, 1000000).toString();\n\t\t\t// High-entropy token backing a reset LINK (32 bytes → 64 hex chars).\n\t\t\t// Not brute-forceable, so its reset path needs no rate limit.\n\t\t\tconst token = randomBytes(32).toString('hex');\n\t\t\tconst expiresAt = new Date(Date.now() + 1000 * 60 * 60);\n\t\t\tawait passwordReset.create(user.id, pin, token, expiresAt);\n\n\t\t\t// Ready-built reset link when the app configured a base URL; '' means\n\t\t\t// the app only surfaces the pin (a blank {{resetUrl}} renders empty).\n\t\t\tconst base = resolved.passwordResetUrl;\n\t\t\tconst resetUrl = base ? `${base}${base.includes('?') ? '&' : '?'}token=${token}` : '';\n\n\t\t\tawait background(bus\n\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\ttype: MESSAGE_KEYS.passwordReset,\n\t\t\t\t\tlocale: user.locale,\n\t\t\t\t\trecipient: { email, phone: null, deviceToken: null },\n\t\t\t\t\tdata: { pin, token, resetUrl },\n\t\t\t\t} satisfies ICourierMessage));\n\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.OK,\n\t\t\t\t'PASSWORD_RESET_EMAIL_SENT',\n\t\t\t\t'Password reset email sent (if account exists).',\n\t\t\t);\n\t\t},\n\n\t\tresetPassword: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst raw = body?.['pin'];\n\t\t\tconst password = body?.['password'];\n\n\t\t\tconst rawToken = body?.['token'];\n\n\t\t\tif (typeof password !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'password is required');\n\t\t\t}\n\t\t\tif (password.length < 8) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'password must be at least 8 characters',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Two credentials redeem a reset: the strong TOKEN (from the email\n\t\t\t// link — not brute-forceable) or the 6-digit PIN (route is\n\t\t\t// IP-rate-limited). Token takes precedence when both are supplied.\n\t\t\tlet row: { userId: string; expiresAt: Date } | null;\n\t\t\tif (typeof rawToken === 'string' && rawToken.trim().length >= 32) {\n\t\t\t\trow = await passwordReset.findByToken(rawToken.trim());\n\t\t\t} else {\n\t\t\t\tif (typeof raw !== 'string' || !/^\\d{6}$/.test(raw.trim())) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t\t'a 6-digit pin or a reset token is required',\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\trow = await passwordReset.findByPin(raw.trim());\n\t\t\t}\n\n\t\t\tif (!row || new Date() > row.expiresAt) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'PASSWORD_RESET_FAILED',\n\t\t\t\t\t'Invalid or expired reset credentials',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst passwordHash = await hashPassword(password);\n\t\t\tawait store.transaction(async (tx) => {\n\t\t\t\tawait Promise.all([\n\t\t\t\t\ttx.query(`UPDATE fonderie_users SET password_hash = $1 WHERE id = $2`, [\n\t\t\t\t\t\tpasswordHash,\n\t\t\t\t\t\trow.userId,\n\t\t\t\t\t]),\n\t\t\t\t\ttx.query(`DELETE FROM fonderie_password_resets WHERE user_id = $1`, [row.userId]),\n\t\t\t\t\t// Revoke every session: the reset exists because the account may\n\t\t\t\t\t// be compromised — a stolen session must not survive it.\n\t\t\t\t\ttx.query(`DELETE FROM fonderie_sessions WHERE user_id = $1`, [row.userId]),\n\t\t\t\t]);\n\t\t\t});\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PASSWORD_RESET_SUCCESSFUL', 'Password reset successfully.');\n\t\t},\n\n\t\tverify: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\t// requireAnyAuth admits pending tokens so the phone-OTP flow can reach\n\t\t\t// this route pre-authentication — but a pending token is ONLY valid for\n\t\t\t// that flow. An email mfaPending token (password verified, MFA not) must\n\t\t\t// finish MFA at /auth/mfa/verify, not side-step into email verification.\n\t\t\tif (ctx.user!.mfaPending && ctx.user!.loginMethod !== 'phone') {\n\t\t\t\treturn setApiResponse(HTTP.FORBIDDEN, 'MFA_REQUIRED', 'Complete MFA verification to continue');\n\t\t\t}\n\n\t\t\t// ── Email early-exit: no pin needed if already verified ──\n\t\t\tif (ctx.user!.loginMethod !== 'phone' && ctx.user!.emailVerifiedAt) {\n\t\t\t\treturn setApiResponse(HTTP.OK, 'VERIFIED', 'Email verified successfully.', {\n\t\t\t\t\tverified: true,\n\t\t\t\t\temail: ctx.user!.email,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst raw = body?.['token'];\n\n\t\t\tif (typeof raw !== 'string' || !/^\\d{6}$/.test(raw.trim())) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'token must be a 6-digit code',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst pin = raw.trim();\n\n\t\t\t// ── Phone branch ─────────────────────────────────────────\n\t\t\tif (ctx.user!.loginMethod === 'phone') {\n\t\t\t\t// A phone sign-in COMPLETES here, not in login(): /auth/login only\n\t\t\t\t// sends the code and issues a pending token, because possession of\n\t\t\t\t// the phone is the sole credential. That is why login history missed\n\t\t\t\t// this path for months — an audit of \"where do logins happen\" reads\n\t\t\t\t// login() and the OAuth callbacks, and never a route named verify.\n\t\t\t\tconst phoneMeta = requestMeta(ctx);\n\t\t\t\tconst record = await phoneVerif.findByUser(ctx.user!.id, pin);\n\t\t\t\tif (!record) {\n\t\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\t\tuserId: ctx.user!.id,\n\t\t\t\t\t\temailAttempted: null,\n\t\t\t\t\t\tmethod: 'phone',\n\t\t\t\t\t\toutcome: 'failed',\n\t\t\t\t\t\tfailureReason: 'invalid_pin',\n\t\t\t\t\t\t...phoneMeta,\n\t\t\t\t\t});\n\t\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'VERIFICATION_FAILED', 'Invalid or expired pin');\n\t\t\t\t}\n\t\t\t\tif (new Date() > record.expiresAt) {\n\t\t\t\t\tawait phoneVerif.deleteByUser(ctx.user!.id);\n\t\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\t\tuserId: ctx.user!.id,\n\t\t\t\t\t\temailAttempted: null,\n\t\t\t\t\t\tmethod: 'phone',\n\t\t\t\t\t\toutcome: 'failed',\n\t\t\t\t\t\tfailureReason: 'expired_pin',\n\t\t\t\t\t\t...phoneMeta,\n\t\t\t\t\t});\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t\t'VERIFICATION_FAILED',\n\t\t\t\t\t\t'Verification code expired',\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tawait phoneVerif.deleteByUser(ctx.user!.id);\n\n\t\t\t\tconst { accessToken, refreshToken, sid } = issueTokenPair(ctx.user!.id, config, {\n\t\t\t\t\tloginMethod: 'phone',\n\t\t\t\t\tphoneVerified: true,\n\t\t\t\t});\n\t\t\t\tawait sessions.create(ctx.user!.id, refreshToken, refreshTokenExpiry(refreshToken), sid, phoneMeta);\n\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\tuserId: ctx.user!.id,\n\t\t\t\t\temailAttempted: null,\n\t\t\t\t\tmethod: 'phone',\n\t\t\t\t\toutcome: 'success',\n\t\t\t\t\t...phoneMeta,\n\t\t\t\t});\n\n\t\t\t\tconst verifiedUser = await users.findById(ctx.user!.id);\n\t\t\t\tif (!verifiedUser) {\n\t\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'User not found');\n\t\t\t\t}\n\t\t\t\tif (verifiedUser.suspended) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.FORBIDDEN,\n\t\t\t\t\t\t'ACCOUNT_SUSPENDED',\n\t\t\t\t\t\t'Account suspended. Please contact support.',\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\treturn Response.json(\n\t\t\t\t\t{\n\t\t\t\t\t\treason: 'VERIFIED',\n\t\t\t\t\t\texplanation: 'Phone verified successfully.',\n\t\t\t\t\t\tresult: {\n\t\t\t\t\t\t\ttokens: { access: accessToken, refresh: refreshToken },\n\t\t\t\t\t\t\tuser: toUserDTO(verifiedUser, true),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tstatus: 200,\n\t\t\t\t\t\theaders: cookieHeaders(tokenPairCookies(accessToken, refreshToken, config)),\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// ── Email branch ─────────────────────────────────────────\n\t\t\tconst row = await emailVerif.findByUser(ctx.user!.id, pin);\n\t\t\tif (!row) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'VERIFICATION_FAILED', 'Invalid or expired pin');\n\t\t\t}\n\t\t\tif (new Date() > row.expiresAt) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'VERIFICATION_FAILED', 'Pin expired');\n\t\t\t}\n\n\t\t\tawait store.transaction(async (tx) => {\n\t\t\t\tawait Promise.all([\n\t\t\t\t\ttx.query(\n\t\t\t\t\t\t`UPDATE fonderie_users SET email_verified_at = now(), updated_at = now() WHERE id = $1`,\n\t\t\t\t\t\t[ctx.user!.id],\n\t\t\t\t\t),\n\t\t\t\t\ttx.query(`DELETE FROM fonderie_email_verifications WHERE user_id = $1 AND token = $2`, [\n\t\t\t\t\t\tctx.user!.id,\n\t\t\t\t\t\tpin,\n\t\t\t\t\t]),\n\t\t\t\t]);\n\t\t\t});\n\n\t\t\treturn setApiResponse(HTTP.OK, 'VERIFIED', 'Email verified successfully.', {\n\t\t\t\tverified: true,\n\t\t\t\temail: ctx.user!.email,\n\t\t\t});\n\t\t},\n\n\t\tsendVerification: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\t// Same pending-token rule as verify(): admitted only for the phone-OTP\n\t\t\t// flow (resending the code), never for an email MFA-pending login.\n\t\t\tif (ctx.user!.mfaPending && ctx.user!.loginMethod !== 'phone') {\n\t\t\t\treturn setApiResponse(HTTP.FORBIDDEN, 'MFA_REQUIRED', 'Complete MFA verification to continue');\n\t\t\t}\n\n\t\t\tconst resolved = { ...config, ...config.resolve?.(ctx) };\n\t\t\tconst cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;\n\n\t\t\tif (ctx.user!.loginMethod === 'phone') {\n\t\t\t\tconst phone = ctx.user!.phone;\n\t\t\t\tif (!phone) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t\t'NO_PHONE_ON_ACCOUNT',\n\t\t\t\t\t\t'No phone number associated with this account',\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tconst remaining = checkCooldown(await phoneVerif.findLastSentAt(ctx.user!.id), cooldown);\n\t\t\t\tif (remaining > 0) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.TOO_MANY_REQUESTS,\n\t\t\t\t\t\t'VERIFICATION_COOLDOWN',\n\t\t\t\t\t\t'Please wait before requesting a new code.',\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tretryAfter: Math.ceil(remaining / 1000),\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tconst otp = randomInt(100000, 1000000).toString();\n\t\t\t\tconst expiresAt = new Date(Date.now() + OTP_TTL_MS);\n\t\t\t\tawait phoneVerif.upsert(ctx.user!.id, phone, otp, expiresAt);\n\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\t\ttype: MESSAGE_KEYS.phoneOtp,\n\t\t\t\t\t\tlocale: ctx.user!.locale,\n\t\t\t\t\t\tdata: { otp },\n\t\t\t\t\t\trecipient: { email: null, phone, deviceToken: null },\n\t\t\t\t\t} satisfies ICourierMessage));\n\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t'VERIFICATION_SENT',\n\t\t\t\t\t'A verification code has been sent to your phone.',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (!ctx.user!.email) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'NO_EMAIL_ON_ACCOUNT',\n\t\t\t\t\t'No email address associated with this account',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (ctx.user!.emailVerifiedAt) {\n\t\t\t\treturn setApiResponse(HTTP.OK, 'EMAIL_VERIFIED', 'Email already verified.', {\n\t\t\t\t\tverified: true,\n\t\t\t\t\temail: ctx.user!.email,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst remaining = checkCooldown(await emailVerif.findLastSentAt(ctx.user!.id), cooldown);\n\t\t\tif (remaining > 0) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.TOO_MANY_REQUESTS,\n\t\t\t\t\t'VERIFICATION_COOLDOWN',\n\t\t\t\t\t'Please wait before requesting a new code.',\n\t\t\t\t\t{\n\t\t\t\t\t\tretryAfter: Math.ceil(remaining / 1000),\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst pin = randomInt(100000, 1000000).toString();\n\t\t\tconst expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24);\n\t\t\tawait emailVerif.replace(ctx.user!.id, pin, expiresAt);\n\n\t\t\tawait background(bus\n\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\ttype: MESSAGE_KEYS.emailVerification,\n\t\t\t\t\tlocale: ctx.user!.locale,\n\t\t\t\t\trecipient: { email: ctx.user!.email, phone: null, deviceToken: null },\n\t\t\t\t\tdata: { pin },\n\t\t\t\t} satisfies ICourierMessage));\n\n\t\t\treturn setApiResponse(HTTP.OK, 'VERIFICATION_SENT', 'Verification email sent.', {\n\t\t\t\temail: ctx.user!.email,\n\t\t\t});\n\t\t},\n\t};\n}\n","export function checkCooldown(lastSentAt: Date | null, cooldownMs: number): number {\n\tif (!lastSentAt) return 0;\n\treturn Math.max(0, cooldownMs - (Date.now() - lastSentAt.getTime()));\n}\n","export function normalizeEmail(email: string): string {\n\tif (typeof email !== 'string' || email.length === 0) {\n\t\tthrow new Error('Invalid email');\n\t}\n\n\tconst lower = email.trim().toLowerCase();\n\n\tif (lower.length === 0) {\n\t\tthrow new Error('Email cannot be empty');\n\t}\n\n\tconst atIndex = lower.indexOf('@');\n\tif (atIndex === -1 || lower.lastIndexOf('@') !== atIndex) {\n\t\tthrow new Error('Invalid email format');\n\t}\n\n\tconst local = lower.substring(0, atIndex);\n\tconst domain = lower.substring(atIndex + 1);\n\n\tif (!local || !domain) {\n\t\tthrow new Error('Invalid email format');\n\t}\n\n\tconst plusIndex = local.indexOf('+');\n\tconst normalizedLocal = plusIndex === -1 ? local : local.substring(0, plusIndex);\n\n\tif (!normalizedLocal) {\n\t\tthrow new Error('Invalid email format');\n\t}\n\n\treturn `${normalizedLocal}@${domain}`;\n}\n\nexport function normalizeEmailSafe(email: string): string | null {\n\ttry {\n\t\treturn normalizeEmail(email);\n\t} catch {\n\t\treturn null;\n\t}\n}\n","import { createHash } from 'node:crypto';\n\nimport type { IStoreAdapter } from '@fonderie/store';\n\n// Store only a hash of the reset credentials — never the plaintext. A DB read\n// (SQLi elsewhere, a backup/log leak) then yields nothing directly usable\n// within the 1h window. SHA-256 is sufficient: the token carries 256 bits of\n// entropy (unguessable), and the 6-digit pin's real protection is the route\n// rate-limiter + short TTL + all-session-revoke, not the hash. Deterministic\n// (unsalted) so the lookup is a single indexed equality on the hash.\nconst hashSecret = (value: string): string => createHash('sha256').update(value).digest('hex');\n\nexport class PasswordResetModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync create(userId: string, pin: string, token: string, expiresAt: Date): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_password_resets (user_id, pin, token, expires_at, created_at)\n\t\t\tVALUES ($1, $2, $3, $4, now())\n\t\t\tON CONFLICT (user_id) DO UPDATE\n\t\t\tSET pin = $2, token = $3, expires_at = $4, created_at = now()`,\n\t\t\t[userId, hashSecret(pin), hashSecret(token), expiresAt],\n\t\t);\n\t}\n\n\tasync findLastSentAt(userId: string): Promise<Date | null> {\n\t\tconst [row] = await this.store.query<{ created_at: Date }>(\n\t\t\t`SELECT created_at FROM fonderie_password_resets WHERE user_id = $1`,\n\t\t\t[userId],\n\t\t);\n\t\tif (!row) return null;\n\t\treturn new Date(row.created_at);\n\t}\n\n\tasync findByPin(pin: string): Promise<{ userId: string; expiresAt: Date } | null> {\n\t\tconst [row] = await this.store.query<{ user_id: string; expires_at: Date }>(\n\t\t\t`SELECT user_id, expires_at FROM fonderie_password_resets WHERE pin = $1`,\n\t\t\t[hashSecret(pin)],\n\t\t);\n\t\tif (!row) return null;\n\t\treturn { userId: row.user_id, expiresAt: new Date(row.expires_at) };\n\t}\n\n\t// Lookup by the high-entropy reset token. Unlike the 6-digit pin this is\n\t// not brute-forceable, so the token path needs no rate limit. Empty/short\n\t// values are refused before the query so a NULL/blank token column can\n\t// never be matched.\n\tasync findByToken(token: string): Promise<{ userId: string; expiresAt: Date } | null> {\n\t\tif (typeof token !== 'string' || token.length < 32) return null;\n\t\tconst [row] = await this.store.query<{ user_id: string; expires_at: Date }>(\n\t\t\t`SELECT user_id, expires_at FROM fonderie_password_resets WHERE token = $1`,\n\t\t\t[hashSecret(token)],\n\t\t);\n\t\tif (!row) return null;\n\t\treturn { userId: row.user_id, expiresAt: new Date(row.expires_at) };\n\t}\n\n\tasync deleteByUser(userId: string): Promise<void> {\n\t\tawait this.store.query(`DELETE FROM fonderie_password_resets WHERE user_id = $1`, [userId]);\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nexport class EmailVerificationModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync create(userId: string, pin: string, expiresAt: Date): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_email_verifications (user_id, token, expires_at)\n\t\t\tVALUES ($1, $2, $3)\n\t\t\tON CONFLICT (user_id) DO UPDATE SET token = $2, expires_at = $3`,\n\t\t\t[userId, pin, expiresAt],\n\t\t);\n\t}\n\n\tasync find(pin: string): Promise<{ userId: string; expiresAt: Date } | null> {\n\t\tconst [row] = await this.store.query<{ user_id: string; expires_at: Date }>(\n\t\t\t`SELECT user_id, expires_at FROM fonderie_email_verifications WHERE token = $1`,\n\t\t\t[pin],\n\t\t);\n\t\tif (!row) return null;\n\t\treturn { userId: row.user_id, expiresAt: new Date(row.expires_at) };\n\t}\n\n\tasync delete(pin: string): Promise<void> {\n\t\tawait this.store.query(`DELETE FROM fonderie_email_verifications WHERE token = $1`, [pin]);\n\t}\n\n\tasync findByUser(userId: string, pin: string): Promise<{ expiresAt: Date } | null> {\n\t\tconst [row] = await this.store.query<{ expires_at: Date }>(\n\t\t\t`SELECT expires_at FROM fonderie_email_verifications WHERE user_id = $1 AND token = $2`,\n\t\t\t[userId, pin],\n\t\t);\n\t\tif (!row) return null;\n\t\treturn { expiresAt: new Date(row.expires_at) };\n\t}\n\n\tasync findLastSentAt(userId: string): Promise<Date | null> {\n\t\tconst [row] = await this.store.query<{ created_at: Date }>(\n\t\t\t`SELECT created_at FROM fonderie_email_verifications WHERE user_id = $1`,\n\t\t\t[userId],\n\t\t);\n\t\tif (!row) return null;\n\t\treturn new Date(row.created_at);\n\t}\n\n\tasync replace(userId: string, pin: string, expiresAt: Date): Promise<void> {\n\t\tawait this.store.transaction(async (tx) => {\n\t\t\tawait tx.query(`DELETE FROM fonderie_email_verifications WHERE user_id = $1`, [userId]);\n\t\t\tawait tx.query(\n\t\t\t\t`INSERT INTO fonderie_email_verifications (token, user_id, expires_at)\n\t\t\t\tVALUES ($1, $2, $3)`,\n\t\t\t\t[pin, userId, expiresAt],\n\t\t\t);\n\t\t});\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nexport class PhoneVerificationModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync upsert(userId: string, phone: string, otp: string, expiresAt: Date): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_phone_verifications (phone, user_id, otp, expires_at)\n\t\t\tVALUES ($1, $2, $3, $4)\n\t\t\tON CONFLICT (phone) DO UPDATE\n\t\t\tSET user_id = $2, otp = $3, expires_at = $4, created_at = now()`,\n\t\t\t[phone, userId, otp, expiresAt],\n\t\t);\n\t}\n\n\tasync findByUser(\n\t\tuserId: string,\n\t\totp: string,\n\t): Promise<{ phone: string; expiresAt: Date } | null> {\n\t\tconst [row] = await this.store.query<{ phone: string; expires_at: Date }>(\n\t\t\t`SELECT phone, expires_at FROM fonderie_phone_verifications WHERE user_id = $1 AND otp = $2`,\n\t\t\t[userId, otp],\n\t\t);\n\t\tif (!row) return null;\n\t\treturn { phone: row.phone, expiresAt: new Date(row.expires_at) };\n\t}\n\n\tasync findLastSentAt(userId: string): Promise<Date | null> {\n\t\tconst [row] = await this.store.query<{ created_at: Date }>(\n\t\t\t`SELECT created_at FROM fonderie_phone_verifications WHERE user_id = $1`,\n\t\t\t[userId],\n\t\t);\n\t\tif (!row) return null;\n\t\treturn new Date(row.created_at);\n\t}\n\n\tasync deleteByUser(userId: string): Promise<void> {\n\t\tawait this.store.query(`DELETE FROM fonderie_phone_verifications WHERE user_id = $1`, [userId]);\n\t}\n}\n","import { clearedTokenCookies, cookieHeaders } from '../services/cookies';\nimport { randomInt } from 'node:crypto';\n\nimport { setApiResponse, HTTP, dateOrEmpty, background } from '@fonderie/core';\nimport type { IFonderieContext, ICourierMessage } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport type { IAuthConfig } from '../config';\n\nimport type { EventBus } from '@fonderie/events';\nimport { NOTIFICATION_EVENT } from '@fonderie/events';\n\nimport { MESSAGE_KEYS, EVENT_KEYS } from '../config';\nimport { toUserDTO } from '../dtos/user';\nimport {\n\ttoLoginHistoryPageDTO,\n\tdecodeLoginCursor,\n\ttoSessionDTO,\n} from '../dtos/login-activity';\nimport type { IUser } from '../types';\nimport { UserModel } from '../models/user.model';\nimport { SessionModel } from '../models/session.model';\nimport { LoginEventModel } from '../models/login-event.model';\nimport type { LoginOutcome } from '../models/login-event.model';\nimport { EmailVerificationModel } from '../models/email-verification.model';\nimport { PhoneVerificationModel } from '../models/phone-verification.model';\nimport { normalizeEmailSafe } from '../services/email';\n\nfunction normalizePhone(phone: string): string {\n\treturn phone.trim().replace(/[\\s()\\-\\.]/g, '');\n}\n\nfunction isValidPhone(phone: unknown): phone is string {\n\treturn typeof phone === 'string' && /^\\+?[1-9]\\d{6,14}$/.test(normalizePhone(phone));\n}\n\nexport function userController(store: IStoreAdapter, config: IAuthConfig, bus?: EventBus) {\n\tconst users = new UserModel(store);\n\tconst sessions = new SessionModel(store, config.location);\n\tconst loginEvents = new LoginEventModel(store, config.location);\n\tconst emailVerif = new EmailVerificationModel(store);\n\tconst phoneVerif = new PhoneVerificationModel(store);\n\n\treturn {\n\t\tme: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst user = await users.findById(ctx.user!.id);\n\t\t\tif (!user) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'User not found');\n\t\t\t}\n\n\t\t\treturn setApiResponse(HTTP.OK, 'USER_ACCOUNT_FETCHED', 'User account fetched successful.', {\n\t\t\t\tuser: toUserDTO(user, ctx.user!.phoneVerified),\n\t\t\t});\n\t\t},\n\n\t\t// ── Login history (append-only; the caller's own attempts) ────────\n\t\tloginHistory: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst params = new URL(ctx.request.url).searchParams;\n\t\t\tconst rawLimit = Number(params.get('limit') ?? 50);\n\t\t\tconst limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;\n\n\t\t\tconst outcomeParam = params.get('outcome');\n\t\t\tif (outcomeParam !== null && outcomeParam !== 'success' && outcomeParam !== 'failed') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', \"outcome must be 'success' or 'failed'\");\n\t\t\t}\n\n\t\t\tconst cursorParam = params.get('cursor');\n\t\t\tlet cursor: { createdAt: string; id: string } | null = null;\n\t\t\tif (cursorParam) {\n\t\t\t\tcursor = decodeLoginCursor(cursorParam);\n\t\t\t\tif (!cursor) {\n\t\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid cursor');\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst query: Parameters<LoginEventModel['listByUser']>[0] = { userId: ctx.user!.id, limit };\n\t\t\tif (outcomeParam) query.outcome = outcomeParam as LoginOutcome;\n\t\t\tif (cursor) query.cursor = cursor;\n\t\t\tconst from = params.get('from');\n\t\t\tconst to = params.get('to');\n\t\t\tif (from) query.from = new Date(from);\n\t\t\tif (to) query.to = new Date(to);\n\n\t\t\tconst page = await loginEvents.listByUser(query);\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.OK,\n\t\t\t\t'LOGIN_HISTORY_FETCHED',\n\t\t\t\t'Login history retrieved.',\n\t\t\t\ttoLoginHistoryPageDTO(page),\n\t\t\t);\n\t\t},\n\n\t\t// ── Active sessions (live only; the caller's own) ─────────────────\n\t\tlistSessions: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst rows = await sessions.listLiveByUser(ctx.user!.id);\n\t\t\tconst currentSid = (ctx.user as { sid?: string | null }).sid ?? null;\n\t\t\treturn setApiResponse(HTTP.OK, 'SESSIONS_FETCHED', 'Active sessions retrieved.', {\n\t\t\t\tsessions: rows.map((r) => toSessionDTO(r, currentSid)),\n\t\t\t});\n\t\t},\n\n\t\tterminateSession: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst id = (ctx.meta['params'] as Record<string, string> | undefined)?.['id'];\n\t\t\tif (!id) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Session not found');\n\t\t\t}\n\t\t\tconst removed = await sessions.terminateById(ctx.user!.id, id);\n\t\t\tif (!removed) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Session not found');\n\t\t\t}\n\t\t\treturn setApiResponse(HTTP.OK, 'SESSION_TERMINATED', 'Session terminated.', { id });\n\t\t},\n\n\t\tterminateOtherSessions: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst currentSid = (ctx.user as { sid?: string | null }).sid ?? null;\n\t\t\tif (!currentSid) {\n\t\t\t\t// Without a resolvable current session there is nothing to spare, so\n\t\t\t\t// refuse rather than nuke every session (which would include this one).\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'NO_CURRENT_SESSION',\n\t\t\t\t\t'Cannot terminate others without a current session context',\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst count = await sessions.terminateOthers(ctx.user!.id, currentSid);\n\t\t\treturn setApiResponse(HTTP.OK, 'SESSIONS_TERMINATED', 'Other sessions terminated.', { count });\n\t\t},\n\n\t\tupdateProfile: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\n\t\t\tconst allowed = ['firstName', 'lastName', 'avatarUrl'] as const;\n\t\t\tconst fields: Record<string, unknown> = {};\n\t\t\tfor (const key of allowed) {\n\t\t\t\tif (body?.[key] !== undefined) fields[key] = body[key];\n\t\t\t}\n\n\t\t\tif (Object.keys(fields).length === 0) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'Provide at least one of: firstName, lastName, avatarUrl',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst row = await users.update(ctx.user!.id, fields);\n\t\t\tif (!row) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'User not found');\n\t\t\t}\n\n\t\t\tconst updated = await users.findById(ctx.user!.id);\n\t\t\treturn setApiResponse(HTTP.OK, 'PROFILE_UPDATED', 'Profile updated.', {\n\t\t\t\tuser: toUserDTO(updated as IUser, ctx.user!.phoneVerified),\n\t\t\t});\n\t\t},\n\n\t\tupdatePreferences: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\n\t\t\tconst fields: Parameters<typeof users.updatePreferences>[1] = {};\n\n\t\t\tif (typeof body?.['locale'] === 'string') fields.locale = body['locale'];\n\t\t\tif (typeof body?.['timezone'] === 'string') fields.timezone = body['timezone'];\n\n\t\t\tconst prefKeys = ['notifications', 'emailDigest', 'dateFormat', 'timeFormat'] as const;\n\t\t\tconst patch: Record<string, unknown> = {};\n\t\t\tfor (const key of prefKeys) {\n\t\t\t\tif (body?.[key] !== undefined) patch[key] = body[key];\n\t\t\t}\n\t\t\tif (Object.keys(patch).length > 0) fields.patch = patch;\n\n\t\t\tconst row = await users.updatePreferences(ctx.user!.id, fields);\n\t\t\tif (!row) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'Provide at least one preference field',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst updated = await users.findById(ctx.user!.id);\n\t\t\treturn setApiResponse(HTTP.OK, 'PREFERENCES_UPDATED', 'Preferences updated.', {\n\t\t\t\tuser: toUserDTO(updated as IUser, ctx.user!.phoneVerified),\n\t\t\t});\n\t\t},\n\n\t\tupdateEmail: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst newEmail = body?.['email'];\n\n\t\t\tconst normalised = typeof newEmail === 'string' ? normalizeEmailSafe(newEmail) : null;\n\t\t\tif (!normalised) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'A valid email address is required',\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst oldEmail = ctx.user!.email;\n\n\t\t\tif (normalised === oldEmail) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'New email must differ from current email',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst existing = await users.findByEmail(normalised);\n\t\t\tif (existing) {\n\t\t\t\treturn setApiResponse(HTTP.CONFLICT, 'EMAIL_IN_USE', 'Email already in use');\n\t\t\t}\n\n\t\t\tconst pin = randomInt(100000, 1000000).toString();\n\t\t\tconst expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24);\n\t\t\tawait emailVerif.replace(ctx.user!.id, pin, expiresAt);\n\t\t\tawait users.updateEmail(ctx.user!.id, normalised);\n\n\t\t\tawait background(bus\n\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\ttype: MESSAGE_KEYS.emailVerification,\n\t\t\t\t\tlocale: ctx.user!.locale,\n\t\t\t\t\tdata: { pin },\n\t\t\t\t\trecipient: { email: normalised, phone: null, deviceToken: null },\n\t\t\t\t} satisfies ICourierMessage));\n\t\t\tif (oldEmail) {\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\t\ttype: MESSAGE_KEYS.emailChanged,\n\t\t\t\t\t\tlocale: ctx.user!.locale,\n\t\t\t\t\t\tdata: { newEmail: normalised },\n\t\t\t\t\t\trecipient: { email: oldEmail, phone: null, deviceToken: null },\n\t\t\t\t\t} satisfies ICourierMessage));\n\t\t\t}\n\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.OK,\n\t\t\t\t'EMAIL_UPDATED',\n\t\t\t\t'Email updated. A verification code has been sent to your new address.',\n\t\t\t\t{\n\t\t\t\t\temail: normalised,\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\tupdatePhone: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst newPhone = body?.['phone'];\n\n\t\t\tif (!isValidPhone(newPhone)) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'A valid phone number is required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst normalised = normalizePhone(newPhone);\n\n\t\t\tconst existing = await users.findByPhone(normalised);\n\t\t\tif (existing) {\n\t\t\t\treturn setApiResponse(HTTP.CONFLICT, 'PHONE_IN_USE', 'Phone number already in use');\n\t\t\t}\n\n\t\t\tconst otp = randomInt(100000, 1000000).toString();\n\t\t\tconst expiresAt = new Date(Date.now() + 10 * 60 * 1000);\n\t\t\tawait phoneVerif.upsert(ctx.user!.id, normalised, otp, expiresAt);\n\t\t\tawait users.updatePhone(ctx.user!.id, normalised);\n\n\t\t\tawait background(bus\n\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\ttype: MESSAGE_KEYS.phoneOtp,\n\t\t\t\t\tlocale: ctx.user!.locale,\n\t\t\t\t\tdata: { otp },\n\t\t\t\t\trecipient: { email: null, phone: normalised, deviceToken: null },\n\t\t\t\t} satisfies ICourierMessage));\n\t\t\tif (ctx.user!.email) {\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\t\ttype: MESSAGE_KEYS.phoneChanged,\n\t\t\t\t\t\tlocale: ctx.user!.locale,\n\t\t\t\t\t\tdata: {},\n\t\t\t\t\t\trecipient: { email: ctx.user!.email, phone: null, deviceToken: null },\n\t\t\t\t\t} satisfies ICourierMessage));\n\t\t\t}\n\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.OK,\n\t\t\t\t'PHONE_UPDATED',\n\t\t\t\t'Phone number updated. A verification code has been sent to your new number.',\n\t\t\t\t{\n\t\t\t\t\tphone: normalised,\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\tchangePassword: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst currentPassword = body?.['currentPassword'];\n\t\t\tconst newPassword = body?.['newPassword'];\n\n\t\t\tif (typeof currentPassword !== 'string' || !currentPassword) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'currentPassword is required');\n\t\t\t}\n\t\t\tif (typeof newPassword !== 'string' || newPassword.length < 8) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'newPassword must be at least 8 characters');\n\t\t\t}\n\n\t\t\tconst user = await users.findById(ctx.user!.id);\n\t\t\tif (!user) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'User not found');\n\n\t\t\tconst { hashPassword, verifyPassword } = await import('../services/password');\n\t\t\tconst valid = await verifyPassword(currentPassword, user.passwordHash ?? '');\n\t\t\tif (!valid) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_CREDENTIAL', 'Current password is incorrect');\n\t\t\t}\n\n\t\t\tconst hash = await hashPassword(newPassword);\n\t\t\tawait users.updatePassword(ctx.user!.id, hash);\n\n\t\t\t// Revoke all sessions so a changed password invalidates any tokens an\n\t\t\t// attacker (or the user's old device) may still hold. The client must\n\t\t\t// re-authenticate after changing its password.\n\t\t\tawait sessions.deleteByUser(ctx.user!.id);\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PASSWORD_CHANGED', 'Password updated successfully.');\n\t\t},\n\n\t\t/**\n\t\t * Remove an OAuth provider from the account.\n\t\t *\n\t\t * Refuses when the account has no password, because unlinking would\n\t\t * then leave NO way to sign in — that is account deletion, not a\n\t\t * settings toggle, and a user clicking \"disconnect Google\" is not\n\t\t * asking for it. The check is inside the UPDATE's WHERE clause, so a\n\t\t * password cannot disappear between the check and the write.\n\t\t *\n\t\t * Notifies the account's email either way: changing how an account can\n\t\t * be signed into is a security event for its owner, who is not\n\t\t * necessarily the person doing it.\n\t\t */\n\t\tunlinkOauth: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst provider = String((ctx.meta['params'] as Record<string, string>)?.['provider'] ?? '');\n\t\t\tconst current = await users.findById(ctx.user!.id);\n\t\t\tif (!current?.provider) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_LINKED', 'No sign-in provider is linked.');\n\t\t\t}\n\t\t\tif (provider && provider.toLowerCase() !== current.provider.toLowerCase()) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.NOT_FOUND,\n\t\t\t\t\t'NOT_LINKED',\n\t\t\t\t\t`This account is not linked to ${provider}.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst removed = await users.clearProvider(ctx.user!.id);\n\t\t\tif (!removed) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.CONFLICT,\n\t\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t\t'Set a password before disconnecting this provider — it is currently the only way to sign in.',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (ctx.user!.email) {\n\t\t\t\tawait background(bus\n\t\t\t\t\t?.emit(NOTIFICATION_EVENT, {\n\t\t\t\t\t\ttype: MESSAGE_KEYS.oauthUnlinked,\n\t\t\t\t\t\tlocale: ctx.user!.locale,\n\t\t\t\t\t\tdata: { provider: current.provider },\n\t\t\t\t\t\trecipient: { email: ctx.user!.email, phone: null, deviceToken: null },\n\t\t\t\t\t} satisfies ICourierMessage));\n\t\t\t}\n\t\t\treturn setApiResponse(HTTP.OK, 'OAUTH_UNLINKED', 'Sign-in provider removed.');\n\t\t},\n\n\t\tdeleteMe: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst userId = ctx.user!.id;\n\t\t\tawait users.softDelete(userId);\n\n\t\t\tconst reqId = ctx.meta['requestId'] as string | undefined;\n\t\t\tawait background(bus\n\t\t\t\t?.emit(\n\t\t\t\t\tEVENT_KEYS.userDeleted,\n\t\t\t\t\t{ userId },\n\t\t\t\t\treqId !== undefined ? { requestId: reqId } : undefined,\n\t\t\t\t));\n\n\t\t\treturn Response.json(\n\t\t\t\t{ reason: 'ACCOUNT_DELETED', explanation: 'Account successfully deleted.' },\n\t\t\t\t{\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\theaders: cookieHeaders(clearedTokenCookies(config)),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\t// Subject Access Request — the authenticated user's own data as a portable\n\t\t// JSON bundle. Only auth-owned data, and only safe fields: no password hash,\n\t\t// no MFA secret, no session tokens (session metadata only).\n\t\texportMe: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst user = await users.findById(ctx.user!.id);\n\t\t\tif (!user) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'User not found');\n\n\t\t\tconst sessionRows = await sessions.listByUser(ctx.user!.id);\n\t\t\tconst bundle = {\n\t\t\t\texportedAt: new Date().toISOString(),\n\t\t\t\tprofile: toUserDTO(user, ctx.user!.phoneVerified),\n\t\t\t\tsessions: sessionRows.map((s) => ({\n\t\t\t\t\tid: s.id,\n\t\t\t\t\tuserAgent: s.userAgent,\n\t\t\t\t\tipAddress: s.ipAddress,\n\t\t\t\t\tcreatedAt: dateOrEmpty(s.createdAt),\n\t\t\t\t\texpiresAt: dateOrEmpty(s.expiresAt),\n\t\t\t\t})),\n\t\t\t\tsecurity: {\n\t\t\t\t\tmfaEnabled: user.mfaEnabled === true,\n\t\t\t\t\temailVerified: user.emailVerifiedAt !== null,\n\t\t\t\t},\n\t\t\t};\n\n\t\t\t// SAR completeness: other modules contribute the caller's data they own\n\t\t\t// (workspaces memberships, billing, …) without auth importing them — the\n\t\t\t// app wires collectors via config.dataExportContributors.\n\t\t\tconst contributors = config.dataExportContributors ?? [];\n\t\t\tif (contributors.length > 0) {\n\t\t\t\tconst modules: Record<string, unknown> = {};\n\t\t\t\tfor (const c of contributors) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tmodules[c.name] = await c.collect(ctx.user!.id);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tmodules[c.name] = null; // a failing contributor never blocks the export\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t(bundle as Record<string, unknown>)['modules'] = modules;\n\t\t\t}\n\n\t\t\treturn setApiResponse(HTTP.OK, 'DATA_EXPORT', 'Your account data.', bundle);\n\t\t},\n\t};\n}\n","import { encodeKeysetCursor, decodeKeysetCursor } from '@fonderie/core';\n\nimport type { ILoginEventRow, ILoginEventPage } from '../models/login-event.model';\nimport { type IRequestLocation, sanitizeLocation } from '../services/request-location';\n\n// ── Login history ────────────────────────────────────────────────\n\nexport interface ILoginEventDTO {\n\tid: string;\n\tmethod: string;\n\toutcome: string;\n\tfailureReason: string | null;\n\tipAddress: string | null;\n\tuserAgent: string | null;\n\t/** Where the attempt came from — present only when the app configures\n\t * `location`. Country is reliable; region/city are approximate. */\n\tlocation: IRequestLocation | null;\n\tcreatedAt: string;\n}\n\nexport interface ILoginHistoryPageDTO {\n\tevents: ILoginEventDTO[];\n\tnextCursor: string | null;\n}\n\nfunction toLoginEventDTO(row: ILoginEventRow): ILoginEventDTO {\n\treturn {\n\t\tid: row.id,\n\t\tmethod: row.method,\n\t\toutcome: row.outcome,\n\t\tfailureReason: row.failureReason,\n\t\tipAddress: row.ipAddress,\n\t\tuserAgent: row.userAgent,\n\t\t// Re-sanitized on read: the column is JSONB and could have been written\n\t\t// by anything with database access.\n\t\tlocation: sanitizeLocation(row.location),\n\t\tcreatedAt: row.createdAt instanceof Date ? row.createdAt.toISOString() : String(row.createdAt),\n\t};\n}\n\nexport function toLoginHistoryPageDTO(page: ILoginEventPage): ILoginHistoryPageDTO {\n\tconst last = page.events[page.events.length - 1];\n\t// Cursor carries created_at at full microsecond precision (createdAtRaw) so\n\t// same-microsecond rows can't be skipped between pages.\n\tconst nextCursor =\n\t\tpage.hasMore && last ? encodeKeysetCursor(last.createdAtRaw ?? last.createdAt.toISOString(), last.id) : null;\n\treturn { events: page.events.map(toLoginEventDTO), nextCursor };\n}\n\n// Shared cursor decode (range-checked in core; a crafted cursor → null → the\n// caller's 422, never a Postgres cast 500).\nexport const decodeLoginCursor = decodeKeysetCursor;\n\n// ── Active sessions ──────────────────────────────────────────────\n\nexport interface ISessionDTO {\n\tid: string;\n\tcurrent: boolean;\n\tipAddress: string | null;\n\tuserAgent: string | null;\n\t/** Where the session was opened from, when `location` is configured. */\n\tlocation: IRequestLocation | null;\n\tcreatedAt: string;\n\texpiresAt: string;\n}\n\nexport function toSessionDTO(\n\trow: {\n\t\tid: string;\n\t\tsid: string | null;\n\t\tuserAgent: string | null;\n\t\tipAddress: string | null;\n\t\tlocation?: unknown;\n\t\tcreatedAt: Date;\n\t\texpiresAt: Date;\n\t},\n\tcurrentSid: string | null,\n): ISessionDTO {\n\treturn {\n\t\tid: row.id,\n\t\tcurrent: currentSid !== null && row.sid === currentSid,\n\t\tipAddress: row.ipAddress,\n\t\tuserAgent: row.userAgent,\n\t\tlocation: sanitizeLocation(row.location),\n\t\tcreatedAt: row.createdAt instanceof Date ? row.createdAt.toISOString() : String(row.createdAt),\n\t\texpiresAt: row.expiresAt instanceof Date ? row.expiresAt.toISOString() : String(row.expiresAt),\n\t};\n}\n","import { randomBytes, createPublicKey, createHash, type KeyObject } from 'node:crypto';\nimport jwt from 'jsonwebtoken';\n\nimport { tokenPairCookies, cookieHeaders } from '../services/cookies';\nimport { setApiResponse, HTTP, constantTimeEqual, background } from '@fonderie/core';\nimport type { IFonderieContext, ICourierMessage } from '@fonderie/core';\nimport type { EventBus } from '@fonderie/events';\nimport { NOTIFICATION_EVENT } from '@fonderie/events';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAuthConfig } from '../config';\nimport { MESSAGE_KEYS, EVENT_KEYS } from '../config';\nimport { issueTokenPair, refreshTokenExpiry } from '../services/jwt';\nimport { toUserDTO } from '../dtos/user';\nimport { UserModel } from '../models/user.model';\nimport { SessionModel } from '../models/session.model';\nimport { LoginEventModel } from '../models/login-event.model';\nimport { ConsumedTokenModel } from '../models/consumed-token.model';\nimport { requestMeta } from '../services/request-meta';\nimport { normalizeEmailSafe } from '../services/email';\n\n// ── Sign in with Apple ───────────────────────────────────────────────────────\n// Apple differs from Google in three ways handled here: (1) the client secret\n// is a short-lived ES256 JWT minted from the .p8 key, not a static string;\n// (2) the web callback is a cross-site POST (response_mode=form_post), so its\n// CSRF cookie must be SameSite=None; (3) the NATIVE identityToken (from the iOS\n// sheet) did not come from our own TLS exchange, so its signature MUST be\n// verified against Apple's published JWKS.\n\nconst APPLE_AUTH_URL = 'https://appleid.apple.com/auth/authorize';\nconst APPLE_TOKEN_URL = 'https://appleid.apple.com/auth/token';\nconst APPLE_KEYS_URL = 'https://appleid.apple.com/auth/keys';\nconst APPLE_ISSUER = 'https://appleid.apple.com';\n\ntype AppleConfig = NonNullable<IAuthConfig['apple']>;\n\ninterface AppleClaims {\n\temail?: string;\n\tsub?: string;\n\t// Apple sends email_verified as a boolean OR the string \"true\".\n\temail_verified?: boolean | string;\n\tnonce?: string;\n\t// Seconds since epoch — the token's own expiry, used as the TTL of the\n\t// single-use replay record.\n\texp?: number;\n}\n\n// Mint the client_secret: a ≤5-min ES256 JWT signed with the .p8 key. Minted\n// per exchange and never stored (Apple allows up to 6 months; short is safer).\nexport function mintAppleClientSecret(apple: AppleConfig): string {\n\tconst now = Math.floor(Date.now() / 1000);\n\treturn jwt.sign(\n\t\t{ iss: apple.teamId, iat: now, exp: now + 300, aud: APPLE_ISSUER, sub: apple.clientId },\n\t\tapple.privateKey,\n\t\t{ algorithm: 'ES256', keyid: apple.keyId },\n\t);\n}\n\n// Apple's signing keys, cached for an hour. Module-level cache is safe: the keys\n// are public. Hardened against unknown-kid abuse: an attacker fully controls the\n// (unverified) kid on POST /auth/apple/native, and a naive \"refetch on unknown\n// kid\" turns every forged token into an outbound call to Apple. So refetches are\n// (a) single-flighted — concurrent callers share one fetch, (b) rate-limited to\n// at most one attempt per minute regardless of how many unknown kids arrive, and\n// (c) bounded by a request timeout so a slow/hung Apple never ties up handlers.\ninterface AppleJwk { kid: string; kty: string; n: string; e: string; alg: string; use?: string }\nlet appleKeyCache: { keys: AppleJwk[]; fetchedAt: number } | null = null;\nlet appleKeyInflight: Promise<void> | null = null;\nlet appleKeyLastAttempt = 0;\nconst APPLE_KEYS_TTL_MS = 60 * 60 * 1000;\nconst APPLE_KEYS_MIN_REFETCH_MS = 60 * 1000;\nconst APPLE_KEYS_FETCH_TIMEOUT_MS = 5000;\n\nfunction refreshAppleKeys(): Promise<void> {\n\t// Single-flight: one in-flight fetch is shared by all concurrent callers.\n\tif (appleKeyInflight) return appleKeyInflight;\n\tappleKeyLastAttempt = Date.now();\n\tappleKeyInflight = (async () => {\n\t\ttry {\n\t\t\tconst res = await fetch(APPLE_KEYS_URL, { signal: AbortSignal.timeout(APPLE_KEYS_FETCH_TIMEOUT_MS) });\n\t\t\tconst data = (await res.json()) as { keys?: AppleJwk[] };\n\t\t\tif (Array.isArray(data.keys)) appleKeyCache = { keys: data.keys, fetchedAt: Date.now() };\n\t\t} catch {\n\t\t\t// Keep any stale cache rather than failing outright.\n\t\t} finally {\n\t\t\tappleKeyInflight = null;\n\t\t}\n\t})();\n\treturn appleKeyInflight;\n}\n\nasync function getApplePublicKey(kid: string): Promise<KeyObject | null> {\n\tconst find = () => appleKeyCache?.keys.find((k) => k.kid === kid) ?? null;\n\tconst cacheFresh = () => !!appleKeyCache && Date.now() - appleKeyCache.fetchedAt < APPLE_KEYS_TTL_MS;\n\n\tlet jwk = find();\n\t// Refetch only when the kid is missing or the cache is stale — AND not more\n\t// than once per minute, so a flood of attacker-chosen kids can trigger at\n\t// most one outbound fetch per minute, never one per request.\n\tif ((!jwk || !cacheFresh()) && Date.now() - appleKeyLastAttempt >= APPLE_KEYS_MIN_REFETCH_MS) {\n\t\tawait refreshAppleKeys();\n\t\tjwk = find();\n\t}\n\tif (!jwk) return null;\n\ttry {\n\t\treturn createPublicKey({ key: jwk as unknown as JsonWebKey, format: 'jwk' });\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n// Verify an Apple id_token: signature against the JWKS (by kid), issuer, and\n// audience (Services ID for web; allow-listed bundle ids for native). exp is\n// enforced by jwt.verify. Returns the claims, or null on any failure.\nexport async function verifyAppleIdToken(\n\tidToken: string,\n\topts: { audiences: string[]; nonce?: string | undefined },\n): Promise<AppleClaims | null> {\n\tif (opts.audiences.length === 0) return null;\n\tconst decoded = jwt.decode(idToken, { complete: true });\n\tif (!decoded || typeof decoded === 'string') return null;\n\tconst kid = decoded.header.kid;\n\tif (!kid) return null;\n\tconst key = await getApplePublicKey(kid);\n\tif (!key) return null;\n\ttry {\n\t\tconst payload = jwt.verify(idToken, key, {\n\t\t\talgorithms: ['RS256'],\n\t\t\tissuer: APPLE_ISSUER,\n\t\t\t// jsonwebtoken types `audience` as a non-empty tuple; runtime accepts a\n\t\t\t// list and matches the token's aud against ANY entry (guarded non-empty\n\t\t\t// above).\n\t\t\taudience: opts.audiences as [string, ...string[]],\n\t\t}) as AppleClaims;\n\t\t// Optional session-binding: if the app bound a nonce to the native\n\t\t// request, the token's nonce claim must match it (defends against a token\n\t\t// substituted from a different session). This is NOT replay protection —\n\t\t// verbatim replay is stopped by the single-use guard in appleNative.\n\t\tif (opts.nonce !== undefined && payload.nonce !== opts.nonce) return null;\n\t\treturn payload;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nconst appleEmailVerified = (c: AppleClaims): boolean =>\n\tc.email_verified === true || c.email_verified === 'true';\n\n// Test-only: reset the module-level JWKS cache/rate-limit state so each test can\n// exercise a fresh fetch. Never called in production code paths.\nexport function __resetAppleKeysForTests(): void {\n\tappleKeyCache = null;\n\tappleKeyInflight = null;\n\tappleKeyLastAttempt = 0;\n}\n\nexport function oauthController(store: IStoreAdapter, config: IAuthConfig, bus?: EventBus) {\n\tconst users = new UserModel(store);\n\tconst sessions = new SessionModel(store, config.location);\n\tconst loginEvents = new LoginEventModel(store, config.location);\n\tconst consumedTokens = new ConsumedTokenModel(store);\n\n\t/**\n\t * Announce what an OAuth upsert actually did.\n\t *\n\t * A signing-in user hits this code path on EVERY login, so the default is\n\t * silence; only a change in what the account is gets announced.\n\t *\n\t * • new account → `userRegistered`, the same domain event a password\n\t * registration emits. This is the one that was missing: subscribers\n\t * provision on it (workspaces creates the personal workspace), so an\n\t * OAuth signup previously produced a user with none — invisible until\n\t * something later asked for a workspace that was never created.\n\t * • provider added or changed → a security notice to the address on file,\n\t * because the account gained a new way to be signed into and the owner\n\t * is not necessarily the person who did it.\n\t * • same provider as before → nothing. Emailing here would notify the\n\t * user on every sign-in.\n\t */\n\tconst announceOAuthUpsert = async (\n\t\tctx: IFonderieContext,\n\t\tupserted: {\n\t\t\tid: string;\n\t\t\tinserted: boolean;\n\t\t\tpreviousProvider: string | null;\n\t\t\tclearedUnverifiedPassword?: boolean;\n\t\t},\n\t\tprovider: string,\n\t\tuser: { email: string | null; firstName: string | null; lastName: string | null; locale: string },\n\t): Promise<void> => {\n\t\tif (!bus) return;\n\t\tconst reqId = ctx.meta['requestId'] as string | undefined;\n\t\tconst reqOpts = reqId !== undefined ? { requestId: reqId } : undefined;\n\t\tconst label = provider.charAt(0).toUpperCase() + provider.slice(1);\n\t\tconst recipient = { email: user.email, phone: null, deviceToken: null };\n\n\t\t// Announced regardless of which branch follows, and first: a password\n\t\t// that silently stops working is the one change a user cannot diagnose.\n\t\t// The recipient is now PROVEN to own this mailbox — that is what the\n\t\t// provider just established — so this reaches the right person.\n\t\tif (upserted.clearedUnverifiedPassword && user.email) {\n\t\t\tawait background(bus.emit(\n\t\t\t\tNOTIFICATION_EVENT,\n\t\t\t\t{\n\t\t\t\t\ttype: MESSAGE_KEYS.passwordRevoked,\n\t\t\t\t\tlocale: user.locale,\n\t\t\t\t\tdata: { provider: label },\n\t\t\t\t\trecipient,\n\t\t\t\t} satisfies ICourierMessage,\n\t\t\t\treqOpts,\n\t\t\t));\n\t\t}\n\n\t\tif (upserted.inserted) {\n\t\t\tawait background(bus.emit(\n\t\t\t\tEVENT_KEYS.userRegistered,\n\t\t\t\t{\n\t\t\t\t\tuserId: upserted.id,\n\t\t\t\t\temail: user.email,\n\t\t\t\t\tfirstName: user.firstName,\n\t\t\t\t\tlastName: user.lastName,\n\t\t\t\t\tloginMethod: provider,\n\t\t\t\t},\n\t\t\t\treqOpts,\n\t\t\t));\n\t\t\tif (user.email) {\n\t\t\t\tawait background(bus.emit(\n\t\t\t\t\tNOTIFICATION_EVENT,\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: MESSAGE_KEYS.oauthRegistration,\n\t\t\t\t\t\tlocale: user.locale,\n\t\t\t\t\t\tdata: { provider: label, appName: config.appName ?? 'Fonderie' },\n\t\t\t\t\t\trecipient,\n\t\t\t\t\t} satisfies ICourierMessage,\n\t\t\t\t\treqOpts,\n\t\t\t\t));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (upserted.previousProvider !== provider && user.email) {\n\t\t\tawait background(bus.emit(\n\t\t\t\tNOTIFICATION_EVENT,\n\t\t\t\t{\n\t\t\t\t\ttype: MESSAGE_KEYS.oauthLinked,\n\t\t\t\t\tlocale: user.locale,\n\t\t\t\t\tdata: { provider: label },\n\t\t\t\t\trecipient,\n\t\t\t\t} satisfies ICourierMessage,\n\t\t\t\treqOpts,\n\t\t\t));\n\t\t}\n\t};\n\n\t// Shared tail for both Apple flows (web callback + native token): given\n\t// verified claims, upsert the account by email, open a session, record the\n\t// login event, and return the same token/user envelope the Google flow does.\n\tconst completeAppleLogin = async (\n\t\tctx: IFonderieContext,\n\t\tclaims: { email?: string | undefined; sub?: string | undefined; emailVerified: boolean },\n\t): Promise<Response> => {\n\t\tconst meta = requestMeta(ctx);\n\t\tif (!claims.email) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'APPLE_AUTH_FAILED', 'No email in Apple identity token');\n\t\t}\n\t\t// Linking is BY EMAIL — an unverified address would let its holder take\n\t\t// over an account registered with it. Apple verifies both real and\n\t\t// private-relay addresses; require it.\n\t\tif (!claims.emailVerified) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'APPLE_AUTH_FAILED', 'Apple account email is not verified');\n\t\t}\n\t\tconst normalizedEmail = normalizeEmailSafe(claims.email);\n\t\tif (!normalizedEmail) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'APPLE_AUTH_FAILED', 'Invalid email in Apple identity token');\n\t\t}\n\n\t\tconst upserted = await users.upsertByProvider(normalizedEmail, 'apple', claims.sub ?? '');\n\t\tif (!upserted) {\n\t\t\tawait loginEvents.recordSafe({\n\t\t\t\tuserId: null,\n\t\t\t\temailAttempted: normalizedEmail,\n\t\t\t\tmethod: 'oauth-apple',\n\t\t\t\toutcome: 'failed',\n\t\t\t\tfailureReason: 'provider_upsert_failed',\n\t\t\t\t...meta,\n\t\t\t});\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Apple login failed');\n\t\t}\n\n\t\tconst fullUser = await users.findById(upserted.id);\n\t\tif (!fullUser) {\n\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Apple login failed');\n\t\t}\n\n\t\tawait announceOAuthUpsert(ctx, upserted, 'apple', fullUser);\n\n\t\tconst { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, { loginMethod: 'apple' });\n\t\tawait sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);\n\t\tawait loginEvents.recordSafe({\n\t\t\tuserId: upserted.id,\n\t\t\temailAttempted: normalizedEmail,\n\t\t\tmethod: 'oauth-apple',\n\t\t\toutcome: 'success',\n\t\t\t...meta,\n\t\t});\n\n\t\treturn Response.json(\n\t\t\t{\n\t\t\t\treason: 'APPLE_AUTH_SUCCESS',\n\t\t\t\texplanation: 'Apple authentication successful.',\n\t\t\t\tresult: {\n\t\t\t\t\ttokens: { access: accessToken, refresh: refreshToken },\n\t\t\t\t\tuser: toUserDTO(fullUser),\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tstatus: 200,\n\t\t\t\theaders: cookieHeaders([\n\t\t\t\t\t...tokenPairCookies(accessToken, refreshToken, config),\n\t\t\t\t\t// One-time value — clear the (SameSite=None) state cookie.\n\t\t\t\t\t'oauth_state=; HttpOnly; SameSite=None; Secure; Path=/; Max-Age=0',\n\t\t\t\t]),\n\t\t\t},\n\t\t);\n\t};\n\n\treturn {\n\t\tgoogleInit: async (_ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst google = config.google;\n\t\t\tif (!google) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.NOT_IMPLEMENTED,\n\t\t\t\t\t'NOT_CONFIGURED',\n\t\t\t\t\t'Google OAuth not configured',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// CSRF `state`: a random value that must round-trip — bound to THIS\n\t\t\t// browser via a short-lived cookie and echoed back by Google in the\n\t\t\t// callback query. Without it, an attacker can complete the callback\n\t\t\t// with a code from THEIR OWN Google account and silently log the\n\t\t\t// victim's browser into the attacker's account (login CSRF).\n\t\t\tconst state = randomBytes(16).toString('hex');\n\n\t\t\tconst params = new URLSearchParams({\n\t\t\t\tclient_id: google.clientId,\n\t\t\t\tredirect_uri: google.redirectUri,\n\t\t\t\tresponse_type: 'code',\n\t\t\t\tscope: 'openid email profile',\n\t\t\t\tstate,\n\t\t\t});\n\n\t\t\tconst url = `https://accounts.google.com/o/oauth2/v2/auth?${params}`;\n\n\t\t\tconst secure = (config.secureCookies ?? process.env['NODE_ENV'] === 'production') ? '; Secure' : '';\n\t\t\treturn Response.json(\n\t\t\t\t{\n\t\t\t\t\treason: 'GOOGLE_AUTH_URL',\n\t\t\t\t\texplanation: 'Redirect the user to the returned URL to begin Google OAuth.',\n\t\t\t\t\tresult: { url },\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\t// SameSite=Lax (not Strict): the cookie must be sent on the\n\t\t\t\t\t// top-level cross-site redirect back from Google.\n\t\t\t\t\theaders: cookieHeaders([\n\t\t\t\t\t\t`oauth_state=${state}; HttpOnly; SameSite=Lax; Path=/; Max-Age=600${secure}`,\n\t\t\t\t\t]),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\tgoogleCallback: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst google = config.google;\n\t\t\tif (!google) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.NOT_IMPLEMENTED,\n\t\t\t\t\t'NOT_CONFIGURED',\n\t\t\t\t\t'Google OAuth not configured',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst url = new URL(ctx.request.url);\n\t\t\tconst code = url.searchParams.get('code');\n\n\t\t\tif (!code) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_PARAMETER', 'Missing code');\n\t\t\t}\n\n\t\t\t// CSRF check: the state Google echoed back must equal the value we\n\t\t\t// bound to this browser in googleInit's cookie.\n\t\t\tconst returnedState = url.searchParams.get('state') ?? '';\n\t\t\tconst cookieHeader = ctx.request.headers.get('cookie') ?? '';\n\t\t\tconst expectedState = cookieHeader.match(/(?:^|;\\s*)oauth_state=([^;]+)/)?.[1] ?? '';\n\t\t\tif (\n\t\t\t\t!returnedState ||\n\t\t\t\t!expectedState ||\n\t\t\t\t!constantTimeEqual(Buffer.from(returnedState), Buffer.from(expectedState))\n\t\t\t) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'GOOGLE_AUTH_FAILED',\n\t\t\t\t\t'OAuth state mismatch — restart the sign-in flow',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst tokenRes = await fetch('https://oauth2.googleapis.com/token', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: { 'content-type': 'application/x-www-form-urlencoded' },\n\t\t\t\tbody: new URLSearchParams({\n\t\t\t\t\tcode,\n\t\t\t\t\tclient_id: google.clientId,\n\t\t\t\t\tclient_secret: google.clientSecret,\n\t\t\t\t\tredirect_uri: google.redirectUri,\n\t\t\t\t\tgrant_type: 'authorization_code',\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\tconst tokenData = (await tokenRes.json()) as { id_token?: string };\n\t\t\tif (!tokenData.id_token) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'GOOGLE_AUTH_FAILED',\n\t\t\t\t\t'OAuth token exchange failed',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// The id_token arrives DIRECTLY from Google's token endpoint over TLS\n\t\t\t// in a confidential-client code exchange, so per OIDC Core §3.1.3.7\n\t\t\t// signature verification may be skipped — but the claims still must\n\t\t\t// be checked before trusting them.\n\t\t\tconst payload = JSON.parse(\n\t\t\t\tBuffer.from(tokenData.id_token.split('.')[1] ?? '', 'base64url').toString(),\n\t\t\t) as { email?: string; sub?: string; aud?: string; iss?: string; exp?: number; email_verified?: boolean };\n\n\t\t\tif (payload.aud !== google.clientId) {\n\t\t\t\t// A token minted for a DIFFERENT client must never log anyone in here.\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'GOOGLE_AUTH_FAILED', 'OAuth token audience mismatch');\n\t\t\t}\n\t\t\tif (payload.iss !== 'https://accounts.google.com' && payload.iss !== 'accounts.google.com') {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'GOOGLE_AUTH_FAILED', 'OAuth token issuer mismatch');\n\t\t\t}\n\t\t\tif (typeof payload.exp !== 'number' || payload.exp * 1000 < Date.now()) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'GOOGLE_AUTH_FAILED', 'OAuth token expired');\n\t\t\t}\n\n\t\t\tif (!payload.email) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'GOOGLE_AUTH_FAILED', 'No email in OAuth response');\n\t\t\t}\n\n\t\t\t// Account linking is BY EMAIL (upsertByProvider), so an unverified\n\t\t\t// Google email would let its holder take over an existing account\n\t\t\t// that registered with that address. Require Google's verification.\n\t\t\tif (payload.email_verified !== true) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'GOOGLE_AUTH_FAILED',\n\t\t\t\t\t'Google account email is not verified',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst normalizedEmail = normalizeEmailSafe(payload.email);\n\t\t\tif (!normalizedEmail) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'GOOGLE_AUTH_FAILED', 'Invalid email in OAuth response');\n\t\t\t}\n\n\t\t\tconst meta = requestMeta(ctx);\n\n\t\t\tconst upserted = await users.upsertByProvider(normalizedEmail, 'google', payload.sub ?? '');\n\t\t\tif (!upserted) {\n\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\tuserId: null,\n\t\t\t\t\temailAttempted: normalizedEmail,\n\t\t\t\t\tmethod: 'oauth-google',\n\t\t\t\t\toutcome: 'failed',\n\t\t\t\t\tfailureReason: 'provider_upsert_failed',\n\t\t\t\t\t...meta,\n\t\t\t\t});\n\t\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'OAuth login failed');\n\t\t\t}\n\n\t\t\tconst fullUser = await users.findById(upserted.id);\n\t\t\tif (!fullUser) {\n\t\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'OAuth login failed');\n\t\t\t}\n\n\t\t\tawait announceOAuthUpsert(ctx, upserted, 'google', fullUser);\n\n\t\t\tconst { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, {\n\t\t\t\tloginMethod: 'google',\n\t\t\t});\n\t\t\tawait sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);\n\t\t\tawait loginEvents.recordSafe({\n\t\t\t\tuserId: upserted.id,\n\t\t\t\temailAttempted: normalizedEmail,\n\t\t\t\tmethod: 'oauth-google',\n\t\t\t\toutcome: 'success',\n\t\t\t\t...meta,\n\t\t\t});\n\n\t\t\treturn Response.json(\n\t\t\t\t{\n\t\t\t\t\treason: 'GOOGLE_AUTH_SUCCESS',\n\t\t\t\t\texplanation: 'Google authentication successful.',\n\t\t\t\t\tresult: {\n\t\t\t\t\t\ttokens: { access: accessToken, refresh: refreshToken },\n\t\t\t\t\t\tuser: toUserDTO(fullUser),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\theaders: cookieHeaders([\n\t\t\t\t\t\t...tokenPairCookies(accessToken, refreshToken, config),\n\t\t\t\t\t\t// One-time value — clear it once the flow completes.\n\t\t\t\t\t\t'oauth_state=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0',\n\t\t\t\t\t]),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\tappleInit: async (_ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst apple = config.apple;\n\t\t\tif (!apple) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_IMPLEMENTED, 'NOT_CONFIGURED', 'Apple OAuth not configured');\n\t\t\t}\n\n\t\t\tconst state = randomBytes(16).toString('hex');\n\t\t\tconst params = new URLSearchParams({\n\t\t\t\tclient_id: apple.clientId,\n\t\t\t\tredirect_uri: apple.redirectUri,\n\t\t\t\tresponse_type: 'code',\n\t\t\t\t// form_post is REQUIRED to receive name/email; Apple then POSTs the\n\t\t\t\t// callback cross-site.\n\t\t\t\tresponse_mode: 'form_post',\n\t\t\t\tscope: 'name email',\n\t\t\t\tstate,\n\t\t\t});\n\t\t\tconst url = `${APPLE_AUTH_URL}?${params}`;\n\n\t\t\t// SameSite=None; Secure (NOT Lax like Google): the state cookie must\n\t\t\t// survive Apple's cross-site POST back to the callback. Requires HTTPS,\n\t\t\t// which Sign in with Apple mandates for redirect URIs anyway.\n\t\t\treturn Response.json(\n\t\t\t\t{\n\t\t\t\t\treason: 'APPLE_AUTH_URL',\n\t\t\t\t\texplanation: 'Redirect the user to the returned URL to begin Apple OAuth.',\n\t\t\t\t\tresult: { url },\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\theaders: cookieHeaders([\n\t\t\t\t\t\t`oauth_state=${state}; HttpOnly; SameSite=None; Secure; Path=/; Max-Age=600`,\n\t\t\t\t\t]),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\n\t\tappleCallback: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst apple = config.apple;\n\t\t\tif (!apple) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_IMPLEMENTED, 'NOT_CONFIGURED', 'Apple OAuth not configured');\n\t\t\t}\n\n\t\t\t// Apple posts application/x-www-form-urlencoded (response_mode=form_post).\n\t\t\t// Prefer a pre-parsed body if the pipeline produced one, else read raw.\n\t\t\tconst pre = ctx.meta['body'];\n\t\t\tconst form =\n\t\t\t\tpre && typeof pre === 'object'\n\t\t\t\t\t? new URLSearchParams(pre as Record<string, string>)\n\t\t\t\t\t: new URLSearchParams(await ctx.request.text());\n\n\t\t\tconst code = form.get('code');\n\t\t\tif (!code) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_PARAMETER', 'Missing code');\n\t\t\t}\n\n\t\t\t// CSRF: the state Apple echoes must equal the value appleInit bound to\n\t\t\t// this browser via cookie.\n\t\t\tconst returnedState = form.get('state') ?? '';\n\t\t\tconst cookieHeader = ctx.request.headers.get('cookie') ?? '';\n\t\t\tconst expectedState = cookieHeader.match(/(?:^|;\\s*)oauth_state=([^;]+)/)?.[1] ?? '';\n\t\t\tif (\n\t\t\t\t!returnedState ||\n\t\t\t\t!expectedState ||\n\t\t\t\t!constantTimeEqual(Buffer.from(returnedState), Buffer.from(expectedState))\n\t\t\t) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'APPLE_AUTH_FAILED',\n\t\t\t\t\t'OAuth state mismatch — restart the sign-in flow',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst tokenRes = await fetch(APPLE_TOKEN_URL, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: { 'content-type': 'application/x-www-form-urlencoded' },\n\t\t\t\tbody: new URLSearchParams({\n\t\t\t\t\tcode,\n\t\t\t\t\tclient_id: apple.clientId,\n\t\t\t\t\tclient_secret: mintAppleClientSecret(apple),\n\t\t\t\t\tredirect_uri: apple.redirectUri,\n\t\t\t\t\tgrant_type: 'authorization_code',\n\t\t\t\t}),\n\t\t\t});\n\t\t\tconst tokenData = (await tokenRes.json()) as { id_token?: string };\n\t\t\tif (!tokenData.id_token) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'APPLE_AUTH_FAILED', 'OAuth token exchange failed');\n\t\t\t}\n\n\t\t\t// Web audience is the Services ID. Full JWKS signature check (one shared\n\t\t\t// verifier for both flows; stronger than skipping it).\n\t\t\tconst claims = await verifyAppleIdToken(tokenData.id_token, { audiences: [apple.clientId] });\n\t\t\tif (!claims) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'APPLE_AUTH_FAILED', 'Invalid Apple identity token');\n\t\t\t}\n\t\t\treturn completeAppleLogin(ctx, {\n\t\t\t\temail: claims.email,\n\t\t\t\tsub: claims.sub,\n\t\t\t\temailVerified: appleEmailVerified(claims),\n\t\t\t});\n\t\t},\n\n\t\tappleNative: async (ctx: IFonderieContext): Promise<Response> => {\n\t\t\tconst apple = config.apple;\n\t\t\tif (!apple) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_IMPLEMENTED, 'NOT_CONFIGURED', 'Apple OAuth not configured');\n\t\t\t}\n\t\t\tconst body = ctx.meta['body'] as { identityToken?: string; nonce?: string } | undefined;\n\t\t\tconst identityToken = body?.identityToken;\n\t\t\tif (!identityToken) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_PARAMETER', 'Missing identityToken');\n\t\t\t}\n\n\t\t\t// Native identityTokens carry aud = the app's bundle id (allow-listed\n\t\t\t// via nativeClientIds), not the Services ID, and did NOT come from our\n\t\t\t// TLS exchange — so verifyAppleIdToken's JWKS signature check is the\n\t\t\t// only thing between a forged token and a login.\n\t\t\tconst audiences =\n\t\t\t\tapple.nativeClientIds && apple.nativeClientIds.length\n\t\t\t\t\t? apple.nativeClientIds\n\t\t\t\t\t: [apple.clientId];\n\t\t\tconst claims = await verifyAppleIdToken(identityToken, { audiences, nonce: body?.nonce });\n\t\t\tif (!claims) {\n\t\t\t\tconst meta = requestMeta(ctx);\n\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\tuserId: null,\n\t\t\t\t\temailAttempted: null,\n\t\t\t\t\tmethod: 'oauth-apple',\n\t\t\t\t\toutcome: 'failed',\n\t\t\t\t\tfailureReason: 'invalid_identity_token',\n\t\t\t\t\t...meta,\n\t\t\t\t});\n\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'APPLE_AUTH_FAILED', 'Invalid Apple identity token');\n\t\t\t}\n\n\t\t\t// Single-use guard: a verified native identityToken may be redeemed\n\t\t\t// exactly once. Apple does not one-time it for us (unlike the web `code`\n\t\t\t// exchange), so without this a captured token could be replayed until\n\t\t\t// its exp. Consume by hash with TTL = the token's exp; a replay is a\n\t\t\t// recorded security event, never a login.\n\t\t\tconst tokenHash = createHash('sha256').update(identityToken).digest('hex');\n\t\t\tconst expMs = typeof claims.exp === 'number' ? claims.exp * 1000 : Date.now() + 5 * 60 * 1000;\n\t\t\tconst firstUse = await consumedTokens.consumeOnce(tokenHash, new Date(expMs));\n\t\t\tif (!firstUse) {\n\t\t\t\tconst meta = requestMeta(ctx);\n\t\t\t\tawait loginEvents.recordSafe({\n\t\t\t\t\tuserId: null,\n\t\t\t\t\temailAttempted: claims.email ?? null,\n\t\t\t\t\tmethod: 'oauth-apple',\n\t\t\t\t\toutcome: 'failed',\n\t\t\t\t\tfailureReason: 'token_replayed',\n\t\t\t\t\t...meta,\n\t\t\t\t});\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNAUTHORIZED,\n\t\t\t\t\t'APPLE_AUTH_FAILED',\n\t\t\t\t\t'This Apple identity token has already been used',\n\t\t\t\t);\n\t\t\t}\n\t\t\tconsumedTokens.sweepExpiredSafe();\n\n\t\t\treturn completeAppleLogin(ctx, {\n\t\t\t\temail: claims.email,\n\t\t\t\tsub: claims.sub,\n\t\t\t\temailVerified: appleEmailVerified(claims),\n\t\t\t});\n\t\t},\n\t};\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\n// Single-use guard for native OAuth identity tokens. Unlike the web `code`\n// exchange (which Apple/Google make one-time), a native identityToken is\n// presented directly by the client, so without this it could be replayed until\n// its exp. consumeOnce() records a hash of the token and reports whether THIS\n// call was the first to consume it — an atomic INSERT ... ON CONFLICT DO\n// NOTHING, so concurrent replays race safely (exactly one wins). Only a hash is\n// stored, never the token.\nexport class ConsumedTokenModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\t// True if the token had not been consumed before (caller may proceed); false\n\t// if this is a replay. `expiresAt` is the token's own exp — its TTL here.\n\tasync consumeOnce(tokenHash: string, expiresAt: Date): Promise<boolean> {\n\t\tconst rows = await this.store.query<{ token_hash: string }>(\n\t\t\t`INSERT INTO fonderie_consumed_tokens (token_hash, expires_at)\n\t\t\t VALUES ($1, $2)\n\t\t\t ON CONFLICT (token_hash) DO NOTHING\n\t\t\t RETURNING token_hash`,\n\t\t\t[tokenHash, expiresAt],\n\t\t);\n\t\treturn rows.length > 0;\n\t}\n\n\t// Opportunistic, fire-and-forget cleanup of expired rows (index-backed).\n\t// Recording history must never block or fail a login.\n\tsweepExpiredSafe(): void {\n\t\tvoid this.store\n\t\t\t.query(`DELETE FROM fonderie_consumed_tokens WHERE expires_at < now()`)\n\t\t\t.catch(() => {});\n\t}\n}\n","import type { IReadinessProblem } from '@fonderie/core';\nimport { MIN_SECRET_LENGTH, PLACEHOLDER_SECRET, secretStrengthProblem } from '@fonderie/core';\nimport type { IAuthConfig } from '../config';\n\nconst MODULE = '@fonderie/auth';\n\n// Pure production-readiness assessment (no side effects) — shared by the\n// boot-time guard and `AuthModule.checkReadiness`. Evaluates \"is this ready for\n// production?\" regardless of the current NODE_ENV. Secret-strength rule\n// (min length + placeholder denylist) is the shared @fonderie/core one.\nexport function collectAuthConfigProblems(config: IAuthConfig): IReadinessProblem[] {\n\tconst problems: IReadinessProblem[] = [];\n\tconst secret = config.jwtSecret ?? '';\n\n\tconst secretProblem = secretStrengthProblem(secret);\n\tif (secretProblem === 'too-short') {\n\t\tproblems.push({\n\t\t\tmodule: MODULE,\n\t\t\tseverity: 'error',\n\t\t\tmessage: `jwtSecret must be at least ${MIN_SECRET_LENGTH} characters (got ${secret.length})`,\n\t\t});\n\t} else if (secretProblem === 'placeholder') {\n\t\tproblems.push({\n\t\t\tmodule: MODULE,\n\t\t\tseverity: 'error',\n\t\t\tmessage: 'jwtSecret looks like a placeholder or dev-default value',\n\t\t});\n\t}\n\n\t// Explicitly disabling Secure cookies ships session cookies over plaintext —\n\t// an error in production (fails the boot gate), a warning elsewhere.\n\tif (config.secureCookies === false) {\n\t\tproblems.push({\n\t\t\tmodule: MODULE,\n\t\t\tseverity: process.env['NODE_ENV'] === 'production' ? 'error' : 'warning',\n\t\t\tmessage: 'secureCookies is false — auth cookies may be sent over non-HTTPS connections in production',\n\t\t});\n\t}\n\n\t// Google OAuth secrets: if the provider is wired up, its clientSecret is a\n\t// bearer credential to Google — a placeholder or blank value is as unsafe as\n\t// a weak jwtSecret, so it's a boot-blocking error in production.\n\tif (config.google) {\n\t\tconst clientSecret = config.google.clientSecret ?? '';\n\t\tif (!clientSecret || !config.google.clientId || !config.google.redirectUri) {\n\t\t\tproblems.push({\n\t\t\t\tmodule: MODULE,\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: 'google OAuth is configured but clientId, clientSecret, or redirectUri is missing',\n\t\t\t});\n\t\t} else if (PLACEHOLDER_SECRET.test(clientSecret)) {\n\t\t\tproblems.push({\n\t\t\t\tmodule: MODULE,\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: 'google.clientSecret looks like a placeholder or dev-default value',\n\t\t\t});\n\t\t}\n\t}\n\n\t// Sign in with Apple: the .p8 private key is a bearer credential to Apple. If\n\t// the provider is wired, its required fields must be present and the key must\n\t// look like a real PEM — a missing field or placeholder is a boot-blocking\n\t// error in production, same posture as google.clientSecret.\n\tif (config.apple) {\n\t\tconst a = config.apple;\n\t\tif (!a.clientId || !a.teamId || !a.keyId || !a.privateKey || !a.redirectUri) {\n\t\t\tproblems.push({\n\t\t\t\tmodule: MODULE,\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: 'apple OAuth is configured but clientId, teamId, keyId, privateKey, or redirectUri is missing',\n\t\t\t});\n\t\t} else if (!/-----BEGIN [A-Z ]*PRIVATE KEY-----/.test(a.privateKey)) {\n\t\t\tproblems.push({\n\t\t\t\tmodule: MODULE,\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: 'apple.privateKey does not look like a PEM .p8 key (expected `-----BEGIN PRIVATE KEY-----`)',\n\t\t\t});\n\t\t}\n\t\t// nativeClientIds is the audience allow-list for POST /auth/apple/native —\n\t\t// a native identityToken is accepted iff its aud is one of these. An empty\n\t\t// or wildcard entry would accept tokens minted for OTHER Apple apps, so\n\t\t// it's a boot-blocking error (only exact bundle ids belong here).\n\t\tif (a.nativeClientIds?.some((id) => !id || id.trim() === '' || id.includes('*'))) {\n\t\t\tproblems.push({\n\t\t\t\tmodule: MODULE,\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: 'apple.nativeClientIds must be exact bundle ids — empty or wildcard entries would accept identity tokens minted for other apps',\n\t\t\t});\n\t\t}\n\t}\n\n\t// MFA is on but TOTP secrets have no at-rest encryption key — they'd be\n\t// stored plaintext. An error in production (fails the boot gate); a warning\n\t// elsewhere so dev/test with backward-compatible defaults still run.\n\tif (config.mfa && !config.mfaSecretKey) {\n\t\tproblems.push({\n\t\t\tmodule: MODULE,\n\t\t\tseverity: process.env['NODE_ENV'] === 'production' ? 'error' : 'warning',\n\t\t\tmessage: 'mfa is enabled without mfaSecretKey — TOTP secrets are stored plaintext at rest; set a 32-byte key (openssl rand -hex 32)',\n\t\t});\n\t}\n\n\t// A malformed key can never decrypt: catch it at boot rather than on the\n\t// first MFA request. 32 bytes = 64 hex chars.\n\tif (config.mfaSecretKey && !/^[0-9a-fA-F]{64}$/.test(config.mfaSecretKey)) {\n\t\tproblems.push({\n\t\t\tmodule: MODULE,\n\t\t\tseverity: 'error',\n\t\t\tmessage: 'mfaSecretKey must be 64 hex characters (32 bytes, e.g. `openssl rand -hex 32`)',\n\t\t});\n\t}\n\n\treturn problems;\n}\n\n// Boot-time guard, run automatically when an `AuthModule` is constructed\n// (fail-fast, before boot). In production a weak `jwtSecret` is fatal — a\n// forgeable token is an auth bypass — so we refuse to boot; outside production\n// the same errors are a loud warning so dev/test still run. `secureCookies`\n// warnings are only surfaced in production (dev intentionally uses non-secure\n// cookies over localhost).\nexport function validateAuthConfig(config: IAuthConfig): void {\n\tconst isProduction = process.env['NODE_ENV'] === 'production';\n\tconst problems = collectAuthConfigProblems(config);\n\tconst errors = problems.filter((p) => p.severity === 'error');\n\n\tif (isProduction && errors.length > 0) {\n\t\tthrow new Error(\n\t\t\t`[auth] insecure config — ${errors.map((e) => e.message).join('; ')}. ` +\n\t\t\t\t'Set a long, random jwtSecret (e.g. `openssl rand -base64 32`). ' +\n\t\t\t\t'Refusing to boot in production.',\n\t\t);\n\t}\n\n\tif (isProduction) {\n\t\tfor (const p of problems) console.warn(`[auth] ${p.message}`);\n\t} else {\n\t\tfor (const p of errors) {\n\t\t\tconsole.warn(`[auth] ${p.message} (insecure — would refuse to boot in production)`);\n\t\t}\n\t}\n}\n","import type { Middleware } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAuthConfig } from '../config';\nimport { verifyToken } from '../services/jwt';\nimport type { IAccessPayload } from '../services/jwt';\nimport { UserModel } from '../models/user.model';\nimport { SessionModel } from '../models/session.model';\n\n// Reads the Bearer token or session cookie, populates ctx.user\n// Does NOT reject — anonymous requests pass through\nexport function withSession(store: IStoreAdapter, config: IAuthConfig): Middleware {\n\tconst users = new UserModel(store);\n\tconst sessions = new SessionModel(store);\n\n\treturn async (ctx, next) => {\n\t\tconst token = extractToken(ctx.request);\n\t\tif (!token) {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst payload = verifyToken(token, config);\n\t\tif (!payload || payload.type !== 'access') {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Revocation: access tokens are bound to their refresh session via the\n\t\t// sid claim. Logout, rotation, and password change delete that row, so\n\t\t// the access token dies with it instead of living out its JWT expiry.\n\t\t// Tokens without a sid pass through: short-lived mfaPending tokens\n\t\t// (no session exists yet) and legacy tokens from before session\n\t\t// binding, which age out within one accessTokenDuration of deploy.\n\t\tif (payload.sid && !(await sessions.aliveBySid(payload.sid))) {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst user = await users.findById(payload.sub);\n\t\tif (!user || user.suspended || user.deletedAt) {\n\t\t\treturn next();\n\t\t}\n\n\t\tObject.assign(ctx, {\n\t\t\tuser: {\n\t\t\t\t...user,\n\t\t\t\tloginMethod: payload.loginMethod ?? 'email',\n\t\t\t\tphoneVerified: payload.phoneVerified ?? false,\n\t\t\t\tmfaPending: (payload as IAccessPayload).mfaPending ?? false,\n\t\t\t\t// The session this request is authenticated on, so the sessions\n\t\t\t\t// API can flag the current row and \"terminate others\" can spare it.\n\t\t\t\tsid: payload.sid ?? null,\n\t\t\t},\n\t\t});\n\n\t\treturn next();\n\t};\n}\n\nfunction extractToken(request: Request): string | null {\n\tconst auth = request.headers.get('authorization');\n\tif (auth?.startsWith('Bearer ')) {\n\t\treturn auth.slice(7);\n\t}\n\n\tconst cookie = request.headers.get('cookie') ?? '';\n\tconst match = cookie.match(/(?:^|;\\s*)access_token=([^;]+)/);\n\treturn match?.[1] ?? null;\n}\n","import type { IAdminRoute, IFonderieContext, Middleware } from '@fonderie/core';\nimport { HTTP, encodeKeysetCursor, setApiResponse } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { decodeLoginCursor, toLoginHistoryPageDTO, toSessionDTO } from './dtos/login-activity';\nimport { toUserDTO } from './dtos/user';\nimport type { IUserDTO } from './dtos/user';\nimport { LoginEventModel } from './models/login-event.model';\nimport { SessionModel } from './models/session.model';\nimport { UserModel } from './models/user.model';\nimport type { IUserPage } from './models/user.model';\nimport type { IUser } from './types';\n\n// What the operator sees: the app's own user shape plus the fields support\n// asks about first. Never the password hash, never the MFA secret.\nexport interface IAdminUserDTO extends IUserDTO {\n\tsuspended: boolean;\n\tdeletedAt: string | null;\n\tcreatedAt: string;\n}\n\nexport function toAdminUserDTO(user: IUser): IAdminUserDTO {\n\treturn {\n\t\t...toUserDTO(user),\n\t\tsuspended: Boolean(user.suspended),\n\t\tdeletedAt: user.deletedAt ? user.deletedAt.toISOString() : null,\n\t\tcreatedAt: user.createdAt.toISOString(),\n\t};\n}\n\nexport interface IAdminUserPageDTO {\n\tusers: IAdminUserDTO[];\n\tnextCursor: string | null;\n}\n\nexport function toAdminUserPageDTO(page: IUserPage): IAdminUserPageDTO {\n\tconst last = page.users[page.users.length - 1];\n\t// Full microsecond precision (createdAtRaw) so same-microsecond rows are\n\t// not skipped between pages.\n\tconst nextCursor =\n\t\tpage.hasMore && last\n\t\t\t? encodeKeysetCursor(last.createdAtRaw ?? last.createdAt.toISOString(), last.id)\n\t\t\t: null;\n\treturn { users: page.users.map(toAdminUserDTO), nextCursor };\n}\n\nconst ADMIN_PREFIX = '/_admin';\nconst idOf = (ctx: IFonderieContext) => ctx.meta.params?.['id'] ?? '';\nconst NOT_FOUND = () => setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such user');\n\n// Declared at the default admin path so the route table reads literally;\n// @fonderie/admin re-bases them under its prefix. There is no standalone\n// surface: these exist only through composition.\nfunction adminRouteTable(store: IStoreAdapter): Array<[string, string, Middleware]> {\n\tconst users = new UserModel(store);\n\tconst sessions = new SessionModel(store);\n\tconst events = new LoginEventModel(store);\n\n\tconst setSuspended =\n\t\t(suspended: boolean): Middleware =>\n\t\tasync (ctx) => {\n\t\t\tconst ok = await users.setSuspended(idOf(ctx), suspended);\n\t\t\tif (!ok) return NOT_FOUND();\n\t\t\tconst user = await users.findById(idOf(ctx));\n\t\t\treturn user\n\t\t\t\t? setApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\tsuspended ? 'USER_SUSPENDED' : 'USER_UNSUSPENDED',\n\t\t\t\t\t\tsuspended ? 'User suspended' : 'User unsuspended',\n\t\t\t\t\t\ttoAdminUserDTO(user),\n\t\t\t\t\t)\n\t\t\t\t: NOT_FOUND();\n\t\t};\n\n\treturn [\n\t\t[\n\t\t\t'GET',\n\t\t\t'/_admin/users',\n\t\t\t// With an email, the exact lookup support reaches for. Without one, a\n\t\t\t// page of users — an operator who has to know a name before they can\n\t\t\t// see anything cannot discover who signed up this morning.\n\t\t\tasync (ctx) => {\n\t\t\t\tconst params = new URL(ctx.request.url).searchParams;\n\t\t\t\tconst email = params.get('email')?.trim().toLowerCase();\n\t\t\t\tif (email) {\n\t\t\t\t\tconst user = await users.findByEmail(email);\n\t\t\t\t\treturn user ? setApiResponse(HTTP.OK, 'USER', 'User', toAdminUserDTO(user)) : NOT_FOUND();\n\t\t\t\t}\n\n\t\t\t\tconst rawLimit = Number(params.get('limit') ?? 50);\n\t\t\t\tconst limit = Number.isFinite(rawLimit)\n\t\t\t\t\t? Math.min(Math.max(Math.trunc(rawLimit), 1), 200)\n\t\t\t\t\t: 50;\n\t\t\t\tconst cursorParam = params.get('cursor');\n\t\t\t\tconst cursor = cursorParam ? decodeLoginCursor(cursorParam) : null;\n\t\t\t\tif (cursorParam && !cursor)\n\t\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid cursor');\n\t\t\t\tconst deleted = params.get('deleted') === '1' || params.get('deleted') === 'true';\n\t\t\t\tconst page = await users.list({\n\t\t\t\t\tlimit,\n\t\t\t\t\t...(cursor ? { cursor } : {}),\n\t\t\t\t\t...(deleted ? { deleted } : {}),\n\t\t\t\t});\n\t\t\t\treturn setApiResponse(HTTP.OK, 'USERS', 'Users', toAdminUserPageDTO(page));\n\t\t\t},\n\t\t],\n\t\t[\n\t\t\t'GET',\n\t\t\t'/_admin/users/:id',\n\t\t\tasync (ctx) => {\n\t\t\t\t// Read-only view: a soft-deleted account resolves (deletedAt set), so\n\t\t\t\t// an operator arriving from billing sees what happened to it.\n\t\t\t\tconst user = await users.findByIdIncludingDeleted(idOf(ctx));\n\t\t\t\treturn user ? setApiResponse(HTTP.OK, 'USER', 'User', toAdminUserDTO(user)) : NOT_FOUND();\n\t\t\t},\n\t\t],\n\t\t[\n\t\t\t'GET',\n\t\t\t'/_admin/users/:id/sessions',\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!(await users.findById(idOf(ctx)))) return NOT_FOUND();\n\t\t\t\tconst rows = await sessions.listLiveByUser(idOf(ctx));\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t'SESSIONS',\n\t\t\t\t\t'Live sessions',\n\t\t\t\t\trows.map((r) => toSessionDTO(r, null)),\n\t\t\t\t);\n\t\t\t},\n\t\t],\n\t\t// Signs the user out everywhere. The one write support reaches for first.\n\t\t[\n\t\t\t'DELETE',\n\t\t\t'/_admin/users/:id/sessions',\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!(await users.findById(idOf(ctx)))) return NOT_FOUND();\n\t\t\t\tawait sessions.deleteByUser(idOf(ctx));\n\t\t\t\treturn setApiResponse(HTTP.OK, 'SESSIONS_REVOKED', 'All sessions revoked');\n\t\t\t},\n\t\t],\n\t\t[\n\t\t\t'GET',\n\t\t\t'/_admin/users/:id/login-history',\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!(await users.findById(idOf(ctx)))) return NOT_FOUND();\n\t\t\t\tconst params = new URL(ctx.request.url).searchParams;\n\t\t\t\tconst rawLimit = Number(params.get('limit') ?? 50);\n\t\t\t\tconst limit = Number.isFinite(rawLimit)\n\t\t\t\t\t? Math.min(Math.max(Math.trunc(rawLimit), 1), 200)\n\t\t\t\t\t: 50;\n\t\t\t\tconst cursorParam = params.get('cursor');\n\t\t\t\tconst cursor = cursorParam ? decodeLoginCursor(cursorParam) : null;\n\t\t\t\tif (cursorParam && !cursor)\n\t\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid cursor');\n\t\t\t\tconst page = await events.listByUser({\n\t\t\t\t\tuserId: idOf(ctx),\n\t\t\t\t\tlimit,\n\t\t\t\t\t...(cursor ? { cursor } : {}),\n\t\t\t\t});\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t'LOGIN_HISTORY',\n\t\t\t\t\t'Login history',\n\t\t\t\t\ttoLoginHistoryPageDTO(page),\n\t\t\t\t);\n\t\t\t},\n\t\t],\n\t\t['POST', '/_admin/users/:id/suspend', setSuspended(true)],\n\t\t['POST', '/_admin/users/:id/unsuspend', setSuspended(false)],\n\t];\n}\n\nexport function describeAuthAdminRoutes(store: IStoreAdapter): IAdminRoute[] {\n\treturn adminRouteTable(store).map(([method, path, h]) => ({\n\t\tmethod,\n\t\tpath: path.slice(ADMIN_PREFIX.length),\n\t\thandlers: [h],\n\t}));\n}\n","import type { IAdminDescription, IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport type { EventBus } from '@fonderie/events';\n\nimport type { IReadinessProblem } from '@fonderie/core';\nimport { buildAuthRoutes } from './routes';\nimport type { IAuthConfig } from './config';\nimport { validateAuthConfig, collectAuthConfigProblems } from './services/config-guard';\nimport { withSession } from './middlewares/session';\nimport { describeAuthAdminRoutes } from './admin';\n\nexport class AuthModule implements IFonderieModule {\n\treadonly name = '@fonderie/auth';\n\t// Baked in at build time by tsup.base, so the operator's Modules page can\n\t// say what is actually deployed rather than 'not reported'.\n\treadonly version = process.env['FONDERIE_PKG_VERSION'] ?? '0.0.0-dev';\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate config: IAuthConfig,\n\t\tprivate bus?: EventBus,\n\t) {\n\t\t// Fail fast on an insecure jwtSecret (fatal in production) before boot.\n\t\tvalidateAuthConfig(config);\n\t}\n\n\t// Users, sessions, login history, suspend — only through @fonderie/admin.\n\tdescribeAdmin(): IAdminDescription {\n\t\treturn { routes: describeAuthAdminRoutes(this.store) };\n\t}\n\n\t// Report config problems for app.checkProductionReadiness() (data, not throw).\n\tcheckReadiness(): IReadinessProblem[] {\n\t\treturn collectAuthConfigProblems(this.config);\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\tapp.use(withSession(this.store, this.config));\n\n\t\t// New users start in the app's system locale (core owns it) unless they\n\t\t// signed up in another one.\n\t\tconst routes = buildAuthRoutes(this.store, this.config, this.bus, app.locales);\n\t\tfor (const [method, path, ...handlers] of routes) {\n\t\t\tapp.addRoute(method, path, ...handlers);\n\t\t}\n\t}\n}\n","import type { IDefaultTemplate } from '@fonderie/core';\nimport { withTranslations } from '@fonderie/core';\n\nimport { MESSAGE_KEYS, type AuthMessageKey } from './config';\nimport { ES_TEMPLATES } from './templates.es';\nimport { FR_TEMPLATES } from './templates.fr';\n\n// Built-in default copy for every @fonderie/auth notification, shipped so the\n// emails render out of the box — no per-app template authoring, and never the\n// raw-JSON fallback. `html` values are BODY FRAGMENTS: courier injects them into\n// its branded layout shell (an app rebrands via its own `_layout`), then\n// interpolates {{vars}}. An app overrides any single key by shipping its own\n// template (DB row / FS file) for that key.\n//\n// The `satisfies Record<AuthMessageKey, IDefaultTemplate>` is the completeness\n// guarantee: add a key to MESSAGE_KEYS without a default here and it will not\n// compile. `phone-otp` is SMS — text only, no subject/html.\nconst EN_TEMPLATES = {\n\t[MESSAGE_KEYS.emailRegistration]: {\n\t\tsubject: 'Confirm your account',\n\t\thtml: `<h1>Welcome aboard</h1>\n<p>Hi {{firstName}},</p>\n<p>Thanks for signing up. Confirm your account with this code:</p>\n<p><span class=\"pin-code\">{{pin}}</span></p>\n<p class=\"muted\">This code expires in 24 hours. If you didn’t create an account, you can safely ignore this email.</p>`,\n\t\ttext: `Welcome aboard\n\nHi {{firstName}},\n\nThanks for signing up. Confirm your account with this code: {{pin}}\n\nThis code expires in 24 hours. If you didn't create an account, you can safely ignore this email.`,\n\t},\n\n\t[MESSAGE_KEYS.emailVerification]: {\n\t\tsubject: 'Your verification code',\n\t\thtml: `<h1>Verify your email</h1>\n<p>Hi {{firstName}},</p>\n<p>Use this code to finish setting up your account:</p>\n<p><span class=\"pin-code\">{{pin}}</span></p>\n<p class=\"muted\">This code expires in 24 hours. If you didn’t create an account, you can safely ignore this email.</p>`,\n\t\ttext: `Verify your email\n\nHi {{firstName}},\n\nUse this code to finish setting up your account: {{pin}}\n\nThis code expires in 24 hours. If you didn't create an account, you can safely ignore this email.`,\n\t},\n\n\t[MESSAGE_KEYS.passwordReset]: {\n\t\tsubject: 'Reset your password',\n\t\thtml: `<h1>Reset your password</h1>\n<p>We received a request to reset your password. Use this code to continue:</p>\n<p><span class=\"pin-code\">{{pin}}</span></p>\n<p class=\"muted\">This code expires soon. If you didn’t request a reset, no action is needed — your password stays the same.</p>`,\n\t\ttext: `Reset your password\n\nWe received a request to reset your password. Use this code to continue: {{pin}}\n\nThis code expires soon. If you didn't request a reset, no action is needed — your password stays the same.`,\n\t},\n\n\t// SMS — text only, no subject/html.\n\t[MESSAGE_KEYS.phoneOtp]: {\n\t\ttext: '{{otp}} is your verification code. It expires in 10 minutes.',\n\t},\n\n\t[MESSAGE_KEYS.mfaEnabled]: {\n\t\tsubject: 'Two-factor authentication is on',\n\t\thtml: `<h1>Two-factor authentication enabled</h1>\n<p>Two-factor authentication was just turned on for your account. From now on you’ll enter a code from your authenticator app when you sign in.</p>\n<p class=\"muted\">If you didn’t do this, contact support right away — someone may have access to your account.</p>`,\n\t\ttext: `Two-factor authentication enabled\n\nTwo-factor authentication was just turned on for your account. From now on you'll enter a code from your authenticator app when you sign in.\n\nIf you didn't do this, contact support right away — someone may have access to your account.`,\n\t},\n\n\t[MESSAGE_KEYS.mfaDisabled]: {\n\t\tsubject: 'Two-factor authentication is off',\n\t\thtml: `<h1>Two-factor authentication disabled</h1>\n<p>Two-factor authentication was just turned off for your account. Your account is now protected by your password alone.</p>\n<p class=\"muted\">If you didn’t do this, contact support right away and re-enable two-factor authentication — someone may have access to your account.</p>`,\n\t\ttext: `Two-factor authentication disabled\n\nTwo-factor authentication was just turned off for your account. Your account is now protected by your password alone.\n\nIf you didn't do this, contact support right away and re-enable two-factor authentication — someone may have access to your account.`,\n\t},\n\n\t[MESSAGE_KEYS.mfaBackupCodesRegenerated]: {\n\t\tsubject: 'Your backup codes were regenerated',\n\t\thtml: `<h1>New backup codes generated</h1>\n<p>A new set of two-factor backup codes was just generated for your account. Your previous backup codes no longer work.</p>\n<p class=\"muted\">If you didn’t do this, contact support right away — someone may have access to your account.</p>`,\n\t\ttext: `New backup codes generated\n\nA new set of two-factor backup codes was just generated for your account. Your previous backup codes no longer work.\n\nIf you didn't do this, contact support right away — someone may have access to your account.`,\n\t},\n\n\t[MESSAGE_KEYS.emailChanged]: {\n\t\tsubject: 'Your email address was changed',\n\t\thtml: `<h1>Your email was changed</h1>\n<p>The email address on your account was just changed to <strong>{{newEmail}}</strong>.</p>\n<p class=\"muted\">If you made this change, you’re all set. If not, contact support right away — someone may have access to your account.</p>`,\n\t\ttext: `Your email was changed\n\nThe email address on your account was just changed to {{newEmail}}.\n\nIf you made this change, you're all set. If not, contact support right away — someone may have access to your account.`,\n\t},\n\n\t[MESSAGE_KEYS.phoneChanged]: {\n\t\tsubject: 'Your phone number was changed',\n\t\thtml: `<h1>Your phone number was changed</h1>\n<p>The phone number on your account was just updated.</p>\n<p class=\"muted\">If you made this change, you’re all set. If not, contact support right away — someone may have access to your account.</p>`,\n\t\ttext: `Your phone number was changed\n\nThe phone number on your account was just updated.\n\nIf you made this change, you're all set. If not, contact support right away — someone may have access to your account.`,\n\t},\n\n\t[MESSAGE_KEYS.passwordRevoked]: {\n\t\tsubject: 'The password on your account was removed',\n\t\thtml: `<h1>Your password was removed</h1>\n<p>You just signed in with <strong>{{provider}}</strong>. A password had been set on this account, but this email address had never been confirmed — so we could not tell who set it, and it has been removed.</p>\n<p>Signing in with {{provider}} works as before. If you want a password as well, set one from your account settings.</p>\n<p class=\"muted\">If you never set a password here, someone else may have tried to register with your address. Nothing further is needed — the account is yours.</p>`,\n\t\ttext: `Your password was removed\n\nYou just signed in with {{provider}}. A password had been set on this account,\nbut this email address had never been confirmed — so we could not tell who set\nit, and it has been removed.\n\nSigning in with {{provider}} works as before. If you want a password as well,\nset one from your account settings.\n\nIf you never set a password here, someone else may have tried to register with\nyour address. Nothing further is needed — the account is yours.`,\n\t},\n\n\t[MESSAGE_KEYS.oauthRegistration]: {\n\t\tsubject: 'Welcome to {{appName}}',\n\t\thtml: `<h1>Welcome to {{appName}}</h1>\n<p>Your account was created using <strong>{{provider}}</strong>. Sign in any time with the same {{provider}} account — there is no password to remember.</p>\n<p class=\"muted\">If you did not create this account, contact support.</p>`,\n\t\ttext: `Welcome\n\nYour account was created using {{provider}}. Sign in any time with the same\n{{provider}} account — there is no password to remember.\n\nIf you did not create this account, contact support.`,\n\t},\n\n\t[MESSAGE_KEYS.oauthLinked]: {\n\t\tsubject: 'A new sign-in method was added to your account',\n\t\thtml: `<h1>{{provider}} sign-in was added</h1>\n<p>Your account can now also be signed into with <strong>{{provider}}</strong>.</p>\n<p class=\"muted\">If you did this, nothing more is needed. If not, contact support right away and change your password — someone else may be able to sign in as you.</p>`,\n\t\ttext: `{{provider}} sign-in was added\n\nYour account can now also be signed into with {{provider}}.\n\nIf you did this, nothing more is needed. If not, contact support right away and\nchange your password — someone else may be able to sign in as you.`,\n\t},\n\n\t[MESSAGE_KEYS.oauthUnlinked]: {\n\t\tsubject: 'A sign-in method was removed from your account',\n\t\thtml: `<h1>{{provider}} sign-in was removed</h1>\n<p><strong>{{provider}}</strong> can no longer be used to sign in. Your email and password still work.</p>\n<p class=\"muted\">If you did this, you’re all set. If not, contact support right away.</p>`,\n\t\ttext: `{{provider}} sign-in was removed\n\n{{provider}} can no longer be used to sign in. Your email and password still work.\n\nIf you did this, you're all set. If not, contact support right away.`,\n\t},\n} satisfies Record<AuthMessageKey, IDefaultTemplate>;\n\n// The English above, with every email's French and Spanish attached. Courier\n// sends the one matching the recipient's language (an app's own saved version\n// still wins); anything else gets the English.\nexport const DEFAULT_TEMPLATES = withTranslations(EN_TEMPLATES, { fr: FR_TEMPLATES, es: ES_TEMPLATES });\n\n// Representative payloads for the coverage test: every {{var}} a default uses\n// must appear here. Security-notice keys carry no variables ({}). The\n// registration sample uses an empty firstName to prove the blank-name path\n// renders cleanly (the emitter may pass firstName: '').\nexport const SAMPLE_PAYLOADS: Record<AuthMessageKey, Record<string, unknown>> = {\n\t[MESSAGE_KEYS.passwordRevoked]: { provider: 'Google' },\n\t[MESSAGE_KEYS.oauthRegistration]: { provider: 'Google', appName: 'Fonderie' },\n\t[MESSAGE_KEYS.oauthLinked]: { provider: 'Google' },\n\t[MESSAGE_KEYS.oauthUnlinked]: { provider: 'Google' },\n\t[MESSAGE_KEYS.emailRegistration]: { firstName: '', pin: '123456' },\n\t[MESSAGE_KEYS.emailVerification]: { firstName: 'Ada', pin: '123456' },\n\t[MESSAGE_KEYS.passwordReset]: {\n\t\tpin: '123456',\n\t\ttoken: '0'.repeat(64),\n\t\tresetUrl: 'https://app.example.com/reset?token=' + '0'.repeat(64),\n\t},\n\t[MESSAGE_KEYS.phoneOtp]: { otp: '123456' },\n\t[MESSAGE_KEYS.mfaEnabled]: {},\n\t[MESSAGE_KEYS.mfaDisabled]: {},\n\t[MESSAGE_KEYS.mfaBackupCodesRegenerated]: {},\n\t[MESSAGE_KEYS.emailChanged]: { newEmail: 'new@example.com' },\n\t[MESSAGE_KEYS.phoneChanged]: {},\n};\n","import type { IDefaultTemplateCopy } from '@fonderie/core';\n\nimport { MESSAGE_KEYS, type AuthMessageKey } from './config';\n\n// Spanish copy of every built-in auth email. Same keys and the same\n// {{variables}} as the English (./templates.ts) — the `satisfies` makes a\n// missing email a compile error, and the coverage test checks the variables\n// match. Addressed with \"tú\", as product email in Spanish usually is.\nexport const ES_TEMPLATES = {\n\t[MESSAGE_KEYS.emailRegistration]: {\n\t\tsubject: 'Confirma tu cuenta',\n\t\thtml: `<h1>Te damos la bienvenida</h1>\n<p>Hola {{firstName}}:</p>\n<p>Gracias por registrarte. Confirma tu cuenta con este código:</p>\n<p><span class=\"pin-code\">{{pin}}</span></p>\n<p class=\"muted\">Este código caduca en 24 horas. Si no creaste una cuenta, puedes ignorar este correo.</p>`,\n\t\ttext: `Te damos la bienvenida\n\nHola {{firstName}}:\n\nGracias por registrarte. Confirma tu cuenta con este código: {{pin}}\n\nEste código caduca en 24 horas. Si no creaste una cuenta, puedes ignorar este correo.`,\n\t},\n\n\t[MESSAGE_KEYS.emailVerification]: {\n\t\tsubject: 'Tu código de verificación',\n\t\thtml: `<h1>Verifica tu correo electrónico</h1>\n<p>Hola {{firstName}}:</p>\n<p>Usa este código para terminar de configurar tu cuenta:</p>\n<p><span class=\"pin-code\">{{pin}}</span></p>\n<p class=\"muted\">Este código caduca en 24 horas. Si no creaste una cuenta, puedes ignorar este correo.</p>`,\n\t\ttext: `Verifica tu correo electrónico\n\nHola {{firstName}}:\n\nUsa este código para terminar de configurar tu cuenta: {{pin}}\n\nEste código caduca en 24 horas. Si no creaste una cuenta, puedes ignorar este correo.`,\n\t},\n\n\t[MESSAGE_KEYS.passwordReset]: {\n\t\tsubject: 'Restablece tu contraseña',\n\t\thtml: `<h1>Restablece tu contraseña</h1>\n<p>Recibimos una solicitud para restablecer tu contraseña. Usa este código para continuar:</p>\n<p><span class=\"pin-code\">{{pin}}</span></p>\n<p class=\"muted\">Este código caduca pronto. Si no lo solicitaste, no tienes que hacer nada — tu contraseña sigue siendo la misma.</p>`,\n\t\ttext: `Restablece tu contraseña\n\nRecibimos una solicitud para restablecer tu contraseña. Usa este código para continuar: {{pin}}\n\nEste código caduca pronto. Si no lo solicitaste, no tienes que hacer nada — tu contraseña sigue siendo la misma.`,\n\t},\n\n\t// SMS — text only.\n\t[MESSAGE_KEYS.phoneOtp]: {\n\t\ttext: '{{otp}} es tu código de verificación. Caduca en 10 minutos.',\n\t},\n\n\t[MESSAGE_KEYS.mfaEnabled]: {\n\t\tsubject: 'La verificación en dos pasos está activada',\n\t\thtml: `<h1>Verificación en dos pasos activada</h1>\n<p>Se acaba de activar la verificación en dos pasos en tu cuenta. A partir de ahora, introducirás un código de tu aplicación de autenticación al iniciar sesión.</p>\n<p class=\"muted\">Si no fuiste tú, contacta con soporte de inmediato — alguien podría tener acceso a tu cuenta.</p>`,\n\t\ttext: `Verificación en dos pasos activada\n\nSe acaba de activar la verificación en dos pasos en tu cuenta. A partir de ahora, introducirás un código de tu aplicación de autenticación al iniciar sesión.\n\nSi no fuiste tú, contacta con soporte de inmediato — alguien podría tener acceso a tu cuenta.`,\n\t},\n\n\t[MESSAGE_KEYS.mfaDisabled]: {\n\t\tsubject: 'La verificación en dos pasos está desactivada',\n\t\thtml: `<h1>Verificación en dos pasos desactivada</h1>\n<p>Se acaba de desactivar la verificación en dos pasos en tu cuenta. Ahora tu cuenta está protegida solo por tu contraseña.</p>\n<p class=\"muted\">Si no fuiste tú, contacta con soporte de inmediato y vuelve a activar la verificación en dos pasos — alguien podría tener acceso a tu cuenta.</p>`,\n\t\ttext: `Verificación en dos pasos desactivada\n\nSe acaba de desactivar la verificación en dos pasos en tu cuenta. Ahora tu cuenta está protegida solo por tu contraseña.\n\nSi no fuiste tú, contacta con soporte de inmediato y vuelve a activar la verificación en dos pasos — alguien podría tener acceso a tu cuenta.`,\n\t},\n\n\t[MESSAGE_KEYS.mfaBackupCodesRegenerated]: {\n\t\tsubject: 'Se regeneraron tus códigos de respaldo',\n\t\thtml: `<h1>Nuevos códigos de respaldo generados</h1>\n<p>Se acaba de generar un nuevo conjunto de códigos de respaldo de verificación en dos pasos para tu cuenta. Tus códigos anteriores ya no funcionan.</p>\n<p class=\"muted\">Si no fuiste tú, contacta con soporte de inmediato — alguien podría tener acceso a tu cuenta.</p>`,\n\t\ttext: `Nuevos códigos de respaldo generados\n\nSe acaba de generar un nuevo conjunto de códigos de respaldo de verificación en dos pasos para tu cuenta. Tus códigos anteriores ya no funcionan.\n\nSi no fuiste tú, contacta con soporte de inmediato — alguien podría tener acceso a tu cuenta.`,\n\t},\n\n\t[MESSAGE_KEYS.emailChanged]: {\n\t\tsubject: 'Se cambió tu dirección de correo electrónico',\n\t\thtml: `<h1>Se cambió tu correo electrónico</h1>\n<p>La dirección de correo electrónico de tu cuenta se acaba de cambiar a <strong>{{newEmail}}</strong>.</p>\n<p class=\"muted\">Si hiciste este cambio, todo está en orden. Si no, contacta con soporte de inmediato — alguien podría tener acceso a tu cuenta.</p>`,\n\t\ttext: `Se cambió tu correo electrónico\n\nLa dirección de correo electrónico de tu cuenta se acaba de cambiar a {{newEmail}}.\n\nSi hiciste este cambio, todo está en orden. Si no, contacta con soporte de inmediato — alguien podría tener acceso a tu cuenta.`,\n\t},\n\n\t[MESSAGE_KEYS.phoneChanged]: {\n\t\tsubject: 'Se cambió tu número de teléfono',\n\t\thtml: `<h1>Se cambió tu número de teléfono</h1>\n<p>El número de teléfono de tu cuenta se acaba de actualizar.</p>\n<p class=\"muted\">Si hiciste este cambio, todo está en orden. Si no, contacta con soporte de inmediato — alguien podría tener acceso a tu cuenta.</p>`,\n\t\ttext: `Se cambió tu número de teléfono\n\nEl número de teléfono de tu cuenta se acaba de actualizar.\n\nSi hiciste este cambio, todo está en orden. Si no, contacta con soporte de inmediato — alguien podría tener acceso a tu cuenta.`,\n\t},\n\n\t[MESSAGE_KEYS.passwordRevoked]: {\n\t\tsubject: 'Se eliminó la contraseña de tu cuenta',\n\t\thtml: `<h1>Se eliminó tu contraseña</h1>\n<p>Acabas de iniciar sesión con <strong>{{provider}}</strong>. Esta cuenta tenía una contraseña, pero esta dirección de correo nunca se había confirmado — así que no podíamos saber quién la estableció, y se ha eliminado.</p>\n<p>Iniciar sesión con {{provider}} funciona como antes. Si también quieres una contraseña, puedes crearla en la configuración de tu cuenta.</p>\n<p class=\"muted\">Si nunca estableciste una contraseña aquí, es posible que alguien intentara registrarse con tu dirección. No tienes que hacer nada más — la cuenta es tuya.</p>`,\n\t\ttext: `Se eliminó tu contraseña\n\nAcabas de iniciar sesión con {{provider}}. Esta cuenta tenía una contraseña, pero\nesta dirección de correo nunca se había confirmado — así que no podíamos saber\nquién la estableció, y se ha eliminado.\n\nIniciar sesión con {{provider}} funciona como antes. Si también quieres una\ncontraseña, puedes crearla en la configuración de tu cuenta.\n\nSi nunca estableciste una contraseña aquí, es posible que alguien intentara\nregistrarse con tu dirección. No tienes que hacer nada más — la cuenta es tuya.`,\n\t},\n\n\t[MESSAGE_KEYS.oauthRegistration]: {\n\t\tsubject: 'Te damos la bienvenida a {{appName}}',\n\t\thtml: `<h1>Te damos la bienvenida a {{appName}}</h1>\n<p>Tu cuenta se creó con <strong>{{provider}}</strong>. Inicia sesión cuando quieras con la misma cuenta de {{provider}} — no hay ninguna contraseña que recordar.</p>\n<p class=\"muted\">Si no creaste esta cuenta, contacta con soporte.</p>`,\n\t\ttext: `Te damos la bienvenida\n\nTu cuenta se creó con {{provider}}. Inicia sesión cuando quieras con la misma\ncuenta de {{provider}} — no hay ninguna contraseña que recordar.\n\nSi no creaste esta cuenta, contacta con soporte.`,\n\t},\n\n\t[MESSAGE_KEYS.oauthLinked]: {\n\t\tsubject: 'Se añadió un nuevo método de inicio de sesión a tu cuenta',\n\t\thtml: `<h1>Se añadió el inicio de sesión con {{provider}}</h1>\n<p>Ahora también puedes iniciar sesión en tu cuenta con <strong>{{provider}}</strong>.</p>\n<p class=\"muted\">Si fuiste tú, no tienes que hacer nada más. Si no, contacta con soporte de inmediato y cambia tu contraseña — otra persona podría iniciar sesión como tú.</p>`,\n\t\ttext: `Se añadió el inicio de sesión con {{provider}}\n\nAhora también puedes iniciar sesión en tu cuenta con {{provider}}.\n\nSi fuiste tú, no tienes que hacer nada más. Si no, contacta con soporte de\ninmediato y cambia tu contraseña — otra persona podría iniciar sesión como tú.`,\n\t},\n\n\t[MESSAGE_KEYS.oauthUnlinked]: {\n\t\tsubject: 'Se eliminó un método de inicio de sesión de tu cuenta',\n\t\thtml: `<h1>Se eliminó el inicio de sesión con {{provider}}</h1>\n<p><strong>{{provider}}</strong> ya no se puede usar para iniciar sesión. Tu correo electrónico y tu contraseña siguen funcionando.</p>\n<p class=\"muted\">Si fuiste tú, todo está en orden. Si no, contacta con soporte de inmediato.</p>`,\n\t\ttext: `Se eliminó el inicio de sesión con {{provider}}\n\n{{provider}} ya no se puede usar para iniciar sesión. Tu correo electrónico y tu contraseña siguen funcionando.\n\nSi fuiste tú, todo está en orden. Si no, contacta con soporte de inmediato.`,\n\t},\n} satisfies Record<AuthMessageKey, IDefaultTemplateCopy>;\n","import type { IDefaultTemplateCopy } from '@fonderie/core';\n\nimport { MESSAGE_KEYS, type AuthMessageKey } from './config';\n\n// French copy of every built-in auth email. Same keys and the same {{variables}}\n// as the English (./templates.ts) — the `satisfies` makes a missing email a\n// compile error, and the coverage test checks the variables match. Addressed\n// with \"vous\", as product email in French usually is.\nexport const FR_TEMPLATES = {\n\t[MESSAGE_KEYS.emailRegistration]: {\n\t\tsubject: 'Confirmez votre compte',\n\t\thtml: `<h1>Bienvenue</h1>\n<p>Bonjour {{firstName}},</p>\n<p>Merci pour votre inscription. Confirmez votre compte avec ce code :</p>\n<p><span class=\"pin-code\">{{pin}}</span></p>\n<p class=\"muted\">Ce code expire dans 24 heures. Si vous n’avez pas créé de compte, vous pouvez ignorer cet e-mail.</p>`,\n\t\ttext: `Bienvenue\n\nBonjour {{firstName}},\n\nMerci pour votre inscription. Confirmez votre compte avec ce code : {{pin}}\n\nCe code expire dans 24 heures. Si vous n'avez pas créé de compte, vous pouvez ignorer cet e-mail.`,\n\t},\n\n\t[MESSAGE_KEYS.emailVerification]: {\n\t\tsubject: 'Votre code de vérification',\n\t\thtml: `<h1>Vérifiez votre adresse e-mail</h1>\n<p>Bonjour {{firstName}},</p>\n<p>Utilisez ce code pour terminer la création de votre compte :</p>\n<p><span class=\"pin-code\">{{pin}}</span></p>\n<p class=\"muted\">Ce code expire dans 24 heures. Si vous n’avez pas créé de compte, vous pouvez ignorer cet e-mail.</p>`,\n\t\ttext: `Vérifiez votre adresse e-mail\n\nBonjour {{firstName}},\n\nUtilisez ce code pour terminer la création de votre compte : {{pin}}\n\nCe code expire dans 24 heures. Si vous n'avez pas créé de compte, vous pouvez ignorer cet e-mail.`,\n\t},\n\n\t[MESSAGE_KEYS.passwordReset]: {\n\t\tsubject: 'Réinitialisez votre mot de passe',\n\t\thtml: `<h1>Réinitialisez votre mot de passe</h1>\n<p>Nous avons reçu une demande de réinitialisation de votre mot de passe. Utilisez ce code pour continuer :</p>\n<p><span class=\"pin-code\">{{pin}}</span></p>\n<p class=\"muted\">Ce code expire bientôt. Si vous n’avez rien demandé, aucune action n’est nécessaire — votre mot de passe reste inchangé.</p>`,\n\t\ttext: `Réinitialisez votre mot de passe\n\nNous avons reçu une demande de réinitialisation de votre mot de passe. Utilisez ce code pour continuer : {{pin}}\n\nCe code expire bientôt. Si vous n'avez rien demandé, aucune action n'est nécessaire — votre mot de passe reste inchangé.`,\n\t},\n\n\t// SMS — text only.\n\t[MESSAGE_KEYS.phoneOtp]: {\n\t\ttext: '{{otp}} est votre code de vérification. Il expire dans 10 minutes.',\n\t},\n\n\t[MESSAGE_KEYS.mfaEnabled]: {\n\t\tsubject: 'La double authentification est activée',\n\t\thtml: `<h1>Double authentification activée</h1>\n<p>La double authentification vient d’être activée sur votre compte. Désormais, vous saisirez un code de votre application d’authentification à chaque connexion.</p>\n<p class=\"muted\">Si ce n’est pas vous, contactez immédiatement le support — quelqu’un pourrait avoir accès à votre compte.</p>`,\n\t\ttext: `Double authentification activée\n\nLa double authentification vient d'être activée sur votre compte. Désormais, vous saisirez un code de votre application d'authentification à chaque connexion.\n\nSi ce n'est pas vous, contactez immédiatement le support — quelqu'un pourrait avoir accès à votre compte.`,\n\t},\n\n\t[MESSAGE_KEYS.mfaDisabled]: {\n\t\tsubject: 'La double authentification est désactivée',\n\t\thtml: `<h1>Double authentification désactivée</h1>\n<p>La double authentification vient d’être désactivée sur votre compte. Votre compte n’est désormais protégé que par votre mot de passe.</p>\n<p class=\"muted\">Si ce n’est pas vous, contactez immédiatement le support et réactivez la double authentification — quelqu’un pourrait avoir accès à votre compte.</p>`,\n\t\ttext: `Double authentification désactivée\n\nLa double authentification vient d'être désactivée sur votre compte. Votre compte n'est désormais protégé que par votre mot de passe.\n\nSi ce n'est pas vous, contactez immédiatement le support et réactivez la double authentification — quelqu'un pourrait avoir accès à votre compte.`,\n\t},\n\n\t[MESSAGE_KEYS.mfaBackupCodesRegenerated]: {\n\t\tsubject: 'Vos codes de secours ont été régénérés',\n\t\thtml: `<h1>Nouveaux codes de secours générés</h1>\n<p>Une nouvelle série de codes de secours pour la double authentification vient d’être générée pour votre compte. Vos anciens codes ne fonctionnent plus.</p>\n<p class=\"muted\">Si ce n’est pas vous, contactez immédiatement le support — quelqu’un pourrait avoir accès à votre compte.</p>`,\n\t\ttext: `Nouveaux codes de secours générés\n\nUne nouvelle série de codes de secours pour la double authentification vient d'être générée pour votre compte. Vos anciens codes ne fonctionnent plus.\n\nSi ce n'est pas vous, contactez immédiatement le support — quelqu'un pourrait avoir accès à votre compte.`,\n\t},\n\n\t[MESSAGE_KEYS.emailChanged]: {\n\t\tsubject: 'Votre adresse e-mail a été modifiée',\n\t\thtml: `<h1>Votre adresse e-mail a été modifiée</h1>\n<p>L’adresse e-mail de votre compte vient d’être remplacée par <strong>{{newEmail}}</strong>.</p>\n<p class=\"muted\">Si vous êtes à l’origine de ce changement, tout est en ordre. Sinon, contactez immédiatement le support — quelqu’un pourrait avoir accès à votre compte.</p>`,\n\t\ttext: `Votre adresse e-mail a été modifiée\n\nL'adresse e-mail de votre compte vient d'être remplacée par {{newEmail}}.\n\nSi vous êtes à l'origine de ce changement, tout est en ordre. Sinon, contactez immédiatement le support — quelqu'un pourrait avoir accès à votre compte.`,\n\t},\n\n\t[MESSAGE_KEYS.phoneChanged]: {\n\t\tsubject: 'Votre numéro de téléphone a été modifié',\n\t\thtml: `<h1>Votre numéro de téléphone a été modifié</h1>\n<p>Le numéro de téléphone de votre compte vient d’être mis à jour.</p>\n<p class=\"muted\">Si vous êtes à l’origine de ce changement, tout est en ordre. Sinon, contactez immédiatement le support — quelqu’un pourrait avoir accès à votre compte.</p>`,\n\t\ttext: `Votre numéro de téléphone a été modifié\n\nLe numéro de téléphone de votre compte vient d'être mis à jour.\n\nSi vous êtes à l'origine de ce changement, tout est en ordre. Sinon, contactez immédiatement le support — quelqu'un pourrait avoir accès à votre compte.`,\n\t},\n\n\t[MESSAGE_KEYS.passwordRevoked]: {\n\t\tsubject: 'Le mot de passe de votre compte a été supprimé',\n\t\thtml: `<h1>Votre mot de passe a été supprimé</h1>\n<p>Vous venez de vous connecter avec <strong>{{provider}}</strong>. Un mot de passe avait été défini sur ce compte, mais cette adresse e-mail n’avait jamais été confirmée — nous ne pouvions donc pas savoir qui l’avait défini, et il a été supprimé.</p>\n<p>La connexion avec {{provider}} fonctionne comme avant. Si vous souhaitez aussi un mot de passe, définissez-en un dans les paramètres de votre compte.</p>\n<p class=\"muted\">Si vous n’avez jamais défini de mot de passe ici, quelqu’un a peut-être tenté de s’inscrire avec votre adresse. Vous n’avez rien d’autre à faire — le compte vous appartient.</p>`,\n\t\ttext: `Votre mot de passe a été supprimé\n\nVous venez de vous connecter avec {{provider}}. Un mot de passe avait été défini\nsur ce compte, mais cette adresse e-mail n'avait jamais été confirmée — nous ne\npouvions donc pas savoir qui l'avait défini, et il a été supprimé.\n\nLa connexion avec {{provider}} fonctionne comme avant. Si vous souhaitez aussi un\nmot de passe, définissez-en un dans les paramètres de votre compte.\n\nSi vous n'avez jamais défini de mot de passe ici, quelqu'un a peut-être tenté de\ns'inscrire avec votre adresse. Vous n'avez rien d'autre à faire — le compte vous\nappartient.`,\n\t},\n\n\t[MESSAGE_KEYS.oauthRegistration]: {\n\t\tsubject: 'Bienvenue sur {{appName}}',\n\t\thtml: `<h1>Bienvenue sur {{appName}}</h1>\n<p>Votre compte a été créé avec <strong>{{provider}}</strong>. Connectez-vous à tout moment avec ce même compte {{provider}} — aucun mot de passe à retenir.</p>\n<p class=\"muted\">Si vous n’avez pas créé ce compte, contactez le support.</p>`,\n\t\ttext: `Bienvenue\n\nVotre compte a été créé avec {{provider}}. Connectez-vous à tout moment avec ce\nmême compte {{provider}} — aucun mot de passe à retenir.\n\nSi vous n'avez pas créé ce compte, contactez le support.`,\n\t},\n\n\t[MESSAGE_KEYS.oauthLinked]: {\n\t\tsubject: 'Une nouvelle méthode de connexion a été ajoutée à votre compte',\n\t\thtml: `<h1>Connexion avec {{provider}} ajoutée</h1>\n<p>Vous pouvez désormais aussi vous connecter à votre compte avec <strong>{{provider}}</strong>.</p>\n<p class=\"muted\">Si c’est bien vous, vous n’avez rien d’autre à faire. Sinon, contactez immédiatement le support et changez votre mot de passe — quelqu’un d’autre pourrait se connecter à votre place.</p>`,\n\t\ttext: `Connexion avec {{provider}} ajoutée\n\nVous pouvez désormais aussi vous connecter à votre compte avec {{provider}}.\n\nSi c'est bien vous, vous n'avez rien d'autre à faire. Sinon, contactez\nimmédiatement le support et changez votre mot de passe — quelqu'un d'autre\npourrait se connecter à votre place.`,\n\t},\n\n\t[MESSAGE_KEYS.oauthUnlinked]: {\n\t\tsubject: 'Une méthode de connexion a été retirée de votre compte',\n\t\thtml: `<h1>Connexion avec {{provider}} retirée</h1>\n<p><strong>{{provider}}</strong> ne peut plus être utilisé pour vous connecter. Votre adresse e-mail et votre mot de passe fonctionnent toujours.</p>\n<p class=\"muted\">Si c’est bien vous, tout est en ordre. Sinon, contactez immédiatement le support.</p>`,\n\t\ttext: `Connexion avec {{provider}} retirée\n\n{{provider}} ne peut plus être utilisé pour vous connecter. Votre adresse e-mail et votre mot de passe fonctionnent toujours.\n\nSi c'est bien vous, tout est en ordre. Sinon, contactez immédiatement le support.`,\n\t},\n} satisfies Record<AuthMessageKey, IDefaultTemplateCopy>;\n","export { requireAuth, requireAnyAuth } from '@fonderie/core/middlewares';\n","import type { IStoreAdapter } from '@fonderie/store';\n\n// Migrating an existing user base onto Fonderie auth. `UserModel.create` is for\n// fresh sign-ups — it generates a new id and sets `created_at`/verification to\n// their defaults. A migration needs the opposite: preserve each user's original\n// identity so foreign keys still resolve and history isn't rewritten. Pair this\n// with the `legacyVerify` config option: import the legacy password hash as-is,\n// and Fonderie upgrades it to bcrypt on the user's first login (rehash-on-login).\nexport interface IImportUser {\n\t// Preserve the legacy id so existing foreign keys keep pointing at this user.\n\t// Must be a UUID (the column type); omit only if you are remapping ids and\n\t// will supply the freshly-generated one. Omitting lets Postgres generate one.\n\tid?: string;\n\temail: string;\n\t// The user's existing password hash, in whatever format the old system used.\n\t// Left untouched here; `legacyVerify` validates it on login, then it's\n\t// re-stored as bcrypt. Pass null for accounts with no password (e.g. OAuth).\n\tpasswordHash?: string | null;\n\tfirstName?: string | null;\n\tlastName?: string | null;\n\tphone?: string | null;\n\tprofileImageUrl?: string | null;\n\tlocale?: string;\n\ttimezone?: string;\n\t// Preserve verification state — don't force already-verified users to\n\t// re-verify. A Date marks the account verified; null/omitted leaves it\n\t// unverified.\n\temailVerifiedAt?: Date | null;\n\tmfaEnabled?: boolean;\n\t// Preserve the original signup time; omitted defaults to now().\n\tcreatedAt?: Date;\n}\n\n// Insert one migrated user, preserving whichever identity fields are supplied\n// and letting the table defaults fill the rest. Returns the row's id (the\n// preserved one, or the generated one). Not idempotent by itself — run against a\n// fresh target, or manage replay (truncate-and-reload) at the migration level.\nexport async function importUser(\n\tstore: IStoreAdapter,\n\tuser: IImportUser,\n): Promise<{ id: string }> {\n\tconst cols: string[] = [];\n\tconst vals: unknown[] = [];\n\tconst placeholders: string[] = [];\n\tconst add = (col: string, val: unknown) => {\n\t\tcols.push(col);\n\t\tvals.push(val);\n\t\tplaceholders.push(`$${vals.length}`);\n\t};\n\n\tif (user.id !== undefined) add('id', user.id);\n\tadd('email', user.email.toLowerCase().trim());\n\tif (user.passwordHash !== undefined) add('password_hash', user.passwordHash);\n\tif (user.firstName !== undefined) add('first_name', user.firstName);\n\tif (user.lastName !== undefined) add('last_name', user.lastName);\n\tif (user.phone !== undefined) add('phone', user.phone);\n\tif (user.profileImageUrl !== undefined) add('profile_image_url', user.profileImageUrl);\n\tif (user.locale !== undefined) add('locale', user.locale);\n\tif (user.timezone !== undefined) add('timezone', user.timezone);\n\tif (user.emailVerifiedAt !== undefined) add('email_verified_at', user.emailVerifiedAt);\n\tif (user.mfaEnabled !== undefined) add('mfa_enabled', user.mfaEnabled);\n\tif (user.createdAt !== undefined) add('created_at', user.createdAt);\n\n\tconst [row] = await store.query<{ id: string }>(\n\t\t`INSERT INTO fonderie_users (${cols.join(', ')})\n\t\t VALUES (${placeholders.join(', ')})\n\t\t RETURNING id`,\n\t\tvals,\n\t);\n\tif (!row) throw new Error('[auth] importUser: insert returned no row');\n\treturn row;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\n// Data-retention / right-to-erasure support. `deleteMe` soft-deletes a user\n// (sets deleted_at) so the row stops resolving but history is preserved. A\n// retention policy then hard-deletes those rows once they age past the window —\n// call this from a scheduled job (e.g. daily). Related auth rows (sessions,\n// resets, verifications, MFA, backup codes) are removed by ON DELETE CASCADE.\n\nexport interface IPurgeOptions {\n\t// Hard-delete users whose deleted_at is older than this many days.\n\tolderThanDays: number;\n\t// Given ⇒ `fonderie.user.purged` { userId } is emitted for each removed\n\t// account, so other bricks can erase what they hold about it (billing\n\t// deletes the payment provider's customer record). Emitted after the delete.\n\tbus?: { emit(type: string, payload: unknown): Promise<void> } | undefined;\n}\n\nexport async function purgeSoftDeletedUsers(\n\tstore: IStoreAdapter,\n\t{ olderThanDays, bus }: IPurgeOptions,\n): Promise<number> {\n\tif (!Number.isFinite(olderThanDays) || olderThanDays < 0) {\n\t\tthrow new Error('[auth] purgeSoftDeletedUsers: olderThanDays must be a non-negative number');\n\t}\n\tconst rows = await store.query<{ id: string }>(\n\t\t`DELETE FROM fonderie_users\n\t\t WHERE deleted_at IS NOT NULL\n\t\t AND deleted_at < now() - make_interval(days => $1)\n\t\t RETURNING id`,\n\t\t[olderThanDays],\n\t);\n\tif (bus) {\n\t\tfor (const { id } of rows) await bus.emit('fonderie.user.purged', { userId: id });\n\t}\n\treturn rows.length;\n}\n\n// Scheduled hard-deletion of aged soft-deleted users (SOC 2 C1/P4, right to\n// erasure follow-through). Runs once immediately, then every intervalMs.\n// Non-blocking (timer unref'd). .stop() cancels.\nexport interface IUserRetentionScheduleOptions extends IPurgeOptions {\n\tintervalMs?: number; // default 24h\n\tonPurge?: (deleted: number) => void;\n}\n\nexport function startUserRetention(\n\tstore: IStoreAdapter,\n\toptions: IUserRetentionScheduleOptions,\n): { stop: () => void } {\n\tconst intervalMs = options.intervalMs ?? 24 * 60 * 60 * 1000;\n\tlet stopped = false;\n\tconst run = async () => {\n\t\tif (stopped) return;\n\t\ttry {\n\t\t\tconst deleted = await purgeSoftDeletedUsers(store, {\n\t\t\t\tolderThanDays: options.olderThanDays,\n\t\t\t\t...(options.bus ? { bus: options.bus } : {}),\n\t\t\t});\n\t\t\toptions.onPurge?.(deleted);\n\t\t} catch (err) {\n\t\t\tconsole.error('[auth] scheduled user retention purge failed:', err);\n\t\t}\n\t};\n\tconst timer = setInterval(run, intervalMs);\n\tif (typeof (timer as { unref?: () => void }).unref === 'function') (timer as { unref: () => void }).unref();\n\tvoid run();\n\treturn { stop: () => { stopped = true; clearInterval(timer); } };\n}\n"],"mappings":"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","names":["bcrypt","import_core","import_middlewares","import_core","jwt","import_node_crypto","import_core","email","import_node_crypto","import_core","email","phone","resolve","import_core","QRCode","import_node_crypto","import_events","import_core","email","import_node_crypto","phone","phone","email","password","remaining","expiresAt","import_node_crypto","import_core","import_events","import_core","normalizePhone","phone","isValidPhone","hashPassword","verifyPassword","import_node_crypto","import_jsonwebtoken","import_core","import_events","jwt","import_core","import_core","email","import_core","import_middlewares"]}
|