@fonderie/auth 7.2.0 → 7.3.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/outcomes.md +9 -2
- package/dist/index.cjs +85 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +86 -10
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +2 -1
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.js +2 -1
- package/dist/middlewares/index.js.map +1 -1
- package/dist/migrations/sql/016_consumed_tokens.sql +13 -0
- package/package.json +1 -1
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/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/services/config-guard.ts","../src/middlewares/session.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';\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';\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { Middleware } 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);\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// 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\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['GET', '/auth/google/callback', 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.\n\t\t\t['GET', '/auth/apple', oauth.appleInit],\n\t\t\t['POST', '/auth/apple/callback', 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 } 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);\n\tconst backupCodes = new BackupCodeModel(store);\n\tconst loginEvents = new LoginEventModel(store);\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 = () =>\n\t\t\t\tloginEvents.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\tbus\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\t\t\t\t\t.catch(() => {});\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) 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) 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\tloginEvents.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\tbus\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\t\t\t\t.catch(() => {});\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\tbus\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\t\t\t\t.catch(() => {});\n\n\t\t\treturn setApiResponse(HTTP.OK, 'MFA_DISABLED', 'MFA disabled successfully.');\n\t\t},\n\t};\n}\n","import 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} 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\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// 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| '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\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\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\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\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\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\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\tasync upsertByProvider(\n\t\temail: string,\n\t\tprovider: string,\n\t\tproviderId: string,\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, email_verified_at, provider, provider_id)\n\t\t\tVALUES ($1, now(), $2, $3)\n\t\t\tON CONFLICT (email) DO UPDATE\n\t\t\tSET provider = $2, provider_id = $3\n\t\t\tRETURNING id`,\n\t\t\t[email, provider, providerId],\n\t\t);\n\t\treturn row ?? null;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IRequestMeta } from '../services/request-meta';\n\nexport class SessionModel {\n\tconstructor(private store: IStoreAdapter) {}\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\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid, user_agent, ip_address)\n\t\t\tVALUES ($1, $2, $3, $4, $5, $6)\n\t\t\tON CONFLICT (token) DO NOTHING`,\n\t\t\t[userId, token, expiresAt, sid ?? null, meta?.userAgent ?? null, meta?.ipAddress ?? null],\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\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 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';\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.\nexport type LoginMethod = 'password' | 'phone' | 'mfa' | 'oauth-google' | 'oauth-apple';\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}\n\nexport class LoginEventModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync record(e: ILoginEventInput): Promise<void> {\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)\n\t\t\t VALUES ($1, $2, $3, $4, $5, $6, $7)`,\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],\n\t\t);\n\t}\n\n\t// Fire-and-forget variant for the login paths: recording history must never\n\t// block or fail a login (same posture as the bus.emit(...).catch(() => {})\n\t// notification sends).\n\trecordSafe(e: ILoginEventInput): void {\n\t\tvoid this.record(e).catch(() => {});\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\",\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\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}\n\n// Cap the stored UA: real ones are <300 chars; anything longer is noise or abuse.\nconst UA_MAX = 512;\n\nexport function requestMeta(ctx: IFonderieContext): IRequestMeta {\n\tconst ip = ctx.meta['clientIp'];\n\tconst ua = ctx.request.headers.get('user-agent');\n\treturn {\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};\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 } 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);\n\tconst loginEvents = new LoginEventModel(store);\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\tbus\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\t\t.catch(() => {});\n\t\t\t\tbus\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\t\t\t\t\t.catch(() => {});\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, requestMeta(ctx));\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\tbus\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\t\t.catch(() => {});\n\t\t\t\tbus\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\t\t\t\t\t.catch(() => {});\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\tloginEvents.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\tloginEvents.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\tloginEvents.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\tbus\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\t\t\t\t\t.catch(() => {});\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\tbus\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\t\t\t\t.catch(() => {});\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\tconst record = await phoneVerif.findByUser(ctx.user!.id, pin);\n\t\t\t\tif (!record) {\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\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, requestMeta(ctx));\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\tbus\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\t\t\t\t\t.catch(() => {});\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\tbus\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\t\t\t\t.catch(() => {});\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 } 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);\n\tconst loginEvents = new LoginEventModel(store);\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\tbus\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\t\t.catch(() => {});\n\t\t\tif (oldEmail) {\n\t\t\t\tbus\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\t\t.catch(() => {});\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\tbus\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\t\t.catch(() => {});\n\t\t\tif (ctx.user!.email) {\n\t\t\t\tbus\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\t\t.catch(() => {});\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\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\tbus\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\t\t\t\t.catch(() => {});\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';\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\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\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\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\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\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, type KeyObject } from 'node:crypto';\nimport jwt from 'jsonwebtoken';\n\nimport { tokenPairCookies, cookieHeaders } from '../services/cookies';\nimport { setApiResponse, HTTP, constantTimeEqual } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAuthConfig } 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 { 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}\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 and refetched on an unknown kid\n// (Apple rotates them). Module-level cache is safe: the keys are public.\ninterface AppleJwk { kid: string; kty: string; n: string; e: string; alg: string; use?: string }\nlet appleKeyCache: { keys: AppleJwk[]; fetchedAt: number } | null = null;\nconst APPLE_KEYS_TTL_MS = 60 * 60 * 1000;\n\nasync function getApplePublicKey(kid: string): Promise<KeyObject | null> {\n\tconst fresh = appleKeyCache && Date.now() - appleKeyCache.fetchedAt < APPLE_KEYS_TTL_MS;\n\tif (!fresh || !appleKeyCache!.keys.some((k) => k.kid === kid)) {\n\t\ttry {\n\t\t\tconst res = await fetch(APPLE_KEYS_URL);\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 we have rather than failing outright.\n\t\t}\n\t}\n\tconst jwk = appleKeyCache?.keys.find((k) => k.kid === kid);\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// If the app bound a nonce to the native request, it must round-trip.\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\nexport function oauthController(store: IStoreAdapter, config: IAuthConfig) {\n\tconst users = new UserModel(store);\n\tconst sessions = new SessionModel(store);\n\tconst loginEvents = new LoginEventModel(store);\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\tloginEvents.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\tconst { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, { loginMethod: 'apple' });\n\t\tawait sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);\n\t\tloginEvents.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\tloginEvents.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\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\tloginEvents.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\tloginEvents.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\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 { 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}\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 { 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';\n\nexport class AuthModule implements IFonderieModule {\n\treadonly name = '@fonderie/auth';\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// 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} 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.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}\n\nexport async function purgeSoftDeletedUsers(\n\tstore: IStoreAdapter,\n\t{ olderThanDays }: 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\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, { olderThanDays: options.olderThanDays });\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;;;ACKA,IAAAC,sBAA6D;;;ACL7D,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,eAAqC;AAIrC,oBAAmC;;;ACN5B,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;AACf;AAIO,IAAM,aAAa;AAAA,EACzB,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,eAAe;AAAA,EACf,iBAAiB;AAClB;;;AC1EA,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;AAwB3D,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,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;;;AC3EA,IAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuBd,IAAM,YAAN,MAAgB;AAAA,EACtB,YAAoB,OAAsB;AAAtB;AAAA,EAAuB;AAAA,EAAvB;AAAA,EAEpB,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,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,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,EAEA,MAAM,iBACLD,QACA,UACA,YACiC;AACjC,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B;AAAA;AAAA;AAAA;AAAA;AAAA,MAKA,CAACA,QAAO,UAAU,UAAU;AAAA,IAC7B;AACA,WAAO,OAAO;AAAA,EACf;AACD;;;ACnQO,IAAM,eAAN,MAAmB;AAAA,EACzB,YAAoB,OAAsB;AAAtB;AAAA,EAAuB;AAAA,EAAvB;AAAA,EAEpB,MAAM,OACL,QACA,OACA,WACA,KACA,MACgB;AAChB,UAAM,KAAK,MAAM;AAAA,MAChB;AAAA;AAAA;AAAA,MAGA,CAAC,QAAQ,OAAO,WAAW,OAAO,MAAM,MAAM,aAAa,MAAM,MAAM,aAAa,IAAI;AAAA,IACzF;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,QASnB;AACD,WAAO,KAAK,MAAM;AAAA,MACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAQA,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;;;ACtHO,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;;;AChBO,IAAM,kBAAN,MAAsB;AAAA,EAC5B,YAAoB,OAAsB;AAAtB;AAAA,EAAuB;AAAA,EAAvB;AAAA,EAEpB,MAAM,OAAO,GAAoC;AAChD,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,MACH;AAAA,IACD;AAAA,EACD;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,GAA2B;AACrC,SAAK,KAAK,OAAO,CAAC,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EACnC;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;;;AC1E1B,IAAM,SAAS;AAER,SAAS,YAAY,KAAqC;AAChE,QAAM,KAAK,IAAI,KAAK,UAAU;AAC9B,QAAM,KAAK,IAAI,QAAQ,QAAQ,IAAI,YAAY;AAC/C,SAAO;AAAA,IACN,WAAW,OAAO,OAAO,YAAY,GAAG,SAAS,IAAI,KAAK;AAAA,IAC1D,WAAW,MAAM,GAAG,SAAS,IAAI,GAAG,MAAM,GAAG,MAAM,IAAI;AAAA,EACxD;AACD;;;AVKO,SAAS,cACf,OACA,QACA,QACA,KACC;AACD,QAAM,QAAQ,IAAI,UAAU,KAAK;AACjC,QAAM,WAAW,IAAI,aAAa,KAAK;AACvC,QAAM,cAAc,IAAI,gBAAgB,KAAK;AAC7C,QAAM,cAAc,IAAI,gBAAgB,KAAK;AAE7C,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,cAAAE,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,MACjB,YAAY,WAAW;AAAA,QACtB,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,aACG,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,EAC1B,MAAM,MAAM;AAAA,QAAC,CAAC;AAEhB,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,WAAU;AACjC,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,WAAU;AACjC,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,oBAAY,WAAW;AAAA,UACtB,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,WACG,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,EAC1B,MAAM,MAAM;AAAA,MAAC,CAAC;AAEhB,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,WACG,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,EAC1B,MAAM,MAAM;AAAA,MAAC,CAAC;AAEhB,iBAAO,6BAAe,kBAAK,IAAI,gBAAgB,4BAA4B;AAAA,IAC5E;AAAA,EACD;AACD;;;AW/QA,IAAAC,sBAAuC;AAIvC,IAAAC,iBAAmC;AACnC,IAAAC,eAAqC;;;ACN9B,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,KAAK;AACvC,QAAM,cAAc,IAAI,gBAAgB,KAAK;AAC7C,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,aACG;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,EACC,MAAM,MAAM;AAAA,QAAC,CAAC;AAChB,aACG;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,EACC,MAAM,MAAM;AAAA,QAAC,CAAC;AAEhB,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,YAAY,GAAG,CAAC;AAEpG,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,aACG;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,EACC,MAAM,MAAM;AAAA,QAAC,CAAC;AAChB,aACG;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,EACC,MAAM,MAAM;AAAA,QAAC,CAAC;AAQhB,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,sBAAY,WAAW;AAAA,YACtB,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,sBAAY,WAAW;AAAA,YACtB,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,oBAAY,WAAW;AAAA,UACtB,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,aACG,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,EAC1B,MAAM,MAAM;AAAA,QAAC,CAAC;AAOhB,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,WACG,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,EAC1B,MAAM,MAAM;AAAA,MAAC,CAAC;AAEhB,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;AACtC,cAAM,SAAS,MAAM,WAAW,WAAW,IAAI,KAAM,IAAI,GAAG;AAC5D,YAAI,CAAC,QAAQ;AACZ,qBAAO,6BAAe,kBAAK,aAAa,uBAAuB,wBAAwB;AAAA,QACxF;AACA,YAAI,oBAAI,KAAK,IAAI,OAAO,WAAW;AAClC,gBAAM,WAAW,aAAa,IAAI,KAAM,EAAE;AAC1C,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,YAAY,GAAG,CAAC;AAEzG,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,aACG,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,EAC1B,MAAM,MAAM;AAAA,QAAC,CAAC;AAEhB,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,WACG,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,EAC1B,MAAM,MAAM;AAAA,MAAC,CAAC;AAEhB,iBAAO,6BAAe,kBAAK,IAAI,qBAAqB,4BAA4B;AAAA,QAC/E,OAAO,IAAI,KAAM;AAAA,MAClB,CAAC;AAAA,IACF;AAAA,EACD;AACD;;;AMlxBA,IAAAK,sBAA0B;AAE1B,IAAAC,eAAkD;AAMlD,IAAAC,iBAAmC;;;ACTnC,IAAAC,eAAuD;AAqBvD,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,IACf,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;AAa1B,SAAS,aACf,KAQA,YACc;AACd,SAAO;AAAA,IACN,IAAI,IAAI;AAAA,IACR,SAAS,eAAe,QAAQ,IAAI,QAAQ;AAAA,IAC5C,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,IACf,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;;;ADjDA,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,KAAK;AACvC,QAAM,cAAc,IAAI,gBAAgB,KAAK;AAC7C,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,WACG,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,EAC1B,MAAM,MAAM;AAAA,MAAC,CAAC;AAChB,UAAI,UAAU;AACb,aACG,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,EAC1B,MAAM,MAAM;AAAA,QAAC,CAAC;AAAA,MACjB;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,WACG,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,EAC1B,MAAM,MAAM;AAAA,MAAC,CAAC;AAChB,UAAI,IAAI,KAAM,OAAO;AACpB,aACG,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,EAC1B,MAAM,MAAM;AAAA,QAAC,CAAC;AAAA,MACjB;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,IAEA,UAAU,OAAO,QAA6C;AAC7D,YAAM,SAAS,IAAI,KAAM;AACzB,YAAM,MAAM,WAAW,MAAM;AAE7B,YAAM,QAAQ,IAAI,KAAK,WAAW;AAClC,WACG;AAAA,QACD,WAAW;AAAA,QACX,EAAE,OAAO;AAAA,QACT,UAAU,SAAY,EAAE,WAAW,MAAM,IAAI;AAAA,MAC9C,EACC,MAAM,MAAM;AAAA,MAAC,CAAC;AAEhB,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;;;AE5YA,IAAAE,sBAA6D;AAC7D,IAAAC,uBAAgB;AAGhB,IAAAC,eAAwD;AAqBxD,IAAM,iBAAiB;AACvB,IAAM,kBAAkB;AACxB,IAAM,iBAAiB;AACvB,IAAM,eAAe;AAcd,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;AAKA,IAAI,gBAAgE;AACpE,IAAM,oBAAoB,KAAK,KAAK;AAEpC,eAAe,kBAAkB,KAAwC;AACxE,QAAM,QAAQ,iBAAiB,KAAK,IAAI,IAAI,cAAc,YAAY;AACtE,MAAI,CAAC,SAAS,CAAC,cAAe,KAAK,KAAK,CAAC,MAAM,EAAE,QAAQ,GAAG,GAAG;AAC9D,QAAI;AACH,YAAM,MAAM,MAAM,MAAM,cAAc;AACtC,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;AAAA,EACD;AACA,QAAM,MAAM,eAAe,KAAK,KAAK,CAAC,MAAM,EAAE,QAAQ,GAAG;AACzD,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;AAED,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;AAE5C,SAAS,gBAAgB,OAAsB,QAAqB;AAC1E,QAAM,QAAQ,IAAI,UAAU,KAAK;AACjC,QAAM,WAAW,IAAI,aAAa,KAAK;AACvC,QAAM,cAAc,IAAI,gBAAgB,KAAK;AAK7C,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,kBAAY,WAAW;AAAA,QACtB,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,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,gBAAY,WAAW;AAAA,MACtB,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,oBAAY,WAAW;AAAA,UACtB,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,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,kBAAY,WAAW;AAAA,QACtB,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,oBAAY,WAAW;AAAA,UACtB,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;AACA,aAAO,mBAAmB,KAAK;AAAA,QAC9B,OAAO,OAAO;AAAA,QACd,KAAK,OAAO;AAAA,QACZ,eAAe,mBAAmB,MAAM;AAAA,MACzC,CAAC;AAAA,IACF;AAAA,EACD;AACD;;;AzBveO,SAAS,gBACf,OACA,QACA,KACoB;AACpB,QAAM,OAAO,eAAe,OAAO,QAAQ,GAAG;AAC9C,QAAM,OAAO,eAAe,OAAO,QAAQ,GAAG;AAC9C,QAAM,QAAQ,gBAAgB,OAAO,MAAM;AAC3C,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,IAEjC,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,IAC9G,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,MACxC,CAAC,OAAO,yBAAyB,MAAM,cAAc;AAAA,IACtD;AAAA,EACD;AAEA,MAAI,OAAO,UAAU,SAAS,OAAO,GAAG;AACvC,WAAO;AAAA;AAAA;AAAA,MAGN,CAAC,OAAO,eAAe,MAAM,SAAS;AAAA,MACtC,CAAC,QAAQ,wBAAwB,MAAM,aAAa;AAAA;AAAA;AAAA,MAGpD,CAAC,QAAQ,sBAAsB,QAAQ,OAAO,OAAG,6BAAS,iBAAiB,GAAG,MAAM,WAAW;AAAA,IAChG;AAAA,EACD;AAEA,SAAO;AACR;;;A0B1IA,IAAAC,eAA6E;AAG7E,IAAM,SAAS;AAMR,SAAS,0BAA0B,QAA0C;AACnF,QAAM,WAAgC,CAAC;AACvC,QAAM,SAAS,OAAO,aAAa;AAEnC,QAAM,oBAAgB,oCAAsB,MAAM;AAClD,MAAI,kBAAkB,aAAa;AAClC,aAAS,KAAK;AAAA,MACb,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,SAAS,8BAA8B,8BAAiB,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,gCAAmB,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;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;;;ACvHO,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;;;ACxDO,IAAM,aAAN,MAA4C;AAAA,EAGlD,YACS,OACA,QACA,KACP;AAHO;AACA;AACA;AAGR,uBAAmB,MAAM;AAAA,EAC1B;AAAA,EANS;AAAA,EACA;AAAA,EACA;AAAA,EALA,OAAO;AAAA;AAAA,EAYhB,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;;;ACrBO,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;AACD;AAMO,IAAM,kBAAmE;AAAA,EAC/E,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;;;AChJA,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;;;AC1DA,eAAsB,sBACrB,OACA,EAAE,cAAc,GACE;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,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,EAAE,eAAe,QAAQ,cAAc,CAAC;AAC3F,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_middlewares","import_core","jwt","import_node_crypto","import_core","email","import_node_crypto","import_core","email","phone","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","jwt","import_core","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/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/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';\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';\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { Middleware } 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);\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// 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\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 } 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);\n\tconst backupCodes = new BackupCodeModel(store);\n\tconst loginEvents = new LoginEventModel(store);\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 = () =>\n\t\t\t\tloginEvents.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\tbus\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\t\t\t\t\t.catch(() => {});\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) 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) 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\tloginEvents.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\tbus\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\t\t\t\t.catch(() => {});\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\tbus\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\t\t\t\t.catch(() => {});\n\n\t\t\treturn setApiResponse(HTTP.OK, 'MFA_DISABLED', 'MFA disabled successfully.');\n\t\t},\n\t};\n}\n","import 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} 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\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// 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| '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\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\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\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\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\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\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\tasync upsertByProvider(\n\t\temail: string,\n\t\tprovider: string,\n\t\tproviderId: string,\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, email_verified_at, provider, provider_id)\n\t\t\tVALUES ($1, now(), $2, $3)\n\t\t\tON CONFLICT (email) DO UPDATE\n\t\t\tSET provider = $2, provider_id = $3,\n\t\t\t email_verified_at = COALESCE(fonderie_users.email_verified_at, now())\n\t\t\tRETURNING id`,\n\t\t\t[email, provider, providerId],\n\t\t);\n\t\treturn row ?? null;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IRequestMeta } from '../services/request-meta';\n\nexport class SessionModel {\n\tconstructor(private store: IStoreAdapter) {}\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\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid, user_agent, ip_address)\n\t\t\tVALUES ($1, $2, $3, $4, $5, $6)\n\t\t\tON CONFLICT (token) DO NOTHING`,\n\t\t\t[userId, token, expiresAt, sid ?? null, meta?.userAgent ?? null, meta?.ipAddress ?? null],\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\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 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';\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.\nexport type LoginMethod = 'password' | 'phone' | 'mfa' | 'oauth-google' | 'oauth-apple';\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}\n\nexport class LoginEventModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync record(e: ILoginEventInput): Promise<void> {\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)\n\t\t\t VALUES ($1, $2, $3, $4, $5, $6, $7)`,\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],\n\t\t);\n\t}\n\n\t// Fire-and-forget variant for the login paths: recording history must never\n\t// block or fail a login (same posture as the bus.emit(...).catch(() => {})\n\t// notification sends).\n\trecordSafe(e: ILoginEventInput): void {\n\t\tvoid this.record(e).catch(() => {});\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\",\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\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}\n\n// Cap the stored UA: real ones are <300 chars; anything longer is noise or abuse.\nconst UA_MAX = 512;\n\nexport function requestMeta(ctx: IFonderieContext): IRequestMeta {\n\tconst ip = ctx.meta['clientIp'];\n\tconst ua = ctx.request.headers.get('user-agent');\n\treturn {\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};\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 } 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);\n\tconst loginEvents = new LoginEventModel(store);\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\tbus\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\t\t.catch(() => {});\n\t\t\t\tbus\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\t\t\t\t\t.catch(() => {});\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, requestMeta(ctx));\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\tbus\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\t\t.catch(() => {});\n\t\t\t\tbus\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\t\t\t\t\t.catch(() => {});\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\tloginEvents.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\tloginEvents.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\tloginEvents.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\tbus\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\t\t\t\t\t.catch(() => {});\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\tbus\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\t\t\t\t.catch(() => {});\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\tconst record = await phoneVerif.findByUser(ctx.user!.id, pin);\n\t\t\t\tif (!record) {\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\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, requestMeta(ctx));\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\tbus\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\t\t\t\t\t.catch(() => {});\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\tbus\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\t\t\t\t.catch(() => {});\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 } 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);\n\tconst loginEvents = new LoginEventModel(store);\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\tbus\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\t\t.catch(() => {});\n\t\t\tif (oldEmail) {\n\t\t\t\tbus\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\t\t.catch(() => {});\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\tbus\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\t\t.catch(() => {});\n\t\t\tif (ctx.user!.email) {\n\t\t\t\tbus\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\t\t.catch(() => {});\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\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\tbus\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\t\t\t\t.catch(() => {});\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';\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\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\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\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\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\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 } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAuthConfig } 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) {\n\tconst users = new UserModel(store);\n\tconst sessions = new SessionModel(store);\n\tconst loginEvents = new LoginEventModel(store);\n\tconst consumedTokens = new ConsumedTokenModel(store);\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\tloginEvents.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\tconst { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, { loginMethod: 'apple' });\n\t\tawait sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);\n\t\tloginEvents.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\tloginEvents.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\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\tloginEvents.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\tloginEvents.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\tloginEvents.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 { 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';\n\nexport class AuthModule implements IFonderieModule {\n\treadonly name = '@fonderie/auth';\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// 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} 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.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}\n\nexport async function purgeSoftDeletedUsers(\n\tstore: IStoreAdapter,\n\t{ olderThanDays }: 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\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, { olderThanDays: options.olderThanDays });\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_middlewares","import_core","jwt","import_node_crypto","import_core","email","import_node_crypto","import_core","email","phone","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","jwt","import_core","import_middlewares"]}
|