@fonderie/auth 3.0.0 → 5.0.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.
@@ -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/dtos/user.ts","../src/models/user.model.ts","../src/models/session.model.ts","../src/models/backup-code.model.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/controllers/oauth.controller.ts","../src/middlewares/session.ts","../src/module.ts","../src/middlewares/require-auth.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","// ── Public API ───────────────────────────────────────────────────\nexport type { IUser, ISession, IMfaChallenge } from './types';\nexport { AuthModule } from './module';\nexport type { IAuthConfig, IAuthSecrets, IAuthRuntimeConfig } from './config';\nexport { AUTH_CONFIG_KEYS, MESSAGE_KEYS } from './config';\nexport type { AuthMessageKey } from './config';\n\n// DTOs\nexport type { IUserDTO } from './dtos/user';\nexport { toUserDTO } from './dtos/user';\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// 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} 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', validate(resetPasswordSchema), auth.resetPassword),\n\n\t\t// Verification (Protected — email or phone, determined by loginMethod)\n\t\tR('verifyEmail', 'POST', '/auth/verify', requireAuth, validate(verifySchema), auth.verify),\n\t\tR('sendVerification', 'GET', '/auth/send-verification', requireAuth, 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\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\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' | '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// 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// 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\nexport const resetPasswordSchema = z.object({ pin: sixDigitPin, password });\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\tnotifications: z.unknown().optional(),\n\t\temailDigest: z.unknown().optional(),\n\t\tdateFormat: z.unknown().optional(),\n\t\ttimeFormat: z.unknown().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 { toUserDTO } from '../dtos/user';\nimport { UserModel } from '../models/user.model';\nimport { SessionModel } from '../models/session.model';\nimport { BackupCodeModel } from '../models/backup-code.model';\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\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, 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\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 pendingSecret = await users.getMfaPendingSecret(ctx.user!.id);\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\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 secret = await users.getMfaSecret(ctx.user!.id);\n\t\t\t\tif (!secret) {\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\tif (!verifyTotpToken(token, secret)) {\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});\n\t\t\tawait sessions.create(ctx.user!.id, refreshToken, refreshTokenExpiry(refreshToken), sid);\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),\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 secret = await users.getMfaSecret(ctx.user!.id);\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\tconst secret = (user as unknown as { mfaSecret: string | null }).mfaSecret;\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\tgoogle?: {\n\t\tclientId: string;\n\t\tclientSecret: string;\n\t\tredirectUri: 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')[];\n\tappName?: 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}\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'\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';\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';\n\tphoneVerified: boolean;\n\tsid?: string;\n}\n\nexport interface ITokenOptions {\n\tloginMethod: 'email' | 'phone' | 'google';\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\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\tif (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 { stringOrEmpty, booleanOrFalse } 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\nexport function toUserDTO(user: IUser, phoneVerified = false): IUserDTO {\n\tconst prefs = user.preferences ?? ({} as IUserPreferences);\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: user.lastLogin instanceof Date ? user.lastLogin.toISOString() : '',\n\t\tpreferences: {\n\t\t\t...DEFAULT_PREFERENCES,\n\t\t\t...prefs,\n\t\t\tlocale: user.locale || prefs.locale || 'en-US',\n\t\t\ttimezone: user.timezone || prefs.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: user.createdAt instanceof Date ? user.createdAt.toISOString() : '',\n\t\tupdatedAt: user.updatedAt instanceof Date ? user.updatedAt.toISOString() : '',\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\tmfa_secret AS \"mfaSecret\",\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\nexport class SessionModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync create(userId: string, token: string, expiresAt: Date, sid?: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid)\n\t\t\tVALUES ($1, $2, $3, $4)\n\t\t\tON CONFLICT (token) DO NOTHING`,\n\t\t\t[userId, token, expiresAt, sid ?? 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\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 { tokenPairCookies, clearedTokenCookies, cookieHeaders } from '../services/cookies';\nimport { 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 { hashPassword, verifyPassword } 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 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);\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\tconst { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {\n\t\t\t\t\tloginMethod: 'phone',\n\t\t\t\t});\n\t\t\t\tawait sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);\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\ttokens: { access: accessToken, refresh: refreshToken },\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{\n\t\t\t\t\t\tstatus: 202,\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\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\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\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_CREDENTIALS', 'Invalid credentials');\n\t\t\t\t}\n\n\t\t\t\tconst valid = await verifyPassword(password, user.passwordHash);\n\t\t\t\tif (!valid) {\n\t\t\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'INVALID_CREDENTIALS', 'Invalid credentials');\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\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);\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\tconst { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {\n\t\t\t\t\tloginMethod: 'phone',\n\t\t\t\t});\n\t\t\t\tawait sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);\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: {\n\t\t\t\t\t\t\ttokens: { access: accessToken, refresh: refreshToken },\n\t\t\t\t\t\t\tuser: toUserDTO(user, false),\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: 202,\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\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\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);\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\tconst expiresAt = new Date(Date.now() + 1000 * 60 * 60);\n\t\t\tawait passwordReset.create(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.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 },\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\tif (typeof raw !== 'string' || typeof password !== 'string') {\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'pin and password are required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (!/^\\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'pin must be a 6-digit code',\n\t\t\t\t);\n\t\t\t}\n\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\tconst pin = raw.trim();\n\t\t\tconst row = await passwordReset.findByPin(pin);\n\t\t\tif (!row || new Date() > row.expiresAt) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'PASSWORD_RESET_FAILED', 'Invalid or expired pin');\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]);\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// ── 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);\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\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 type { IStoreAdapter } from '@fonderie/store';\n\nexport class PasswordResetModel {\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_password_resets (user_id, pin, expires_at, created_at)\n\t\t\tVALUES ($1, $2, $3, now())\n\t\t\tON CONFLICT (user_id) DO UPDATE\n\t\t\tSET pin = $2, expires_at = $3, created_at = now()`,\n\t\t\t[userId, pin, 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[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 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 } 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 type { IUser } from '../types';\nimport { UserModel } from '../models/user.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 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\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\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\t};\n}\n","import { tokenPairCookies, cookieHeaders } from '../services/cookies';\nimport { setApiResponse, HTTP } 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 { normalizeEmailSafe } from '../services/email';\n\nexport function oauthController(store: IStoreAdapter, config: IAuthConfig) {\n\tconst users = new UserModel(store);\n\tconst sessions = new SessionModel(store);\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\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});\n\n\t\t\tconst url = `https://accounts.google.com/o/oauth2/v2/auth?${params}`;\n\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.OK,\n\t\t\t\t'GOOGLE_AUTH_URL',\n\t\t\t\t'Redirect the user to the returned URL to begin Google OAuth.',\n\t\t\t\t{ url },\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\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\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 };\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\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 upserted = await users.upsertByProvider(normalizedEmail, 'google', payload.sub ?? '');\n\t\t\tif (!upserted) {\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);\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(tokenPairCookies(accessToken, refreshToken, config)),\n\t\t\t\t},\n\t\t\t);\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},\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 { buildAuthRoutes } from './routes';\nimport type { IAuthConfig } from './config';\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\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","export { requireAuth, requireAnyAuth } from '@fonderie/core/middlewares';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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;AAVA,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;;;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,EAEA,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;;;ACxGA;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;AAGM,IAAM,gBAAgB,aAAE,OAAO,EAAE,cAAc,aAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC;AAE7E,IAAM,uBAAuB,aAAE,OAAO,EAAE,MAAM,CAAC;AAE/C,IAAM,sBAAsB,aAAE,OAAO,EAAE,KAAK,aAAa,SAAS,CAAC;AAEnE,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,EACtC,eAAe,aAAE,QAAQ,EAAE,SAAS;AAAA,EACpC,aAAa,aAAE,QAAQ,EAAE,SAAS;AAAA,EAClC,YAAY,aAAE,QAAQ,EAAE,SAAS;AAAA,EACjC,YAAY,aAAE,QAAQ,EAAE,SAAS;AAClC,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;;;ACrElF,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;AAsB/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;;;ACjDA,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;AAIxC,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;AACrC,QAAI,KAAK,QAAQ,IAAI,CAAC,MAAM,OAAO;AAClC,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;;;AHnGA;;;AIlBA,IAAAC,eAA8C;AAwB9C,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;AAEO,SAAS,UAAU,MAAa,gBAAgB,OAAiB;AACvE,QAAM,QAAQ,KAAK,eAAgB,CAAC;AACpC,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,WAAW,KAAK,qBAAqB,OAAO,KAAK,UAAU,YAAY,IAAI;AAAA,IAC3E,aAAa;AAAA,MACZ,GAAG;AAAA,MACH,GAAG;AAAA,MACH,QAAQ,KAAK,UAAU,MAAM,UAAU;AAAA,MACvC,UAAU,KAAK,YAAY,MAAM,YAAY;AAAA,IAC9C;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,WAAW,KAAK,qBAAqB,OAAO,KAAK,UAAU,YAAY,IAAI;AAAA,IAC3E,WAAW,KAAK,qBAAqB,OAAO,KAAK,UAAU,YAAY,IAAI;AAAA,EAC5E;AACD;;;AC9CA,IAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwBd,IAAM,YAAN,MAAgB;AAAA,EACtB,YAAoB,OAAsB;AAAtB;AAAA,EAAuB;AAAA,EAAvB;AAAA,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;;;ACtQO,IAAM,eAAN,MAAmB;AAAA,EACzB,YAAoB,OAAsB;AAAtB;AAAA,EAAuB;AAAA,EAAvB;AAAA,EAEpB,MAAM,OAAO,QAAgB,OAAe,WAAiB,KAA6B;AACzF,UAAM,KAAK,MAAM;AAAA,MAChB;AAAA;AAAA;AAAA,MAGA,CAAC,QAAQ,OAAO,WAAW,OAAO,IAAI;AAAA,IACvC;AAAA,EACD;AAAA,EAEA,MAAM,OAAO,OAA8B;AAC1C,UAAM,KAAK,MAAM,MAAM,kDAAkD,CAAC,KAAK,CAAC;AAAA,EACjF;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;;;AChCO,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;;;APXO,SAAS,cACf,OACA,QACA,QACA,KACC;AACD,QAAM,QAAQ,IAAI,UAAU,KAAK;AACjC,QAAM,WAAW,IAAI,aAAa,KAAK;AACvC,QAAM,cAAc,IAAI,gBAAgB,KAAK;AAE7C,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,MAAM;AAAA,QAC/C,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;AAE5B,UAAI,OAAO,UAAU,UAAU;AAC9B,mBAAO,6BAAe,kBAAK,eAAe,qBAAqB,mBAAmB;AAAA,MACnF;AAEA,YAAM,gBAAgB,MAAM,MAAM,oBAAoB,IAAI,KAAM,EAAE;AAElE,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,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,SAAS,MAAM,MAAM,aAAa,IAAI,KAAM,EAAE;AACpD,YAAI,CAAC,QAAQ;AACZ,qBAAO,6BAAe,kBAAK,aAAa,sBAAsB,oBAAoB;AAAA,QACnF;AACA,YAAI,CAAC,gBAAgB,OAAO,MAAM,GAAG;AACpC,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,MACxB,CAAC;AACD,YAAM,SAAS,OAAO,IAAI,KAAM,IAAI,cAAc,mBAAmB,YAAY,GAAG,GAAG;AAEvF,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,QAAQ;AAAA,UACzB;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,SAAS,MAAM,MAAM,aAAa,IAAI,KAAM,EAAE;AACpD,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;AAEA,YAAM,SAAU,KAAiD;AACjE,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;;;AQ1OA,IAAAC,sBAA0B;AAI1B,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;;;ADYA;;;AEfO,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;;;ACrCO,IAAM,qBAAN,MAAyB;AAAA,EAC/B,YAAoB,OAAsB;AAAtB;AAAA,EAAuB;AAAA,EAAvB;AAAA,EAEpB,MAAM,OAAO,QAAgB,KAAa,WAAgC;AACzE,UAAM,KAAK,MAAM;AAAA,MAChB;AAAA;AAAA;AAAA;AAAA,MAIA,CAAC,QAAQ,KAAK,SAAS;AAAA,IACxB;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,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,aAAa,QAA+B;AACjD,UAAM,KAAK,MAAM,MAAM,2DAA2D,CAAC,MAAM,CAAC;AAAA,EAC3F;AACD;;;AClCO,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;;;ALXA,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,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,GAAG;AAElF,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;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,GAAG;AAElF,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,YACrB;AAAA,UACD;AAAA,UACA;AAAA,YACC,QAAQ;AAAA,YACR,SAAS,cAAc,iBAAiB,aAAa,cAAc,MAAM,CAAC;AAAA,UAC3E;AAAA,QACD;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;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,qBAAO,6BAAe,kBAAK,cAAc,uBAAuB,qBAAqB;AAAA,QACtF;AAEA,cAAM,QAAQ,MAAM,eAAeC,WAAU,KAAK,YAAY;AAC9D,YAAI,CAAC,OAAO;AACX,qBAAO,6BAAe,kBAAK,cAAc,uBAAuB,qBAAqB;AAAA,QACtF;AAEA,YAAI,KAAK,WAAW;AACnB,qBAAO;AAAA,YACN,kBAAK;AAAA,YACL;AAAA,YACA;AAAA,UACD;AAAA,QACD;AAEA,YAAI,KAAK,YAAY;AACpB,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,GAAG;AAElF,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,YAAMF,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;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,GAAG;AAElF,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,MAAM,KAAK;AAAA,YAC5B;AAAA,UACD;AAAA,UACA;AAAA,YACC,QAAQ;AAAA,YACR,SAAS,cAAc,iBAAiB,aAAa,cAAc,MAAM,CAAC;AAAA,UAC3E;AAAA,QACD;AAAA,MACD;AAEA,iBAAO;AAAA,QACN,kBAAK;AAAA,QACL;AAAA,QACA;AAAA,MACD;AAAA,IACD;AAAA,IAEA,QAAQ,OAAO,QAA6C;AAC3D,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,GAAG;AAElF,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;AAChD,YAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,MAAO,KAAK,EAAE;AACtD,YAAM,cAAc,OAAO,KAAK,IAAI,KAAK,SAAS;AAElD,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,IAAI;AAAA,MACb,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,UAAI,OAAO,QAAQ,YAAY,OAAOA,cAAa,UAAU;AAC5D,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,UAAI,CAAC,UAAU,KAAK,IAAI,KAAK,CAAC,GAAG;AAChC,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,UAAIA,UAAS,SAAS,GAAG;AACxB,mBAAO;AAAA,UACN,kBAAK;AAAA,UACL;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAEA,YAAM,MAAM,IAAI,KAAK;AACrB,YAAM,MAAM,MAAM,cAAc,UAAU,GAAG;AAC7C,UAAI,CAAC,OAAO,oBAAI,KAAK,IAAI,IAAI,WAAW;AACvC,mBAAO,6BAAe,kBAAK,aAAa,yBAAyB,wBAAwB;AAAA,MAC1F;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,QACjF,CAAC;AAAA,MACF,CAAC;AAED,iBAAO,6BAAe,kBAAK,IAAI,6BAA6B,8BAA8B;AAAA,IAC3F;AAAA,IAEA,QAAQ,OAAO,QAA6C;AAE3D,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,GAAG;AAEvF,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;AACrE,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;;;AMlsBA,IAAAK,sBAA0B;AAE1B,IAAAC,eAAqC;AAMrC,IAAAC,iBAAmC;AAUnC,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,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,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;AAE7C,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,EACD;AACD;;;ACxQA,IAAAE,eAAqC;AAW9B,SAAS,gBAAgB,OAAsB,QAAqB;AAC1E,QAAM,QAAQ,IAAI,UAAU,KAAK;AACjC,QAAM,WAAW,IAAI,aAAa,KAAK;AAEvC,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;AAEA,YAAM,SAAS,IAAI,gBAAgB;AAAA,QAClC,WAAW,OAAO;AAAA,QAClB,cAAc,OAAO;AAAA,QACrB,eAAe;AAAA,QACf,OAAO;AAAA,MACR,CAAC;AAED,YAAM,MAAM,gDAAgD,MAAM;AAElE,iBAAO;AAAA,QACN,kBAAK;AAAA,QACL;AAAA,QACA;AAAA,QACA,EAAE,IAAI;AAAA,MACP;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;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;AAEA,YAAM,UAAU,KAAK;AAAA,QACpB,OAAO,KAAK,UAAU,SAAS,MAAM,GAAG,EAAE,CAAC,KAAK,IAAI,WAAW,EAAE,SAAS;AAAA,MAC3E;AAEA,UAAI,CAAC,QAAQ,OAAO;AACnB,mBAAO,6BAAe,kBAAK,aAAa,sBAAsB,4BAA4B;AAAA,MAC3F;AAEA,YAAM,kBAAkB,mBAAmB,QAAQ,KAAK;AACxD,UAAI,CAAC,iBAAiB;AACrB,mBAAO,6BAAe,kBAAK,aAAa,sBAAsB,iCAAiC;AAAA,MAChG;AAEA,YAAM,WAAW,MAAM,MAAM,iBAAiB,iBAAiB,UAAU,QAAQ,OAAO,EAAE;AAC1F,UAAI,CAAC,UAAU;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,GAAG;AAEtF,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,iBAAiB,aAAa,cAAc,MAAM,CAAC;AAAA,QAC3E;AAAA,MACD;AAAA,IACD;AAAA,EACD;AACD;;;ArB/FO,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,yBAAqB,6BAAS,mBAAmB,GAAG,KAAK,aAAa;AAAA;AAAA,IAGjG,EAAE,eAAe,QAAQ,gBAAgB,qCAAa,6BAAS,YAAY,GAAG,KAAK,MAAM;AAAA,IACzF,EAAE,oBAAoB,OAAO,2BAA2B,iCAAa,KAAK,gBAAgB;AAAA;AAAA,IAG1F,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;AAAA;AAAA,IAIxE,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,SAAO;AACR;;;AsBtGO,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,MACvD;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;;;ACvDO,IAAM,aAAN,MAA4C;AAAA,EAGlD,YACS,OACA,QACA,KACP;AAHO;AACA;AACA;AAAA,EACN;AAAA,EAHM;AAAA,EACA;AAAA,EACA;AAAA,EALA,OAAO;AAAA,EAQhB,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;;;ACzBA,IAAAC,sBAA4C;","names":["bcrypt","import_middlewares","import_core","jwt","import_node_crypto","email","import_core","email","phone","QRCode","import_node_crypto","import_events","import_core","email","phone","phone","email","password","remaining","expiresAt","import_node_crypto","import_core","import_events","normalizePhone","phone","isValidPhone","hashPassword","verifyPassword","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/dtos/user.ts","../src/models/user.model.ts","../src/models/session.model.ts","../src/models/backup-code.model.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/controllers/oauth.controller.ts","../src/services/config-guard.ts","../src/middlewares/session.ts","../src/module.ts","../src/middlewares/require-auth.ts","../src/migrate.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 } from './config';\nexport { AUTH_CONFIG_KEYS, MESSAGE_KEYS } from './config';\nexport type { AuthMessageKey } from './config';\n\n// DTOs\nexport type { IUserDTO } from './dtos/user';\nexport { toUserDTO } from './dtos/user';\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 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} 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', validate(resetPasswordSchema), auth.resetPassword),\n\n\t\t// Verification (Protected — email or phone, determined by loginMethod)\n\t\tR('verifyEmail', 'POST', '/auth/verify', requireAuth, validate(verifySchema), auth.verify),\n\t\tR('sendVerification', 'GET', '/auth/send-verification', requireAuth, 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\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\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' | '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// 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// 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\nexport const resetPasswordSchema = z.object({ pin: sixDigitPin, password });\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\tnotifications: z.unknown().optional(),\n\t\temailDigest: z.unknown().optional(),\n\t\tdateFormat: z.unknown().optional(),\n\t\ttimeFormat: z.unknown().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 { toUserDTO } from '../dtos/user';\nimport { UserModel } from '../models/user.model';\nimport { SessionModel } from '../models/session.model';\nimport { BackupCodeModel } from '../models/backup-code.model';\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\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, 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\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 pendingSecret = await users.getMfaPendingSecret(ctx.user!.id);\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\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 secret = await users.getMfaSecret(ctx.user!.id);\n\t\t\t\tif (!secret) {\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\tif (!verifyTotpToken(token, secret)) {\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});\n\t\t\tawait sessions.create(ctx.user!.id, refreshToken, refreshTokenExpiry(refreshToken), sid);\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),\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 secret = await users.getMfaSecret(ctx.user!.id);\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\tconst secret = (user as unknown as { mfaSecret: string | null }).mfaSecret;\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\tgoogle?: {\n\t\tclientId: string;\n\t\tclientSecret: string;\n\t\tredirectUri: 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')[];\n\tappName?: 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}\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'\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';\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';\n\tphoneVerified: boolean;\n\tsid?: string;\n}\n\nexport interface ITokenOptions {\n\tloginMethod: 'email' | 'phone' | 'google';\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\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\tif (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 { stringOrEmpty, booleanOrFalse } 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\nexport function toUserDTO(user: IUser, phoneVerified = false): IUserDTO {\n\tconst prefs = user.preferences ?? ({} as IUserPreferences);\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: user.lastLogin instanceof Date ? user.lastLogin.toISOString() : '',\n\t\tpreferences: {\n\t\t\t...DEFAULT_PREFERENCES,\n\t\t\t...prefs,\n\t\t\tlocale: user.locale || prefs.locale || 'en-US',\n\t\t\ttimezone: user.timezone || prefs.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: user.createdAt instanceof Date ? user.createdAt.toISOString() : '',\n\t\tupdatedAt: user.updatedAt instanceof Date ? user.updatedAt.toISOString() : '',\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\tmfa_secret AS \"mfaSecret\",\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\nexport class SessionModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync create(userId: string, token: string, expiresAt: Date, sid?: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid)\n\t\t\tVALUES ($1, $2, $3, $4)\n\t\t\tON CONFLICT (token) DO NOTHING`,\n\t\t\t[userId, token, expiresAt, sid ?? 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\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 { tokenPairCookies, clearedTokenCookies, cookieHeaders } from '../services/cookies';\nimport { 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 { 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 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);\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\tconst { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {\n\t\t\t\t\tloginMethod: 'phone',\n\t\t\t\t});\n\t\t\t\tawait sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);\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\ttokens: { access: accessToken, refresh: refreshToken },\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{\n\t\t\t\t\t\tstatus: 202,\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\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\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\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\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\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);\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\tconst { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {\n\t\t\t\t\tloginMethod: 'phone',\n\t\t\t\t});\n\t\t\t\tawait sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);\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: {\n\t\t\t\t\t\t\ttokens: { access: accessToken, refresh: refreshToken },\n\t\t\t\t\t\t\tuser: toUserDTO(user, false),\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: 202,\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\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\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);\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\tconst expiresAt = new Date(Date.now() + 1000 * 60 * 60);\n\t\t\tawait passwordReset.create(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.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 },\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\tif (typeof raw !== 'string' || typeof password !== 'string') {\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'pin and password are required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (!/^\\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'pin must be a 6-digit code',\n\t\t\t\t);\n\t\t\t}\n\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\tconst pin = raw.trim();\n\t\t\tconst row = await passwordReset.findByPin(pin);\n\t\t\tif (!row || new Date() > row.expiresAt) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'PASSWORD_RESET_FAILED', 'Invalid or expired pin');\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]);\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// ── 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);\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\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 type { IStoreAdapter } from '@fonderie/store';\n\nexport class PasswordResetModel {\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_password_resets (user_id, pin, expires_at, created_at)\n\t\t\tVALUES ($1, $2, $3, now())\n\t\t\tON CONFLICT (user_id) DO UPDATE\n\t\t\tSET pin = $2, expires_at = $3, created_at = now()`,\n\t\t\t[userId, pin, 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[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 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 } 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 type { IUser } from '../types';\nimport { UserModel } from '../models/user.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 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\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\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\t};\n}\n","import { tokenPairCookies, cookieHeaders } from '../services/cookies';\nimport { setApiResponse, HTTP } 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 { normalizeEmailSafe } from '../services/email';\n\nexport function oauthController(store: IStoreAdapter, config: IAuthConfig) {\n\tconst users = new UserModel(store);\n\tconst sessions = new SessionModel(store);\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\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});\n\n\t\t\tconst url = `https://accounts.google.com/o/oauth2/v2/auth?${params}`;\n\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.OK,\n\t\t\t\t'GOOGLE_AUTH_URL',\n\t\t\t\t'Redirect the user to the returned URL to begin Google OAuth.',\n\t\t\t\t{ url },\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\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\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 };\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\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 upserted = await users.upsertByProvider(normalizedEmail, 'google', payload.sub ?? '');\n\t\t\tif (!upserted) {\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);\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(tokenPairCookies(accessToken, refreshToken, config)),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t};\n}\n","import type { IReadinessProblem } from '@fonderie/core';\nimport type { IAuthConfig } from '../config';\n\nconst MODULE = '@fonderie/auth';\n\n// Placeholder / dev-default secrets we never want signing tokens in production.\n// These distinctive fragments won't appear in a real random secret (e.g. the\n// output of `openssl rand -base64 32`), so matching one is a strong signal the\n// secret was copy-pasted from an example rather than generated.\nconst PLACEHOLDER_SECRET =\n\t/dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|min-32-chars/i;\n\n// Minimum length for an HS256 signing secret — 256 bits.\nconst MIN_SECRET_LENGTH = 32;\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.\nexport function collectAuthConfigProblems(config: IAuthConfig): IReadinessProblem[] {\n\tconst problems: IReadinessProblem[] = [];\n\tconst secret = config.jwtSecret ?? '';\n\n\tif (secret.length < MIN_SECRET_LENGTH) {\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 (PLACEHOLDER_SECRET.test(secret)) {\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\tif (config.secureCookies === false) {\n\t\tproblems.push({\n\t\t\tmodule: MODULE,\n\t\t\tseverity: '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\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},\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","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"],"mappings":"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","names":["bcrypt","import_middlewares","import_core","jwt","import_node_crypto","email","import_core","email","phone","QRCode","import_node_crypto","import_events","import_core","email","phone","phone","email","password","remaining","expiresAt","import_node_crypto","import_core","import_events","normalizePhone","phone","isValidPhone","hashPassword","verifyPassword","import_core","import_middlewares"]}