@luckystack/login 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ All notable changes to `@luckystack/login` are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [0.7.3] - 2026-07-20
11
+
12
+ ### Fixed
13
+
14
+ - **OAuth token exchange follows the actually-bound dev port.** The
15
+ `redirect_uri` sent at token exchange now applies core's `resolveDevCallbackUrl`
16
+ rewrite (same as the authorize step in `@luckystack/server`), so a `localhost`
17
+ callback reaches the live server after a dev auto-increment hop. The two
18
+ `redirect_uri` values stay byte-identical (OAuth requires it) because the rewrite
19
+ reads the process-constant bound port, not the request. No-op in production.
20
+
8
21
  ## [0.6.0] - 2026-07-12
9
22
 
10
23
  ### Added
package/dist/index.js CHANGED
@@ -142,7 +142,7 @@ var getPostLoginRedirect = () => activeResolver;
142
142
 
143
143
  // src/login.ts
144
144
  import { URLSearchParams as URLSearchParams2 } from "url";
145
- import { tryCatch as tryCatch2, tryCatchSync, redis as redisClient, getUploadsDir, dispatchHook, getLogger as getLogger3, formatKey, getProjectConfig as getProjectConfig4, getCookieValue } from "@luckystack/core";
145
+ import { tryCatch as tryCatch2, tryCatchSync, redis as redisClient, getUploadsDir, dispatchHook, getLogger as getLogger3, formatKey, getProjectConfig as getProjectConfig4, getCookieValue, resolveDevCallbackUrl } from "@luckystack/core";
146
146
  import bcrypt from "bcryptjs";
147
147
  import { randomBytes, createHash, timingSafeEqual } from "crypto";
148
148
  import validator from "validator";
@@ -773,11 +773,12 @@ var isAllowedRedirectUrl = (url) => {
773
773
  });
774
774
  };
775
775
  var exchangeOAuthToken = async (provider, code, codeVerifier) => {
776
+ const redirectUri = resolveDevCallbackUrl(provider.callbackURL);
776
777
  const values = {
777
778
  code,
778
779
  client_id: provider.clientID,
779
780
  client_secret: provider.clientSecret,
780
- redirect_uri: provider.callbackURL,
781
+ redirect_uri: redirectUri,
781
782
  grant_type: "authorization_code",
782
783
  ...codeVerifier ? { code_verifier: codeVerifier } : {}
783
784
  };
@@ -799,7 +800,7 @@ var exchangeOAuthToken = async (provider, code, codeVerifier) => {
799
800
  formParams.append("client_secret", provider.clientSecret);
800
801
  formParams.append("code", values.code);
801
802
  formParams.append("grant_type", "authorization_code");
802
- formParams.append("redirect_uri", provider.callbackURL);
803
+ formParams.append("redirect_uri", redirectUri);
803
804
  if (codeVerifier) {
804
805
  formParams.append("code_verifier", codeVerifier);
805
806
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/authLockout.ts","../src/redirectResolver.ts","../src/login.ts","../src/accountStrategy.ts","../src/passwordPolicy.ts","../src/data/commonPasswords.ts","../src/passwordReset.ts","../src/forgotPassword.ts","../src/emailModuleLoader.ts","../src/emailChange.ts","../src/emailChangeNotification.ts","../src/twoFactor.ts","../src/emailOtp.ts","../src/totp.ts","../src/emailCodeLogin.ts"],"sourcesContent":["// Registers module augmentations on @luckystack/core for auth/session hooks.\nimport './hookPayloads';\n\n//? Decoupling seam (0.2.0): register this package's session implementation into\n//? @luckystack/core so api/sync/presence/server read sessions through core's\n//? null-safe accessors WITHOUT importing @luckystack/login directly. This is\n//? what makes login a genuine OPTIONAL package — when it isn't installed nothing\n//? registers and the accessors return null (unauthenticated). Side-effect at\n//? module load: any app that imports @luckystack/login (its overlay, LoginForm,\n//? config) wires this before the first request is served.\nimport { registerSessionProvider } from '@luckystack/core';\nimport { saveSession as _saveSession, getSession as _getSession, deleteSession as _deleteSession } from './session';\nimport { logout as _logout } from './logout';\nregisterSessionProvider({\n getSession: _getSession,\n saveSession: _saveSession,\n deleteSession: _deleteSession,\n logout: ({ token, socket, userId, skipSessionDelete }) =>\n _logout({ token, socket, userId: userId ?? null, skipSessionDelete }),\n});\n\n//? Subscribe the per-account brute-force lockout to the `loginFailed` hook at\n//? boot (F7). Idempotent + cheap; the recorder no-ops unless\n//? `rateLimiting.auth.enabled` is set, so this is safe to wire unconditionally.\nimport { registerAuthLockoutHook } from './authLockout';\nregisterAuthLockoutHook();\n\nexport type { BaseSessionLayout, SessionLocation, AuthProps } from './sessionLayout';\nexport type {\n PreLoginPayload,\n PostLoginPayload,\n PreRegisterPayload,\n PostRegisterPayload,\n PreLogoutPayload,\n PostLogoutPayload,\n PreSessionCreatePayload,\n PostSessionCreatePayload,\n PreSessionDeletePayload,\n PostSessionDeletePayload,\n PasswordResetRequestedPayload,\n PrePasswordResetCompletedPayload,\n PasswordResetCompletedPayload,\n PrePasswordChangedPayload,\n PasswordChangedPayload,\n PreEmailChangePayload,\n PostEmailChangeRequestedPayload,\n PostEmailChangedPayload,\n LoginFailedPayload,\n PreAccountDeletePayload,\n PostAccountDeletePayload,\n} from './hookPayloads';\nexport { saveSession, getSession, deleteSession, getAllSessions, revokeUserSessions, sessionKeyFor, activeUsersKeyFor } from './session';\nexport { registerSessionAdapter, getSessionAdapter, redisSessionAdapter } from './sessionAdapter';\nexport type { SessionAdapter } from './sessionAdapter';\nexport { registerSessionSanitizer, getSessionSanitizer } from './sessionSanitizer';\nexport type { SessionSanitizer } from './sessionSanitizer';\nexport { loginWithCredentials, loginCallback, createOAuthState, registerWithCredentials, loginWithCredentialsCore, finalizeLogin, registerTwoFactorGate } from './login';\nexport type {\n CredentialsLoginResult,\n CredentialsLoginSuccess,\n CredentialsLoginChallenge,\n CredentialsLoginFailure,\n TwoFactorMethod,\n TwoFactorGate,\n OAuthCallbackResult,\n} from './login';\nexport type { CreateOAuthStateResult } from './login';\nexport { OAUTH_STATE_COOKIE_NAME } from './login';\nexport { logout } from './logout';\n\n// Per-account brute-force lockout (F7). The hook is auto-registered at boot;\n// these are exported for consumers who want to query/clear a lock explicitly\n// (e.g. an admin \"unlock account\" action) or build their own surface on top.\nexport { isAccountLocked, recordAuthFailure, clearAuthFailures } from './authLockout';\n\n// Password-reset primitives. Used by the framework's `framework`-mode\n// forgot-password flow AND exported for consumers who picked `'custom'`.\nexport {\n createPasswordResetToken,\n consumePasswordResetToken,\n updatePasswordHash,\n verifyPassword,\n PasswordPolicyError,\n} from './passwordReset';\nexport { validatePassword } from './passwordPolicy';\nexport { sendPasswordResetEmail } from './forgotPassword';\n\n// Email-change primitives. Used by the framework's settings flow to confirm\n// a new email address before applying it (token mailed to NEW address proves\n// ownership of the new mailbox).\nexport {\n createEmailChangeToken,\n consumeEmailChangeToken,\n} from './emailChange';\nexport type { EmailChangePayload } from './emailChange';\nexport { sendEmailChangeConfirmation } from './emailChangeNotification';\n\n// OAuth provider registry + composable helpers.\nexport {\n registerOAuthProviders,\n registerOAuthProviderFactory,\n getOAuthProviders,\n isFullOAuthProvider,\n asOAuthUserData,\n credentialsProvider,\n googleProvider,\n githubProvider,\n discordProvider,\n facebookProvider,\n microsoftProvider,\n} from './oauthProviders';\nexport type {\n OAuthProvider,\n CredentialsProvider,\n FullOAuthProvider,\n OAuthUserData,\n} from './oauthProviders';\n\n// User adapter registry: lets consumers swap out the Prisma User model behind auth flows.\nexport {\n registerUserAdapter,\n getUserAdapter,\n isUserAdapterRegistered,\n defaultPrismaUserAdapter,\n} from './userAdapter';\nexport type {\n UserAdapter,\n UserAdapterCreateInput,\n UserRecord,\n} from './userAdapter';\n\n// 2FA (ADR 0024): TOTP (authenticator apps) + email-code fallback + recovery\n// codes. IMPORTING THIS MODULE ARMS THE LOGIN GATE (it registers itself into\n// login.ts at init) — which happens right here in the package index, so any\n// app with @luckystack/login installed gets the challenge step for enrolled\n// users once `auth.twoFactor` is 'optional'.\nexport {\n beginTotpEnrollment,\n confirmTotpEnrollment,\n disableTwoFactor,\n regenerateRecoveryCodes,\n verifyTwoFactorChallenge,\n requestTwoFactorEmailCode,\n availableTwoFactorMethods,\n createTwoFactorChallengeIfRequired,\n} from './twoFactor';\nexport type { TotpEnrollmentStart, ConfirmTotpEnrollmentResult, VerifyTwoFactorInput } from './twoFactor';\nexport { verifyTotp, generateTotpSecret, buildOtpauthUri, hotp, base32Encode, base32Decode } from './totp';\n\n// Passwordless email-code login (ADR 0024) + the reusable numeric-OTP store.\nexport { requestEmailLoginCode, verifyEmailLoginCode } from './emailCodeLogin';\nexport type { RequestEmailLoginCodeInput, RequestEmailLoginCodeResult, VerifyEmailLoginCodeInput } from './emailCodeLogin';\nexport { issueEmailCode, verifyEmailCode, clearEmailCode, generateNumericCode } from './emailOtp';\nexport type { EmailOtpPurpose, EmailCodeVerdict } from './emailOtp';\n\n// Post-login redirect resolver — lets consumers compute the OAuth callback\n// destination dynamically (per-user, per-tenant, per-provider).\nexport {\n registerPostLoginRedirect,\n getPostLoginRedirect,\n} from './redirectResolver';\nexport type {\n PostLoginRedirectResolver,\n PostLoginRedirectInput,\n} from './redirectResolver';\n","//? Per-account brute-force lockout (F7 / MIS-017). The generic per-IP throttle\n//? on `/auth/api/credentials` (in @luckystack/server) does NOT stop distributed\n//? credential stuffing against a SINGLE account from many IPs. This module adds\n//? a per-account failed-attempt counter, keyed independently from the api/ip\n//? limits, that locks an account after `rateLimiting.auth.maxAttempts` failures\n//? within `rateLimiting.auth.windowMs`.\n//?\n//? It is OPT-IN: when `rateLimiting.auth.enabled === false` (the default) every\n//? function here is a no-op and behaviour is byte-identical to before — only\n//? the existing per-IP throttle applies.\n//?\n//? The counter rides on the framework rate limiter (`checkRateLimit`) so it\n//? inherits the configured store (memory/redis) and multi-instance behaviour.\n//? `recordAuthFailure` increments; `isAccountLocked` reads WITHOUT incrementing\n//? (so merely checking the lock can't itself trip it); `clearAuthFailures`\n//? resets the counter on a successful login.\n\nimport {\n getProjectConfig,\n checkRateLimit,\n getRateLimitStatus,\n clearRateLimit,\n getLogger,\n registerHook,\n tryCatch,\n} from '@luckystack/core';\n\n//? DD-LOGIN-F5: build the lockout bucket key. When `requesterIp` is supplied\n//? the key is an IP+account composite (`auth:<email>:<ip>`) so a remote attacker\n//? from one IP cannot lock out an account for legitimate users on different IPs.\n//? Without an IP the key is the bare account key (`auth:<email>`) — same\n//? behaviour as before. The composite bucket is cleared on success the same way\n//? (same key is used in `clearAuthFailures`). Lower-case + trim the account\n//? identifier so `Alice@x.com` and `alice@x.com` share one lockout bucket\n//? (matches the credentials email normalization).\nconst lockoutKey = (accountKey: string, requesterIp?: string): string => {\n const base = `auth:${accountKey.trim().toLowerCase()}`;\n return requesterIp ? `${base}:${requesterIp}` : base;\n};\n\n//? One-shot guard so the inert-config warning below fires once per process, not\n//? on every login attempt.\nlet warnedLockoutInert = false;\n\n//? One-shot guard for the M1 warning: surfaces the silent gap where credentials\n//? logins are FAILING but the per-account lockout is disabled entirely, so only\n//? the proxy-fragile per-IP cap stands between an attacker and distributed\n//? credential stuffing. Fires once, on the first real credentials-login failure.\nlet warnedLockoutDisabled = false;\n\n/** Read the active auth-lockout config (returns null when the feature is off). */\nconst getAuthLockoutConfig = (): { maxAttempts: number; maxAttemptsPerAccount: number; windowMs: number } | null => {\n const rateLimiting = getProjectConfig().rateLimiting;\n if (!rateLimiting.auth.enabled) return null;\n //? The counter rides the framework rate limiter, which short-circuits when the\n //? GLOBAL `rateLimiting.enabled` is false — so `auth.enabled:true` +\n //? `enabled:false` makes recordAuthFailure/isAccountLocked silent no-ops, i.e. a\n //? believed-on-but-INERT brute-force defense. Warn once so the conflict surfaces.\n if (!rateLimiting.enabled && !warnedLockoutInert) {\n warnedLockoutInert = true;\n getLogger().warn(\n '[LuckyStack] rateLimiting.auth.enabled is true but rateLimiting.enabled is false — the per-account brute-force lockout is INERT (it rides the global rate limiter, which is off). Set rateLimiting.enabled to true for the lockout to take effect.',\n );\n }\n return {\n maxAttempts: rateLimiting.auth.maxAttempts,\n maxAttemptsPerAccount: rateLimiting.auth.maxAttemptsPerAccount,\n windowMs: rateLimiting.auth.windowMs,\n };\n};\n\n/**\n * Whether `accountKey` is currently locked out (failed-attempt counter already\n * at/over the cap). Non-incrementing — safe to call on every login attempt.\n * Always `false` when the feature is disabled.\n * Pass `requesterIp` to check the IP+account composite bucket (DD-LOGIN-F5).\n */\nexport const isAccountLocked = async (accountKey: string, requesterIp?: string): Promise<boolean> => {\n const cfg = getAuthLockoutConfig();\n if (!cfg || !accountKey) return false;\n //? DUAL counter (DD-LOGIN-F5 + the cross-IP fix). Lock if EITHER trips:\n //? - the bare-account bucket (`auth:<email>`) against the cross-IP cap\n //? (`maxAttemptsPerAccount`) — the distributed-credential-stuffing defense, and\n //? - the IP+account composite (`auth:<email>:<ip>`) against the per-IP cap\n //? (`maxAttempts`) — bounds one IP + shields other IPs from a victim-lock DoS.\n const accountStatus = await getRateLimitStatus(lockoutKey(accountKey), cfg.maxAttemptsPerAccount);\n if (accountStatus.remaining <= 0) return true;\n if (requesterIp) {\n const ipStatus = await getRateLimitStatus(lockoutKey(accountKey, requesterIp), cfg.maxAttempts);\n if (ipStatus.remaining <= 0) return true;\n }\n return false;\n};\n\n/**\n * Increment the failed-attempt counter for `accountKey`. Call on every failed\n * credentials login for a known/looked-up account. No-op when disabled.\n * Pass `requesterIp` to increment the IP+account composite bucket (DD-LOGIN-F5).\n */\nexport const recordAuthFailure = async (accountKey: string, requesterIp?: string): Promise<void> => {\n const cfg = getAuthLockoutConfig();\n if (!cfg || !accountKey) return;\n //? Increment BOTH the cross-IP bare-account counter and (when an IP is known)\n //? the per-IP composite counter — mirrors the dual gate in isAccountLocked.\n const account = await checkRateLimit({\n key: lockoutKey(accountKey),\n limit: cfg.maxAttemptsPerAccount,\n windowMs: cfg.windowMs,\n });\n let ipBlocked = false;\n if (requesterIp) {\n const ip = await checkRateLimit({\n key: lockoutKey(accountKey, requesterIp),\n limit: cfg.maxAttempts,\n windowMs: cfg.windowMs,\n });\n ipBlocked = !ip.allowed;\n }\n if (!account.allowed || ipBlocked) {\n getLogger().warn('[authLockout] account temporarily locked after repeated failures', {\n maxAttempts: cfg.maxAttempts,\n maxAttemptsPerAccount: cfg.maxAttemptsPerAccount,\n crossIp: !account.allowed,\n });\n }\n};\n\n/**\n * Reset the failed-attempt counter for `accountKey` (call after a SUCCESSFUL\n * login so a user who eventually gets their password right isn't penalised by\n * earlier typos). No-op when disabled.\n * Pass `requesterIp` to clear the IP+account composite bucket (DD-LOGIN-F5).\n */\nexport const clearAuthFailures = async (accountKey: string, requesterIp?: string): Promise<void> => {\n const cfg = getAuthLockoutConfig();\n if (!cfg || !accountKey) return;\n //? Clear BOTH counters on a successful login so earlier typos don't penalise the\n //? user — the cross-IP bare-account bucket and the per-IP composite bucket.\n //? L6: callers invoke this fire-and-forget (`void clearAuthFailures(...)`) on the\n //? success path, so a transient Redis error must NOT become an unhandled\n //? rejection. Swallow + log; a stale lockout counter self-expires within the\n //? window, so a missed clear is non-fatal.\n const [error] = await tryCatch(async () => {\n await clearRateLimit(lockoutKey(accountKey));\n if (requesterIp) await clearRateLimit(lockoutKey(accountKey, requesterIp));\n });\n if (error) {\n getLogger().warn('[authLockout] clearAuthFailures failed (non-fatal — counter self-expires)', { error });\n }\n};\n\n//? ADR 0012 — the lockout counter must reflect GENUINE credential failures only.\n//? This is an ALLOW-LIST, not a deny-list: only a real wrong-password attempt\n//? (`login.wrongPassword`, covering both the no-account/no-hash dummy-compare\n//? path and a failed bcrypt compare) feeds the per-account counter. Everything\n//? else is excluded by construction — `preLogin` veto rejections (whose reason\n//? is a consumer-controlled `errorCode`), password-POLICY rejections (M-15),\n//? input-shape errors, the `login.accountLocked` self-trip, and infra failures\n//? (DB find / bcrypt throw, surfaced via `toReasonKey`). A deny-list let any of\n//? those — most dangerously a consumer `preLogin` veto an attacker can trigger\n//? with just a victim's email and no password — silently drive the lockout and\n//? DoS the account. Keep this allow-list narrow.\nconst COUNTING_REASONS = new Set([\n 'login.wrongPassword',\n]);\n\nlet lockoutHookRegistered = false;\n\n/**\n * Wire the per-account lockout into the `loginFailed` hook (F7). Subscribes once\n * (idempotent) and increments the failed-attempt counter only on a credentials\n * LOGIN failure that carries an email AND is a genuine wrong-password attempt\n * (see `COUNTING_REASONS`). Registration is unconditional (cheap); the recorder\n * itself no-ops when `rateLimiting.auth.enabled` is false, so toggling the\n * config at runtime is honoured without re-wiring.\n */\nexport const registerAuthLockoutHook = (): void => {\n if (lockoutHookRegistered) return;\n lockoutHookRegistered = true;\n registerHook('loginFailed', async ({ email, provider, reason, stage, requesterIp }) => {\n if (stage !== 'login' || provider !== 'credentials') return;\n //? M1: a credentials login is failing while the per-account lockout is OFF.\n //? Warn ONCE so operators of a credentials app don't ship with distributed\n //? credential stuffing unthrottled beyond the (proxy-fragile) per-IP cap. Kept\n //? as a loud default rather than force-enabling the lockout, because enabling\n //? the cross-IP counter also introduces a victim-account-lock DoS tradeoff the\n //? operator should opt into consciously (set rateLimiting.auth.enabled:true).\n if (!getProjectConfig().rateLimiting.auth.enabled && !warnedLockoutDisabled) {\n warnedLockoutDisabled = true;\n getLogger().warn(\n '[LuckyStack] a credentials login failed but the per-account brute-force lockout is DISABLED (rateLimiting.auth.enabled:false). Only the per-IP API cap protects credentials login, so distributed credential stuffing against one account (many IPs) is unthrottled. Set rateLimiting.auth.enabled:true to enable the per-account lockout (note: the cross-IP counter also enables a bounded victim-account-lock DoS — see docs/ARCHITECTURE_AUTH.md).',\n );\n }\n if (!email || !COUNTING_REASONS.has(reason)) return;\n //? DD-LOGIN-F5: use the IP+account composite key when an IP was threaded in.\n await recordAuthFailure(email, requesterIp);\n });\n};\n","//? Post-login redirect resolver. Lets consumers compute the OAuth callback\n//? redirect URL dynamically (per-user, per-tenant, per-provider).\n//?\n//? Why a dedicated registry instead of a hook:\n//? The framework's hooks are \"stop-or-continue\" — handlers return undefined\n//? or a stop signal. They don't carry a value. A redirect-resolution\n//? handler must RETURN a string, so it lives in its own registry.\n//?\n//? Validation: the resolved URL is checked against\n//? `ProjectConfig.http.cors.allowedOrigins` (plus the localhost convenience when\n//? `http.cors.allowLocalhost` is on) before being used. An invalid URL falls back\n//? to `ProjectConfig.loginRedirectUrl`.\n\nexport interface PostLoginRedirectInput {\n userId: string;\n provider: string;\n isNewUser: boolean;\n defaultUrl: string;\n}\n\nexport type PostLoginRedirectResolver = (\n input: PostLoginRedirectInput,\n) => string | Promise<string>;\n\nlet activeResolver: PostLoginRedirectResolver | null = null;\n\nexport const registerPostLoginRedirect = (\n resolver: PostLoginRedirectResolver,\n): PostLoginRedirectResolver => {\n activeResolver = resolver;\n return resolver;\n};\n\nexport const getPostLoginRedirect = (): PostLoginRedirectResolver | null => activeResolver;\n","import { getOAuthProviders, isFullOAuthProvider, type FullOAuthProvider, type OAuthUserData } from './oauthProviders';\nimport { getPostLoginRedirect } from './redirectResolver';\nimport { IncomingMessage, ServerResponse } from 'node:http';\nimport { URLSearchParams } from 'node:url';\nimport { tryCatch, tryCatchSync, redis as redisClient, getUploadsDir, dispatchHook, getLogger, formatKey, getProjectConfig, getCookieValue } from '@luckystack/core';\nimport bcrypt from 'bcryptjs';\nimport { randomBytes, createHash, timingSafeEqual } from 'node:crypto';\nimport { saveSession } from \"./session\"\nimport validator from 'validator';\nimport type { BaseSessionLayout as SessionLayout } from './sessionLayout';\nimport { getUserAdapter, type UserRecord } from './userAdapter';\nimport { resolveUserByEmail } from './accountStrategy';\nimport { validatePassword } from './passwordPolicy';\nimport { isAccountLocked, clearAuthFailures } from './authLockout';\nimport path from 'node:path';\nimport { promises as fsPromises } from 'node:fs';\n\n//? Combined login/register request body. When `name` + `confirmPassword` are\n//? both present the dispatcher registers; otherwise it logs in.\ninterface LoginOrRegisterParams {\n email?: string,\n password?: string,\n name?: string,\n confirmPassword?: string,\n}\n\n//? Discriminated result of the credentials login/register surface (QUA-080).\n//? Exporting this lets `@luckystack/server`'s auth route narrow on `status`\n//? instead of casting the result to an inline shape. On a full success,\n//? `newToken` + `session` are always present; on `status: false` only `reason`\n//? is meaningful (the failure branch never mints a token). The CHALLENGE\n//? member (ADR 0024) is the 2FA half-way state: the first factor was correct\n//? but NO session exists yet — the route relays `challengeToken` to the client\n//? and `/auth/api/2fa` completes the login. The `?: undefined` markers keep\n//? property access legal across the whole union (`result.newToken`).\nexport type TwoFactorMethod = 'totp' | 'email-code' | 'recovery-code';\nexport interface CredentialsLoginSuccess {\n status: true;\n reason: string;\n newToken: string;\n session: SessionLayout;\n requiresTwoFactor?: undefined;\n}\nexport interface CredentialsLoginChallenge {\n status: true;\n reason: string;\n requiresTwoFactor: true;\n challengeToken: string;\n twoFactorMethods: TwoFactorMethod[];\n newToken?: undefined;\n session?: undefined;\n}\nexport interface CredentialsLoginFailure {\n status: false;\n reason: string;\n newToken?: undefined;\n session?: undefined;\n requiresTwoFactor?: undefined;\n}\nexport type CredentialsLoginResult = CredentialsLoginSuccess | CredentialsLoginChallenge | CredentialsLoginFailure;\n\n//? Resolved at call time via getUploadsDir() so consumer path overrides win.\nconst uploadsFolder = (): string => getUploadsDir();\n//? Resolve at call time so dotenv/test-setup timing can't capture a stale\n//? value at module load. Routes through `projectConfig.logging.devLogs`\n//? which is the standard predicate everywhere else in the codebase.\nconst isDevMode = (): boolean => getProjectConfig().logging.devLogs;\nconst { compare, genSalt, hash } = bcrypt;\n\n//? LOGIN-F22: the dummy hash used on the user-not-found path must be generated\n//? at the CONFIGURED bcrypt rounds, not hard-pinned to cost 10. If the consumer\n//? raises `auth.bcryptRounds` (e.g. to 12), a cost-10 dummy compare finishes in\n//? ~80 ms while a real compare takes ~400 ms — the 5× difference reopens the\n//? user-enumeration timing channel we closed with the dummy compare. We lazily\n//? generate one hash per unique rounds value (cached so the per-request compare\n//? stays O(1) after the first login at a given cost) and use that hash for all\n//? subsequent not-found compares at the same cost.\nconst dummyHashCache = new Map<number, string>();\nconst getDummyBcryptHash = async (): Promise<string> => {\n const rounds = getProjectConfig().auth.bcryptRounds;\n const cached = dummyHashCache.get(rounds);\n if (cached) return cached;\n const salt = await genSalt(rounds);\n const generated = await hash(randomBytes(16).toString('hex'), salt);\n dummyHashCache.set(rounds, generated);\n return generated;\n};\n//? `validator` is a CommonJS module whose helpers hang off the default export;\n//? destructuring the default is the documented usage. The named-export warning\n//? is a false positive for this CJS interop shape.\n// eslint-disable-next-line import-x/no-named-as-default-member\nconst { escape, isEmail } = validator;\n\n//? Use the shared `getProjectName()` helper so the OAuth state Redis key\n//? prefix matches the namespace used everywhere else (sessions, activeUsers,\n//? rate-limit, password-reset). Previously this used a separate\n//? `auth.oauthStateProjectNameFallback` config field which could silently\n//? drift from `session.projectName`.\nconst getOAuthStateKey = (providerName: string, state: string): string => {\n return formatKey('-oauth-state', `${providerName}:${state}`);\n};\n\n//? Cookie the server sets at the OAuth-authorize step (F1). It carries a random\n//? nonce bound to the Redis state entry; the callback requires the cookie nonce\n//? to hash to the value stored alongside the state before it accepts the\n//? callback. This proves the SAME browser that started the flow is completing it\n//? (closes the login-CSRF / session-fixation hole where an attacker delivers\n//? their own valid `code+state` to a victim). Server + login MUST agree on this\n//? name — it is exported so `@luckystack/server`'s authorize route reuses it.\nexport const OAUTH_STATE_COOKIE_NAME = 'ls-oauth-state';\n\n//? Hash the browser-binding nonce before persisting it in Redis: the Redis\n//? state value is server-side-readable, so storing only the SHA-256 means a\n//? leaked state entry can't be replayed without the matching cookie nonce.\nconst hashStateNonce = (nonce: string): string =>\n createHash('sha256').update(nonce).digest('hex');\n\n//? Generate a PKCE verifier + its S256 challenge (RFC 7636). Only used when a\n//? provider opts in via `usePkce` — default OAuth flows are byte-identical to\n//? before. The verifier is stored server-side (in the Redis state value) and\n//? replayed at token exchange; the challenge is handed to the authorize step.\nconst base64Url = (buf: Buffer): string =>\n buf.toString('base64').replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');\nconst generatePkcePair = (): { verifier: string; challenge: string } => {\n const verifier = base64Url(randomBytes(32));\n const challenge = base64Url(createHash('sha256').update(verifier).digest());\n return { verifier, challenge };\n};\n\n//? Server-side payload persisted under the Redis state key. `nonceHash` binds\n//? the flow to the initiating browser's cookie; `codeVerifier` (PKCE) is the\n//? secret replayed at token exchange.\ninterface OAuthStateEntry {\n nonceHash: string;\n codeVerifier?: string;\n //? Frontend URL to redirect to after the OAuth callback completes. Stored\n //? server-side in Redis (never echoed from the client at callback time) so it\n //? is tamper-proof. Validated against allowedOrigins + allowLocalhost before\n //? use — see isAllowedRedirectUrl. Absent when the auth initiation did not\n //? carry a return_url param (falls back to the configured loginRedirectUrl).\n returnUrl?: string;\n}\n\nexport interface CreateOAuthStateResult {\n /** Opaque `state` param put on the authorize URL + echoed back on callback. */\n state: string;\n /**\n * Random nonce the server must set as the value of the {@link OAUTH_STATE_COOKIE_NAME}\n * cookie (HttpOnly, SameSite=Lax, short TTL) at the authorize redirect. The\n * callback requires this back to accept the flow (browser-binding, F1).\n */\n stateCookie: string;\n /**\n * PKCE `code_challenge` (S256) when the provider opted into PKCE — the server\n * appends `code_challenge=<value>&code_challenge_method=S256` to the authorize\n * URL. `undefined` for non-PKCE providers (default).\n */\n codeChallenge?: string;\n}\n\nexport const createOAuthState = async (\n providerName: string,\n options?: { usePkce?: boolean; returnUrl?: string },\n): Promise<CreateOAuthStateResult | null> => {\n const state = randomBytes(32).toString('hex');\n const nonce = randomBytes(32).toString('hex');\n const key = getOAuthStateKey(providerName, state);\n\n const pkce = options?.usePkce ? generatePkcePair() : null;\n const entry: OAuthStateEntry = {\n nonceHash: hashStateNonce(nonce),\n ...(pkce ? { codeVerifier: pkce.verifier } : {}),\n ...(options?.returnUrl ? { returnUrl: options.returnUrl } : {}),\n };\n\n //? State TTL is consumer-configurable via `auth.oauthStateTtlSeconds`\n //? (default 600s / 10 min). Too short and a legitimate, slow provider\n //? round-trip expires mid-flow and the callback fails state validation\n //? (UX/soft-DoS); too long and a stolen state stays replayable. The `NX`\n //? guard keeps each state single-use even before the TTL elapses, and\n //? `consumeOAuthState` deletes it on first redemption. The value is now a\n //? JSON envelope (nonce hash + optional PKCE verifier) instead of the literal\n //? `'1'` so the flow can be bound to the initiating browser (F1) and carry a\n //? PKCE secret (F11).\n const result = await redisClient.set(\n key,\n JSON.stringify(entry),\n 'EX',\n getProjectConfig().auth.oauthStateTtlSeconds,\n 'NX',\n );\n\n if (result !== 'OK') {\n return null;\n }\n\n return {\n state,\n stateCookie: nonce,\n ...(pkce ? { codeChallenge: pkce.challenge } : {}),\n };\n};\n\n//? Atomically read + delete the state entry (single-use), then verify the\n//? browser-binding nonce. Returns the parsed entry on success (so the caller can\n//? read `codeVerifier` for the PKCE token exchange), or `null` on any failure:\n//? missing state, missing/empty cookie nonce, Redis miss, malformed payload, or\n//? a nonce that doesn't match the stored hash. `cookieNonce` is the value the\n//? server read from the {@link OAUTH_STATE_COOKIE_NAME} cookie on the callback\n//? request.\nconst consumeOAuthState = async (\n providerName: string,\n state: string,\n cookieNonce: string,\n): Promise<OAuthStateEntry | null> => {\n if (!state || !cookieNonce) {\n return null;\n }\n\n const key = getOAuthStateKey(providerName, state);\n const txResult = await redisClient.multi().get(key).del(key).exec();\n if (!txResult || txResult.length < 2) {\n return null;\n }\n\n const getResult = txResult[0];\n if (!getResult || getResult[0]) {\n return null;\n }\n\n //? Verify the DEL also succeeded (txResult[1][0] is the per-command error slot).\n //? If DEL failed the state key was not consumed — fail closed so the OAuth state\n //? cannot be replayed with the same nonce before its TTL expires (mirrors the\n //? consumeOneTimeToken hardening in @luckystack/core).\n const delResult = txResult[1];\n if (!delResult || delResult[0]) {\n return null;\n }\n\n const rawValue = getResult[1];\n if (typeof rawValue !== 'string') {\n return null;\n }\n\n const [parseErr, entry] = tryCatchSync(() => JSON.parse(rawValue) as OAuthStateEntry);\n if (parseErr || !entry || typeof entry.nonceHash !== 'string') {\n return null;\n }\n\n //? Constant-time compare of the two fixed-length (SHA-256) hex digests via\n //? `timingSafeEqual` (QUA-21). Both operands are 64-char hex strings, so their\n //? byte buffers are equal-length; a length mismatch (malformed stored value)\n //? falls through to reject. A mismatch means the browser completing the flow is\n //? NOT the one that started it — reject (F1).\n const incomingHash = Buffer.from(hashStateNonce(cookieNonce));\n const storedHash = Buffer.from(entry.nonceHash);\n if (incomingHash.length !== storedHash.length || !timingSafeEqual(incomingHash, storedHash)) {\n return null;\n }\n\n return entry;\n};\n\nconst asRecord = (value: unknown): Record<string, unknown> => {\n if (value && typeof value === 'object') {\n return value as Record<string, unknown>;\n }\n return {};\n};\n\n//? Read a string-ish field from an untyped provider JSON record. Strings pass\n//? through; finite numbers / booleans are coerced (preserving the previous\n//? `String(value)` behaviour for primitive fields); objects, arrays, null and\n//? undefined collapse to `fallback` instead of stringifying to `[object Object]`.\nconst readStringField = (record: Record<string, unknown>, key: string, fallback = ''): string => {\n const value = record[key];\n if (typeof value === 'string') return value;\n if (typeof value === 'number' && Number.isFinite(value)) return String(value);\n if (typeof value === 'boolean') return String(value);\n return fallback;\n};\n\n//? Random 6-digit hex color used as the avatar placeholder for users without\n//? an uploaded image. Shared by the credentials-register and OAuth-create paths.\nconst generateAvatarFallbackColor = (): string =>\n `#${Math.floor(Math.random() * 0xFF_FF_FF).toString(16).padStart(6, \"0\")}`;\n\n//? LOGIN-F18: check whether a user has an uploaded webp avatar on disk and\n//? return the filename when found. Used by both the credentials and OAuth login\n//? paths to avoid duplicating the async stat pattern.\nconst resolveUploadedAvatar = async (userId: string): Promise<string | null> => {\n const filePath = path.join(uploadsFolder(), `${userId}.webp`);\n const [, avatarStat] = await tryCatch(() => fsPromises.stat(filePath));\n return avatarStat ? `${userId}.webp` : null;\n};\n\n//? Strips every column that must never reach Redis or the client: the bcrypt\n//? hash AND the 2FA material (ADR 0024). Consumers with more sensitive columns\n//? add a `registerSessionSanitizer` on top; this is the always-on floor.\nconst sanitizeUserForSession = <T extends { password?: unknown; totpSecret?: unknown; recoveryCodes?: unknown }>(\n user: T,\n): Omit<T, 'password' | 'totpSecret' | 'recoveryCodes'> => {\n const { password: _password, totpSecret: _totpSecret, recoveryCodes: _recoveryCodes, ...safeUser } = user;\n return safeUser;\n};\n\n//? Fire-and-forget observational failure signal. Dispatched on every failed\n//? login/register/OAuth attempt so consumers can audit, feed a SIEM, or build\n//? per-account lockout (HOK-10). Never awaited and never throws — a failing\n//? handler must not change the auth outcome the user already got.\nconst emitLoginFailed = (payload: {\n email?: string;\n userId?: string;\n provider: string;\n reason: string;\n stage: 'login' | 'register' | 'oauth';\n //? DD-LOGIN-F5: optional resolved client IP — threaded from the HTTP auth\n //? route so the per-account lockout can build an IP+account composite key.\n requesterIp?: string;\n}): void => {\n void dispatchHook('loginFailed', payload);\n};\n\nconst toReasonKey = (error: unknown, fallback = 'api.internalServerError'): string => {\n if (typeof error === 'string' && error.length > 0) {\n return error;\n }\n\n if (error instanceof Error && error.message) {\n const message = error.message.toLowerCase();\n if (message.includes('authentication failed') || message.includes('scram failure')) {\n return 'api.internalServerError';\n }\n }\n\n return fallback;\n};\n\ninterface NormalizedCredentials {\n email: string;\n password: string;\n name: string | undefined;\n confirmPassword: string | undefined;\n}\n\nconst normalizeCredentials = (params: LoginOrRegisterParams): NormalizedCredentials => ({\n //? Don't HTML-escape password OR confirmPassword before bcrypt — neither\n //? reaches HTML, and they're memory-compared for equality during register.\n //? Escaping confirmPassword while leaving password raw made any password\n //? containing `& < > \" '` fail the `password !== confirmPassword` check\n //? (regression introduced in pass-1 fix #9 which only un-escaped password).\n //? Email is normalized (lowercase + trim) and validated via `isEmail`;\n //? HTML-escaping it would make legacy raw rows unfindable.\n email: (params.email ?? '').trim().toLowerCase(),\n password: params.password ?? '',\n name: params.name ? escape(params.name) : undefined,\n //? Empty-string confirmPassword must collapse to `undefined` so the\n //? register-vs-login dispatcher's truthiness check treats a blank field as\n //? \"absent\". `??` would keep the empty string (only nullish collapses), so the\n //? falsy-coercing `||` is deliberate here.\n // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional empty-string → undefined collapse\n confirmPassword: params.confirmPassword || undefined,\n});\n\n//? Shape-validate normalized credentials. `mode` decides whether the\n//? password-POLICY check runs:\n//? - `'register'` enforces the full policy (length, complexity, common-list,\n//? customValidator) — a new account MUST meet the active policy.\n//? - `'login'` does NOT run the policy (M-15). A login must accept ANY password\n//? string and let the bcrypt compare decide pass/fail. Running the policy on\n//? login is both a lockout-DoS vector (an attacker POSTing policy-violating\n//? passwords for a victim's email could otherwise feed the per-account\n//? failed-attempt counter) AND a correctness bug (tightening the policy would\n//? lock out existing users whose stored password no longer meets it). Empty /\n//? email-shape checks still run in both modes.\nconst validateCredentialsShape = (\n creds: NormalizedCredentials,\n mode: 'login' | 'register',\n): { status: false; reason: string } | null => {\n const authLimits = getProjectConfig().auth;\n if (!creds.email || !creds.password) return { status: false, reason: 'login.empty' };\n if (creds.email.length > authLimits.emailMaxLength) return { status: false, reason: 'login.emailCharacterLimit' };\n if (creds.name && creds.name.length > authLimits.nameMaxLength) return { status: false, reason: 'login.nameCharacterLimit' };\n if (!isEmail(creds.email)) return { status: false, reason: 'login.invalidEmailFormat' };\n if (mode === 'register') {\n //? Full password-policy check (length, complexity, common-list, customValidator).\n //? Policy lives in `projectConfig.auth.passwordPolicy`; the deprecated\n //? `passwordMinLength`/`passwordMaxLength` top-level fields are still read\n //? from the policy's `minLength`/`maxLength` defaults. REGISTER-only — see the\n //? function doc for why the login branch skips it (M-15).\n const passwordReason = validatePassword(creds.password);\n if (passwordReason) return { status: false, reason: passwordReason };\n }\n return null;\n};\n\nconst registerWithCredentials = async (\n {\n email,\n password,\n name,\n confirmPassword,\n }: {\n email: string;\n password: string;\n name: string;\n confirmPassword: string;\n },\n options?: { supersedeToken?: string },\n): Promise<CredentialsLoginResult> => {\n if (password !== confirmPassword) {\n emitLoginFailed({ email, provider: 'credentials', reason: 'login.passwordNotMatch', stage: 'register' });\n return { status: false, reason: 'login.passwordNotMatch' };\n }\n\n const preRegisterResult = await dispatchHook('preRegister', { email, provider: 'credentials', name });\n if (preRegisterResult.stopped) {\n emitLoginFailed({ email, provider: 'credentials', reason: preRegisterResult.signal.errorCode, stage: 'register' });\n return { status: false, reason: preRegisterResult.signal.errorCode };\n }\n\n //? LOGIN-F15: apply the per-account lockout check before the email-existence\n //? lookup. Without this an attacker can call the register endpoint repeatedly\n //? for a victim's email and perform unbounded `resolveUserByEmail` probes —\n //? effectively bypassing the lockout by choosing the register code path. A\n //? locked account means the credentials flow is temporarily suspended for that\n //? email; the same sentinel applies regardless of which surface (login vs\n //? register) is being used. No-op when `rateLimiting.auth.enabled` is false.\n if (await isAccountLocked(email)) {\n emitLoginFailed({ email, provider: 'credentials', reason: 'login.accountLocked', stage: 'register' });\n return { status: false, reason: 'login.accountLocked' };\n }\n\n const userAdapter = getUserAdapter();\n const [checkEmailError, checkEmailResponse] = await tryCatch(() =>\n resolveUserByEmail(userAdapter, { email, provider: 'credentials' })\n );\n if (checkEmailError) {\n getLogger().error('login: findByEmail failed during register', checkEmailError);\n const reason = toReasonKey(checkEmailError);\n emitLoginFailed({ email, provider: 'credentials', reason, stage: 'register' });\n return { status: false, reason };\n }\n if (checkEmailResponse) {\n emitLoginFailed({ email, provider: 'credentials', reason: 'login.emailExists', stage: 'register' });\n return { status: false, reason: 'login.emailExists' };\n }\n\n const createNewUser = async () => {\n const salt = await genSalt(getProjectConfig().auth.bcryptRounds);\n const hashedPassword = await hash(password, salt);\n return await userAdapter.create({\n email,\n provider: 'credentials',\n name,\n password: hashedPassword,\n avatar: '',\n avatarFallback: generateAvatarFallbackColor(),\n language: getProjectConfig().defaultLanguage,\n });\n };\n\n const [createNewUserError, createNewUserResponse] = await tryCatch(createNewUser);\n if (createNewUserError) {\n //? LOGIN-F7: treat a unique-constraint violation as a TOCTOU race — another\n //? concurrent request created this row between our findFirst and our create.\n //? Under the `'unified'` strategy (where `email @unique` is recommended) this\n //? is the expected concurrent-registration signal; return `login.emailExists`\n //? so the client can prompt the user to log in instead.\n const errorMessage =\n createNewUserError instanceof Error ? createNewUserError.message : String(createNewUserError);\n const isUniqueViolation =\n errorMessage.includes('Unique constraint') ||\n errorMessage.includes('unique constraint') ||\n // Prisma error code P2002 covers all DB engines\n (createNewUserError as { code?: unknown }).code === 'P2002';\n if (isUniqueViolation) {\n emitLoginFailed({ email, provider: 'credentials', reason: 'login.emailExists', stage: 'register' });\n return { status: false, reason: 'login.emailExists' };\n }\n const reason = toReasonKey(createNewUserError);\n emitLoginFailed({ email, provider: 'credentials', reason, stage: 'register' });\n return { status: false, reason };\n }\n if (!createNewUserResponse) {\n emitLoginFailed({ email, provider: 'credentials', reason: 'login.createUserFailed', stage: 'register' });\n return { status: false, reason: 'login.createUserFailed' };\n }\n\n await dispatchHook('postRegister', { userId: createNewUserResponse.id, provider: 'credentials' });\n\n //? Auto-login after register (ARCHITECTURE_AUTH.md: both flows return\n //? `{ status, session, newToken }`). Mirrors the login branch: mint a token,\n //? persist the session, and hand the token back so the route can set the\n //? cookie / the client can store it and land on `loginRedirectUrl`.\n const newToken = randomBytes(32).toString('hex');\n const nowLogin = new Date();\n const newUser: SessionLayout = {\n ...sanitizeUserForSession(createNewUserResponse),\n token: newToken,\n lastLogin: nowLogin,\n previousLogin: null,\n };\n\n const saved = await saveSession(newToken, newUser, true, { supersedeToken: options?.supersedeToken });\n if (!saved.ok) {\n //? Account exists but the session never persisted — surface the failure so\n //? the route does not report an authenticated state it cannot back up. The\n //? user can recover by logging in normally.\n emitLoginFailed({ email, userId: newUser.id, provider: 'credentials', reason: saved.errorCode, stage: 'register' });\n return { status: false, reason: saved.errorCode };\n }\n await dispatchHook('postLogin', { userId: newUser.id, provider: 'credentials', isNewUser: true, token: newToken });\n\n return {\n status: true,\n reason: 'login.userCreated',\n newToken,\n session: newUser,\n };\n};\n\n//? ─── 2FA gate slot (ADR 0024) ───\n//? DI registry (same idiom as registerUserAdapter / registerSessionAdapter)\n//? instead of importing twoFactor.ts here — that would be a login ↔ twoFactor\n//? module cycle. twoFactor.ts registers the gate at module init (pulled in via\n//? the package index); with nothing registered, first-factor logins complete\n//? directly — which is also why every existing unit test keeps its behavior.\nexport type TwoFactorGate = (\n user: UserRecord,\n context: { requesterIp?: string },\n) => Promise<CredentialsLoginChallenge | null>;\nlet twoFactorGate: TwoFactorGate | null = null;\nexport const registerTwoFactorGate = (gate: TwoFactorGate): void => {\n twoFactorGate = gate;\n};\nconst applyTwoFactorGate: TwoFactorGate = async (user, context) =>\n (twoFactorGate ? twoFactorGate(user, context) : null);\n\n//? Shared post-authentication tail (ADR 0024): every first-factor-verified\n//? path — password login, email-code login, and the completed 2FA challenge —\n//? funnels through here so the session-minting behavior can never drift\n//? between them. Mint token → best-effort lastLogin → session layout →\n//? saveSession → postLogin hook → clear lockout counters (when email known).\nexport const finalizeLogin = async (\n user: UserRecord,\n options: { provider: string; email?: string; supersedeToken?: string; requesterIp?: string },\n): Promise<CredentialsLoginResult> => {\n const newToken = randomBytes(32).toString('hex');\n const previousLogin = user.lastLogin ?? null;\n const nowLogin = new Date();\n\n // Best-effort lastLogin update — silently no-ops if the user adapter\n // (or User schema) doesn't accept the field.\n await tryCatch(() => getUserAdapter().update(user.id, { lastLogin: nowLogin }));\n\n const newUser: SessionLayout = {\n ...sanitizeUserForSession(user),\n token: newToken,\n lastLogin: nowLogin,\n previousLogin,\n };\n\n const uploadedAvatar = await resolveUploadedAvatar(newUser.id);\n if (uploadedAvatar) {\n newUser.avatar = uploadedAvatar;\n }\n\n const saved = await saveSession(newToken, newUser, true, { supersedeToken: options.supersedeToken });\n if (!saved.ok) {\n //? Session never persisted (adapter blip / preSessionCreate veto). Fail the\n //? login so the route does NOT set a cookie or delete the prior session for\n //? a token that getSession() can't resolve.\n emitLoginFailed({ email: options.email, userId: newUser.id, provider: options.provider, reason: saved.errorCode, stage: 'login', requesterIp: options.requesterIp });\n return { status: false, reason: saved.errorCode };\n }\n await dispatchHook('postLogin', { userId: newUser.id, provider: options.provider, isNewUser: false, token: newToken });\n //? Reset the brute-force counter on success so earlier typos don't keep a\n //? legitimate user locked out (F7). No-op when the feature is disabled.\n //? Clear the IP+account composite key (DD-LOGIN-F5) AND the bare account key\n //? so that failures recorded without an IP (e.g. from a different surface) are\n //? also cleared after a confirmed legitimate login.\n if (options.email) {\n void clearAuthFailures(options.email, options.requesterIp);\n if (options.requesterIp) void clearAuthFailures(options.email);\n }\n if (isDevMode()) {\n getLogger().debug(`${options.provider} login success for user ${newUser.id}`);\n }\n return { status: true, reason: 'login.loggedIn', newToken, session: newUser };\n};\n\nconst loginWithCredentialsCore = async (\n { email, password }: { email: string; password: string },\n //? DD-LOGIN-F5: `requesterIp` is optional so all existing callers stay\n //? compatible. The HTTP auth route resolves and passes the IP; socket paths\n //? omit it and fall back to the pure-account lockout bucket.\n options?: { supersedeToken?: string; requesterIp?: string },\n): Promise<CredentialsLoginResult> => {\n const requesterIp = options?.requesterIp;\n const preLoginResult = await dispatchHook('preLogin', { email, provider: 'credentials' });\n if (preLoginResult.stopped) {\n emitLoginFailed({ email, provider: 'credentials', reason: preLoginResult.signal.errorCode, stage: 'login', requesterIp });\n return { status: false, reason: preLoginResult.signal.errorCode };\n }\n\n //? Per-account brute-force lockout (F7 / MIS-017). When the auth rate-limit\n //? slot is enabled and this account has exhausted its failed-attempt budget,\n //? refuse the attempt BEFORE any bcrypt work — distributed credential stuffing\n //? against one account (which the per-IP throttle can't see) is throttled\n //? per-account here. The counter is incremented by the registered `loginFailed`\n //? handler (see `registerAuthLockoutHook`); merely checking the lock does not\n //? increment it. No-op when `rateLimiting.auth.enabled` is false (default).\n if (await isAccountLocked(email, requesterIp)) {\n emitLoginFailed({ email, provider: 'credentials', reason: 'login.accountLocked', stage: 'login', requesterIp });\n return { status: false, reason: 'login.accountLocked' };\n }\n\n const userAdapter = getUserAdapter();\n const [findUserError, findUserResponse] = await tryCatch(() =>\n resolveUserByEmail(userAdapter, { email, provider: 'credentials' })\n );\n if (findUserError) {\n getLogger().error('login: findByEmail failed', findUserError);\n const reason = toReasonKey(findUserError);\n emitLoginFailed({ email, provider: 'credentials', reason, stage: 'login', requesterIp });\n return { status: false, reason };\n }\n //? Anti-enumeration: a missing account and a missing/invalid password hash\n //? must be INDISTINGUISHABLE from a wrong password — same reason key AND\n //? similar timing. Returning a distinct `login.userNotFound` (and short-\n //? circuiting before any bcrypt work) let an attacker probe which addresses\n //? have credentials accounts via both the response key and a timing side\n //? channel. When there is no real hash to compare, run a bcrypt compare\n //? against a fixed dummy hash so the not-found path spends roughly the same\n //? CPU as a real wrong-password compare, then return the shared key.\n const passwordHash = findUserResponse?.password ?? null;\n if (!findUserResponse || !passwordHash) {\n const dummyHash = await getDummyBcryptHash();\n await tryCatch(() => compare(password, dummyHash));\n //? M3: AWAIT the failure dispatch on the counting (wrong-password) path so the\n //? per-account lockout increment (registered on `loginFailed`) lands BEFORE we\n //? respond — unlike the fire-and-forget `emitLoginFailed` used for observational\n //? failures. This closes the sequential check-then-increment TOCTOU (a retry now\n //? sees the updated counter). `dispatchHook` isolates handler errors, so this\n //? still can't change the auth outcome. A residual window remains for requests\n //? already in flight concurrently (bounded by the atomic per-IP cap) — inherent\n //? to any check-then-act gate.\n await dispatchHook('loginFailed', { email, userId: findUserResponse?.id, provider: 'credentials', reason: 'login.wrongPassword', stage: 'login', requesterIp });\n return { status: false, reason: 'login.wrongPassword' };\n }\n\n const [checkPasswordError, checkPasswordResponse] = await tryCatch(() =>\n compare(password, passwordHash)\n );\n if (checkPasswordError) {\n getLogger().error('login: bcrypt compare failed', checkPasswordError);\n const reason = toReasonKey(checkPasswordError);\n emitLoginFailed({ email, userId: findUserResponse.id, provider: 'credentials', reason, stage: 'login', requesterIp });\n return { status: false, reason };\n }\n if (!checkPasswordResponse) {\n //? M3: awaited on the counting path (see the no-account branch above) so the\n //? lockout increment lands before the response — closes the sequential TOCTOU.\n await dispatchHook('loginFailed', { email, userId: findUserResponse.id, provider: 'credentials', reason: 'login.wrongPassword', stage: 'login', requesterIp });\n return { status: false, reason: 'login.wrongPassword' };\n }\n\n //? Second factor (ADR 0024): the password is verified, but an enrolled user\n //? must answer a 2FA challenge before any session is minted. The registered\n //? gate returns null when 2FA is disabled or the user never enrolled — then\n //? the login completes directly through the shared tail.\n const challenge = await applyTwoFactorGate(findUserResponse, { requesterIp });\n if (challenge) return challenge;\n\n return finalizeLogin(findUserResponse, {\n provider: 'credentials',\n email,\n supersedeToken: options?.supersedeToken,\n requesterIp,\n });\n};\n\n//? Thin dispatcher: keeps the existing single-entry HTTP surface but forwards\n//? to the dedicated register / login functions. `name && confirmPassword`\n//? present in the body means the client is registering; otherwise it's a\n//? login.\nconst loginWithCredentials = async (\n params: LoginOrRegisterParams,\n //? DD-LOGIN-F5: `requesterIp` forwarded to `loginWithCredentialsCore` to\n //? enable IP+account composite lockout keys on the HTTP auth path.\n options?: { supersedeToken?: string; requesterIp?: string },\n): Promise<CredentialsLoginResult> => {\n const creds = normalizeCredentials(params);\n\n if (isDevMode()) {\n getLogger().debug(`credentials auth attempt for ${creds.email || 'unknown-email'}`);\n }\n\n //? Decide the branch BEFORE shape-validation so the password-POLICY check only\n //? runs on register (M-15): `name && confirmPassword` present => register;\n //? otherwise login. A login must accept any password string (the bcrypt\n //? compare is the only authority), so an attacker can't trip a victim's\n //? lockout counter by POSTing policy-violating passwords for their email.\n //? Narrowing on the destructured locals keeps `name`/`confirmPassword` typed as\n //? `string` inside the register branch (no cast needed).\n const { name, confirmPassword } = creds;\n if (name && confirmPassword) {\n const validationError = validateCredentialsShape(creds, 'register');\n if (validationError) return validationError;\n //? Public-registration gate (F18). When `auth.allowRegistration === false`\n //? (invite-only / admin-provisioned apps) refuse the register branch with a\n //? dedicated reason key BEFORE any user lookup or hook. OAuth-driven\n //? first-login account creation is governed separately by the provider flow.\n //? Default `true` keeps today's open-registration behavior.\n if (!getProjectConfig().auth.allowRegistration) {\n emitLoginFailed({ email: creds.email, provider: 'credentials', reason: 'auth.registrationDisabled', stage: 'register' });\n return { status: false, reason: 'auth.registrationDisabled' };\n }\n return registerWithCredentials({\n email: creds.email,\n password: creds.password,\n name,\n confirmPassword,\n }, options);\n }\n\n const validationError = validateCredentialsShape(creds, 'login');\n if (validationError) return validationError;\n return loginWithCredentialsCore({ email: creds.email, password: creds.password }, options);\n};\n\nexport interface OAuthCallbackResult {\n token: string;\n redirectUrl: string;\n userId: string;\n provider: string;\n isNewUser: boolean;\n}\n\nconst isAllowedRedirectUrl = (url: string): boolean => {\n if (!URL.canParse(url, 'http://placeholder')) return false;\n //? Reject relative URLs whose path starts with a backslash or contains any\n //? backslash before the same-origin shortcut (SEC-12): `new URL('/\\\\evil.com',\n //? base)` keeps the placeholder origin and would pass the same-origin check,\n //? yet browsers normalise `/\\` → `//` (protocol-relative) → open-redirect to\n //? `evil.com`. A legitimate same-origin path never needs a backslash.\n if (url.includes('\\\\')) return false;\n const parsed = new URL(url, 'http://placeholder');\n if (parsed.origin === 'http://placeholder') {\n // relative URL — same-origin, always safe\n return true;\n }\n //? `allowedOrigins` is `string[] | (origin: string) => boolean` since we\n //? added the function-resolver variant. Branch on the shape.\n //? Defensive default at a security boundary (redirect-origin validation):\n //? the type is non-nullable, but a malformed consumer config could still\n //? hand us `undefined` at runtime, so the `?? []` fail-closed guard stays.\n //? `allowLocalhost` is the dev convenience that accepts any http://localhost:*\n //? origin without needing an explicit allowedOrigins entry. Safe in production\n //? because allowLocalhost defaults to false there.\n const cfg = getProjectConfig();\n if (cfg.http.cors.allowLocalhost && parsed.hostname === 'localhost') return true;\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime-defensive guard at a security boundary\n const allowed = cfg.http.cors.allowedOrigins ?? [];\n if (typeof allowed === 'function') {\n return allowed(parsed.origin);\n }\n return allowed.some((origin) => {\n if (!URL.canParse(origin)) return false;\n return new URL(origin).origin === parsed.origin;\n });\n};\n\nconst exchangeOAuthToken = async (\n provider: FullOAuthProvider,\n code: string,\n codeVerifier?: string,\n): Promise<string | null> => {\n //? `redirect_uri` is pinned to the registered, immutable `provider.callbackURL`\n //? — the SAME static config value the authorization request is built from.\n //? It is never derived from the incoming request, so there is no per-request\n //? drift to re-validate against (the authorization-time value and the\n //? exchange-time value are guaranteed identical by construction). Persisting\n //? it into the Redis state would be a behaviour-preserving no-op here; if the\n //? callback URL ever becomes request-/tenant-derived, store it alongside the\n //? state in `createOAuthState` and compare before exchange.\n //? `code_verifier` (PKCE, F11) is included ONLY when the provider opted in and\n //? a verifier was stored with the state — for every existing flow it is\n //? `undefined` and the exchange body is unchanged.\n const values = {\n code,\n client_id: provider.clientID,\n client_secret: provider.clientSecret,\n redirect_uri: provider.callbackURL,\n grant_type: 'authorization_code',\n ...(codeVerifier ? { code_verifier: codeVerifier } : {}),\n };\n\n //? `Promise<unknown>` return annotation: `response.json()` is `any`, and an\n //? explicit `unknown` boundary type forces every caller to narrow via\n //? `asRecord(...)` instead of leaking `any` through the token-exchange seam.\n const getToken = async (): Promise<unknown> => {\n if (provider.tokenExchangeMethod === 'json') {\n const response = await fetch(provider.tokenExchangeURL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },\n body: JSON.stringify(values),\n });\n if (!response.ok) {\n getLogger().warn('oauth: token exchange returned non-OK status', { provider: provider.name, status: response.status });\n return null;\n }\n return await response.json();\n }\n //? `tokenExchangeMethod` is the closed union `'json' | 'form'`; the `json`\n //? branch returned above, so this is the exhaustive `form` case.\n const formParams = new URLSearchParams();\n formParams.append('client_id', provider.clientID);\n formParams.append('client_secret', provider.clientSecret);\n formParams.append('code', values.code);\n formParams.append('grant_type', 'authorization_code');\n formParams.append('redirect_uri', provider.callbackURL);\n if (codeVerifier) {\n formParams.append('code_verifier', codeVerifier);\n }\n\n if (isDevMode()) {\n //? NEVER stringify the raw form — it carries `client_secret` (a\n //? long-lived credential) and the single-use authorization `code`. Log\n //? only the param NAMES so the dev sees the shape without leaking secrets\n //? into the log sink.\n getLogger().debug('oauth: token-exchange form params', { paramNames: [...formParams.keys()] });\n }\n const response = await fetch(provider.tokenExchangeURL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' },\n body: formParams.toString(),\n });\n if (!response.ok) {\n getLogger().warn('oauth: token exchange returned non-OK status', { provider: provider.name, status: response.status });\n return null;\n }\n return await response.json();\n };\n\n const [error, response] = await tryCatch(getToken);\n if (error) {\n getLogger().error('oauth: token exchange failed', error);\n return null;\n }\n const tokenData = asRecord(response);\n const accessToken = typeof tokenData.access_token === 'string' ? tokenData.access_token : '';\n if (!accessToken) {\n getLogger().warn('oauth: no access_token in token response', { provider: provider.name });\n return null;\n }\n return accessToken;\n};\n\ninterface OAuthProfile {\n email: string | undefined;\n name: string;\n avatar: string;\n /**\n * `true` only when the provider POSITIVELY reported the email as verified\n * (via `emailVerifiedKey`) or supplied it through a `getEmail` accessor that\n * itself filters on a verified flag (GitHub). `false` when the provider\n * exposes NO verified-email signal at all — used to fail-closed on\n * cross-provider account LINKING under the `'unified'` strategy (SEC-21/SEC-40),\n * where linking an unverified address to a victim's existing account is an\n * account-takeover vector. First-time account CREATION is unaffected.\n */\n emailVerified: boolean;\n /**\n * The RAW, un-stripped provider userInfo record. Threaded through so the\n * `extraSessionFields` hook receives every provider-specific claim (`sub`,\n * tenant id, custom claims) instead of the trimmed `{email,name,avatar}`\n * projection — the documented contract (SEC-3 / oauthProviders.ts hook docs).\n */\n rawUserData: OAuthUserData;\n}\n\nconst fetchOAuthProfile = async (\n provider: FullOAuthProvider,\n accessToken: string,\n): Promise<OAuthProfile | null> => {\n const getUserData = async (): Promise<unknown> => {\n const response = await fetch(provider.userInfoURL, {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'Authorization': `Bearer ${accessToken}`,\n },\n });\n if (!response.ok) {\n getLogger().warn('oauth: userInfo fetch returned non-OK status', { provider: provider.name, status: response.status });\n return null;\n }\n return await response.json();\n };\n\n const [error, response] = await tryCatch(getUserData);\n if (error) {\n getLogger().error('oauth: userInfo fetch failed', error);\n return null;\n }\n\n const userData = asRecord(response);\n //? Provider responses are untyped JSON; read each field through `readString`\n //? so a non-string (object/number/missing) value falls back instead of\n //? stringifying to `[object Object]`.\n //? A present-but-empty name string falls back too (mirrors the old\n //? `String(value || 'didnt find a name')` truthy-coercion).\n const resolvedName = readStringField(userData, provider.nameKey);\n const name = resolvedName === '' ? 'didnt find a name' : resolvedName;\n const emailValue = userData[provider.emailKey];\n let email: string | undefined = typeof emailValue === 'string' ? emailValue : undefined;\n\n //? Track whether the provider POSITIVELY confirmed this email as verified.\n //? `emailVerifiedKey === true` (or any non-false value when the key is set and\n //? present) counts as verified; a provider with NO `emailVerifiedKey` exposes\n //? no signal, so the email stays `emailVerified: false` and only first-time\n //? account creation (not cross-provider linking) is allowed downstream.\n let emailVerified = false;\n\n //? Reject a provider-supplied email that the provider explicitly marks\n //? unverified (e.g. Discord's `verified: false`). A missing flag is treated\n //? as \"no signal\" (not verified, but not rejected outright — see the\n //? cross-provider link guard in `findOrCreateOAuthUser`). Defense-in-depth\n //? against account-linking takeover under `'unified'` (SEC-21).\n if (email && provider.emailVerifiedKey) {\n const verifiedFlag = userData[provider.emailVerifiedKey];\n //? Only a STRICT boolean `true` counts as verified. A present-but-not-true\n //? value (`false`, the strings \"false\"/\"0\", `0`, `null`) is an explicit\n //? not-verified signal → reject. This hardens against a CUSTOM provider that\n //? encodes the flag as a string/number (a truthy `\"false\"` previously slipped\n //? through as verified, re-opening cross-provider account-linking takeover).\n //? A MISSING flag (undefined) is \"no signal\" — not verified, not rejected here;\n //? the cross-provider link guard in findOrCreateOAuthUser handles it (SEC-21).\n if (verifiedFlag !== undefined && verifiedFlag !== true) {\n getLogger().warn('oauth: provider email is not verified — rejecting', { provider: provider.name });\n return null;\n }\n if (verifiedFlag === true) emailVerified = true;\n }\n\n //? Providers whose userinfo only returns an email once it is CONFIRMED (e.g.\n //? Facebook Graph /me) expose no separate verified flag — the presence of the\n //? email IS the verified signal. `emailImpliesVerified` opts such a provider in\n //? so cross-provider LINK + first-login CREATION both treat it as verified.\n if (email && provider.emailImpliesVerified) {\n emailVerified = true;\n }\n\n const avatarId = provider.avatarCodeKey ? userData[provider.avatarCodeKey] : undefined;\n const avatarValue = provider.avatarKey\n ? readStringField(userData, provider.avatarKey)\n : (provider.getAvatar\n ? await provider.getAvatar({ userData, avatarId: typeof avatarId === 'string' ? avatarId : '', accessToken })\n : '');\n const avatar = typeof avatarValue === 'string' ? avatarValue : '';\n\n //? Some providers (GitHub) don't return email in /userinfo — fall back to the\n //? provider's `getEmail` accessor. GitHub's accessor only ever selects a\n //? `verified === true` address (see `githubProvider.getEmail`), so an email\n //? obtained this way is treated as positively verified for the cross-provider\n //? link guard (SEC-21).\n if (!email && provider.getEmail) {\n const selectedEmail = await provider.getEmail(accessToken);\n if (!selectedEmail) {\n getLogger().warn('oauth: no email found via provider.getEmail', { provider: provider.name });\n return null;\n }\n email = selectedEmail;\n emailVerified = true;\n }\n\n //? Normalize OAuth emails through the SAME gate as credentials (trim +\n //? lowercase + `isEmail`) before they reach the adapter. Credentials emails\n //? are normalized at `normalizeCredentials`; taking the provider value\n //? verbatim meant `Victim@x.com` (OAuth) and `victim@x.com` (credentials)\n //? resolved to different rows — which under the `'unified'` account strategy\n //? (case-sensitive lookup) silently DEFEATS cross-provider linking. A\n //? present-but-malformed provider email is treated as \"no usable email\".\n if (email !== undefined) {\n const normalizedEmail = email.trim().toLowerCase();\n if (!isEmail(normalizedEmail)) {\n getLogger().warn('oauth: provider returned a malformed email', { provider: provider.name });\n return null;\n }\n email = normalizedEmail;\n }\n\n return { email, name, avatar, emailVerified, rawUserData: userData };\n};\n\ninterface ResolvedOAuthUser {\n user: SessionLayout;\n isNewUser: boolean;\n}\n\nconst findOrCreateOAuthUser = async (\n provider: FullOAuthProvider,\n profile: OAuthProfile,\n): Promise<ResolvedOAuthUser | null> => {\n const { email, name, avatar, emailVerified } = profile;\n if (!email) return null;\n\n const preLoginResult = await dispatchHook('preLogin', { email, provider: provider.name });\n if (preLoginResult.stopped) {\n getLogger().warn(`oauth login aborted by preLogin hook`, { errorCode: preLoginResult.signal.errorCode });\n return null;\n }\n\n const userAdapter = getUserAdapter();\n //? Under `'unified'` this links to an existing account with the same email\n //? created via ANY provider (incl. credentials); under `'per-provider'` it\n //? stays scoped to this provider (existing behavior).\n const [findError, findResponse] = await tryCatch(() =>\n resolveUserByEmail(userAdapter, { email, provider: provider.name })\n );\n if (findError) {\n getLogger().error('oauth: findByEmail failed', findError);\n return null;\n }\n\n if (findResponse?.id) {\n //? Fail-closed cross-provider account-LINK guard (SEC-21/SEC-40). Under the\n //? `'unified'` strategy `resolveUserByEmail` matches by email across every\n //? provider, so an OAuth sign-in can LINK into an account originally created\n //? by a DIFFERENT provider (incl. credentials). If the current provider did\n //? NOT positively verify the email (no `emailVerifiedKey` / `getEmail`\n //? verified-filter), an attacker who controls an unverified address equal to\n //? a victim's could take over that account. Refuse the link in that case —\n //? same-provider re-login and first-time account CREATION are unaffected, so\n //? existing flows for built-ins that DO verify (Google/GitHub/Discord) keep\n //? working. `'per-provider'` always matches within the same provider, so the\n //? cross-provider condition is never met there (no behaviour change).\n const existingProvider = findResponse.provider ?? null;\n //? Fail CLOSED on a missing provider field: treat unknown (null/undefined) as a\n //? cross-provider link so an unverified provider email can't link into an\n //? existing account whose record omits `provider` (a custom UserAdapter may not\n //? populate it). Same-provider re-login still matches (=== provider.name → not\n //? cross), and first-time creation is unaffected (this branch needs an existing id).\n const isCrossProviderLink = existingProvider !== provider.name;\n if (isCrossProviderLink && !emailVerified) {\n getLogger().warn(\n 'oauth: refusing to link an unverified provider email to an existing account created by a different provider (account-takeover guard)',\n { provider: provider.name, existingProvider },\n );\n return null;\n }\n\n //? LOGIN-F18 (OAuth path): same async stat lookup via the shared helper.\n const uploadedAvatar = await resolveUploadedAvatar(findResponse.id);\n if (uploadedAvatar) {\n findResponse.avatar = uploadedAvatar;\n }\n\n const previousLogin = findResponse.lastLogin ?? null;\n const nowLogin = new Date();\n await tryCatch(() => userAdapter.update(findResponse.id, { lastLogin: nowLogin }));\n\n return {\n user: {\n ...sanitizeUserForSession(findResponse),\n token: '',\n lastLogin: nowLogin,\n previousLogin,\n },\n isNewUser: false,\n };\n }\n\n //? Fail-closed account-CREATION guard (M2), symmetric with the cross-provider\n //? LINK guard above. When NO account exists, a provider that did not positively\n //? verify the email (no `emailVerifiedKey === true` / `getEmail` /\n //? `emailImpliesVerified`) must not be able to seed a brand-new account bound to\n //? an address the signer-in may not own — a custom, non-verifying provider would\n //? otherwise let an attacker SQUAT a victim's email (blocking the victim's later\n //? signup with `login.emailExists` and, under `'unified'`, absorbing the victim's\n //? later verified sign-in into the attacker-seeded row). All built-in providers\n //? set `emailVerified` (Google/GitHub/Microsoft/Discord/Facebook), so this only\n //? refuses genuinely-unverified CUSTOM providers; such a provider must verify the\n //? email (set `emailVerifiedKey` / `getEmail` / `emailImpliesVerified`) to enable\n //? OAuth signup, rather than the framework trusting it by default.\n if (!emailVerified) {\n getLogger().warn(\n 'oauth: refusing first-login account creation for an unverified provider email (account-squatting guard)',\n { provider: provider.name },\n );\n return null;\n }\n\n //? Gate OAuth first-login account CREATION on the same `auth.allowRegistration`\n //? flag the credentials register branch enforces. Without this, invite-only /\n //? admin-provisioned deployments (allowRegistration:false) are silently\n //? bypassable: anyone with an enabled provider's account completes the OAuth\n //? flow and gets a fresh account. Existing users hit the find-branch above and\n //? are unaffected; only NEW-account creation is refused. A consumer preRegister\n //? hook can still veto, but the documented gate must hold by construction.\n if (!getProjectConfig().auth.allowRegistration) {\n getLogger().warn('oauth: registration disabled — refusing first-login account creation');\n return null;\n }\n\n const preRegisterResult = await dispatchHook('preRegister', { email, provider: provider.name, name });\n if (preRegisterResult.stopped) {\n getLogger().warn(`oauth register aborted by preRegister hook`, { errorCode: preRegisterResult.signal.errorCode });\n return null;\n }\n\n const createNewUser = async () => userAdapter.create({\n email,\n provider: provider.name,\n name,\n avatar,\n avatarFallback: generateAvatarFallbackColor(),\n language: getProjectConfig().defaultLanguage,\n });\n const [createError, createResponse] = await tryCatch(createNewUser);\n if (createError) {\n getLogger().error('oauth: user create failed', createError);\n return null;\n }\n if (!createResponse) return null;\n\n return {\n user: { ...sanitizeUserForSession(createResponse), token: '' },\n isNewUser: true,\n };\n};\n\nconst resolvePostLoginRedirect = async ({\n fallbackUrl,\n userId,\n providerName,\n isNewUser,\n}: {\n fallbackUrl: string;\n userId: string;\n providerName: string;\n isNewUser: boolean;\n}): Promise<string> => {\n const resolver = getPostLoginRedirect();\n if (!resolver) return fallbackUrl;\n\n const [resolverError, resolved] = await tryCatch(() => resolver({\n userId,\n provider: providerName,\n isNewUser,\n defaultUrl: fallbackUrl,\n }));\n if (resolverError) {\n getLogger().warn(`[oauth] postLoginRedirect resolver threw`, { message: resolverError.message });\n return fallbackUrl;\n }\n if (resolved && isAllowedRedirectUrl(resolved)) return resolved;\n if (resolved) {\n getLogger().warn(\n `[oauth] postLoginRedirect returned a URL not in allowed origins — falling back`,\n { resolved, fallbackUrl },\n );\n }\n return fallbackUrl;\n};\n\n//? Whitelist the URL path segment against registered providers and verify the\n//? request carries a URL. Returns the matched full provider + its canonical name,\n//? or null when the segment is absent, unregistered, or an incomplete provider\n//? shape. Separating this keeps the callback routing concern out of the main flow.\nconst resolveProviderFromCallback = (\n pathname: string,\n url: string | undefined,\n): { provider: FullOAuthProvider; providerName: string } | null => {\n const rawProviderSegment = pathname.split('/')[3]; // google/github/etc.\n if (!rawProviderSegment || !url) return null;\n const found = getOAuthProviders().find(p => p.name === rawProviderSegment);\n if (!found || !isFullOAuthProvider(found)) return null;\n return { provider: found, providerName: found.name };\n};\n\n//? Merge per-provider runtime extras (calendar tokens, tenant ids, etc.) into\n//? the session object IN PLACE before it is persisted. Errors are non-fatal —\n//? a failing or absent hook must not block the user from signing in (SEC-10).\nconst applyExtraSessionFields = async (\n user: SessionLayout,\n provider: FullOAuthProvider,\n profile: OAuthProfile,\n accessToken: string,\n): Promise<void> => {\n const { extraSessionFields } = provider;\n if (!extraSessionFields) return;\n\n //? Pass the RAW provider userData (SEC-3) so the hook sees every provider\n //? claim (`sub`, tenant id, custom claims) rather than the trimmed projection.\n const [extraErr, extra] = await tryCatch(async () =>\n extraSessionFields({ userData: profile.rawUserData, accessToken }),\n );\n if (extraErr) {\n getLogger().warn(`[oauth:${provider.name}] extraSessionFields hook threw — continuing without extras`, { err: extraErr });\n return;\n }\n if (!extra) return;\n\n //? Strip framework-owned session keys before merging (SEC-10).\n const { id, token, csrfToken, password, ...safeExtra } = extra;\n if (id !== undefined || token !== undefined || csrfToken !== undefined || password !== undefined) {\n getLogger().warn(\n `[oauth:${provider.name}] extraSessionFields returned framework-owned key(s) (id/token/csrfToken/password) — dropped`,\n );\n }\n //? SEC: warn when extra fields appear to contain bearer credentials. Access\n //? tokens stored in the Redis session are broadcast to connected sockets via\n //? updateSession — they should not be in session fields unless intentional.\n //? The stripped set above already removes \"token\"; this covers other key names.\n const CREDENTIAL_PATTERN = /token|secret|key|auth/i;\n const suspiciousKeys = Object.keys(safeExtra).filter(k => CREDENTIAL_PATTERN.test(k));\n if (suspiciousKeys.length > 0) {\n getLogger().warn(\n `[oauth:${provider.name}] extraSessionFields contains field(s) that look like credentials (${suspiciousKeys.join(', ')}) — these will be stored in the Redis session and broadcast to connected sockets. Consider keeping them server-side only.`,\n );\n }\n Object.assign(user, safeExtra);\n};\n\n//? Assemble the fallback redirect URL for the OAuth callback by preferring the\n//? tamper-proof return URL stored server-side with the OAuth state, then the\n//? caller-supplied default, then the configured `loginRedirectUrl`, then '/'.\nconst resolveOAuthFallbackUrl = (\n stateEntry: OAuthStateEntry,\n options: { defaultRedirectUrl?: string },\n): string => {\n const stateReturnUrl =\n stateEntry.returnUrl && isAllowedRedirectUrl(stateEntry.returnUrl)\n ? stateEntry.returnUrl\n : undefined;\n\n return (\n stateReturnUrl\n ?? options.defaultRedirectUrl\n ?? getProjectConfig().loginRedirectUrl\n ?? '/'\n );\n};\n\n// Route that handles the callback from the OAuth provider\nconst loginCallback = async (\n pathname: string,\n req: IncomingMessage,\n _res: ServerResponse,\n options: { defaultRedirectUrl?: string; supersedeToken?: string } = {},\n): Promise<OAuthCallbackResult | false> => {\n const providerResolution = resolveProviderFromCallback(pathname, req.url);\n if (!providerResolution) return false;\n const { provider, providerName } = providerResolution;\n\n // req.url is guaranteed non-null here: resolveProviderFromCallback returns null when url is absent\n const queryString = (req.url ?? '').split('?')[1] ?? '';\n const params = new URLSearchParams(queryString);\n const code = params.get('code');\n const state = params.get('state');\n\n //? Browser-binding (F1): the nonce the server set as the `ls-oauth-state`\n //? cookie at the authorize step must round-trip back and match the value\n //? stored alongside the Redis state entry. A missing/mismatched cookie means\n //? this browser did NOT start the flow — reject before any token exchange.\n const cookieNonce = getCookieValue(req.headers.cookie, OAUTH_STATE_COOKIE_NAME) ?? '';\n const stateEntry = await consumeOAuthState(provider.name, state ?? '', cookieNonce);\n if (!stateEntry) {\n getLogger().warn('oauth: invalid, missing, or unbound state', { provider: provider.name });\n emitLoginFailed({ provider: providerName, reason: 'oauth.invalidState', stage: 'oauth' });\n return false;\n }\n\n if (!code) {\n getLogger().warn('oauth: no code provided in callback url', { provider: provider.name });\n emitLoginFailed({ provider: providerName, reason: 'oauth.noCode', stage: 'oauth' });\n return false;\n }\n\n //? PKCE (F11): replay the verifier stored with the state entry at token\n //? exchange when the provider opted in. `undefined` for non-PKCE providers,\n //? in which case the exchange is byte-identical to before.\n const accessToken = await exchangeOAuthToken(provider, code, stateEntry.codeVerifier);\n if (!accessToken) {\n emitLoginFailed({ provider: providerName, reason: 'oauth.tokenExchangeFailed', stage: 'oauth' });\n return false;\n }\n\n const profile = await fetchOAuthProfile(provider, accessToken);\n if (!profile) {\n emitLoginFailed({ provider: providerName, reason: 'oauth.profileFetchFailed', stage: 'oauth' });\n return false;\n }\n\n const resolved = await findOrCreateOAuthUser(provider, profile);\n if (!resolved) {\n emitLoginFailed({ provider: providerName, email: profile.email, reason: 'oauth.userResolveFailed', stage: 'oauth' });\n return false;\n }\n\n const newToken = randomBytes(32).toString('hex');\n resolved.user.token = newToken;\n\n //? Per-provider runtime extras (calendar tokens, tenant ids, etc.) are merged\n //? into the session BEFORE save. Errors are non-fatal — see applyExtraSessionFields.\n await applyExtraSessionFields(resolved.user, provider, profile, accessToken);\n\n const saved = await saveSession(newToken, resolved.user, true, { supersedeToken: options.supersedeToken });\n if (!saved.ok) {\n //? Session never persisted — abort the OAuth callback so authCallbackRoute\n //? returns \"Login failed\" instead of redirecting with a dead token.\n getLogger().error(`[oauth:${providerName}] saveSession failed — aborting callback`, { errorCode: saved.errorCode });\n emitLoginFailed({ provider: providerName, userId: resolved.user.id, reason: saved.errorCode, stage: 'oauth' });\n return false;\n }\n\n if (resolved.isNewUser) {\n await dispatchHook('postRegister', { userId: resolved.user.id, provider: providerName });\n }\n await dispatchHook('postLogin', {\n userId: resolved.user.id,\n provider: providerName,\n isNewUser: resolved.isNewUser,\n token: newToken,\n });\n\n if (isDevMode()) {\n getLogger().debug(`oauth login success for user ${resolved.user.id}`);\n }\n\n const fallbackUrl = resolveOAuthFallbackUrl(stateEntry, options);\n const redirectUrl = await resolvePostLoginRedirect({\n fallbackUrl,\n userId: resolved.user.id,\n providerName,\n isNewUser: resolved.isNewUser,\n });\n\n return {\n token: newToken,\n redirectUrl,\n userId: resolved.user.id,\n provider: providerName,\n isNewUser: resolved.isNewUser,\n };\n};\n\nexport { loginWithCredentials, loginCallback, registerWithCredentials, loginWithCredentialsCore }\n","//? @adr 0019 — Email uniqueness is opt-in and governed by this knob: the default\n//? `'per-provider'` strategy INTENTIONALLY does not require `email @unique`\n//? (same address across providers = separate rows); only `'unified'` needs the\n//? consumer to add the constraint. A missing `@unique` on `User.email` is\n//? by-design, not a bug — see ADR 0019 before flagging it.\n//?\n//? Implements `auth.providerAccountStrategy` (CFG-04). The knob was declared\n//? and documented but no code read it — every lookup was provider-scoped, so\n//? `'unified'` silently behaved like `'per-provider'` and produced duplicate\n//? accounts for the same email across providers.\n//?\n//? - `'per-provider'` (default): resolve a user by (email, provider). The\n//? same address via Google and GitHub is two separate User rows. No schema\n//? change required.\n//? - `'unified'`: resolve a user by email alone (across providers) so the\n//? address maps to a single User; a sign-in via a new provider LINKS to the\n//? existing account instead of creating a duplicate. Requires the User\n//? schema's `email` to be unique (see @luckystack/login README — \"Unified\n//? account strategy\").\n//?\n//? Custom `UserAdapter`s that predate this feature may not implement\n//? `findByEmailAnyProvider`. When `'unified'` is configured but the adapter\n//? lacks it, we log ONCE and fall back to provider-scoped lookup so the project\n//? degrades loudly rather than silently mis-resolving.\n\nimport { getProjectConfig, getLogger } from '@luckystack/core';\n\nimport type { UserAdapter, UserRecord } from './userAdapter';\n\nlet warnedMissingAnyProvider = false;\n\n/**\n * Resolve a user account honoring the configured `providerAccountStrategy`.\n * Use this everywhere the login flow needs \"does an account for this email\n * exist?\" — register dedupe, credentials login, and OAuth find-or-create.\n */\nexport const resolveUserByEmail = async (\n adapter: UserAdapter,\n { email, provider }: { email: string; provider: string },\n): Promise<UserRecord | null> => {\n const strategy = getProjectConfig().auth.providerAccountStrategy;\n\n if (strategy === 'unified') {\n if (adapter.findByEmailAnyProvider) {\n return adapter.findByEmailAnyProvider({ email });\n }\n if (!warnedMissingAnyProvider) {\n warnedMissingAnyProvider = true;\n getLogger().warn(\n '[login] auth.providerAccountStrategy is \"unified\" but the registered UserAdapter does not implement findByEmailAnyProvider — falling back to provider-scoped lookup (duplicate accounts per provider are still possible). Implement findByEmailAnyProvider on your adapter to enable unified accounts.',\n );\n }\n }\n\n return adapter.findByEmail({ email, provider });\n};\n\n/** Test-only: reset the one-time missing-method warning latch. */\nexport const resetAccountStrategyWarningForTests = (): void => {\n warnedMissingAnyProvider = false;\n};\n","//? Password-policy enforcement for credentials auth. Read by\n//? `registerWithCredentials` and `updatePasswordHash` to validate plaintext\n//? passwords against the consumer-tunable `projectConfig.auth.passwordPolicy`.\n//?\n//? Returns null when the password passes every check, or a reason-key\n//? (i18n-style errorCode) describing the first failure. Callers surface\n//? the reason to the client; the framework's i18n layer handles\n//? translation.\n\nimport { getProjectConfig } from '@luckystack/core';\n\nimport { COMMON_PASSWORDS } from './data/commonPasswords';\n\nconst UPPERCASE_REGEX = /[A-Z]/;\nconst LOWERCASE_REGEX = /[a-z]/;\nconst NUMBER_REGEX = /\\d/;\nconst SPECIAL_REGEX = /[^A-Za-z0-9]/;\n\n//? bcrypt silently truncates at 72 utf8-encoded bytes. Anything over 72 bytes\n//? hashes identically to its 72-byte prefix, so a longer password gives no\n//? extra security and actively misleads the user. Cap at 72 bytes so the\n//? stored hash always reflects the full secret the user typed.\nconst BCRYPT_MAX_BYTES = 72;\n\n/**\n * Validate `password` against the active project's password policy.\n * Returns null on success, a string reason-key on the first failure.\n *\n * The policy lives in `projectConfig.auth.passwordPolicy` and is read at\n * call time, so consumers can override defaults via `registerProjectConfig`.\n */\nexport const validatePassword = (password: string): string | null => {\n const policy = getProjectConfig().auth.passwordPolicy;\n\n if (password.length < policy.minLength) return 'login.passwordCharacterMinimum';\n //? Byte-length cap takes precedence over the policy maxLength when the policy\n //? exceeds BCRYPT_MAX_BYTES — reject before bcrypt silently truncates.\n const byteLength = Buffer.byteLength(password, 'utf8');\n if (byteLength > BCRYPT_MAX_BYTES) return 'login.passwordCharacterLimit';\n if (password.length > policy.maxLength) return 'login.passwordCharacterLimit';\n if (policy.requireUppercase && !UPPERCASE_REGEX.test(password)) return 'login.passwordRequiresUppercase';\n if (policy.requireLowercase && !LOWERCASE_REGEX.test(password)) return 'login.passwordRequiresLowercase';\n if (policy.requireNumber && !NUMBER_REGEX.test(password)) return 'login.passwordRequiresNumber';\n if (policy.requireSpecial && !SPECIAL_REGEX.test(password)) return 'login.passwordRequiresSpecial';\n if (policy.forbidCommon && COMMON_PASSWORDS.has(password.toLowerCase())) return 'login.passwordTooCommon';\n if (policy.customValidator) {\n const customReason = policy.customValidator(password);\n if (customReason) return customReason;\n }\n\n return null;\n};\n","//? Built-in common-password blocklist. Sourced from the public-domain\n//? SecLists Top-passwords list (Daniel Miessler), trimmed to the 200 most\n//? common entries to keep the package size small. Consumers needing a\n//? larger list (10k+ entries) can supply one through\n//? `projectConfig.auth.passwordPolicy.customValidator` and check their\n//? own data source there.\n//?\n//? Lookups are case-insensitive (the validator lower-cases before\n//? checking). Adding additional entries here is a non-breaking change.\n\nconst COMMON_PASSWORD_LIST: readonly string[] = [\n '123456', 'password', '12345678', 'qwerty', '123456789', '12345', '1234', '111111',\n '1234567', 'dragon', '123123', 'baseball', 'abc123', 'football', 'monkey', 'letmein',\n '696969', 'shadow', 'master', '666666', 'qwertyuiop', '123321', 'mustang', '1234567890',\n 'michael', '654321', 'pussy', 'superman', '1qaz2wsx', '7777777', 'fuckyou', '121212',\n '000000', 'qazwsx', '123qwe', 'killer', 'trustno1', 'jordan', 'jennifer', 'zxcvbnm',\n 'asdfgh', 'hunter', 'buster', 'soccer', 'harley', 'batman', 'andrew', 'tigger',\n 'sunshine', 'iloveyou', 'fuckme', '2000', 'charlie', 'robert', 'thomas', 'hockey',\n 'ranger', 'daniel', 'starwars', 'klaster', '112233', 'george', 'asshole', 'computer',\n 'michelle', 'jessica', 'pepper', '1111', 'zxcvbn', '555555', '11111111', '131313',\n 'freedom', '777777', 'pass', 'fuck', 'maggie', '159753', 'aaaaaa', 'ginger',\n 'princess', 'joshua', 'cheese', 'amanda', 'summer', 'love', 'ashley', '6969',\n 'nicole', 'chelsea', 'biteme', 'matthew', 'access', 'yankees', '987654321', 'dallas',\n 'austin', 'thunder', 'taylor', 'matrix', 'william', 'corvette', 'hello', 'martin',\n 'heather', 'secret', 'fucker', 'merlin', 'diamond', '1234qwer', 'gfhjkm', 'hammer',\n 'silver', 'richard', 'samantha', 'iceman', 'tigers', 'purple', 'andrea', 'horney',\n 'dakota', 'aaaaa', 'player', 'sucker', 'mercedes', 'whatever', 'orange', 'qweqwe',\n 'mickey', 'panties', 'hannah', 'fucking', 'paul', 'pakistan', 'patrick', 'martha',\n 'morgan', 'iwantu', 'lakers', 'rachel', 'slayer', 'scott', '2112', 'fish',\n 'porn', 'matt', 'qwert', 'cookie', 'eagle1', 'london', 'phoenix', 'chicago',\n 'badboy', 'braves', 'yankee', 'lover', 'barney', 'edward', 'raiders', 'green',\n 'maverick', 'rangers', 'joseph', 'maddog', 'mike', 'jasmine', 'arsenal', 'beach',\n 'tiger', 'snoopy', 'natasha', 'matthews', 'jackson', 'cumshot', 'chevy', 'gateway',\n 'gators', 'angel', 'junior', 'samson', '6666', 'amateur', 'gemini', 'apples',\n 'august', 'biscuit', '5150', 'apollo', 'parker', 'qwerty123', 'rabbit', 'angela',\n 'love123', 'rocket', 'scooby', 'beaver', 'oliver', 'walter', 'blowme', 'spider',\n '4321', 'cumshot1', 'lucky', 'helpme', 'jackie', 'monica',\n];\n\n/** Frozen Set for O(1) `.has()` lookups; entries are pre-lowercased. */\nexport const COMMON_PASSWORDS: ReadonlySet<string> = new Set(\n COMMON_PASSWORD_LIST.map((p) => p.toLowerCase()),\n);\n","//? Password-reset primitives. Used by the framework's own forgot-password\n//? flow when `ProjectConfig.auth.forgotPassword === 'framework'`, AND\n//? exported as building blocks for consumers who picked `'custom'` and want\n//? to wire their own UI / email layer without re-implementing crypto + Redis.\n//?\n//? Token format: 32 random bytes hex-encoded → 64-char URL-safe string.\n//? Storage: `<projectName>-pwreset:<sha256(token)> → userId` with a configurable\n//? TTL (`auth.passwordResetTtlSeconds`, default 3600 = 1 hour).\n//?\n//? HASHED AT REST (0.2.0): the raw token is NEVER stored — we persist only\n//? `sha256(token)` (the Redis key) via the `@luckystack/core` one-time-token\n//? primitive, so a leaked Redis keyspace can't be replayed to mint a reset.\n//? The raw token is returned to the caller exactly once for the emailed URL.\n\n// eslint-disable-next-line import-x/no-named-as-default -- bcryptjs ships both a default and namespace export; using the default mirrors its README\nimport bcrypt from 'bcryptjs';\n/* eslint-disable import-x/no-named-as-default-member -- accessing genSalt/hash/compare via the default import is the documented pattern */\n\nimport { getProjectConfig, issueOneTimeToken, consumeOneTimeToken, formatKey, redis, oneTimeTokenKey } from '@luckystack/core';\n\nimport { validatePassword } from './passwordPolicy';\nimport { getUserAdapter } from './userAdapter';\n\n//? Shared key namespace for the one-time-token primitive. `formatKey('-pwreset',\n//? sha256(token))` is applied inside the primitive, so the project-name prefix /\n//? multi-tenant formatter still apply — only the SUFFIX is now the hash, not the\n//? raw token (the historical `<projectName>-pwreset:<token>` shape becomes\n//? `<projectName>-pwreset:<sha256(token)>`).\nconst PWRESET_NAMESPACE = '-pwreset';\n\n//? Per-user pointer key that records the hash-key of the currently-active reset\n//? token (LOGIN-F16). On each new issue we delete the prior hash key (if any) so\n//? at most one reset link per user is redeemable at a time — multiple live links\n//? until TTL is a session-hijack / takeover risk if the first link is intercepted.\nconst pwresetUserKey = (userId: string): string => formatKey('-pwreset-user', userId);\n\n/**\n * Create a one-time password-reset token bound to a user id. Any previously\n * issued (but not yet consumed) token for the same user is invalidated first\n * (LOGIN-F16). Stored in Redis with the configured TTL — only `sha256(token)`\n * is persisted (hash-at-rest). Returns the raw token string — caller emails it\n * to the user (typically embedded in a URL).\n */\nexport const createPasswordResetToken = async (userId: string): Promise<string> => {\n const ttl = getProjectConfig().auth.passwordResetTtlSeconds;\n\n //? Invalidate the prior active token before issuing the new one (LOGIN-F16).\n //? The pointer key holds the exact Redis key of the prior token hash, so we\n //? can delete it without possessing the raw token.\n const priorKey = await redis.get(pwresetUserKey(userId));\n if (priorKey) {\n await redis.del(priorKey);\n }\n\n const handle = issueOneTimeToken(PWRESET_NAMESPACE, ttl, userId);\n await handle.store();\n\n //? Record the new hash-key so the NEXT issue can invalidate this one.\n //? TTL matches the token so the pointer never outlives what it points to.\n //? `oneTimeTokenKey(namespace, rawToken)` derives the same Redis key the\n //? primitive stored — we never persist the raw token itself.\n const newHashKey = oneTimeTokenKey(PWRESET_NAMESPACE, handle.token);\n await redis.set(pwresetUserKey(userId), newHashKey, 'EX', ttl);\n\n return handle.token;\n};\n\n/**\n * Validate and consume a password-reset token. Returns the bound userId on\n * success and removes the token (one-time use, atomic GET+DEL). Returns null\n * when the token is missing, expired, malformed, or already consumed.\n */\nexport const consumePasswordResetToken = async (token: string): Promise<string | null> => {\n return consumeOneTimeToken(PWRESET_NAMESPACE, token);\n};\n\n/**\n * Bcrypt-hash a plaintext password and write it to the user record via the\n * registered user adapter. Used by the reset flow and by the settings\n * password-change flow. Validates against the active password policy\n * (`projectConfig.auth.passwordPolicy`) before hashing — throws a `PasswordPolicyError`\n * when the plaintext fails policy. Catch and surface the error code to the\n * client; framework-mode reset/change flows already do this.\n *\n * ADR — DD-LOGIN-F17: this primitive intentionally does NOT revoke sessions.\n * Session revocation is the responsibility of each calling route because the\n * right behaviour differs per use-case:\n * - `reset-password/confirmReset_v1` revokes ALL sessions (`revokeUserSessions(userId, null)`)\n * because a forgot-password reset means the old credential is compromised.\n * - `settings/changePassword_v1` revokes every OTHER session but keeps the\n * current one active (`revokeUserSessions(userId, user.token)`) so the\n * user is not kicked from the device they changed the password on.\n * - Custom consumers may want a third behaviour (e.g. re-use the existing\n * token after an admin password-force) — keeping the primitive pure lets\n * them compose it freely.\n * If you call this function directly, YOU must call `revokeUserSessions`\n * afterwards unless you have a deliberate reason not to.\n */\nexport const updatePasswordHash = async (userId: string, plaintext: string): Promise<void> => {\n const reason = validatePassword(plaintext);\n if (reason) {\n throw new PasswordPolicyError(reason);\n }\n const salt = await bcrypt.genSalt(getProjectConfig().auth.bcryptRounds);\n const hashedPassword = await bcrypt.hash(plaintext, salt);\n const patch: Record<string, unknown> = { password: hashedPassword };\n await getUserAdapter().update(userId, patch);\n};\n\n/**\n * Thrown by `updatePasswordHash` when the supplied plaintext violates the\n * active password policy. `errorCode` matches the i18n reason key the rest\n * of the login flow uses (e.g. `login.passwordRequiresUppercase`).\n */\nexport class PasswordPolicyError extends Error {\n readonly errorCode: string;\n constructor(errorCode: string) {\n super(`Password policy violation: ${errorCode}`);\n this.name = 'PasswordPolicyError';\n this.errorCode = errorCode;\n }\n}\n\n/** Verify a plaintext password against a stored bcrypt hash. */\nexport const verifyPassword = async (plaintext: string, hash: string): Promise<boolean> => {\n return bcrypt.compare(plaintext, hash);\n};\n","//? Framework-mode forgot-password orchestration. Used only when\n//? `ProjectConfig.auth.forgotPassword === 'framework'`. Pulls in\n//? `@luckystack/email` lazily so the login package doesn't list it as a\n//? hard dep — `'custom'` and `'disabled'` modes never reach this code.\n\nimport { getProjectConfig, dispatchHook, getLogger } from '@luckystack/core';\n\nimport { loadEmailModule } from './emailModuleLoader';\nimport { createPasswordResetToken } from './passwordReset';\nimport { getUserAdapter } from './userAdapter';\n\ninterface SendResetEmailArgs {\n email: string;\n /**\n * App-facing display name used in the email greeting + subject. If omitted,\n * falls back to `projectConfig.auth.passwordResetBrand`, then to `'LuckyStack'`\n * as the absolute fallback. Real consumers should set this on either the\n * call site OR via `projectConfig.auth.passwordResetBrand`.\n */\n brand?: string;\n}\n\n/**\n * Look up the user by credentials-provider email, generate a reset token,\n * and email them a reset link. Always resolves successfully regardless of\n * whether the email matched a user (anti-enumeration). Returns the result\n * of the email send for diagnostics.\n */\nexport const sendPasswordResetEmail = async ({ email, brand }: SendResetEmailArgs): Promise<{ ok: boolean; reason?: string }> => {\n const config = getProjectConfig();\n //? Log mode but NOT the raw email at start — the email is a PII field.\n getLogger().info('[forgotPassword] start', { forgotPasswordMode: config.auth.forgotPassword });\n if (config.auth.forgotPassword !== 'framework') {\n getLogger().warn('[forgotPassword] auth.forgotPassword is not \"framework\" — nothing sent', { mode: config.auth.forgotPassword });\n return { ok: false, reason: 'forgotPassword-not-framework' };\n }\n\n const resolvedBrand = brand ?? config.auth.passwordResetBrand ?? 'LuckyStack';\n\n // Lazy import — the email package is an optional peer dep so its types\n // may not be resolvable at the framework's compile time.\n //? Mirror testEmail's robustness: catch a failed dynamic import instead of\n //? throwing (an uncaught throw here would bubble out of the anti-enumeration\n //? wrapper as a generic 500). Surfaces the real reason in the server log.\n const emailModule = await loadEmailModule().catch((error: unknown) => {\n getLogger().warn('[forgotPassword] failed to load @luckystack/email — is it installed?', { error: String(error) });\n return null;\n });\n if (!emailModule) {\n return { ok: false, reason: 'email-module-load-failed' };\n }\n const { sendEmail } = emailModule;\n\n const userAdapter = getUserAdapter();\n const user = await userAdapter.findByEmail({ email, provider: 'credentials' });\n //? Log found/userId without the raw email — logging email+found together is\n //? an enumeration oracle (any reader of the log can probe the user table).\n getLogger().info('[forgotPassword] credentials user lookup', { found: Boolean(user), userId: user?.id ?? null });\n\n // Anti-enumeration: always pretend it succeeded if the email isn't found.\n if (!user) {\n //? The client always gets \"success\" (anti-enumeration). This server-side\n //? line is the ONLY signal that the address simply isn't a credentials\n //? (email+password) account — by far the most common reason a reset email\n //? never arrives (OAuth-only signups and unknown/typo'd addresses have no\n //? credentials row, so there is nothing to reset).\n //? Do NOT include the email here — this line together with the email would\n //? let a log reader determine which addresses have credentials accounts.\n getLogger().warn('[forgotPassword] no credentials account for this email — nothing sent. Use the email+password account you registered with.');\n void dispatchHook('passwordResetRequested', {\n email,\n matched: false,\n });\n return { ok: true };\n }\n\n const token = await createPasswordResetToken(user.id);\n //? SEC: do NOT emit the raw token on the hook bus — hook subscribers are\n //? untrusted code and the token is a single-use credential. Consumers who need\n //? to deliver the email themselves should call `createPasswordResetToken`\n //? directly and build the URL without the framework exposing the secret here.\n void dispatchHook('passwordResetRequested', {\n email,\n matched: true,\n userId: user.id,\n ttlSeconds: config.auth.passwordResetTtlSeconds,\n });\n const baseUrl = (config.app.publicUrl || '').replace(/\\/+$/, '');\n //? ADR — DD-ROOTSRC-O8: the reset token is in the URL query string\n //? (`?token=<hex>`), not in the URL fragment (`#token=<hex>`).\n //? Tradeoff: query strings appear in server access logs and Referer headers\n //? if the user's mail client renders the link via a tracking proxy. Fragment\n //? identifiers (`#`) are never sent to the server and are therefore\n //? invisible to any log / proxy in the chain — they are more private.\n //? Decision: query-string is the standard email-link pattern understood by\n //? every SPA router, and tokens here are one-time + hashed at rest (Redis\n //? holds only `sha256(token)`), so the exposure window is the TTL of a\n //? single-use credential, not a replayable secret. Logging is mitigated\n //? by not logging the full URL at the point of issue (we log `userId`, not\n //? `resetUrl`). A future `auth.tokenLinkMode: 'fragment'` option can be\n //? introduced without touching this file — callers receive the raw token\n //? and can build their own URL.\n const resetUrl = `${baseUrl}/reset-password?token=${encodeURIComponent(token)}`;\n\n const ttlMinutes = Math.round(config.auth.passwordResetTtlSeconds / 60);\n\n //? Dispatch via the `'password-reset'` template rather than building the\n //? email inline. `@luckystack/email` ships a built-in for this name, so the\n //? out-of-the-box copy is unchanged — but a consumer can now\n //? `registerEmailTemplate('password-reset', …)` to translate or rebrand it\n //? without forking this flow (CFG-05). `adapterHint: 'transactional'` routes\n //? through a dedicated transactional sender when one is registered, else the\n //? default sender.\n const result = await sendEmail({\n to: user.email,\n template: 'password-reset',\n data: { resetUrl, userName: user.name, brand: resolvedBrand, ttlMinutes },\n adapterHint: 'transactional',\n });\n\n if (result.ok) {\n //? SEC: log only userId for correlation — not the email address (PII).\n getLogger().info('[forgotPassword] reset email dispatched', { userId: user.id });\n } else {\n getLogger().warn('[forgotPassword] reset email send FAILED', { userId: user.id, reason: result.reason });\n }\n\n return result.ok ? { ok: true } : { ok: false, reason: result.reason };\n};\n","//? Shared lazy-loader for the optional `@luckystack/email` peer dep. Both\n//? `forgotPassword.ts` and `emailChangeNotification.ts` need the SAME dynamic\n//? import + the SAME minimal `EmailModule` shape, so the interface + the\n//? `@ts-expect-error` import + the cast live here once (finding 37).\n//?\n//? IMPORTANT — this helper does NOT swallow a failed import. It resolves to the\n//? module or REJECTS exactly like the raw `import(...)` would. That preserves\n//? the two callers' DIFFERENT failure behavior verbatim:\n//? - `emailChangeNotification` awaits it directly (a load failure throws).\n//? - `forgotPassword` wraps it in `.catch(...)` (a load failure → null + warn).\n//? Centralizing the catch here would change one of those — do NOT.\n\n/** Minimal surface of `@luckystack/email` that the login orchestrators use. */\nexport interface EmailModule {\n sendEmail: (input: Record<string, unknown>) => Promise<{ ok: boolean; reason?: string }>;\n}\n\n/**\n * Dynamically import the optional `@luckystack/email` peer. Resolves to the\n * module, or REJECTS if the package is not installed (same as a bare\n * `import('@luckystack/email')`). Callers decide whether to `.catch(...)`.\n */\nexport const loadEmailModule = (): Promise<EmailModule> =>\n // @ts-expect-error optional peer dep — installed only when the consumer wires email sending\n import('@luckystack/email') as Promise<EmailModule>;\n","//? Email-change token primitives. Mirrors `passwordReset.ts` for the\n//? confirm-via-email-link flow: user submits a new address, framework mails\n//? a tokenized URL to the NEW address, click-through confirms and updates\n//? `User.email` + revokes all sessions.\n//?\n//? Token format: 32 random bytes hex-encoded -> 64-char URL-safe string.\n//? Storage: `<projectName>-email-change:<sha256(token)>` -> JSON payload\n//? `{ userId: string; newEmail: string }`\n//? TTL: `auth.emailChangeTtlSeconds` (default 3600 = 1 hour).\n//?\n//? HASHED AT REST (0.2.0): only `sha256(token)` is persisted (the Redis key)\n//? via the `@luckystack/core` one-time-token primitive, so a leaked Redis\n//? keyspace can't be replayed to confirm an email change. The raw token is\n//? returned to the caller exactly once for the emailed URL.\n\nimport { getProjectConfig, issueOneTimeToken, consumeOneTimeTokenJson, formatKey, redis, oneTimeTokenKey } from '@luckystack/core';\n\nconst EMAIL_CHANGE_NAMESPACE = '-email-change';\n\n//? Per-user pointer key for prior-token invalidation (LOGIN-F16), mirroring the\n//? same pattern used for password-reset tokens. Stored as a formatted key so the\n//? multi-tenant key formatter applies uniformly.\nconst emailChangeUserKey = (userId: string): string => formatKey('-email-change-user', userId);\n\nexport interface EmailChangePayload {\n userId: string;\n newEmail: string;\n}\n\n/**\n * Create a one-time email-change token bound to a user id + the new email\n * address. Any previously issued (but not yet consumed) token for the same\n * user is invalidated first (LOGIN-F16) so at most one confirmation link per\n * user is redeemable. Stored in Redis with the configured TTL — only\n * `sha256(token)` is persisted (hash-at-rest). Returns the raw token string —\n * caller emails the token (embedded in a URL) to the NEW address so the\n * confirmation click proves ownership of the new mailbox.\n */\nexport const createEmailChangeToken = async (userId: string, newEmail: string): Promise<string> => {\n const ttl = getProjectConfig().auth.emailChangeTtlSeconds;\n\n //? Invalidate the prior active email-change token (LOGIN-F16). Without this,\n //? multiple links could be outstanding concurrently — a link sent to an\n //? attacker-controlled address (via a race or a compromised session) would\n //? remain redeemable even after the user re-requests the flow.\n const priorKey = await redis.get(emailChangeUserKey(userId));\n if (priorKey) {\n await redis.del(priorKey);\n }\n\n const handle = issueOneTimeToken(\n EMAIL_CHANGE_NAMESPACE,\n ttl,\n { userId, newEmail } satisfies EmailChangePayload,\n );\n await handle.store();\n\n //? Record the new hash-key so the NEXT issue can invalidate this one.\n const newHashKey = oneTimeTokenKey(EMAIL_CHANGE_NAMESPACE, handle.token);\n await redis.set(emailChangeUserKey(userId), newHashKey, 'EX', ttl);\n\n return handle.token;\n};\n\n/**\n * Validate and consume an email-change token. Returns the bound\n * `{ userId, newEmail }` on success and removes the token (one-time use, atomic\n * GET+DEL). Returns null when the token is missing, expired, malformed, or\n * already consumed.\n */\nexport const consumeEmailChangeToken = async (token: string): Promise<EmailChangePayload | null> => {\n const parsed = await consumeOneTimeTokenJson<EmailChangePayload>(EMAIL_CHANGE_NAMESPACE, token);\n if (\n !parsed\n || typeof parsed !== 'object'\n || typeof parsed.userId !== 'string'\n || typeof parsed.newEmail !== 'string'\n ) {\n return null;\n }\n return parsed;\n};\n","//? Framework-mode \"email change confirmation\" orchestration. Renders the\n//? confirmation email + sends it to the NEW address (proves the user can\n//? receive at the new mailbox). Mirrors the lazy-import pattern from\n//? `forgotPassword.ts` so `@luckystack/email` stays an optional peer.\n\nimport { getProjectConfig } from '@luckystack/core';\n\nimport { createEmailChangeToken } from './emailChange';\nimport { loadEmailModule } from './emailModuleLoader';\n\ninterface SendEmailChangeArgs {\n userId: string;\n newEmail: string;\n /** Optional display name used in the email greeting (falls back to \"there\"). */\n userName?: string | null;\n /**\n * App-facing display name used in the email greeting + subject. Falls back\n * to `projectConfig.auth.passwordResetBrand` (the shared brand label), then\n * to `'LuckyStack'` as the absolute fallback.\n */\n brand?: string;\n}\n\n/**\n * Mint a one-shot email-change token and send a confirmation email to the\n * NEW address with the tokenized URL. Returns the send result for diagnostics\n * + the token itself (callers can audit-log it through the\n * `postEmailChangeRequested` hook).\n */\nexport const sendEmailChangeConfirmation = async (\n { userId, newEmail, userName, brand }: SendEmailChangeArgs,\n): Promise<{ ok: boolean; reason?: string; token: string }> => {\n const config = getProjectConfig();\n const resolvedBrand = brand ?? config.auth.passwordResetBrand ?? 'LuckyStack';\n\n // Lazy import — `@luckystack/email` is an optional peer dep. A failed load\n //? rejects here (unlike forgotPassword's catch); this caller's behavior is\n //? unchanged — the rejection bubbles out exactly as the inline import did.\n const { sendEmail } = await loadEmailModule();\n\n const token = await createEmailChangeToken(userId, newEmail);\n const baseUrl = (config.app.publicUrl || '').replace(/\\/+$/, '');\n //? ADR — DD-ROOTSRC-O8: same query-string tradeoff as the password-reset\n //? flow (see `forgotPassword.ts`). Tokens are one-time + hashed at rest;\n //? the raw token is never persisted. The confirm URL is only sent to the\n //? NEW address (verifying mailbox ownership), so it never appears in an\n //? access log that an attacker of the OLD address could read.\n //? #64: honor the configurable `auth.emailChangeConfirmPath` (where the\n //? consumer's confirm-email page lives) instead of a hardcoded path; the\n //? prior hardcoded value is the fallback when the key is empty.\n const confirmPath = config.auth.emailChangeConfirmPath || '/settings/confirm-email';\n const confirmUrl = `${baseUrl}${confirmPath}?token=${encodeURIComponent(token)}`;\n const ttlMinutes = Math.round(config.auth.emailChangeTtlSeconds / 60);\n\n //? Dispatch via the built-in `'email-change'` template so consumers can\n //? override the copy with `registerEmailTemplate('email-change', …)` without\n //? forking (CFG-05). Default copy is unchanged.\n const result = await sendEmail({\n to: newEmail,\n template: 'email-change',\n data: { confirmUrl, userName, brand: resolvedBrand, ttlMinutes },\n adapterHint: 'transactional',\n });\n\n return result.ok ? { ok: true, token } : { ok: false, reason: result.reason, token };\n};\n","//? 2FA flow layer (ADR 0024) on top of the totp.ts primitive. Methods:\n//? - 'totp' — authenticator apps (Google/Microsoft Authenticator,\n//? Authy, …) via the open TOTP standard; primary channel.\n//? - 'email-code' — \"send the code to my email instead\" fallback\n//? (config `auth.twoFactorEmailFallback`, needs email).\n//? - 'recovery-code' — one-time backup codes minted at enrollment.\n//?\n//? Login integration: this module registers the 2FA GATE into login.ts at\n//? module init (registerTwoFactorGate — the DI idiom that avoids a module\n//? cycle). After the first factor verifies, the gate parks the login as a\n//? short-lived Redis CHALLENGE (high-entropy token, hashed at rest) instead of\n//? minting a session; `verifyTwoFactorChallenge` completes it through the same\n//? `finalizeLogin` tail the password path uses.\n//?\n//? The challenge is deliberately NOT the core one-time-token primitive: a\n//? wrong 2FA code must not burn the whole challenge (the user gets\n//? `twoFactorMaxAttempts` tries), so reads don't consume — success or the\n//? spent attempt budget does.\n\nimport crypto from 'node:crypto';\nimport { acquireLease, dispatchHook, formatKey, getLogger, getProjectConfig, getProjectName, redis, releaseLease, tryCatch } from '@luckystack/core';\n\nimport { issueEmailCode, verifyEmailCode, clearEmailCode } from './emailOtp';\nimport { loadEmailModule } from './emailModuleLoader';\nimport { buildOtpauthUri, generateTotpSecret, verifyTotp } from './totp';\nimport { getUserAdapter, type UserRecord } from './userAdapter';\nimport {\n finalizeLogin,\n registerTwoFactorGate,\n type CredentialsLoginChallenge,\n type CredentialsLoginResult,\n type TwoFactorMethod,\n} from './login';\n\nconst sha256 = (value: string): string => crypto.createHash('sha256').update(value).digest('hex');\n\n//? ─────────────── TOTP secret at rest (AES-256-GCM, optional) ───────────────\n//? With `TOTP_ENCRYPTION_KEY` set (any string; key = sha256 of it) secrets are\n//? stored as `gcm:<iv>:<tag>:<ciphertext>`; without it they are stored as the\n//? plain base32 string (warned once). Decrypt accepts BOTH forms, so adding\n//? the env key later upgrades new writes without breaking existing users.\n\nconst ENCRYPTED_PREFIX = 'gcm:';\nlet warnedPlaintextSecret = false;\n\nconst encryptionKey = (): Buffer | null => {\n const raw = process.env.TOTP_ENCRYPTION_KEY;\n if (!raw) return null;\n return crypto.createHash('sha256').update(raw).digest();\n};\n\nexport const encryptTotpSecret = (secret: string): string => {\n const key = encryptionKey();\n if (!key) {\n if (!warnedPlaintextSecret) {\n warnedPlaintextSecret = true;\n getLogger().warn('[2fa] TOTP_ENCRYPTION_KEY is not set — TOTP secrets are stored unencrypted. Set it in .env.local to encrypt new enrollments at rest.');\n }\n return secret;\n }\n const iv = crypto.randomBytes(12);\n const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);\n const ciphertext = Buffer.concat([cipher.update(secret, 'utf8'), cipher.final()]);\n return `${ENCRYPTED_PREFIX}${iv.toString('base64')}:${cipher.getAuthTag().toString('base64')}:${ciphertext.toString('base64')}`;\n};\n\nexport const decryptTotpSecret = (stored: string): string | null => {\n if (!stored.startsWith(ENCRYPTED_PREFIX)) return stored; //? legacy/keyless plaintext\n const key = encryptionKey();\n if (!key) {\n getLogger().error('[2fa] found an encrypted TOTP secret but TOTP_ENCRYPTION_KEY is not set — cannot verify TOTP codes.');\n return null;\n }\n const [ivPart, tagPart, dataPart] = stored.slice(ENCRYPTED_PREFIX.length).split(':');\n if (!ivPart || !tagPart || !dataPart) return null;\n try {\n const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(ivPart, 'base64'));\n decipher.setAuthTag(Buffer.from(tagPart, 'base64'));\n return Buffer.concat([decipher.update(Buffer.from(dataPart, 'base64')), decipher.final()]).toString('utf8');\n } catch {\n getLogger().error('[2fa] TOTP secret decryption failed (wrong TOTP_ENCRYPTION_KEY?).');\n return null;\n }\n};\n\n//? ─────────────── challenge store ───────────────\n\ninterface ChallengeRecord {\n userId: string;\n email?: string;\n}\n\nconst challengeKey = (token: string): string => formatKey('-2fa-challenge', sha256(token));\nconst challengeAttemptsKey = (token: string): string => formatKey('-2fa-challenge-attempts', sha256(token));\n//? Replay guard: per-(user, timestep) used-marker. Set with `NX` so the\n//? claim is ATOMIC — two concurrent verifies of the same code race on the\n//? same key and exactly one wins (the earlier get-highest-step-then-set was a\n//? TOCTOU: both could read the old value and both pass). TTL covers the ±drift\n//? window; after it the timestep is already outside `now ± drift` so verifyTotp\n//? rejects it anyway.\nconst usedTimestepKey = (userId: string, timestep: number): string => formatKey('-2fa-usedstep', `${userId}:${String(timestep)}`);\n//? Cross-challenge, identity-keyed 2FA failure counter. The per-challenge\n//? budget alone is resettable — a password-holding attacker mints a fresh\n//? challenge per try — so this account-level counter locks the second factor\n//? after too many failures in the window, independent of IP (a botnet can't\n//? scale past it).\nconst accountFailureKey = (userId: string): string => formatKey('-2fa-accountfail', userId);\nconst RECOVERY_LEASE_MS = 5000;\nconst recoveryLeaseName = (userId: string): string => `2fa-recovery:${userId}`;\n//? Pending (unconfirmed) enrollment secret — only written to the user record\n//? once the user proves the app is set up by entering a first valid code.\nconst pendingSecretKey = (userId: string): string => formatKey('-2fa-pending', userId);\n\n//? Account-level 2FA lockout window/limit. Fixed (not a new config key to keep\n//? the surface lean) — 10 failed second-factor attempts per 15 min per account\n//? across ALL challenges/IPs. Generous for a fat-fingered legit user, a hard\n//? ceiling for a grind.\nconst ACCOUNT_FAIL_LIMIT = 10;\nconst ACCOUNT_FAIL_WINDOW_SECONDS = 15 * 60;\n\nconst isTwoFactorAccountLocked = async (userId: string): Promise<boolean> => {\n const count = Number((await redis.get(accountFailureKey(userId))) ?? '0');\n return count >= ACCOUNT_FAIL_LIMIT;\n};\nconst recordTwoFactorAccountFailure = async (userId: string): Promise<void> => {\n const key = accountFailureKey(userId);\n const count = await redis.incr(key);\n if (count === 1) await redis.expire(key, ACCOUNT_FAIL_WINDOW_SECONDS);\n};\nconst clearTwoFactorAccountFailures = async (userId: string): Promise<void> => {\n await redis.del(accountFailureKey(userId));\n};\n\nconst twoFactorConfig = () => getProjectConfig().auth;\n\nexport const availableTwoFactorMethods = (user: UserRecord): TwoFactorMethod[] => {\n const methods: TwoFactorMethod[] = [];\n if (user.totpSecret) methods.push('totp');\n if (twoFactorConfig().twoFactorEmailFallback && user.email) methods.push('email-code');\n if ((user.recoveryCodes ?? []).length > 0) methods.push('recovery-code');\n return methods;\n};\n\n//? The login gate: park the verified first factor as a challenge when the\n//? user enrolled 2FA (and the feature isn't globally disabled).\nexport const createTwoFactorChallengeIfRequired = async (\n user: UserRecord,\n _context: { requesterIp?: string },\n): Promise<CredentialsLoginChallenge | null> => {\n if (twoFactorConfig().twoFactor === 'disabled') return null;\n if (!user.twoFactorEnabled || !user.totpSecret) return null;\n\n const methods = availableTwoFactorMethods(user);\n const token = crypto.randomBytes(32).toString('hex');\n const record: ChallengeRecord = { userId: user.id, email: user.email ?? undefined };\n await redis.set(challengeKey(token), JSON.stringify(record), 'EX', twoFactorConfig().twoFactorChallengeTtlSeconds);\n return {\n status: true,\n reason: 'login.twoFactorRequired',\n requiresTwoFactor: true,\n challengeToken: token,\n twoFactorMethods: methods,\n };\n};\n\nconst readChallenge = async (token: string): Promise<ChallengeRecord | null> => {\n if (!/^[a-f0-9]{64}$/.test(token)) return null;\n const raw = await redis.get(challengeKey(token));\n if (!raw) return null;\n const [parseError, parsed] = await tryCatch(() => Promise.resolve(JSON.parse(raw) as ChallengeRecord));\n if (parseError || typeof parsed?.userId !== 'string') return null;\n return parsed;\n};\n\nconst burnChallenge = async (token: string): Promise<void> => {\n await redis.del(challengeKey(token));\n await redis.del(challengeAttemptsKey(token));\n};\n\n//? ─────────────── verification (completes the login) ───────────────\n\n//? `enforceSingleUse` — the timestep replay guard is a LOGIN-boundary defense:\n//? an intercepted code must not be replayable to complete a challenge. It is\n//? applied on the login path only. Session-authenticated management actions\n//? (disable / regenerate) do a plain proof-of-possession verify WITHOUT the\n//? guard — otherwise a user who just logged in with a TOTP code could not\n//? manage 2FA until their authenticator rolled to the next code (their app\n//? still shows the one they just burned). Those actions are already gated by\n//? the live session, so replaying a management code buys an attacker nothing\n//? they couldn't already do with that session.\nconst verifyTotpForUser = async (user: UserRecord, code: string, enforceSingleUse: boolean): Promise<boolean> => {\n if (!user.totpSecret) return false;\n //? Pin to the 6 digits we provision (otpauth URI declares digits=6). The\n //? generic verifyTotp accepts 6–8 for RFC completeness; the auth surface\n //? must not (a longer guess is a spec deviation with no benefit).\n if (!/^\\d{6}$/.test(code.trim())) return false;\n const secret = decryptTotpSecret(user.totpSecret);\n if (!secret) return false;\n const result = verifyTotp({ secret, code });\n if (!result.valid || result.timestep === null) return false;\n if (!enforceSingleUse) return true;\n\n //? Single-use on the login boundary: atomically CLAIM this (user, timestep)\n //? with `NX`. If the marker already exists the code was already redeemed —\n //? refuse. `NX` makes the claim race-free (fixes the earlier get-then-set\n //? TOCTOU where two concurrent verifies of the same code both passed).\n const claimed = await redis.set(usedTimestepKey(user.id, result.timestep), '1', 'EX', 60 * 2, 'NX');\n return claimed !== null;\n};\n\nconst matchRecoveryHash = (hashes: string[], code: string): number => {\n const submitted = Buffer.from(sha256(code.trim().toLowerCase()));\n return hashes.findIndex((hash) => {\n const stored = Buffer.from(hash);\n return stored.length === submitted.length && crypto.timingSafeEqual(stored, submitted);\n });\n};\n\n//? Burning a recovery code is a read-modify-write on the user record's array,\n//? which the generic UserAdapter can't do atomically. Two concurrent uses of\n//? the SAME code could both match + both persist (double-spend), and two\n//? concurrent uses of DIFFERENT codes could last-writer-wins-overwrite each\n//? other (a lost update that resurrects a burned code). Serialize the whole\n//? read→match→burn per user with a short Redis lease, and RE-READ inside the\n//? lease so the burn always writes against fresh state. If the lease can't be\n//? taken (contention), fail closed — the caller can retry.\nconst consumeRecoveryCode = async (user: UserRecord, code: string): Promise<boolean> => {\n if ((user.recoveryCodes ?? []).length === 0) return false;\n const leaseToken = await acquireLease(recoveryLeaseName(user.id), RECOVERY_LEASE_MS);\n if (!leaseToken) {\n getLogger().warn('[2fa] recovery-code burn is contended (lease busy) — refusing this attempt, retry');\n return false;\n }\n try {\n //? Re-read under the lease: `user` was fetched before the lease, so its\n //? recoveryCodes array may be stale relative to a concurrent burn.\n const fresh = await getUserAdapter().findById(user.id);\n const hashes = fresh?.recoveryCodes ?? [];\n const index = matchRecoveryHash(hashes, code);\n if (index === -1) return false;\n const remaining = hashes.filter((_, position) => position !== index);\n const [updateError] = await tryCatch(() => getUserAdapter().update(user.id, { recoveryCodes: remaining }));\n //? Fail CLOSED: if the burn can't be persisted the code stays reusable — refuse it.\n if (updateError) {\n getLogger().error('[2fa] could not persist recovery-code consumption — refusing the code', updateError);\n return false;\n }\n return true;\n } finally {\n await releaseLease(recoveryLeaseName(user.id), leaseToken);\n }\n};\n\nexport interface VerifyTwoFactorInput {\n challengeToken: string;\n code: string;\n /** Defaults to 'totp'; the client sends what the user picked. */\n method?: TwoFactorMethod;\n supersedeToken?: string;\n requesterIp?: string;\n}\n\nexport const verifyTwoFactorChallenge = async (input: VerifyTwoFactorInput): Promise<CredentialsLoginResult> => {\n const challenge = await readChallenge(input.challengeToken);\n if (!challenge) return { status: false, reason: 'login.twoFactorChallengeExpired' };\n\n //? Cross-challenge, identity-keyed lockout FIRST (non-incrementing read).\n //? The per-challenge budget below is resettable by minting a fresh challenge\n //? (a password-holder can), so this account-level ceiling is what actually\n //? stops a distributed grind of the second factor.\n if (await isTwoFactorAccountLocked(challenge.userId)) {\n return { status: false, reason: 'login.twoFactorLocked' };\n }\n\n //? Attempt budget on the CHALLENGE (all methods combined). INCR is atomic;\n //? the counter inherits the challenge TTL.\n const counter = challengeAttemptsKey(input.challengeToken);\n const attempts = await redis.incr(counter);\n const remaining = await redis.ttl(challengeKey(input.challengeToken));\n if (remaining > 0) await redis.expire(counter, remaining);\n if (attempts > twoFactorConfig().twoFactorMaxAttempts) {\n await burnChallenge(input.challengeToken);\n return { status: false, reason: 'login.twoFactorLocked' };\n }\n\n const user = await getUserAdapter().findById(challenge.userId);\n if (!user) {\n await burnChallenge(input.challengeToken);\n return { status: false, reason: 'login.twoFactorChallengeExpired' };\n }\n\n const method: TwoFactorMethod = input.method ?? 'totp';\n let verified = false;\n if (method === 'totp') {\n //? Login boundary → enforce the single-use timestep replay guard.\n verified = await verifyTotpForUser(user, input.code, true);\n } else if (method === 'email-code') {\n if (!twoFactorConfig().twoFactorEmailFallback) return { status: false, reason: 'login.twoFactorMethodUnavailable' };\n verified = (await verifyEmailCode({\n purpose: '2fa',\n identity: user.id,\n code: input.code,\n maxAttempts: twoFactorConfig().emailCodeMaxAttempts,\n })) === 'valid';\n } else {\n verified = await consumeRecoveryCode(user, input.code);\n }\n\n if (!verified) {\n await recordTwoFactorAccountFailure(user.id);\n void dispatchHook('loginFailed', { email: user.email ?? undefined, userId: user.id, provider: 'credentials', reason: 'login.twoFactorInvalidCode', stage: 'login', requesterIp: input.requesterIp });\n return { status: false, reason: 'login.twoFactorInvalidCode' };\n }\n\n await burnChallenge(input.challengeToken);\n await clearEmailCode('2fa', user.id);\n await clearTwoFactorAccountFailures(user.id);\n return finalizeLogin(user, {\n provider: 'credentials',\n email: user.email ?? undefined,\n supersedeToken: input.supersedeToken,\n requesterIp: input.requesterIp,\n });\n};\n\n//? \"Send the code to my email instead\" — bound to an ACTIVE challenge so it\n//? can't be used to spam arbitrary users. Always answers ok (the challenge\n//? token itself already proves a verified first factor).\nexport const requestTwoFactorEmailCode = async (challengeToken: string): Promise<{ ok: boolean; reason?: string }> => {\n if (!twoFactorConfig().twoFactorEmailFallback) return { ok: false, reason: 'login.twoFactorMethodUnavailable' };\n const challenge = await readChallenge(challengeToken);\n if (!challenge) return { ok: false, reason: 'login.twoFactorChallengeExpired' };\n const user = await getUserAdapter().findById(challenge.userId);\n if (!user?.email) return { ok: false, reason: 'login.twoFactorChallengeExpired' };\n\n const config = twoFactorConfig();\n const code = await issueEmailCode({\n purpose: '2fa',\n identity: user.id,\n ttlSeconds: config.emailCodeTtlSeconds,\n digits: config.emailCodeLength,\n });\n const sent = await sendCodeEmail(user.email, code, 'Your verification code', `Use this code to finish signing in to ${getProjectName()}.`);\n return sent ? { ok: true } : { ok: false, reason: 'login.emailCodeSendFailed' };\n};\n\n//? Shared minimal code email (also used by the email-code LOGIN flow). Uses a\n//? raw subject/html send so no template registration is required; the code is\n//? digits-only so no HTML escaping concerns.\nexport const sendCodeEmail = async (to: string, code: string, subject: string, intro: string): Promise<boolean> => {\n const [loadError, email] = await tryCatch(() => loadEmailModule());\n if (loadError || !email) {\n getLogger().error('[2fa/email-code] @luckystack/email is not installed — cannot send the code email.');\n return false;\n }\n //? Visual spacing between digits (\"1 2 3 4 5 6\") — codes are digits-only.\n const spaced = code.match(/\\d/g)?.join(' ') ?? code;\n const [sendError, result] = await tryCatch(() => email.sendEmail({\n to,\n subject,\n html: `<p>${intro}</p><p style=\"font-size:28px;font-weight:700;letter-spacing:6px;font-family:monospace\">${spaced}</p><p>The code expires in ${String(Math.round(twoFactorConfig().emailCodeTtlSeconds / 60))} minutes. If you didn't request it, you can ignore this email.</p>`,\n text: `${intro}\\n\\nCode: ${code}\\n\\nThe code expires in ${String(Math.round(twoFactorConfig().emailCodeTtlSeconds / 60))} minutes. If you didn't request it, you can ignore this email.`,\n adapterHint: 'transactional',\n }));\n if (sendError || !result?.ok) {\n getLogger().error('[2fa/email-code] sending the code email failed', sendError ?? new Error(result?.reason ?? 'unknown'));\n return false;\n }\n return true;\n};\n\n//? ─────────────── enrollment (authenticated user) ───────────────\n\nexport type TotpEnrollmentStart =\n | { ok: true; secret: string; otpauthUri: string }\n | { ok: false; reason: string };\n\n//? Guard shared by setup + confirm: enrolling is refused when the feature is\n//? globally disabled (would give a false sense of security — the login gate\n//? never fires) AND when 2FA is ALREADY enabled. The second is the important\n//? one: without it a hijacked (2FA-less) session could call setup+enable to\n//? OVERWRITE the victim's factor with the attacker's — strictly worse than the\n//? disable path, which already requires a current code. Re-enrolling therefore\n//? requires disabling first (which proves current possession).\nconst enrollmentBlockedReason = (user: UserRecord): string | null => {\n if (twoFactorConfig().twoFactor === 'disabled') return 'login.twoFactorDisabledByServer';\n if (user.twoFactorEnabled) return 'login.twoFactorAlreadyEnabled';\n return null;\n};\n\n/**\n * Step 1: mint a secret and park it as PENDING (Redis, 10 min) — the user\n * record is untouched until the user proves the app works via\n * `confirmTotpEnrollment`. Re-calling replaces the pending secret. Refused for\n * an already-enrolled user / a disabled feature (see enrollmentBlockedReason).\n */\nexport const beginTotpEnrollment = async (user: UserRecord): Promise<TotpEnrollmentStart> => {\n const blocked = enrollmentBlockedReason(user);\n if (blocked) return { ok: false, reason: blocked };\n const secret = generateTotpSecret();\n await redis.set(pendingSecretKey(user.id), encryptTotpSecret(secret), 'EX', 60 * 10);\n return {\n ok: true,\n secret,\n otpauthUri: buildOtpauthUri({ secret, accountName: user.email ?? user.id, issuer: getProjectName() }),\n };\n};\n\n//? 80 bits of entropy per code (10 random bytes → 20 hex, grouped 5×4 for\n//? readability). 40 bits (the earlier 5 bytes) is offline-crackable against an\n//? unsalted sha256 store if the user table leaks; 80 bits is not.\nconst generateRecoveryCodes = (): { raw: string[]; hashes: string[] } => {\n const raw = Array.from({ length: 10 }, () => {\n const hex = crypto.randomBytes(10).toString('hex');\n return `${hex.slice(0, 5)}-${hex.slice(5, 10)}-${hex.slice(10, 15)}-${hex.slice(15, 20)}`;\n });\n return { raw, hashes: raw.map((code) => sha256(code.toLowerCase())) };\n};\n\nexport type ConfirmTotpEnrollmentResult =\n | { ok: true; recoveryCodes: string[] }\n | { ok: false; reason: string };\n\n/**\n * Step 2: the user enters the first code from their app. On success the\n * secret + recovery-code hashes land on the user record and 2FA is ON.\n * The RAW recovery codes are returned exactly once — show + let them save.\n */\nexport const confirmTotpEnrollment = async (user: UserRecord, code: string): Promise<ConfirmTotpEnrollmentResult> => {\n //? Re-check the block here too (defense in depth — a stale pending secret\n //? could otherwise be confirmed after 2FA was enabled by another path).\n const blocked = enrollmentBlockedReason(user);\n if (blocked) return { ok: false, reason: blocked };\n const pendingStored = await redis.get(pendingSecretKey(user.id));\n if (!pendingStored) return { ok: false, reason: 'login.twoFactorEnrollmentExpired' };\n const secret = decryptTotpSecret(pendingStored);\n if (!secret) return { ok: false, reason: 'login.twoFactorEnrollmentExpired' };\n\n const result = verifyTotp({ secret, code });\n if (!result.valid) return { ok: false, reason: 'login.twoFactorInvalidCode' };\n\n const { raw, hashes } = generateRecoveryCodes();\n const [updateError] = await tryCatch(() => getUserAdapter().update(user.id, {\n totpSecret: encryptTotpSecret(secret),\n twoFactorEnabled: true,\n recoveryCodes: hashes,\n }));\n if (updateError) {\n getLogger().error('[2fa] enrollment persist failed — is your UserAdapter/schema missing the 2FA fields? (twoFactorEnabled, totpSecret, recoveryCodes)', updateError);\n return { ok: false, reason: 'login.twoFactorPersistFailed' };\n }\n await redis.del(pendingSecretKey(user.id));\n return { ok: true, recoveryCodes: raw };\n};\n\n/**\n * Disable 2FA. Requires a currently-valid TOTP code OR an unused recovery\n * code, so a hijacked (but 2FA-less) session can't silently strip the factor.\n */\nexport const disableTwoFactor = async (user: UserRecord, code: string): Promise<{ ok: boolean; reason?: string }> => {\n //? Management action (session-authenticated) → plain proof-of-possession, no\n //? single-use guard, so a just-logged-in user isn't blocked for 30s.\n const viaTotp = await verifyTotpForUser(user, code, false);\n const viaRecovery = viaTotp ? false : await consumeRecoveryCode(user, code);\n if (!viaTotp && !viaRecovery) return { ok: false, reason: 'login.twoFactorInvalidCode' };\n\n const [updateError] = await tryCatch(() => getUserAdapter().update(user.id, {\n totpSecret: null,\n twoFactorEnabled: false,\n recoveryCodes: [],\n }));\n if (updateError) {\n getLogger().error('[2fa] disable persist failed', updateError);\n return { ok: false, reason: 'login.twoFactorPersistFailed' };\n }\n //? Clear any lingering account-lockout counter so re-enrolling later starts clean.\n await clearTwoFactorAccountFailures(user.id);\n return { ok: true };\n};\n\n/** Replace the recovery-code set (requires a valid current TOTP code). */\nexport const regenerateRecoveryCodes = async (user: UserRecord, code: string): Promise<ConfirmTotpEnrollmentResult> => {\n //? Management action → plain verify (see disableTwoFactor).\n if (!(await verifyTotpForUser(user, code, false))) return { ok: false, reason: 'login.twoFactorInvalidCode' };\n const { raw, hashes } = generateRecoveryCodes();\n const [updateError] = await tryCatch(() => getUserAdapter().update(user.id, { recoveryCodes: hashes }));\n if (updateError) {\n getLogger().error('[2fa] recovery-code regenerate persist failed', updateError);\n return { ok: false, reason: 'login.twoFactorPersistFailed' };\n }\n return { ok: true, recoveryCodes: raw };\n};\n\n//? Register the login gate (see header). Import of this module — via the\n//? package index — is what arms 2FA; nothing changes for code paths that\n//? never import it.\nregisterTwoFactorGate(createTwoFactorChallengeIfRequired);\n","//? Email-OTP primitive: short numeric codes typed over from an email —\n//? passwordless login (\"email-code\") and the 2FA email fallback both build on\n//? this. It deliberately does NOT reuse the core one-time-token primitive:\n//? that one is keyed BY sha256(token), which is only safe for high-entropy\n//? (256-bit) tokens. A 6-digit code has 10^6 possibilities — hashing it as the\n//? KEY would be brute-forceable from a Redis dump and unfindable at verify\n//? time. So codes are keyed by PURPOSE + IDENTITY instead, with the code hash\n//? in the VALUE, a server-side attempt counter, and at most ONE active code\n//? per (purpose, identity): re-issuing overwrites the previous code.\n//?\n//? Consume is at-most-once: the final DEL doubles as the winner-take-all lock\n//? (mirrors consumeOneTimeToken's atomic GET+DEL semantics for parallel\n//? verifies with the correct code).\n\nimport crypto from 'node:crypto';\nimport { formatKey, redis } from '@luckystack/core';\n\nexport type EmailOtpPurpose = 'login' | '2fa';\n\n//? Identity is an email (login) or userId (2fa). Normalize emails the same way\n//? the lockout does so `Foo@Bar.com` and `foo@bar.com` share one code slot.\nconst normalizeIdentity = (identity: string): string => identity.trim().toLowerCase();\n\nconst codeKey = (purpose: EmailOtpPurpose, identity: string): string =>\n formatKey('-emailcode', `${purpose}:${normalizeIdentity(identity)}`);\nconst attemptsKey = (purpose: EmailOtpPurpose, identity: string): string =>\n formatKey('-emailcode-attempts', `${purpose}:${normalizeIdentity(identity)}`);\n\nconst hashCode = (code: string): string => crypto.createHash('sha256').update(code).digest('hex');\n\n//? Uniform random numeric code. `crypto.randomInt` does rejection sampling\n//? internally — NO modulo bias (a plain `randomBytes % 10^n` would skew low\n//? digits), and unlike `hotp()` it is not derived from any counter/secret.\nexport const generateNumericCode = (digits: number): string =>\n String(crypto.randomInt(0, 10 ** digits)).padStart(digits, '0');\n\nexport interface IssueEmailCodeInput {\n purpose: EmailOtpPurpose;\n /** Email address (login) or userId (2fa). */\n identity: string;\n ttlSeconds: number;\n digits: number;\n}\n\n/**\n * Mint + store a fresh code for (purpose, identity). Any previously active\n * code for the same slot is replaced, and its attempt counter reset. Returns\n * the RAW code exactly once — the caller emails it; only sha256(code) is\n * stored (hash-at-rest, ADR 0010 spirit).\n */\nexport const issueEmailCode = async ({ purpose, identity, ttlSeconds, digits }: IssueEmailCodeInput): Promise<string> => {\n const code = generateNumericCode(digits);\n await redis.set(codeKey(purpose, identity), hashCode(code), 'EX', ttlSeconds);\n await redis.del(attemptsKey(purpose, identity));\n return code;\n};\n\nexport type EmailCodeVerdict = 'valid' | 'invalid' | 'expired' | 'locked';\n\nexport interface VerifyEmailCodeInput {\n purpose: EmailOtpPurpose;\n identity: string;\n code: string;\n /** Wrong-code attempts before the code is burned (counter is server-side). */\n maxAttempts: number;\n}\n\n/**\n * Verify + consume. Verdicts: 'valid' (code matched, slot consumed),\n * 'invalid' (wrong code, attempt recorded), 'locked' (attempt budget spent —\n * the active code is burned so guessing can't continue), 'expired' (no active\n * code: TTL passed, never issued, or already consumed).\n */\nexport const verifyEmailCode = async ({ purpose, identity, code, maxAttempts }: VerifyEmailCodeInput): Promise<EmailCodeVerdict> => {\n const key = codeKey(purpose, identity);\n const storedHash = await redis.get(key);\n if (!storedHash) return 'expired';\n\n //? Count the attempt FIRST (INCR is atomic across parallel verifies), and\n //? give the counter the code's own remaining TTL so it can't outlive it.\n //? If the code expired between the GET above and this TTL read (`remaining`\n //? <= 0), still bound the counter with a short fallback so a just-INCR'd\n //? counter can never linger without a TTL.\n const counter = attemptsKey(purpose, identity);\n const attempts = await redis.incr(counter);\n const remaining = await redis.ttl(key);\n await redis.expire(counter, remaining > 0 ? remaining : 600);\n if (attempts > maxAttempts) {\n await redis.del(key);\n await redis.del(counter);\n return 'locked';\n }\n\n const submitted = Buffer.from(hashCode(code.trim()));\n const expected = Buffer.from(storedHash);\n const matches = submitted.length === expected.length && crypto.timingSafeEqual(submitted, expected);\n if (!matches) return 'invalid';\n\n //? Winner-take-all consume: DEL returns how many keys were removed — 0 means\n //? a parallel verify with the same correct code beat us to it.\n const deleted = await redis.del(key);\n await redis.del(counter);\n return deleted > 0 ? 'valid' : 'expired';\n};\n\n/** Drop any active code + counter for the slot (e.g. after a completed login). */\nexport const clearEmailCode = async (purpose: EmailOtpPurpose, identity: string): Promise<void> => {\n await redis.del(codeKey(purpose, identity));\n await redis.del(attemptsKey(purpose, identity));\n};\n","//? TOTP (RFC 6238) + HOTP (RFC 4226) — hand-rolled on node:crypto, zero deps.\n//? Powers authenticator-app 2FA (Google/Microsoft Authenticator, Authy,\n//? 1Password, …): they all speak this open standard via an otpauth:// URI.\n//? Verification is timing-safe; replay protection is the CALLER's job — verify\n//? returns the matched timestep so the flow layer can persist \"last accepted\n//? timestep\" and reject codes at or before it (see twoFactor.ts).\n\nimport crypto from 'node:crypto';\n\n//? RFC 4648 base32 alphabet — authenticator apps expect the shared secret in\n//? base32 (both in the otpauth:// URI and for manual entry).\nconst BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n\nexport const base32Encode = (input: Buffer): string => {\n let bits = 0;\n let value = 0;\n let output = '';\n for (const byte of input) {\n value = (value << 8) | byte;\n bits += 8;\n while (bits >= 5) {\n output += BASE32_ALPHABET[(value >>> (bits - 5)) & 31];\n bits -= 5;\n }\n }\n if (bits > 0) output += BASE32_ALPHABET[(value << (5 - bits)) & 31];\n return output;\n};\n\nexport const base32Decode = (input: string): Buffer | null => {\n //? Tolerant of the formats users paste: lowercase, spaces, `=` padding.\n const normalized = input.toUpperCase().replaceAll(/[\\s=]/g, '');\n if (normalized.length === 0) return null;\n let bits = 0;\n let value = 0;\n const bytes: number[] = [];\n for (const char of normalized) {\n const index = BASE32_ALPHABET.indexOf(char);\n if (index === -1) return null;\n value = (value << 5) | index;\n bits += 5;\n if (bits >= 8) {\n bytes.push((value >>> (bits - 8)) & 0xFF);\n bits -= 8;\n }\n }\n return Buffer.from(bytes);\n};\n\n//? HOTP (RFC 4226 §5.3): HMAC-SHA1 over the big-endian counter, dynamic\n//? truncation, modulo 10^digits. Exported for the RFC test vectors.\nexport const hotp = (key: Buffer, counter: number, digits = 6): string => {\n const counterBuffer = Buffer.alloc(8);\n //? JS bitwise ops are 32-bit; write the 64-bit counter via BigInt.\n counterBuffer.writeBigUInt64BE(BigInt(counter));\n const digest = crypto.createHmac('sha1', key).update(counterBuffer).digest();\n const offset = digest.readUInt8(digest.length - 1) & 0x0F;\n const binary = digest.readUInt32BE(offset) & 0x7F_FF_FF_FF;\n return String(binary % 10 ** digits).padStart(digits, '0');\n};\n\nexport interface TotpVerifyInput {\n /** base32-encoded shared secret (as provisioned to the authenticator app). */\n secret: string;\n /** The user-entered code (digits; surrounding whitespace tolerated). */\n code: string;\n /**\n * Accepted clock drift in 30s steps on EITHER side of \"now\" (default 1 —\n * i.e. the previous, current and next code are accepted; RFC 6238 §5.2\n * recommends at most one backward step).\n */\n window?: number;\n /** Injection point for tests; defaults to the real clock. */\n timestampMs?: number;\n}\n\nexport interface TotpVerifyResult {\n valid: boolean;\n /**\n * The timestep the code matched (only when valid). Persist the highest\n * accepted value per user and refuse `timestep <= lastAccepted` to make\n * every code single-use (replay protection across the drift window).\n */\n timestep: number | null;\n}\n\nconst TOTP_STEP_SECONDS = 30;\n\nexport const verifyTotp = ({ secret, code, window = 1, timestampMs }: TotpVerifyInput): TotpVerifyResult => {\n const key = base32Decode(secret);\n const normalizedCode = code.trim();\n if (!key || key.length === 0 || !/^\\d{6,8}$/.test(normalizedCode)) return { valid: false, timestep: null };\n\n const now = timestampMs ?? Date.now();\n const currentStep = Math.floor(now / 1000 / TOTP_STEP_SECONDS);\n const drift = Math.max(0, Math.min(window, 4)); //? cap: a huge window would defeat the point of TOTP\n\n //? Check EVERY step in the window (no early return) with a timing-safe\n //? comparison, so response time never leaks which step (if any) matched.\n let matched: number | null = null;\n for (let step = currentStep - drift; step <= currentStep + drift; step++) {\n const expected = hotp(key, step, normalizedCode.length);\n const expectedBuffer = Buffer.from(expected);\n const actualBuffer = Buffer.from(normalizedCode);\n if (expectedBuffer.length === actualBuffer.length && crypto.timingSafeEqual(expectedBuffer, actualBuffer)) {\n matched ??= step;\n }\n }\n return matched === null ? { valid: false, timestep: null } : { valid: true, timestep: matched };\n};\n\n/** 20 random bytes (RFC 4226 §4 minimum: 128 bits; 160 recommended), base32. */\nexport const generateTotpSecret = (): string => base32Encode(crypto.randomBytes(20));\n\nexport interface OtpauthUriInput {\n /** base32 secret from generateTotpSecret(). */\n secret: string;\n /** What the authenticator app shows as the account (usually the user's email). */\n accountName: string;\n /** App/brand name shown above the account (defaults to the project title). */\n issuer: string;\n}\n\n//? The provisioning URI encoded into the enrollment QR code. Every\n//? authenticator app understands this format (Google's key-uri spec):\n//? otpauth://totp/<issuer>:<account>?secret=…&issuer=…&algorithm=SHA1&digits=6&period=30\nexport const buildOtpauthUri = ({ secret, accountName, issuer }: OtpauthUriInput): string => {\n const label = `${encodeURIComponent(issuer)}:${encodeURIComponent(accountName)}`;\n const query = new URLSearchParams({\n secret,\n issuer,\n algorithm: 'SHA1',\n digits: '6',\n period: String(TOTP_STEP_SECONDS),\n });\n return `otpauth://totp/${label}?${query.toString()}`;\n};\n","//? Passwordless email-code login (ADR 0024): the user enters their email,\n//? receives a short numeric code, and signs in by typing it. Opt-in via\n//? `auth.emailCodeLogin` (default false); needs @luckystack/email.\n//?\n//? Anti-enumeration mirrors forgotPassword.ts: the REQUEST endpoint always\n//? answers \"sent\" whether or not the address has an account — a code is only\n//? actually issued (and an email only actually sent) when a credentials user\n//? exists. Request throttling is done here (per-email + per-IP) because the\n//? send costs real email quota.\n//?\n//? 2FA interplay: possession of the mailbox is itself an email factor, but an\n//? account that enrolled an authenticator app expects the STRONGER factor to\n//? gate sign-in — so an enrolled user still gets the 2FA challenge after the\n//? email code verifies (same gate as the password path).\n\nimport { checkRateLimit, getLogger, getProjectConfig, getProjectName, tryCatch } from '@luckystack/core';\n\nimport { resolveUserByEmail } from './accountStrategy';\nimport { clearEmailCode, issueEmailCode, verifyEmailCode } from './emailOtp';\nimport { finalizeLogin, type CredentialsLoginResult } from './login';\nimport { sendCodeEmail, createTwoFactorChallengeIfRequired } from './twoFactor';\nimport { getUserAdapter } from './userAdapter';\n\nconst emailCodeConfig = () => getProjectConfig().auth;\n\nconst normalizeEmail = (email: string): string => email.trim().toLowerCase();\n\n//? Same window the auth lockout uses; request budgets are deliberately tight —\n//? each request is an outbound email.\nconst REQUEST_WINDOW_MS = 15 * 60 * 1000;\nconst REQUESTS_PER_EMAIL = 3;\nconst REQUESTS_PER_IP = 10;\n\nexport interface RequestEmailLoginCodeInput {\n email: string;\n requesterIp?: string;\n}\n\nexport type RequestEmailLoginCodeResult =\n | { ok: true }\n | { ok: false; reason: string };\n\nexport const requestEmailLoginCode = async ({ email, requesterIp }: RequestEmailLoginCodeInput): Promise<RequestEmailLoginCodeResult> => {\n const config = emailCodeConfig();\n if (!config.emailCodeLogin) return { ok: false, reason: 'login.emailCodeDisabled' };\n const normalized = normalizeEmail(email);\n if (!normalized || normalized.length > config.emailMaxLength) return { ok: false, reason: 'login.invalidEmailFormat' };\n\n //? Throttle BEFORE any user lookup so the limiter can't be used as an\n //? enumeration oracle either (both outcomes hit the same budget).\n const perEmail = await checkRateLimit({ key: `email-code:req:${normalized}`, limit: REQUESTS_PER_EMAIL, windowMs: REQUEST_WINDOW_MS });\n const perIp = requesterIp\n ? await checkRateLimit({ key: `email-code:req-ip:${requesterIp}`, limit: REQUESTS_PER_IP, windowMs: REQUEST_WINDOW_MS })\n : { allowed: true };\n if (!perEmail.allowed || !perIp.allowed) return { ok: false, reason: 'api.rateLimitExceeded' };\n\n //? Email-code login signs into the CREDENTIALS account for the address\n //? (consistent with forgot-password); under `providerAccountStrategy:\n //? 'unified'` the resolver already collapses providers by email.\n const [findError, user] = await tryCatch(() =>\n resolveUserByEmail(getUserAdapter(), { email: normalized, provider: 'credentials' }),\n );\n if (findError) {\n getLogger().error('email-code login: user lookup failed', findError);\n //? Still answer ok — a backend blip must not become an enumeration signal.\n return { ok: true };\n }\n if (!user) {\n //? Anti-enumeration: identical response; only a server-side log.\n getLogger().info('[email-code] request for unknown address (not sent)');\n return { ok: true };\n }\n\n const code = await issueEmailCode({\n purpose: 'login',\n identity: normalized,\n ttlSeconds: config.emailCodeTtlSeconds,\n digits: config.emailCodeLength,\n });\n //? Fire-and-forget the send. Awaiting it would (a) make the existing-account\n //? path measurably slower than the immediate ghost-account return above — a\n //? timing oracle — and (b) leak existence via a send-failure reason. So we\n //? ALWAYS return the same `{ ok: true }` (identical to the unknown-address\n //? path) and log a send failure server-side only. The user simply requests a\n //? new code if it never arrives.\n void sendCodeEmail(\n normalized,\n code,\n 'Your sign-in code',\n `Use this code to sign in to ${getProjectName()}.`,\n ).then((sent) => {\n if (!sent) getLogger().error('[email-code] login code send failed (account exists) — check the email adapter');\n });\n return { ok: true };\n};\n\nexport interface VerifyEmailLoginCodeInput {\n email: string;\n code: string;\n supersedeToken?: string;\n requesterIp?: string;\n}\n\nexport const verifyEmailLoginCode = async (input: VerifyEmailLoginCodeInput): Promise<CredentialsLoginResult> => {\n const config = emailCodeConfig();\n if (!config.emailCodeLogin) return { status: false, reason: 'login.emailCodeDisabled' };\n const normalized = normalizeEmail(input.email);\n\n const verdict = await verifyEmailCode({\n purpose: 'login',\n identity: normalized,\n code: input.code,\n maxAttempts: config.emailCodeMaxAttempts,\n });\n if (verdict !== 'valid') {\n const reason = verdict === 'invalid' ? 'login.emailCodeInvalid' : (verdict === 'locked' ? 'login.emailCodeLocked' : 'login.emailCodeExpired');\n return { status: false, reason };\n }\n\n const [findError, user] = await tryCatch(() =>\n resolveUserByEmail(getUserAdapter(), { email: normalized, provider: 'credentials' }),\n );\n if (findError || !user) {\n //? The code was valid, so the account existed moments ago — a vanished\n //? user (deleted mid-flow) fails closed.\n getLogger().error('email-code login: user vanished between issue and verify', findError ?? undefined);\n return { status: false, reason: 'login.emailCodeExpired' };\n }\n\n await clearEmailCode('login', normalized);\n\n //? Authenticator-enrolled accounts still answer the stronger factor (see header).\n const challenge = await createTwoFactorChallengeIfRequired(user, { requesterIp: input.requesterIp });\n if (challenge) return challenge;\n\n return finalizeLogin(user, {\n provider: 'credentials',\n email: normalized,\n supersedeToken: input.supersedeToken,\n requesterIp: input.requesterIp,\n });\n};\n"],"mappings":"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","names":["URLSearchParams","tryCatch","getLogger","getProjectConfig","getProjectConfig","getLogger","getProjectConfig","getProjectConfig","getProjectConfig","tryCatch","getLogger","validationError","response","URLSearchParams","bcrypt","getProjectConfig","formatKey","formatKey","getProjectConfig","bcrypt","hash","getProjectConfig","dispatchHook","getLogger","getProjectConfig","getLogger","dispatchHook","getProjectConfig","issueOneTimeToken","formatKey","redis","oneTimeTokenKey","getProjectConfig","getProjectConfig","crypto","dispatchHook","formatKey","getLogger","getProjectConfig","redis","tryCatch","formatKey","redis","crypto","crypto","getLogger","formatKey","redis","getProjectConfig","tryCatch","hash","dispatchHook","checkRateLimit","getLogger","getProjectConfig","getProjectName","tryCatch","getProjectConfig","checkRateLimit","tryCatch","getLogger","getProjectName"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/authLockout.ts","../src/redirectResolver.ts","../src/login.ts","../src/accountStrategy.ts","../src/passwordPolicy.ts","../src/data/commonPasswords.ts","../src/passwordReset.ts","../src/forgotPassword.ts","../src/emailModuleLoader.ts","../src/emailChange.ts","../src/emailChangeNotification.ts","../src/twoFactor.ts","../src/emailOtp.ts","../src/totp.ts","../src/emailCodeLogin.ts"],"sourcesContent":["// Registers module augmentations on @luckystack/core for auth/session hooks.\nimport './hookPayloads';\n\n//? Decoupling seam (0.2.0): register this package's session implementation into\n//? @luckystack/core so api/sync/presence/server read sessions through core's\n//? null-safe accessors WITHOUT importing @luckystack/login directly. This is\n//? what makes login a genuine OPTIONAL package — when it isn't installed nothing\n//? registers and the accessors return null (unauthenticated). Side-effect at\n//? module load: any app that imports @luckystack/login (its overlay, LoginForm,\n//? config) wires this before the first request is served.\nimport { registerSessionProvider } from '@luckystack/core';\nimport { saveSession as _saveSession, getSession as _getSession, deleteSession as _deleteSession } from './session';\nimport { logout as _logout } from './logout';\nregisterSessionProvider({\n getSession: _getSession,\n saveSession: _saveSession,\n deleteSession: _deleteSession,\n logout: ({ token, socket, userId, skipSessionDelete }) =>\n _logout({ token, socket, userId: userId ?? null, skipSessionDelete }),\n});\n\n//? Subscribe the per-account brute-force lockout to the `loginFailed` hook at\n//? boot (F7). Idempotent + cheap; the recorder no-ops unless\n//? `rateLimiting.auth.enabled` is set, so this is safe to wire unconditionally.\nimport { registerAuthLockoutHook } from './authLockout';\nregisterAuthLockoutHook();\n\nexport type { BaseSessionLayout, SessionLocation, AuthProps } from './sessionLayout';\nexport type {\n PreLoginPayload,\n PostLoginPayload,\n PreRegisterPayload,\n PostRegisterPayload,\n PreLogoutPayload,\n PostLogoutPayload,\n PreSessionCreatePayload,\n PostSessionCreatePayload,\n PreSessionDeletePayload,\n PostSessionDeletePayload,\n PasswordResetRequestedPayload,\n PrePasswordResetCompletedPayload,\n PasswordResetCompletedPayload,\n PrePasswordChangedPayload,\n PasswordChangedPayload,\n PreEmailChangePayload,\n PostEmailChangeRequestedPayload,\n PostEmailChangedPayload,\n LoginFailedPayload,\n PreAccountDeletePayload,\n PostAccountDeletePayload,\n} from './hookPayloads';\nexport { saveSession, getSession, deleteSession, getAllSessions, revokeUserSessions, sessionKeyFor, activeUsersKeyFor } from './session';\nexport { registerSessionAdapter, getSessionAdapter, redisSessionAdapter } from './sessionAdapter';\nexport type { SessionAdapter } from './sessionAdapter';\nexport { registerSessionSanitizer, getSessionSanitizer } from './sessionSanitizer';\nexport type { SessionSanitizer } from './sessionSanitizer';\nexport { loginWithCredentials, loginCallback, createOAuthState, registerWithCredentials, loginWithCredentialsCore, finalizeLogin, registerTwoFactorGate } from './login';\nexport type {\n CredentialsLoginResult,\n CredentialsLoginSuccess,\n CredentialsLoginChallenge,\n CredentialsLoginFailure,\n TwoFactorMethod,\n TwoFactorGate,\n OAuthCallbackResult,\n} from './login';\nexport type { CreateOAuthStateResult } from './login';\nexport { OAUTH_STATE_COOKIE_NAME } from './login';\nexport { logout } from './logout';\n\n// Per-account brute-force lockout (F7). The hook is auto-registered at boot;\n// these are exported for consumers who want to query/clear a lock explicitly\n// (e.g. an admin \"unlock account\" action) or build their own surface on top.\nexport { isAccountLocked, recordAuthFailure, clearAuthFailures } from './authLockout';\n\n// Password-reset primitives. Used by the framework's `framework`-mode\n// forgot-password flow AND exported for consumers who picked `'custom'`.\nexport {\n createPasswordResetToken,\n consumePasswordResetToken,\n updatePasswordHash,\n verifyPassword,\n PasswordPolicyError,\n} from './passwordReset';\nexport { validatePassword } from './passwordPolicy';\nexport { sendPasswordResetEmail } from './forgotPassword';\n\n// Email-change primitives. Used by the framework's settings flow to confirm\n// a new email address before applying it (token mailed to NEW address proves\n// ownership of the new mailbox).\nexport {\n createEmailChangeToken,\n consumeEmailChangeToken,\n} from './emailChange';\nexport type { EmailChangePayload } from './emailChange';\nexport { sendEmailChangeConfirmation } from './emailChangeNotification';\n\n// OAuth provider registry + composable helpers.\nexport {\n registerOAuthProviders,\n registerOAuthProviderFactory,\n getOAuthProviders,\n isFullOAuthProvider,\n asOAuthUserData,\n credentialsProvider,\n googleProvider,\n githubProvider,\n discordProvider,\n facebookProvider,\n microsoftProvider,\n} from './oauthProviders';\nexport type {\n OAuthProvider,\n CredentialsProvider,\n FullOAuthProvider,\n OAuthUserData,\n} from './oauthProviders';\n\n// User adapter registry: lets consumers swap out the Prisma User model behind auth flows.\nexport {\n registerUserAdapter,\n getUserAdapter,\n isUserAdapterRegistered,\n defaultPrismaUserAdapter,\n} from './userAdapter';\nexport type {\n UserAdapter,\n UserAdapterCreateInput,\n UserRecord,\n} from './userAdapter';\n\n// 2FA (ADR 0024): TOTP (authenticator apps) + email-code fallback + recovery\n// codes. IMPORTING THIS MODULE ARMS THE LOGIN GATE (it registers itself into\n// login.ts at init) — which happens right here in the package index, so any\n// app with @luckystack/login installed gets the challenge step for enrolled\n// users once `auth.twoFactor` is 'optional'.\nexport {\n beginTotpEnrollment,\n confirmTotpEnrollment,\n disableTwoFactor,\n regenerateRecoveryCodes,\n verifyTwoFactorChallenge,\n requestTwoFactorEmailCode,\n availableTwoFactorMethods,\n createTwoFactorChallengeIfRequired,\n} from './twoFactor';\nexport type { TotpEnrollmentStart, ConfirmTotpEnrollmentResult, VerifyTwoFactorInput } from './twoFactor';\nexport { verifyTotp, generateTotpSecret, buildOtpauthUri, hotp, base32Encode, base32Decode } from './totp';\n\n// Passwordless email-code login (ADR 0024) + the reusable numeric-OTP store.\nexport { requestEmailLoginCode, verifyEmailLoginCode } from './emailCodeLogin';\nexport type { RequestEmailLoginCodeInput, RequestEmailLoginCodeResult, VerifyEmailLoginCodeInput } from './emailCodeLogin';\nexport { issueEmailCode, verifyEmailCode, clearEmailCode, generateNumericCode } from './emailOtp';\nexport type { EmailOtpPurpose, EmailCodeVerdict } from './emailOtp';\n\n// Post-login redirect resolver — lets consumers compute the OAuth callback\n// destination dynamically (per-user, per-tenant, per-provider).\nexport {\n registerPostLoginRedirect,\n getPostLoginRedirect,\n} from './redirectResolver';\nexport type {\n PostLoginRedirectResolver,\n PostLoginRedirectInput,\n} from './redirectResolver';\n","//? Per-account brute-force lockout (F7 / MIS-017). The generic per-IP throttle\n//? on `/auth/api/credentials` (in @luckystack/server) does NOT stop distributed\n//? credential stuffing against a SINGLE account from many IPs. This module adds\n//? a per-account failed-attempt counter, keyed independently from the api/ip\n//? limits, that locks an account after `rateLimiting.auth.maxAttempts` failures\n//? within `rateLimiting.auth.windowMs`.\n//?\n//? It is OPT-IN: when `rateLimiting.auth.enabled === false` (the default) every\n//? function here is a no-op and behaviour is byte-identical to before — only\n//? the existing per-IP throttle applies.\n//?\n//? The counter rides on the framework rate limiter (`checkRateLimit`) so it\n//? inherits the configured store (memory/redis) and multi-instance behaviour.\n//? `recordAuthFailure` increments; `isAccountLocked` reads WITHOUT incrementing\n//? (so merely checking the lock can't itself trip it); `clearAuthFailures`\n//? resets the counter on a successful login.\n\nimport {\n getProjectConfig,\n checkRateLimit,\n getRateLimitStatus,\n clearRateLimit,\n getLogger,\n registerHook,\n tryCatch,\n} from '@luckystack/core';\n\n//? DD-LOGIN-F5: build the lockout bucket key. When `requesterIp` is supplied\n//? the key is an IP+account composite (`auth:<email>:<ip>`) so a remote attacker\n//? from one IP cannot lock out an account for legitimate users on different IPs.\n//? Without an IP the key is the bare account key (`auth:<email>`) — same\n//? behaviour as before. The composite bucket is cleared on success the same way\n//? (same key is used in `clearAuthFailures`). Lower-case + trim the account\n//? identifier so `Alice@x.com` and `alice@x.com` share one lockout bucket\n//? (matches the credentials email normalization).\nconst lockoutKey = (accountKey: string, requesterIp?: string): string => {\n const base = `auth:${accountKey.trim().toLowerCase()}`;\n return requesterIp ? `${base}:${requesterIp}` : base;\n};\n\n//? One-shot guard so the inert-config warning below fires once per process, not\n//? on every login attempt.\nlet warnedLockoutInert = false;\n\n//? One-shot guard for the M1 warning: surfaces the silent gap where credentials\n//? logins are FAILING but the per-account lockout is disabled entirely, so only\n//? the proxy-fragile per-IP cap stands between an attacker and distributed\n//? credential stuffing. Fires once, on the first real credentials-login failure.\nlet warnedLockoutDisabled = false;\n\n/** Read the active auth-lockout config (returns null when the feature is off). */\nconst getAuthLockoutConfig = (): { maxAttempts: number; maxAttemptsPerAccount: number; windowMs: number } | null => {\n const rateLimiting = getProjectConfig().rateLimiting;\n if (!rateLimiting.auth.enabled) return null;\n //? The counter rides the framework rate limiter, which short-circuits when the\n //? GLOBAL `rateLimiting.enabled` is false — so `auth.enabled:true` +\n //? `enabled:false` makes recordAuthFailure/isAccountLocked silent no-ops, i.e. a\n //? believed-on-but-INERT brute-force defense. Warn once so the conflict surfaces.\n if (!rateLimiting.enabled && !warnedLockoutInert) {\n warnedLockoutInert = true;\n getLogger().warn(\n '[LuckyStack] rateLimiting.auth.enabled is true but rateLimiting.enabled is false — the per-account brute-force lockout is INERT (it rides the global rate limiter, which is off). Set rateLimiting.enabled to true for the lockout to take effect.',\n );\n }\n return {\n maxAttempts: rateLimiting.auth.maxAttempts,\n maxAttemptsPerAccount: rateLimiting.auth.maxAttemptsPerAccount,\n windowMs: rateLimiting.auth.windowMs,\n };\n};\n\n/**\n * Whether `accountKey` is currently locked out (failed-attempt counter already\n * at/over the cap). Non-incrementing — safe to call on every login attempt.\n * Always `false` when the feature is disabled.\n * Pass `requesterIp` to check the IP+account composite bucket (DD-LOGIN-F5).\n */\nexport const isAccountLocked = async (accountKey: string, requesterIp?: string): Promise<boolean> => {\n const cfg = getAuthLockoutConfig();\n if (!cfg || !accountKey) return false;\n //? DUAL counter (DD-LOGIN-F5 + the cross-IP fix). Lock if EITHER trips:\n //? - the bare-account bucket (`auth:<email>`) against the cross-IP cap\n //? (`maxAttemptsPerAccount`) — the distributed-credential-stuffing defense, and\n //? - the IP+account composite (`auth:<email>:<ip>`) against the per-IP cap\n //? (`maxAttempts`) — bounds one IP + shields other IPs from a victim-lock DoS.\n const accountStatus = await getRateLimitStatus(lockoutKey(accountKey), cfg.maxAttemptsPerAccount);\n if (accountStatus.remaining <= 0) return true;\n if (requesterIp) {\n const ipStatus = await getRateLimitStatus(lockoutKey(accountKey, requesterIp), cfg.maxAttempts);\n if (ipStatus.remaining <= 0) return true;\n }\n return false;\n};\n\n/**\n * Increment the failed-attempt counter for `accountKey`. Call on every failed\n * credentials login for a known/looked-up account. No-op when disabled.\n * Pass `requesterIp` to increment the IP+account composite bucket (DD-LOGIN-F5).\n */\nexport const recordAuthFailure = async (accountKey: string, requesterIp?: string): Promise<void> => {\n const cfg = getAuthLockoutConfig();\n if (!cfg || !accountKey) return;\n //? Increment BOTH the cross-IP bare-account counter and (when an IP is known)\n //? the per-IP composite counter — mirrors the dual gate in isAccountLocked.\n const account = await checkRateLimit({\n key: lockoutKey(accountKey),\n limit: cfg.maxAttemptsPerAccount,\n windowMs: cfg.windowMs,\n });\n let ipBlocked = false;\n if (requesterIp) {\n const ip = await checkRateLimit({\n key: lockoutKey(accountKey, requesterIp),\n limit: cfg.maxAttempts,\n windowMs: cfg.windowMs,\n });\n ipBlocked = !ip.allowed;\n }\n if (!account.allowed || ipBlocked) {\n getLogger().warn('[authLockout] account temporarily locked after repeated failures', {\n maxAttempts: cfg.maxAttempts,\n maxAttemptsPerAccount: cfg.maxAttemptsPerAccount,\n crossIp: !account.allowed,\n });\n }\n};\n\n/**\n * Reset the failed-attempt counter for `accountKey` (call after a SUCCESSFUL\n * login so a user who eventually gets their password right isn't penalised by\n * earlier typos). No-op when disabled.\n * Pass `requesterIp` to clear the IP+account composite bucket (DD-LOGIN-F5).\n */\nexport const clearAuthFailures = async (accountKey: string, requesterIp?: string): Promise<void> => {\n const cfg = getAuthLockoutConfig();\n if (!cfg || !accountKey) return;\n //? Clear BOTH counters on a successful login so earlier typos don't penalise the\n //? user — the cross-IP bare-account bucket and the per-IP composite bucket.\n //? L6: callers invoke this fire-and-forget (`void clearAuthFailures(...)`) on the\n //? success path, so a transient Redis error must NOT become an unhandled\n //? rejection. Swallow + log; a stale lockout counter self-expires within the\n //? window, so a missed clear is non-fatal.\n const [error] = await tryCatch(async () => {\n await clearRateLimit(lockoutKey(accountKey));\n if (requesterIp) await clearRateLimit(lockoutKey(accountKey, requesterIp));\n });\n if (error) {\n getLogger().warn('[authLockout] clearAuthFailures failed (non-fatal — counter self-expires)', { error });\n }\n};\n\n//? ADR 0012 — the lockout counter must reflect GENUINE credential failures only.\n//? This is an ALLOW-LIST, not a deny-list: only a real wrong-password attempt\n//? (`login.wrongPassword`, covering both the no-account/no-hash dummy-compare\n//? path and a failed bcrypt compare) feeds the per-account counter. Everything\n//? else is excluded by construction — `preLogin` veto rejections (whose reason\n//? is a consumer-controlled `errorCode`), password-POLICY rejections (M-15),\n//? input-shape errors, the `login.accountLocked` self-trip, and infra failures\n//? (DB find / bcrypt throw, surfaced via `toReasonKey`). A deny-list let any of\n//? those — most dangerously a consumer `preLogin` veto an attacker can trigger\n//? with just a victim's email and no password — silently drive the lockout and\n//? DoS the account. Keep this allow-list narrow.\nconst COUNTING_REASONS = new Set([\n 'login.wrongPassword',\n]);\n\nlet lockoutHookRegistered = false;\n\n/**\n * Wire the per-account lockout into the `loginFailed` hook (F7). Subscribes once\n * (idempotent) and increments the failed-attempt counter only on a credentials\n * LOGIN failure that carries an email AND is a genuine wrong-password attempt\n * (see `COUNTING_REASONS`). Registration is unconditional (cheap); the recorder\n * itself no-ops when `rateLimiting.auth.enabled` is false, so toggling the\n * config at runtime is honoured without re-wiring.\n */\nexport const registerAuthLockoutHook = (): void => {\n if (lockoutHookRegistered) return;\n lockoutHookRegistered = true;\n registerHook('loginFailed', async ({ email, provider, reason, stage, requesterIp }) => {\n if (stage !== 'login' || provider !== 'credentials') return;\n //? M1: a credentials login is failing while the per-account lockout is OFF.\n //? Warn ONCE so operators of a credentials app don't ship with distributed\n //? credential stuffing unthrottled beyond the (proxy-fragile) per-IP cap. Kept\n //? as a loud default rather than force-enabling the lockout, because enabling\n //? the cross-IP counter also introduces a victim-account-lock DoS tradeoff the\n //? operator should opt into consciously (set rateLimiting.auth.enabled:true).\n if (!getProjectConfig().rateLimiting.auth.enabled && !warnedLockoutDisabled) {\n warnedLockoutDisabled = true;\n getLogger().warn(\n '[LuckyStack] a credentials login failed but the per-account brute-force lockout is DISABLED (rateLimiting.auth.enabled:false). Only the per-IP API cap protects credentials login, so distributed credential stuffing against one account (many IPs) is unthrottled. Set rateLimiting.auth.enabled:true to enable the per-account lockout (note: the cross-IP counter also enables a bounded victim-account-lock DoS — see docs/ARCHITECTURE_AUTH.md).',\n );\n }\n if (!email || !COUNTING_REASONS.has(reason)) return;\n //? DD-LOGIN-F5: use the IP+account composite key when an IP was threaded in.\n await recordAuthFailure(email, requesterIp);\n });\n};\n","//? Post-login redirect resolver. Lets consumers compute the OAuth callback\n//? redirect URL dynamically (per-user, per-tenant, per-provider).\n//?\n//? Why a dedicated registry instead of a hook:\n//? The framework's hooks are \"stop-or-continue\" — handlers return undefined\n//? or a stop signal. They don't carry a value. A redirect-resolution\n//? handler must RETURN a string, so it lives in its own registry.\n//?\n//? Validation: the resolved URL is checked against\n//? `ProjectConfig.http.cors.allowedOrigins` (plus the localhost convenience when\n//? `http.cors.allowLocalhost` is on) before being used. An invalid URL falls back\n//? to `ProjectConfig.loginRedirectUrl`.\n\nexport interface PostLoginRedirectInput {\n userId: string;\n provider: string;\n isNewUser: boolean;\n defaultUrl: string;\n}\n\nexport type PostLoginRedirectResolver = (\n input: PostLoginRedirectInput,\n) => string | Promise<string>;\n\nlet activeResolver: PostLoginRedirectResolver | null = null;\n\nexport const registerPostLoginRedirect = (\n resolver: PostLoginRedirectResolver,\n): PostLoginRedirectResolver => {\n activeResolver = resolver;\n return resolver;\n};\n\nexport const getPostLoginRedirect = (): PostLoginRedirectResolver | null => activeResolver;\n","import { getOAuthProviders, isFullOAuthProvider, type FullOAuthProvider, type OAuthUserData } from './oauthProviders';\nimport { getPostLoginRedirect } from './redirectResolver';\nimport { IncomingMessage, ServerResponse } from 'node:http';\nimport { URLSearchParams } from 'node:url';\nimport { tryCatch, tryCatchSync, redis as redisClient, getUploadsDir, dispatchHook, getLogger, formatKey, getProjectConfig, getCookieValue, resolveDevCallbackUrl } from '@luckystack/core';\nimport bcrypt from 'bcryptjs';\nimport { randomBytes, createHash, timingSafeEqual } from 'node:crypto';\nimport { saveSession } from \"./session\"\nimport validator from 'validator';\nimport type { BaseSessionLayout as SessionLayout } from './sessionLayout';\nimport { getUserAdapter, type UserRecord } from './userAdapter';\nimport { resolveUserByEmail } from './accountStrategy';\nimport { validatePassword } from './passwordPolicy';\nimport { isAccountLocked, clearAuthFailures } from './authLockout';\nimport path from 'node:path';\nimport { promises as fsPromises } from 'node:fs';\n\n//? Combined login/register request body. When `name` + `confirmPassword` are\n//? both present the dispatcher registers; otherwise it logs in.\ninterface LoginOrRegisterParams {\n email?: string,\n password?: string,\n name?: string,\n confirmPassword?: string,\n}\n\n//? Discriminated result of the credentials login/register surface (QUA-080).\n//? Exporting this lets `@luckystack/server`'s auth route narrow on `status`\n//? instead of casting the result to an inline shape. On a full success,\n//? `newToken` + `session` are always present; on `status: false` only `reason`\n//? is meaningful (the failure branch never mints a token). The CHALLENGE\n//? member (ADR 0024) is the 2FA half-way state: the first factor was correct\n//? but NO session exists yet — the route relays `challengeToken` to the client\n//? and `/auth/api/2fa` completes the login. The `?: undefined` markers keep\n//? property access legal across the whole union (`result.newToken`).\nexport type TwoFactorMethod = 'totp' | 'email-code' | 'recovery-code';\nexport interface CredentialsLoginSuccess {\n status: true;\n reason: string;\n newToken: string;\n session: SessionLayout;\n requiresTwoFactor?: undefined;\n}\nexport interface CredentialsLoginChallenge {\n status: true;\n reason: string;\n requiresTwoFactor: true;\n challengeToken: string;\n twoFactorMethods: TwoFactorMethod[];\n newToken?: undefined;\n session?: undefined;\n}\nexport interface CredentialsLoginFailure {\n status: false;\n reason: string;\n newToken?: undefined;\n session?: undefined;\n requiresTwoFactor?: undefined;\n}\nexport type CredentialsLoginResult = CredentialsLoginSuccess | CredentialsLoginChallenge | CredentialsLoginFailure;\n\n//? Resolved at call time via getUploadsDir() so consumer path overrides win.\nconst uploadsFolder = (): string => getUploadsDir();\n//? Resolve at call time so dotenv/test-setup timing can't capture a stale\n//? value at module load. Routes through `projectConfig.logging.devLogs`\n//? which is the standard predicate everywhere else in the codebase.\nconst isDevMode = (): boolean => getProjectConfig().logging.devLogs;\nconst { compare, genSalt, hash } = bcrypt;\n\n//? LOGIN-F22: the dummy hash used on the user-not-found path must be generated\n//? at the CONFIGURED bcrypt rounds, not hard-pinned to cost 10. If the consumer\n//? raises `auth.bcryptRounds` (e.g. to 12), a cost-10 dummy compare finishes in\n//? ~80 ms while a real compare takes ~400 ms — the 5× difference reopens the\n//? user-enumeration timing channel we closed with the dummy compare. We lazily\n//? generate one hash per unique rounds value (cached so the per-request compare\n//? stays O(1) after the first login at a given cost) and use that hash for all\n//? subsequent not-found compares at the same cost.\nconst dummyHashCache = new Map<number, string>();\nconst getDummyBcryptHash = async (): Promise<string> => {\n const rounds = getProjectConfig().auth.bcryptRounds;\n const cached = dummyHashCache.get(rounds);\n if (cached) return cached;\n const salt = await genSalt(rounds);\n const generated = await hash(randomBytes(16).toString('hex'), salt);\n dummyHashCache.set(rounds, generated);\n return generated;\n};\n//? `validator` is a CommonJS module whose helpers hang off the default export;\n//? destructuring the default is the documented usage. The named-export warning\n//? is a false positive for this CJS interop shape.\n// eslint-disable-next-line import-x/no-named-as-default-member\nconst { escape, isEmail } = validator;\n\n//? Use the shared `getProjectName()` helper so the OAuth state Redis key\n//? prefix matches the namespace used everywhere else (sessions, activeUsers,\n//? rate-limit, password-reset). Previously this used a separate\n//? `auth.oauthStateProjectNameFallback` config field which could silently\n//? drift from `session.projectName`.\nconst getOAuthStateKey = (providerName: string, state: string): string => {\n return formatKey('-oauth-state', `${providerName}:${state}`);\n};\n\n//? Cookie the server sets at the OAuth-authorize step (F1). It carries a random\n//? nonce bound to the Redis state entry; the callback requires the cookie nonce\n//? to hash to the value stored alongside the state before it accepts the\n//? callback. This proves the SAME browser that started the flow is completing it\n//? (closes the login-CSRF / session-fixation hole where an attacker delivers\n//? their own valid `code+state` to a victim). Server + login MUST agree on this\n//? name — it is exported so `@luckystack/server`'s authorize route reuses it.\nexport const OAUTH_STATE_COOKIE_NAME = 'ls-oauth-state';\n\n//? Hash the browser-binding nonce before persisting it in Redis: the Redis\n//? state value is server-side-readable, so storing only the SHA-256 means a\n//? leaked state entry can't be replayed without the matching cookie nonce.\nconst hashStateNonce = (nonce: string): string =>\n createHash('sha256').update(nonce).digest('hex');\n\n//? Generate a PKCE verifier + its S256 challenge (RFC 7636). Only used when a\n//? provider opts in via `usePkce` — default OAuth flows are byte-identical to\n//? before. The verifier is stored server-side (in the Redis state value) and\n//? replayed at token exchange; the challenge is handed to the authorize step.\nconst base64Url = (buf: Buffer): string =>\n buf.toString('base64').replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');\nconst generatePkcePair = (): { verifier: string; challenge: string } => {\n const verifier = base64Url(randomBytes(32));\n const challenge = base64Url(createHash('sha256').update(verifier).digest());\n return { verifier, challenge };\n};\n\n//? Server-side payload persisted under the Redis state key. `nonceHash` binds\n//? the flow to the initiating browser's cookie; `codeVerifier` (PKCE) is the\n//? secret replayed at token exchange.\ninterface OAuthStateEntry {\n nonceHash: string;\n codeVerifier?: string;\n //? Frontend URL to redirect to after the OAuth callback completes. Stored\n //? server-side in Redis (never echoed from the client at callback time) so it\n //? is tamper-proof. Validated against allowedOrigins + allowLocalhost before\n //? use — see isAllowedRedirectUrl. Absent when the auth initiation did not\n //? carry a return_url param (falls back to the configured loginRedirectUrl).\n returnUrl?: string;\n}\n\nexport interface CreateOAuthStateResult {\n /** Opaque `state` param put on the authorize URL + echoed back on callback. */\n state: string;\n /**\n * Random nonce the server must set as the value of the {@link OAUTH_STATE_COOKIE_NAME}\n * cookie (HttpOnly, SameSite=Lax, short TTL) at the authorize redirect. The\n * callback requires this back to accept the flow (browser-binding, F1).\n */\n stateCookie: string;\n /**\n * PKCE `code_challenge` (S256) when the provider opted into PKCE — the server\n * appends `code_challenge=<value>&code_challenge_method=S256` to the authorize\n * URL. `undefined` for non-PKCE providers (default).\n */\n codeChallenge?: string;\n}\n\nexport const createOAuthState = async (\n providerName: string,\n options?: { usePkce?: boolean; returnUrl?: string },\n): Promise<CreateOAuthStateResult | null> => {\n const state = randomBytes(32).toString('hex');\n const nonce = randomBytes(32).toString('hex');\n const key = getOAuthStateKey(providerName, state);\n\n const pkce = options?.usePkce ? generatePkcePair() : null;\n const entry: OAuthStateEntry = {\n nonceHash: hashStateNonce(nonce),\n ...(pkce ? { codeVerifier: pkce.verifier } : {}),\n ...(options?.returnUrl ? { returnUrl: options.returnUrl } : {}),\n };\n\n //? State TTL is consumer-configurable via `auth.oauthStateTtlSeconds`\n //? (default 600s / 10 min). Too short and a legitimate, slow provider\n //? round-trip expires mid-flow and the callback fails state validation\n //? (UX/soft-DoS); too long and a stolen state stays replayable. The `NX`\n //? guard keeps each state single-use even before the TTL elapses, and\n //? `consumeOAuthState` deletes it on first redemption. The value is now a\n //? JSON envelope (nonce hash + optional PKCE verifier) instead of the literal\n //? `'1'` so the flow can be bound to the initiating browser (F1) and carry a\n //? PKCE secret (F11).\n const result = await redisClient.set(\n key,\n JSON.stringify(entry),\n 'EX',\n getProjectConfig().auth.oauthStateTtlSeconds,\n 'NX',\n );\n\n if (result !== 'OK') {\n return null;\n }\n\n return {\n state,\n stateCookie: nonce,\n ...(pkce ? { codeChallenge: pkce.challenge } : {}),\n };\n};\n\n//? Atomically read + delete the state entry (single-use), then verify the\n//? browser-binding nonce. Returns the parsed entry on success (so the caller can\n//? read `codeVerifier` for the PKCE token exchange), or `null` on any failure:\n//? missing state, missing/empty cookie nonce, Redis miss, malformed payload, or\n//? a nonce that doesn't match the stored hash. `cookieNonce` is the value the\n//? server read from the {@link OAUTH_STATE_COOKIE_NAME} cookie on the callback\n//? request.\nconst consumeOAuthState = async (\n providerName: string,\n state: string,\n cookieNonce: string,\n): Promise<OAuthStateEntry | null> => {\n if (!state || !cookieNonce) {\n return null;\n }\n\n const key = getOAuthStateKey(providerName, state);\n const txResult = await redisClient.multi().get(key).del(key).exec();\n if (!txResult || txResult.length < 2) {\n return null;\n }\n\n const getResult = txResult[0];\n if (!getResult || getResult[0]) {\n return null;\n }\n\n //? Verify the DEL also succeeded (txResult[1][0] is the per-command error slot).\n //? If DEL failed the state key was not consumed — fail closed so the OAuth state\n //? cannot be replayed with the same nonce before its TTL expires (mirrors the\n //? consumeOneTimeToken hardening in @luckystack/core).\n const delResult = txResult[1];\n if (!delResult || delResult[0]) {\n return null;\n }\n\n const rawValue = getResult[1];\n if (typeof rawValue !== 'string') {\n return null;\n }\n\n const [parseErr, entry] = tryCatchSync(() => JSON.parse(rawValue) as OAuthStateEntry);\n if (parseErr || !entry || typeof entry.nonceHash !== 'string') {\n return null;\n }\n\n //? Constant-time compare of the two fixed-length (SHA-256) hex digests via\n //? `timingSafeEqual` (QUA-21). Both operands are 64-char hex strings, so their\n //? byte buffers are equal-length; a length mismatch (malformed stored value)\n //? falls through to reject. A mismatch means the browser completing the flow is\n //? NOT the one that started it — reject (F1).\n const incomingHash = Buffer.from(hashStateNonce(cookieNonce));\n const storedHash = Buffer.from(entry.nonceHash);\n if (incomingHash.length !== storedHash.length || !timingSafeEqual(incomingHash, storedHash)) {\n return null;\n }\n\n return entry;\n};\n\nconst asRecord = (value: unknown): Record<string, unknown> => {\n if (value && typeof value === 'object') {\n return value as Record<string, unknown>;\n }\n return {};\n};\n\n//? Read a string-ish field from an untyped provider JSON record. Strings pass\n//? through; finite numbers / booleans are coerced (preserving the previous\n//? `String(value)` behaviour for primitive fields); objects, arrays, null and\n//? undefined collapse to `fallback` instead of stringifying to `[object Object]`.\nconst readStringField = (record: Record<string, unknown>, key: string, fallback = ''): string => {\n const value = record[key];\n if (typeof value === 'string') return value;\n if (typeof value === 'number' && Number.isFinite(value)) return String(value);\n if (typeof value === 'boolean') return String(value);\n return fallback;\n};\n\n//? Random 6-digit hex color used as the avatar placeholder for users without\n//? an uploaded image. Shared by the credentials-register and OAuth-create paths.\nconst generateAvatarFallbackColor = (): string =>\n `#${Math.floor(Math.random() * 0xFF_FF_FF).toString(16).padStart(6, \"0\")}`;\n\n//? LOGIN-F18: check whether a user has an uploaded webp avatar on disk and\n//? return the filename when found. Used by both the credentials and OAuth login\n//? paths to avoid duplicating the async stat pattern.\nconst resolveUploadedAvatar = async (userId: string): Promise<string | null> => {\n const filePath = path.join(uploadsFolder(), `${userId}.webp`);\n const [, avatarStat] = await tryCatch(() => fsPromises.stat(filePath));\n return avatarStat ? `${userId}.webp` : null;\n};\n\n//? Strips every column that must never reach Redis or the client: the bcrypt\n//? hash AND the 2FA material (ADR 0024). Consumers with more sensitive columns\n//? add a `registerSessionSanitizer` on top; this is the always-on floor.\nconst sanitizeUserForSession = <T extends { password?: unknown; totpSecret?: unknown; recoveryCodes?: unknown }>(\n user: T,\n): Omit<T, 'password' | 'totpSecret' | 'recoveryCodes'> => {\n const { password: _password, totpSecret: _totpSecret, recoveryCodes: _recoveryCodes, ...safeUser } = user;\n return safeUser;\n};\n\n//? Fire-and-forget observational failure signal. Dispatched on every failed\n//? login/register/OAuth attempt so consumers can audit, feed a SIEM, or build\n//? per-account lockout (HOK-10). Never awaited and never throws — a failing\n//? handler must not change the auth outcome the user already got.\nconst emitLoginFailed = (payload: {\n email?: string;\n userId?: string;\n provider: string;\n reason: string;\n stage: 'login' | 'register' | 'oauth';\n //? DD-LOGIN-F5: optional resolved client IP — threaded from the HTTP auth\n //? route so the per-account lockout can build an IP+account composite key.\n requesterIp?: string;\n}): void => {\n void dispatchHook('loginFailed', payload);\n};\n\nconst toReasonKey = (error: unknown, fallback = 'api.internalServerError'): string => {\n if (typeof error === 'string' && error.length > 0) {\n return error;\n }\n\n if (error instanceof Error && error.message) {\n const message = error.message.toLowerCase();\n if (message.includes('authentication failed') || message.includes('scram failure')) {\n return 'api.internalServerError';\n }\n }\n\n return fallback;\n};\n\ninterface NormalizedCredentials {\n email: string;\n password: string;\n name: string | undefined;\n confirmPassword: string | undefined;\n}\n\nconst normalizeCredentials = (params: LoginOrRegisterParams): NormalizedCredentials => ({\n //? Don't HTML-escape password OR confirmPassword before bcrypt — neither\n //? reaches HTML, and they're memory-compared for equality during register.\n //? Escaping confirmPassword while leaving password raw made any password\n //? containing `& < > \" '` fail the `password !== confirmPassword` check\n //? (regression introduced in pass-1 fix #9 which only un-escaped password).\n //? Email is normalized (lowercase + trim) and validated via `isEmail`;\n //? HTML-escaping it would make legacy raw rows unfindable.\n email: (params.email ?? '').trim().toLowerCase(),\n password: params.password ?? '',\n name: params.name ? escape(params.name) : undefined,\n //? Empty-string confirmPassword must collapse to `undefined` so the\n //? register-vs-login dispatcher's truthiness check treats a blank field as\n //? \"absent\". `??` would keep the empty string (only nullish collapses), so the\n //? falsy-coercing `||` is deliberate here.\n // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional empty-string → undefined collapse\n confirmPassword: params.confirmPassword || undefined,\n});\n\n//? Shape-validate normalized credentials. `mode` decides whether the\n//? password-POLICY check runs:\n//? - `'register'` enforces the full policy (length, complexity, common-list,\n//? customValidator) — a new account MUST meet the active policy.\n//? - `'login'` does NOT run the policy (M-15). A login must accept ANY password\n//? string and let the bcrypt compare decide pass/fail. Running the policy on\n//? login is both a lockout-DoS vector (an attacker POSTing policy-violating\n//? passwords for a victim's email could otherwise feed the per-account\n//? failed-attempt counter) AND a correctness bug (tightening the policy would\n//? lock out existing users whose stored password no longer meets it). Empty /\n//? email-shape checks still run in both modes.\nconst validateCredentialsShape = (\n creds: NormalizedCredentials,\n mode: 'login' | 'register',\n): { status: false; reason: string } | null => {\n const authLimits = getProjectConfig().auth;\n if (!creds.email || !creds.password) return { status: false, reason: 'login.empty' };\n if (creds.email.length > authLimits.emailMaxLength) return { status: false, reason: 'login.emailCharacterLimit' };\n if (creds.name && creds.name.length > authLimits.nameMaxLength) return { status: false, reason: 'login.nameCharacterLimit' };\n if (!isEmail(creds.email)) return { status: false, reason: 'login.invalidEmailFormat' };\n if (mode === 'register') {\n //? Full password-policy check (length, complexity, common-list, customValidator).\n //? Policy lives in `projectConfig.auth.passwordPolicy`; the deprecated\n //? `passwordMinLength`/`passwordMaxLength` top-level fields are still read\n //? from the policy's `minLength`/`maxLength` defaults. REGISTER-only — see the\n //? function doc for why the login branch skips it (M-15).\n const passwordReason = validatePassword(creds.password);\n if (passwordReason) return { status: false, reason: passwordReason };\n }\n return null;\n};\n\nconst registerWithCredentials = async (\n {\n email,\n password,\n name,\n confirmPassword,\n }: {\n email: string;\n password: string;\n name: string;\n confirmPassword: string;\n },\n options?: { supersedeToken?: string },\n): Promise<CredentialsLoginResult> => {\n if (password !== confirmPassword) {\n emitLoginFailed({ email, provider: 'credentials', reason: 'login.passwordNotMatch', stage: 'register' });\n return { status: false, reason: 'login.passwordNotMatch' };\n }\n\n const preRegisterResult = await dispatchHook('preRegister', { email, provider: 'credentials', name });\n if (preRegisterResult.stopped) {\n emitLoginFailed({ email, provider: 'credentials', reason: preRegisterResult.signal.errorCode, stage: 'register' });\n return { status: false, reason: preRegisterResult.signal.errorCode };\n }\n\n //? LOGIN-F15: apply the per-account lockout check before the email-existence\n //? lookup. Without this an attacker can call the register endpoint repeatedly\n //? for a victim's email and perform unbounded `resolveUserByEmail` probes —\n //? effectively bypassing the lockout by choosing the register code path. A\n //? locked account means the credentials flow is temporarily suspended for that\n //? email; the same sentinel applies regardless of which surface (login vs\n //? register) is being used. No-op when `rateLimiting.auth.enabled` is false.\n if (await isAccountLocked(email)) {\n emitLoginFailed({ email, provider: 'credentials', reason: 'login.accountLocked', stage: 'register' });\n return { status: false, reason: 'login.accountLocked' };\n }\n\n const userAdapter = getUserAdapter();\n const [checkEmailError, checkEmailResponse] = await tryCatch(() =>\n resolveUserByEmail(userAdapter, { email, provider: 'credentials' })\n );\n if (checkEmailError) {\n getLogger().error('login: findByEmail failed during register', checkEmailError);\n const reason = toReasonKey(checkEmailError);\n emitLoginFailed({ email, provider: 'credentials', reason, stage: 'register' });\n return { status: false, reason };\n }\n if (checkEmailResponse) {\n emitLoginFailed({ email, provider: 'credentials', reason: 'login.emailExists', stage: 'register' });\n return { status: false, reason: 'login.emailExists' };\n }\n\n const createNewUser = async () => {\n const salt = await genSalt(getProjectConfig().auth.bcryptRounds);\n const hashedPassword = await hash(password, salt);\n return await userAdapter.create({\n email,\n provider: 'credentials',\n name,\n password: hashedPassword,\n avatar: '',\n avatarFallback: generateAvatarFallbackColor(),\n language: getProjectConfig().defaultLanguage,\n });\n };\n\n const [createNewUserError, createNewUserResponse] = await tryCatch(createNewUser);\n if (createNewUserError) {\n //? LOGIN-F7: treat a unique-constraint violation as a TOCTOU race — another\n //? concurrent request created this row between our findFirst and our create.\n //? Under the `'unified'` strategy (where `email @unique` is recommended) this\n //? is the expected concurrent-registration signal; return `login.emailExists`\n //? so the client can prompt the user to log in instead.\n const errorMessage =\n createNewUserError instanceof Error ? createNewUserError.message : String(createNewUserError);\n const isUniqueViolation =\n errorMessage.includes('Unique constraint') ||\n errorMessage.includes('unique constraint') ||\n // Prisma error code P2002 covers all DB engines\n (createNewUserError as { code?: unknown }).code === 'P2002';\n if (isUniqueViolation) {\n emitLoginFailed({ email, provider: 'credentials', reason: 'login.emailExists', stage: 'register' });\n return { status: false, reason: 'login.emailExists' };\n }\n const reason = toReasonKey(createNewUserError);\n emitLoginFailed({ email, provider: 'credentials', reason, stage: 'register' });\n return { status: false, reason };\n }\n if (!createNewUserResponse) {\n emitLoginFailed({ email, provider: 'credentials', reason: 'login.createUserFailed', stage: 'register' });\n return { status: false, reason: 'login.createUserFailed' };\n }\n\n await dispatchHook('postRegister', { userId: createNewUserResponse.id, provider: 'credentials' });\n\n //? Auto-login after register (ARCHITECTURE_AUTH.md: both flows return\n //? `{ status, session, newToken }`). Mirrors the login branch: mint a token,\n //? persist the session, and hand the token back so the route can set the\n //? cookie / the client can store it and land on `loginRedirectUrl`.\n const newToken = randomBytes(32).toString('hex');\n const nowLogin = new Date();\n const newUser: SessionLayout = {\n ...sanitizeUserForSession(createNewUserResponse),\n token: newToken,\n lastLogin: nowLogin,\n previousLogin: null,\n };\n\n const saved = await saveSession(newToken, newUser, true, { supersedeToken: options?.supersedeToken });\n if (!saved.ok) {\n //? Account exists but the session never persisted — surface the failure so\n //? the route does not report an authenticated state it cannot back up. The\n //? user can recover by logging in normally.\n emitLoginFailed({ email, userId: newUser.id, provider: 'credentials', reason: saved.errorCode, stage: 'register' });\n return { status: false, reason: saved.errorCode };\n }\n await dispatchHook('postLogin', { userId: newUser.id, provider: 'credentials', isNewUser: true, token: newToken });\n\n return {\n status: true,\n reason: 'login.userCreated',\n newToken,\n session: newUser,\n };\n};\n\n//? ─── 2FA gate slot (ADR 0024) ───\n//? DI registry (same idiom as registerUserAdapter / registerSessionAdapter)\n//? instead of importing twoFactor.ts here — that would be a login ↔ twoFactor\n//? module cycle. twoFactor.ts registers the gate at module init (pulled in via\n//? the package index); with nothing registered, first-factor logins complete\n//? directly — which is also why every existing unit test keeps its behavior.\nexport type TwoFactorGate = (\n user: UserRecord,\n context: { requesterIp?: string },\n) => Promise<CredentialsLoginChallenge | null>;\nlet twoFactorGate: TwoFactorGate | null = null;\nexport const registerTwoFactorGate = (gate: TwoFactorGate): void => {\n twoFactorGate = gate;\n};\nconst applyTwoFactorGate: TwoFactorGate = async (user, context) =>\n (twoFactorGate ? twoFactorGate(user, context) : null);\n\n//? Shared post-authentication tail (ADR 0024): every first-factor-verified\n//? path — password login, email-code login, and the completed 2FA challenge —\n//? funnels through here so the session-minting behavior can never drift\n//? between them. Mint token → best-effort lastLogin → session layout →\n//? saveSession → postLogin hook → clear lockout counters (when email known).\nexport const finalizeLogin = async (\n user: UserRecord,\n options: { provider: string; email?: string; supersedeToken?: string; requesterIp?: string },\n): Promise<CredentialsLoginResult> => {\n const newToken = randomBytes(32).toString('hex');\n const previousLogin = user.lastLogin ?? null;\n const nowLogin = new Date();\n\n // Best-effort lastLogin update — silently no-ops if the user adapter\n // (or User schema) doesn't accept the field.\n await tryCatch(() => getUserAdapter().update(user.id, { lastLogin: nowLogin }));\n\n const newUser: SessionLayout = {\n ...sanitizeUserForSession(user),\n token: newToken,\n lastLogin: nowLogin,\n previousLogin,\n };\n\n const uploadedAvatar = await resolveUploadedAvatar(newUser.id);\n if (uploadedAvatar) {\n newUser.avatar = uploadedAvatar;\n }\n\n const saved = await saveSession(newToken, newUser, true, { supersedeToken: options.supersedeToken });\n if (!saved.ok) {\n //? Session never persisted (adapter blip / preSessionCreate veto). Fail the\n //? login so the route does NOT set a cookie or delete the prior session for\n //? a token that getSession() can't resolve.\n emitLoginFailed({ email: options.email, userId: newUser.id, provider: options.provider, reason: saved.errorCode, stage: 'login', requesterIp: options.requesterIp });\n return { status: false, reason: saved.errorCode };\n }\n await dispatchHook('postLogin', { userId: newUser.id, provider: options.provider, isNewUser: false, token: newToken });\n //? Reset the brute-force counter on success so earlier typos don't keep a\n //? legitimate user locked out (F7). No-op when the feature is disabled.\n //? Clear the IP+account composite key (DD-LOGIN-F5) AND the bare account key\n //? so that failures recorded without an IP (e.g. from a different surface) are\n //? also cleared after a confirmed legitimate login.\n if (options.email) {\n void clearAuthFailures(options.email, options.requesterIp);\n if (options.requesterIp) void clearAuthFailures(options.email);\n }\n if (isDevMode()) {\n getLogger().debug(`${options.provider} login success for user ${newUser.id}`);\n }\n return { status: true, reason: 'login.loggedIn', newToken, session: newUser };\n};\n\nconst loginWithCredentialsCore = async (\n { email, password }: { email: string; password: string },\n //? DD-LOGIN-F5: `requesterIp` is optional so all existing callers stay\n //? compatible. The HTTP auth route resolves and passes the IP; socket paths\n //? omit it and fall back to the pure-account lockout bucket.\n options?: { supersedeToken?: string; requesterIp?: string },\n): Promise<CredentialsLoginResult> => {\n const requesterIp = options?.requesterIp;\n const preLoginResult = await dispatchHook('preLogin', { email, provider: 'credentials' });\n if (preLoginResult.stopped) {\n emitLoginFailed({ email, provider: 'credentials', reason: preLoginResult.signal.errorCode, stage: 'login', requesterIp });\n return { status: false, reason: preLoginResult.signal.errorCode };\n }\n\n //? Per-account brute-force lockout (F7 / MIS-017). When the auth rate-limit\n //? slot is enabled and this account has exhausted its failed-attempt budget,\n //? refuse the attempt BEFORE any bcrypt work — distributed credential stuffing\n //? against one account (which the per-IP throttle can't see) is throttled\n //? per-account here. The counter is incremented by the registered `loginFailed`\n //? handler (see `registerAuthLockoutHook`); merely checking the lock does not\n //? increment it. No-op when `rateLimiting.auth.enabled` is false (default).\n if (await isAccountLocked(email, requesterIp)) {\n emitLoginFailed({ email, provider: 'credentials', reason: 'login.accountLocked', stage: 'login', requesterIp });\n return { status: false, reason: 'login.accountLocked' };\n }\n\n const userAdapter = getUserAdapter();\n const [findUserError, findUserResponse] = await tryCatch(() =>\n resolveUserByEmail(userAdapter, { email, provider: 'credentials' })\n );\n if (findUserError) {\n getLogger().error('login: findByEmail failed', findUserError);\n const reason = toReasonKey(findUserError);\n emitLoginFailed({ email, provider: 'credentials', reason, stage: 'login', requesterIp });\n return { status: false, reason };\n }\n //? Anti-enumeration: a missing account and a missing/invalid password hash\n //? must be INDISTINGUISHABLE from a wrong password — same reason key AND\n //? similar timing. Returning a distinct `login.userNotFound` (and short-\n //? circuiting before any bcrypt work) let an attacker probe which addresses\n //? have credentials accounts via both the response key and a timing side\n //? channel. When there is no real hash to compare, run a bcrypt compare\n //? against a fixed dummy hash so the not-found path spends roughly the same\n //? CPU as a real wrong-password compare, then return the shared key.\n const passwordHash = findUserResponse?.password ?? null;\n if (!findUserResponse || !passwordHash) {\n const dummyHash = await getDummyBcryptHash();\n await tryCatch(() => compare(password, dummyHash));\n //? M3: AWAIT the failure dispatch on the counting (wrong-password) path so the\n //? per-account lockout increment (registered on `loginFailed`) lands BEFORE we\n //? respond — unlike the fire-and-forget `emitLoginFailed` used for observational\n //? failures. This closes the sequential check-then-increment TOCTOU (a retry now\n //? sees the updated counter). `dispatchHook` isolates handler errors, so this\n //? still can't change the auth outcome. A residual window remains for requests\n //? already in flight concurrently (bounded by the atomic per-IP cap) — inherent\n //? to any check-then-act gate.\n await dispatchHook('loginFailed', { email, userId: findUserResponse?.id, provider: 'credentials', reason: 'login.wrongPassword', stage: 'login', requesterIp });\n return { status: false, reason: 'login.wrongPassword' };\n }\n\n const [checkPasswordError, checkPasswordResponse] = await tryCatch(() =>\n compare(password, passwordHash)\n );\n if (checkPasswordError) {\n getLogger().error('login: bcrypt compare failed', checkPasswordError);\n const reason = toReasonKey(checkPasswordError);\n emitLoginFailed({ email, userId: findUserResponse.id, provider: 'credentials', reason, stage: 'login', requesterIp });\n return { status: false, reason };\n }\n if (!checkPasswordResponse) {\n //? M3: awaited on the counting path (see the no-account branch above) so the\n //? lockout increment lands before the response — closes the sequential TOCTOU.\n await dispatchHook('loginFailed', { email, userId: findUserResponse.id, provider: 'credentials', reason: 'login.wrongPassword', stage: 'login', requesterIp });\n return { status: false, reason: 'login.wrongPassword' };\n }\n\n //? Second factor (ADR 0024): the password is verified, but an enrolled user\n //? must answer a 2FA challenge before any session is minted. The registered\n //? gate returns null when 2FA is disabled or the user never enrolled — then\n //? the login completes directly through the shared tail.\n const challenge = await applyTwoFactorGate(findUserResponse, { requesterIp });\n if (challenge) return challenge;\n\n return finalizeLogin(findUserResponse, {\n provider: 'credentials',\n email,\n supersedeToken: options?.supersedeToken,\n requesterIp,\n });\n};\n\n//? Thin dispatcher: keeps the existing single-entry HTTP surface but forwards\n//? to the dedicated register / login functions. `name && confirmPassword`\n//? present in the body means the client is registering; otherwise it's a\n//? login.\nconst loginWithCredentials = async (\n params: LoginOrRegisterParams,\n //? DD-LOGIN-F5: `requesterIp` forwarded to `loginWithCredentialsCore` to\n //? enable IP+account composite lockout keys on the HTTP auth path.\n options?: { supersedeToken?: string; requesterIp?: string },\n): Promise<CredentialsLoginResult> => {\n const creds = normalizeCredentials(params);\n\n if (isDevMode()) {\n getLogger().debug(`credentials auth attempt for ${creds.email || 'unknown-email'}`);\n }\n\n //? Decide the branch BEFORE shape-validation so the password-POLICY check only\n //? runs on register (M-15): `name && confirmPassword` present => register;\n //? otherwise login. A login must accept any password string (the bcrypt\n //? compare is the only authority), so an attacker can't trip a victim's\n //? lockout counter by POSTing policy-violating passwords for their email.\n //? Narrowing on the destructured locals keeps `name`/`confirmPassword` typed as\n //? `string` inside the register branch (no cast needed).\n const { name, confirmPassword } = creds;\n if (name && confirmPassword) {\n const validationError = validateCredentialsShape(creds, 'register');\n if (validationError) return validationError;\n //? Public-registration gate (F18). When `auth.allowRegistration === false`\n //? (invite-only / admin-provisioned apps) refuse the register branch with a\n //? dedicated reason key BEFORE any user lookup or hook. OAuth-driven\n //? first-login account creation is governed separately by the provider flow.\n //? Default `true` keeps today's open-registration behavior.\n if (!getProjectConfig().auth.allowRegistration) {\n emitLoginFailed({ email: creds.email, provider: 'credentials', reason: 'auth.registrationDisabled', stage: 'register' });\n return { status: false, reason: 'auth.registrationDisabled' };\n }\n return registerWithCredentials({\n email: creds.email,\n password: creds.password,\n name,\n confirmPassword,\n }, options);\n }\n\n const validationError = validateCredentialsShape(creds, 'login');\n if (validationError) return validationError;\n return loginWithCredentialsCore({ email: creds.email, password: creds.password }, options);\n};\n\nexport interface OAuthCallbackResult {\n token: string;\n redirectUrl: string;\n userId: string;\n provider: string;\n isNewUser: boolean;\n}\n\nconst isAllowedRedirectUrl = (url: string): boolean => {\n if (!URL.canParse(url, 'http://placeholder')) return false;\n //? Reject relative URLs whose path starts with a backslash or contains any\n //? backslash before the same-origin shortcut (SEC-12): `new URL('/\\\\evil.com',\n //? base)` keeps the placeholder origin and would pass the same-origin check,\n //? yet browsers normalise `/\\` → `//` (protocol-relative) → open-redirect to\n //? `evil.com`. A legitimate same-origin path never needs a backslash.\n if (url.includes('\\\\')) return false;\n const parsed = new URL(url, 'http://placeholder');\n if (parsed.origin === 'http://placeholder') {\n // relative URL — same-origin, always safe\n return true;\n }\n //? `allowedOrigins` is `string[] | (origin: string) => boolean` since we\n //? added the function-resolver variant. Branch on the shape.\n //? Defensive default at a security boundary (redirect-origin validation):\n //? the type is non-nullable, but a malformed consumer config could still\n //? hand us `undefined` at runtime, so the `?? []` fail-closed guard stays.\n //? `allowLocalhost` is the dev convenience that accepts any http://localhost:*\n //? origin without needing an explicit allowedOrigins entry. Safe in production\n //? because allowLocalhost defaults to false there.\n const cfg = getProjectConfig();\n if (cfg.http.cors.allowLocalhost && parsed.hostname === 'localhost') return true;\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime-defensive guard at a security boundary\n const allowed = cfg.http.cors.allowedOrigins ?? [];\n if (typeof allowed === 'function') {\n return allowed(parsed.origin);\n }\n return allowed.some((origin) => {\n if (!URL.canParse(origin)) return false;\n return new URL(origin).origin === parsed.origin;\n });\n};\n\nconst exchangeOAuthToken = async (\n provider: FullOAuthProvider,\n code: string,\n codeVerifier?: string,\n): Promise<string | null> => {\n //? `redirect_uri` is pinned to the registered, immutable `provider.callbackURL`\n //? — the SAME static config value the authorization request is built from.\n //? It is never derived from the incoming request, so there is no per-request\n //? drift to re-validate against (the authorization-time value and the\n //? exchange-time value are guaranteed identical by construction). Persisting\n //? it into the Redis state would be a behaviour-preserving no-op here; if the\n //? callback URL ever becomes request-/tenant-derived, store it alongside the\n //? state in `createOAuthState` and compare before exchange.\n //? `code_verifier` (PKCE, F11) is included ONLY when the provider opted in and\n //? a verifier was stored with the state — for every existing flow it is\n //? `undefined` and the exchange body is unchanged.\n //? `resolveDevCallbackUrl` applies the SAME dev-port rewrite the authorize step\n //? used (`getBindAddress()` is process-constant, so the two agree byte-for-byte,\n //? which OAuth's exchange-must-match-authorize rule requires). No-op in prod.\n const redirectUri = resolveDevCallbackUrl(provider.callbackURL);\n const values = {\n code,\n client_id: provider.clientID,\n client_secret: provider.clientSecret,\n redirect_uri: redirectUri,\n grant_type: 'authorization_code',\n ...(codeVerifier ? { code_verifier: codeVerifier } : {}),\n };\n\n //? `Promise<unknown>` return annotation: `response.json()` is `any`, and an\n //? explicit `unknown` boundary type forces every caller to narrow via\n //? `asRecord(...)` instead of leaking `any` through the token-exchange seam.\n const getToken = async (): Promise<unknown> => {\n if (provider.tokenExchangeMethod === 'json') {\n const response = await fetch(provider.tokenExchangeURL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },\n body: JSON.stringify(values),\n });\n if (!response.ok) {\n getLogger().warn('oauth: token exchange returned non-OK status', { provider: provider.name, status: response.status });\n return null;\n }\n return await response.json();\n }\n //? `tokenExchangeMethod` is the closed union `'json' | 'form'`; the `json`\n //? branch returned above, so this is the exhaustive `form` case.\n const formParams = new URLSearchParams();\n formParams.append('client_id', provider.clientID);\n formParams.append('client_secret', provider.clientSecret);\n formParams.append('code', values.code);\n formParams.append('grant_type', 'authorization_code');\n formParams.append('redirect_uri', redirectUri);\n if (codeVerifier) {\n formParams.append('code_verifier', codeVerifier);\n }\n\n if (isDevMode()) {\n //? NEVER stringify the raw form — it carries `client_secret` (a\n //? long-lived credential) and the single-use authorization `code`. Log\n //? only the param NAMES so the dev sees the shape without leaking secrets\n //? into the log sink.\n getLogger().debug('oauth: token-exchange form params', { paramNames: [...formParams.keys()] });\n }\n const response = await fetch(provider.tokenExchangeURL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' },\n body: formParams.toString(),\n });\n if (!response.ok) {\n getLogger().warn('oauth: token exchange returned non-OK status', { provider: provider.name, status: response.status });\n return null;\n }\n return await response.json();\n };\n\n const [error, response] = await tryCatch(getToken);\n if (error) {\n getLogger().error('oauth: token exchange failed', error);\n return null;\n }\n const tokenData = asRecord(response);\n const accessToken = typeof tokenData.access_token === 'string' ? tokenData.access_token : '';\n if (!accessToken) {\n getLogger().warn('oauth: no access_token in token response', { provider: provider.name });\n return null;\n }\n return accessToken;\n};\n\ninterface OAuthProfile {\n email: string | undefined;\n name: string;\n avatar: string;\n /**\n * `true` only when the provider POSITIVELY reported the email as verified\n * (via `emailVerifiedKey`) or supplied it through a `getEmail` accessor that\n * itself filters on a verified flag (GitHub). `false` when the provider\n * exposes NO verified-email signal at all — used to fail-closed on\n * cross-provider account LINKING under the `'unified'` strategy (SEC-21/SEC-40),\n * where linking an unverified address to a victim's existing account is an\n * account-takeover vector. First-time account CREATION is unaffected.\n */\n emailVerified: boolean;\n /**\n * The RAW, un-stripped provider userInfo record. Threaded through so the\n * `extraSessionFields` hook receives every provider-specific claim (`sub`,\n * tenant id, custom claims) instead of the trimmed `{email,name,avatar}`\n * projection — the documented contract (SEC-3 / oauthProviders.ts hook docs).\n */\n rawUserData: OAuthUserData;\n}\n\nconst fetchOAuthProfile = async (\n provider: FullOAuthProvider,\n accessToken: string,\n): Promise<OAuthProfile | null> => {\n const getUserData = async (): Promise<unknown> => {\n const response = await fetch(provider.userInfoURL, {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'Authorization': `Bearer ${accessToken}`,\n },\n });\n if (!response.ok) {\n getLogger().warn('oauth: userInfo fetch returned non-OK status', { provider: provider.name, status: response.status });\n return null;\n }\n return await response.json();\n };\n\n const [error, response] = await tryCatch(getUserData);\n if (error) {\n getLogger().error('oauth: userInfo fetch failed', error);\n return null;\n }\n\n const userData = asRecord(response);\n //? Provider responses are untyped JSON; read each field through `readString`\n //? so a non-string (object/number/missing) value falls back instead of\n //? stringifying to `[object Object]`.\n //? A present-but-empty name string falls back too (mirrors the old\n //? `String(value || 'didnt find a name')` truthy-coercion).\n const resolvedName = readStringField(userData, provider.nameKey);\n const name = resolvedName === '' ? 'didnt find a name' : resolvedName;\n const emailValue = userData[provider.emailKey];\n let email: string | undefined = typeof emailValue === 'string' ? emailValue : undefined;\n\n //? Track whether the provider POSITIVELY confirmed this email as verified.\n //? `emailVerifiedKey === true` (or any non-false value when the key is set and\n //? present) counts as verified; a provider with NO `emailVerifiedKey` exposes\n //? no signal, so the email stays `emailVerified: false` and only first-time\n //? account creation (not cross-provider linking) is allowed downstream.\n let emailVerified = false;\n\n //? Reject a provider-supplied email that the provider explicitly marks\n //? unverified (e.g. Discord's `verified: false`). A missing flag is treated\n //? as \"no signal\" (not verified, but not rejected outright — see the\n //? cross-provider link guard in `findOrCreateOAuthUser`). Defense-in-depth\n //? against account-linking takeover under `'unified'` (SEC-21).\n if (email && provider.emailVerifiedKey) {\n const verifiedFlag = userData[provider.emailVerifiedKey];\n //? Only a STRICT boolean `true` counts as verified. A present-but-not-true\n //? value (`false`, the strings \"false\"/\"0\", `0`, `null`) is an explicit\n //? not-verified signal → reject. This hardens against a CUSTOM provider that\n //? encodes the flag as a string/number (a truthy `\"false\"` previously slipped\n //? through as verified, re-opening cross-provider account-linking takeover).\n //? A MISSING flag (undefined) is \"no signal\" — not verified, not rejected here;\n //? the cross-provider link guard in findOrCreateOAuthUser handles it (SEC-21).\n if (verifiedFlag !== undefined && verifiedFlag !== true) {\n getLogger().warn('oauth: provider email is not verified — rejecting', { provider: provider.name });\n return null;\n }\n if (verifiedFlag === true) emailVerified = true;\n }\n\n //? Providers whose userinfo only returns an email once it is CONFIRMED (e.g.\n //? Facebook Graph /me) expose no separate verified flag — the presence of the\n //? email IS the verified signal. `emailImpliesVerified` opts such a provider in\n //? so cross-provider LINK + first-login CREATION both treat it as verified.\n if (email && provider.emailImpliesVerified) {\n emailVerified = true;\n }\n\n const avatarId = provider.avatarCodeKey ? userData[provider.avatarCodeKey] : undefined;\n const avatarValue = provider.avatarKey\n ? readStringField(userData, provider.avatarKey)\n : (provider.getAvatar\n ? await provider.getAvatar({ userData, avatarId: typeof avatarId === 'string' ? avatarId : '', accessToken })\n : '');\n const avatar = typeof avatarValue === 'string' ? avatarValue : '';\n\n //? Some providers (GitHub) don't return email in /userinfo — fall back to the\n //? provider's `getEmail` accessor. GitHub's accessor only ever selects a\n //? `verified === true` address (see `githubProvider.getEmail`), so an email\n //? obtained this way is treated as positively verified for the cross-provider\n //? link guard (SEC-21).\n if (!email && provider.getEmail) {\n const selectedEmail = await provider.getEmail(accessToken);\n if (!selectedEmail) {\n getLogger().warn('oauth: no email found via provider.getEmail', { provider: provider.name });\n return null;\n }\n email = selectedEmail;\n emailVerified = true;\n }\n\n //? Normalize OAuth emails through the SAME gate as credentials (trim +\n //? lowercase + `isEmail`) before they reach the adapter. Credentials emails\n //? are normalized at `normalizeCredentials`; taking the provider value\n //? verbatim meant `Victim@x.com` (OAuth) and `victim@x.com` (credentials)\n //? resolved to different rows — which under the `'unified'` account strategy\n //? (case-sensitive lookup) silently DEFEATS cross-provider linking. A\n //? present-but-malformed provider email is treated as \"no usable email\".\n if (email !== undefined) {\n const normalizedEmail = email.trim().toLowerCase();\n if (!isEmail(normalizedEmail)) {\n getLogger().warn('oauth: provider returned a malformed email', { provider: provider.name });\n return null;\n }\n email = normalizedEmail;\n }\n\n return { email, name, avatar, emailVerified, rawUserData: userData };\n};\n\ninterface ResolvedOAuthUser {\n user: SessionLayout;\n isNewUser: boolean;\n}\n\nconst findOrCreateOAuthUser = async (\n provider: FullOAuthProvider,\n profile: OAuthProfile,\n): Promise<ResolvedOAuthUser | null> => {\n const { email, name, avatar, emailVerified } = profile;\n if (!email) return null;\n\n const preLoginResult = await dispatchHook('preLogin', { email, provider: provider.name });\n if (preLoginResult.stopped) {\n getLogger().warn(`oauth login aborted by preLogin hook`, { errorCode: preLoginResult.signal.errorCode });\n return null;\n }\n\n const userAdapter = getUserAdapter();\n //? Under `'unified'` this links to an existing account with the same email\n //? created via ANY provider (incl. credentials); under `'per-provider'` it\n //? stays scoped to this provider (existing behavior).\n const [findError, findResponse] = await tryCatch(() =>\n resolveUserByEmail(userAdapter, { email, provider: provider.name })\n );\n if (findError) {\n getLogger().error('oauth: findByEmail failed', findError);\n return null;\n }\n\n if (findResponse?.id) {\n //? Fail-closed cross-provider account-LINK guard (SEC-21/SEC-40). Under the\n //? `'unified'` strategy `resolveUserByEmail` matches by email across every\n //? provider, so an OAuth sign-in can LINK into an account originally created\n //? by a DIFFERENT provider (incl. credentials). If the current provider did\n //? NOT positively verify the email (no `emailVerifiedKey` / `getEmail`\n //? verified-filter), an attacker who controls an unverified address equal to\n //? a victim's could take over that account. Refuse the link in that case —\n //? same-provider re-login and first-time account CREATION are unaffected, so\n //? existing flows for built-ins that DO verify (Google/GitHub/Discord) keep\n //? working. `'per-provider'` always matches within the same provider, so the\n //? cross-provider condition is never met there (no behaviour change).\n const existingProvider = findResponse.provider ?? null;\n //? Fail CLOSED on a missing provider field: treat unknown (null/undefined) as a\n //? cross-provider link so an unverified provider email can't link into an\n //? existing account whose record omits `provider` (a custom UserAdapter may not\n //? populate it). Same-provider re-login still matches (=== provider.name → not\n //? cross), and first-time creation is unaffected (this branch needs an existing id).\n const isCrossProviderLink = existingProvider !== provider.name;\n if (isCrossProviderLink && !emailVerified) {\n getLogger().warn(\n 'oauth: refusing to link an unverified provider email to an existing account created by a different provider (account-takeover guard)',\n { provider: provider.name, existingProvider },\n );\n return null;\n }\n\n //? LOGIN-F18 (OAuth path): same async stat lookup via the shared helper.\n const uploadedAvatar = await resolveUploadedAvatar(findResponse.id);\n if (uploadedAvatar) {\n findResponse.avatar = uploadedAvatar;\n }\n\n const previousLogin = findResponse.lastLogin ?? null;\n const nowLogin = new Date();\n await tryCatch(() => userAdapter.update(findResponse.id, { lastLogin: nowLogin }));\n\n return {\n user: {\n ...sanitizeUserForSession(findResponse),\n token: '',\n lastLogin: nowLogin,\n previousLogin,\n },\n isNewUser: false,\n };\n }\n\n //? Fail-closed account-CREATION guard (M2), symmetric with the cross-provider\n //? LINK guard above. When NO account exists, a provider that did not positively\n //? verify the email (no `emailVerifiedKey === true` / `getEmail` /\n //? `emailImpliesVerified`) must not be able to seed a brand-new account bound to\n //? an address the signer-in may not own — a custom, non-verifying provider would\n //? otherwise let an attacker SQUAT a victim's email (blocking the victim's later\n //? signup with `login.emailExists` and, under `'unified'`, absorbing the victim's\n //? later verified sign-in into the attacker-seeded row). All built-in providers\n //? set `emailVerified` (Google/GitHub/Microsoft/Discord/Facebook), so this only\n //? refuses genuinely-unverified CUSTOM providers; such a provider must verify the\n //? email (set `emailVerifiedKey` / `getEmail` / `emailImpliesVerified`) to enable\n //? OAuth signup, rather than the framework trusting it by default.\n if (!emailVerified) {\n getLogger().warn(\n 'oauth: refusing first-login account creation for an unverified provider email (account-squatting guard)',\n { provider: provider.name },\n );\n return null;\n }\n\n //? Gate OAuth first-login account CREATION on the same `auth.allowRegistration`\n //? flag the credentials register branch enforces. Without this, invite-only /\n //? admin-provisioned deployments (allowRegistration:false) are silently\n //? bypassable: anyone with an enabled provider's account completes the OAuth\n //? flow and gets a fresh account. Existing users hit the find-branch above and\n //? are unaffected; only NEW-account creation is refused. A consumer preRegister\n //? hook can still veto, but the documented gate must hold by construction.\n if (!getProjectConfig().auth.allowRegistration) {\n getLogger().warn('oauth: registration disabled — refusing first-login account creation');\n return null;\n }\n\n const preRegisterResult = await dispatchHook('preRegister', { email, provider: provider.name, name });\n if (preRegisterResult.stopped) {\n getLogger().warn(`oauth register aborted by preRegister hook`, { errorCode: preRegisterResult.signal.errorCode });\n return null;\n }\n\n const createNewUser = async () => userAdapter.create({\n email,\n provider: provider.name,\n name,\n avatar,\n avatarFallback: generateAvatarFallbackColor(),\n language: getProjectConfig().defaultLanguage,\n });\n const [createError, createResponse] = await tryCatch(createNewUser);\n if (createError) {\n getLogger().error('oauth: user create failed', createError);\n return null;\n }\n if (!createResponse) return null;\n\n return {\n user: { ...sanitizeUserForSession(createResponse), token: '' },\n isNewUser: true,\n };\n};\n\nconst resolvePostLoginRedirect = async ({\n fallbackUrl,\n userId,\n providerName,\n isNewUser,\n}: {\n fallbackUrl: string;\n userId: string;\n providerName: string;\n isNewUser: boolean;\n}): Promise<string> => {\n const resolver = getPostLoginRedirect();\n if (!resolver) return fallbackUrl;\n\n const [resolverError, resolved] = await tryCatch(() => resolver({\n userId,\n provider: providerName,\n isNewUser,\n defaultUrl: fallbackUrl,\n }));\n if (resolverError) {\n getLogger().warn(`[oauth] postLoginRedirect resolver threw`, { message: resolverError.message });\n return fallbackUrl;\n }\n if (resolved && isAllowedRedirectUrl(resolved)) return resolved;\n if (resolved) {\n getLogger().warn(\n `[oauth] postLoginRedirect returned a URL not in allowed origins — falling back`,\n { resolved, fallbackUrl },\n );\n }\n return fallbackUrl;\n};\n\n//? Whitelist the URL path segment against registered providers and verify the\n//? request carries a URL. Returns the matched full provider + its canonical name,\n//? or null when the segment is absent, unregistered, or an incomplete provider\n//? shape. Separating this keeps the callback routing concern out of the main flow.\nconst resolveProviderFromCallback = (\n pathname: string,\n url: string | undefined,\n): { provider: FullOAuthProvider; providerName: string } | null => {\n const rawProviderSegment = pathname.split('/')[3]; // google/github/etc.\n if (!rawProviderSegment || !url) return null;\n const found = getOAuthProviders().find(p => p.name === rawProviderSegment);\n if (!found || !isFullOAuthProvider(found)) return null;\n return { provider: found, providerName: found.name };\n};\n\n//? Merge per-provider runtime extras (calendar tokens, tenant ids, etc.) into\n//? the session object IN PLACE before it is persisted. Errors are non-fatal —\n//? a failing or absent hook must not block the user from signing in (SEC-10).\nconst applyExtraSessionFields = async (\n user: SessionLayout,\n provider: FullOAuthProvider,\n profile: OAuthProfile,\n accessToken: string,\n): Promise<void> => {\n const { extraSessionFields } = provider;\n if (!extraSessionFields) return;\n\n //? Pass the RAW provider userData (SEC-3) so the hook sees every provider\n //? claim (`sub`, tenant id, custom claims) rather than the trimmed projection.\n const [extraErr, extra] = await tryCatch(async () =>\n extraSessionFields({ userData: profile.rawUserData, accessToken }),\n );\n if (extraErr) {\n getLogger().warn(`[oauth:${provider.name}] extraSessionFields hook threw — continuing without extras`, { err: extraErr });\n return;\n }\n if (!extra) return;\n\n //? Strip framework-owned session keys before merging (SEC-10).\n const { id, token, csrfToken, password, ...safeExtra } = extra;\n if (id !== undefined || token !== undefined || csrfToken !== undefined || password !== undefined) {\n getLogger().warn(\n `[oauth:${provider.name}] extraSessionFields returned framework-owned key(s) (id/token/csrfToken/password) — dropped`,\n );\n }\n //? SEC: warn when extra fields appear to contain bearer credentials. Access\n //? tokens stored in the Redis session are broadcast to connected sockets via\n //? updateSession — they should not be in session fields unless intentional.\n //? The stripped set above already removes \"token\"; this covers other key names.\n const CREDENTIAL_PATTERN = /token|secret|key|auth/i;\n const suspiciousKeys = Object.keys(safeExtra).filter(k => CREDENTIAL_PATTERN.test(k));\n if (suspiciousKeys.length > 0) {\n getLogger().warn(\n `[oauth:${provider.name}] extraSessionFields contains field(s) that look like credentials (${suspiciousKeys.join(', ')}) — these will be stored in the Redis session and broadcast to connected sockets. Consider keeping them server-side only.`,\n );\n }\n Object.assign(user, safeExtra);\n};\n\n//? Assemble the fallback redirect URL for the OAuth callback by preferring the\n//? tamper-proof return URL stored server-side with the OAuth state, then the\n//? caller-supplied default, then the configured `loginRedirectUrl`, then '/'.\nconst resolveOAuthFallbackUrl = (\n stateEntry: OAuthStateEntry,\n options: { defaultRedirectUrl?: string },\n): string => {\n const stateReturnUrl =\n stateEntry.returnUrl && isAllowedRedirectUrl(stateEntry.returnUrl)\n ? stateEntry.returnUrl\n : undefined;\n\n return (\n stateReturnUrl\n ?? options.defaultRedirectUrl\n ?? getProjectConfig().loginRedirectUrl\n ?? '/'\n );\n};\n\n// Route that handles the callback from the OAuth provider\nconst loginCallback = async (\n pathname: string,\n req: IncomingMessage,\n _res: ServerResponse,\n options: { defaultRedirectUrl?: string; supersedeToken?: string } = {},\n): Promise<OAuthCallbackResult | false> => {\n const providerResolution = resolveProviderFromCallback(pathname, req.url);\n if (!providerResolution) return false;\n const { provider, providerName } = providerResolution;\n\n // req.url is guaranteed non-null here: resolveProviderFromCallback returns null when url is absent\n const queryString = (req.url ?? '').split('?')[1] ?? '';\n const params = new URLSearchParams(queryString);\n const code = params.get('code');\n const state = params.get('state');\n\n //? Browser-binding (F1): the nonce the server set as the `ls-oauth-state`\n //? cookie at the authorize step must round-trip back and match the value\n //? stored alongside the Redis state entry. A missing/mismatched cookie means\n //? this browser did NOT start the flow — reject before any token exchange.\n const cookieNonce = getCookieValue(req.headers.cookie, OAUTH_STATE_COOKIE_NAME) ?? '';\n const stateEntry = await consumeOAuthState(provider.name, state ?? '', cookieNonce);\n if (!stateEntry) {\n getLogger().warn('oauth: invalid, missing, or unbound state', { provider: provider.name });\n emitLoginFailed({ provider: providerName, reason: 'oauth.invalidState', stage: 'oauth' });\n return false;\n }\n\n if (!code) {\n getLogger().warn('oauth: no code provided in callback url', { provider: provider.name });\n emitLoginFailed({ provider: providerName, reason: 'oauth.noCode', stage: 'oauth' });\n return false;\n }\n\n //? PKCE (F11): replay the verifier stored with the state entry at token\n //? exchange when the provider opted in. `undefined` for non-PKCE providers,\n //? in which case the exchange is byte-identical to before.\n const accessToken = await exchangeOAuthToken(provider, code, stateEntry.codeVerifier);\n if (!accessToken) {\n emitLoginFailed({ provider: providerName, reason: 'oauth.tokenExchangeFailed', stage: 'oauth' });\n return false;\n }\n\n const profile = await fetchOAuthProfile(provider, accessToken);\n if (!profile) {\n emitLoginFailed({ provider: providerName, reason: 'oauth.profileFetchFailed', stage: 'oauth' });\n return false;\n }\n\n const resolved = await findOrCreateOAuthUser(provider, profile);\n if (!resolved) {\n emitLoginFailed({ provider: providerName, email: profile.email, reason: 'oauth.userResolveFailed', stage: 'oauth' });\n return false;\n }\n\n const newToken = randomBytes(32).toString('hex');\n resolved.user.token = newToken;\n\n //? Per-provider runtime extras (calendar tokens, tenant ids, etc.) are merged\n //? into the session BEFORE save. Errors are non-fatal — see applyExtraSessionFields.\n await applyExtraSessionFields(resolved.user, provider, profile, accessToken);\n\n const saved = await saveSession(newToken, resolved.user, true, { supersedeToken: options.supersedeToken });\n if (!saved.ok) {\n //? Session never persisted — abort the OAuth callback so authCallbackRoute\n //? returns \"Login failed\" instead of redirecting with a dead token.\n getLogger().error(`[oauth:${providerName}] saveSession failed — aborting callback`, { errorCode: saved.errorCode });\n emitLoginFailed({ provider: providerName, userId: resolved.user.id, reason: saved.errorCode, stage: 'oauth' });\n return false;\n }\n\n if (resolved.isNewUser) {\n await dispatchHook('postRegister', { userId: resolved.user.id, provider: providerName });\n }\n await dispatchHook('postLogin', {\n userId: resolved.user.id,\n provider: providerName,\n isNewUser: resolved.isNewUser,\n token: newToken,\n });\n\n if (isDevMode()) {\n getLogger().debug(`oauth login success for user ${resolved.user.id}`);\n }\n\n const fallbackUrl = resolveOAuthFallbackUrl(stateEntry, options);\n const redirectUrl = await resolvePostLoginRedirect({\n fallbackUrl,\n userId: resolved.user.id,\n providerName,\n isNewUser: resolved.isNewUser,\n });\n\n return {\n token: newToken,\n redirectUrl,\n userId: resolved.user.id,\n provider: providerName,\n isNewUser: resolved.isNewUser,\n };\n};\n\nexport { loginWithCredentials, loginCallback, registerWithCredentials, loginWithCredentialsCore }\n","//? @adr 0019 — Email uniqueness is opt-in and governed by this knob: the default\n//? `'per-provider'` strategy INTENTIONALLY does not require `email @unique`\n//? (same address across providers = separate rows); only `'unified'` needs the\n//? consumer to add the constraint. A missing `@unique` on `User.email` is\n//? by-design, not a bug — see ADR 0019 before flagging it.\n//?\n//? Implements `auth.providerAccountStrategy` (CFG-04). The knob was declared\n//? and documented but no code read it — every lookup was provider-scoped, so\n//? `'unified'` silently behaved like `'per-provider'` and produced duplicate\n//? accounts for the same email across providers.\n//?\n//? - `'per-provider'` (default): resolve a user by (email, provider). The\n//? same address via Google and GitHub is two separate User rows. No schema\n//? change required.\n//? - `'unified'`: resolve a user by email alone (across providers) so the\n//? address maps to a single User; a sign-in via a new provider LINKS to the\n//? existing account instead of creating a duplicate. Requires the User\n//? schema's `email` to be unique (see @luckystack/login README — \"Unified\n//? account strategy\").\n//?\n//? Custom `UserAdapter`s that predate this feature may not implement\n//? `findByEmailAnyProvider`. When `'unified'` is configured but the adapter\n//? lacks it, we log ONCE and fall back to provider-scoped lookup so the project\n//? degrades loudly rather than silently mis-resolving.\n\nimport { getProjectConfig, getLogger } from '@luckystack/core';\n\nimport type { UserAdapter, UserRecord } from './userAdapter';\n\nlet warnedMissingAnyProvider = false;\n\n/**\n * Resolve a user account honoring the configured `providerAccountStrategy`.\n * Use this everywhere the login flow needs \"does an account for this email\n * exist?\" — register dedupe, credentials login, and OAuth find-or-create.\n */\nexport const resolveUserByEmail = async (\n adapter: UserAdapter,\n { email, provider }: { email: string; provider: string },\n): Promise<UserRecord | null> => {\n const strategy = getProjectConfig().auth.providerAccountStrategy;\n\n if (strategy === 'unified') {\n if (adapter.findByEmailAnyProvider) {\n return adapter.findByEmailAnyProvider({ email });\n }\n if (!warnedMissingAnyProvider) {\n warnedMissingAnyProvider = true;\n getLogger().warn(\n '[login] auth.providerAccountStrategy is \"unified\" but the registered UserAdapter does not implement findByEmailAnyProvider — falling back to provider-scoped lookup (duplicate accounts per provider are still possible). Implement findByEmailAnyProvider on your adapter to enable unified accounts.',\n );\n }\n }\n\n return adapter.findByEmail({ email, provider });\n};\n\n/** Test-only: reset the one-time missing-method warning latch. */\nexport const resetAccountStrategyWarningForTests = (): void => {\n warnedMissingAnyProvider = false;\n};\n","//? Password-policy enforcement for credentials auth. Read by\n//? `registerWithCredentials` and `updatePasswordHash` to validate plaintext\n//? passwords against the consumer-tunable `projectConfig.auth.passwordPolicy`.\n//?\n//? Returns null when the password passes every check, or a reason-key\n//? (i18n-style errorCode) describing the first failure. Callers surface\n//? the reason to the client; the framework's i18n layer handles\n//? translation.\n\nimport { getProjectConfig } from '@luckystack/core';\n\nimport { COMMON_PASSWORDS } from './data/commonPasswords';\n\nconst UPPERCASE_REGEX = /[A-Z]/;\nconst LOWERCASE_REGEX = /[a-z]/;\nconst NUMBER_REGEX = /\\d/;\nconst SPECIAL_REGEX = /[^A-Za-z0-9]/;\n\n//? bcrypt silently truncates at 72 utf8-encoded bytes. Anything over 72 bytes\n//? hashes identically to its 72-byte prefix, so a longer password gives no\n//? extra security and actively misleads the user. Cap at 72 bytes so the\n//? stored hash always reflects the full secret the user typed.\nconst BCRYPT_MAX_BYTES = 72;\n\n/**\n * Validate `password` against the active project's password policy.\n * Returns null on success, a string reason-key on the first failure.\n *\n * The policy lives in `projectConfig.auth.passwordPolicy` and is read at\n * call time, so consumers can override defaults via `registerProjectConfig`.\n */\nexport const validatePassword = (password: string): string | null => {\n const policy = getProjectConfig().auth.passwordPolicy;\n\n if (password.length < policy.minLength) return 'login.passwordCharacterMinimum';\n //? Byte-length cap takes precedence over the policy maxLength when the policy\n //? exceeds BCRYPT_MAX_BYTES — reject before bcrypt silently truncates.\n const byteLength = Buffer.byteLength(password, 'utf8');\n if (byteLength > BCRYPT_MAX_BYTES) return 'login.passwordCharacterLimit';\n if (password.length > policy.maxLength) return 'login.passwordCharacterLimit';\n if (policy.requireUppercase && !UPPERCASE_REGEX.test(password)) return 'login.passwordRequiresUppercase';\n if (policy.requireLowercase && !LOWERCASE_REGEX.test(password)) return 'login.passwordRequiresLowercase';\n if (policy.requireNumber && !NUMBER_REGEX.test(password)) return 'login.passwordRequiresNumber';\n if (policy.requireSpecial && !SPECIAL_REGEX.test(password)) return 'login.passwordRequiresSpecial';\n if (policy.forbidCommon && COMMON_PASSWORDS.has(password.toLowerCase())) return 'login.passwordTooCommon';\n if (policy.customValidator) {\n const customReason = policy.customValidator(password);\n if (customReason) return customReason;\n }\n\n return null;\n};\n","//? Built-in common-password blocklist. Sourced from the public-domain\n//? SecLists Top-passwords list (Daniel Miessler), trimmed to the 200 most\n//? common entries to keep the package size small. Consumers needing a\n//? larger list (10k+ entries) can supply one through\n//? `projectConfig.auth.passwordPolicy.customValidator` and check their\n//? own data source there.\n//?\n//? Lookups are case-insensitive (the validator lower-cases before\n//? checking). Adding additional entries here is a non-breaking change.\n\nconst COMMON_PASSWORD_LIST: readonly string[] = [\n '123456', 'password', '12345678', 'qwerty', '123456789', '12345', '1234', '111111',\n '1234567', 'dragon', '123123', 'baseball', 'abc123', 'football', 'monkey', 'letmein',\n '696969', 'shadow', 'master', '666666', 'qwertyuiop', '123321', 'mustang', '1234567890',\n 'michael', '654321', 'pussy', 'superman', '1qaz2wsx', '7777777', 'fuckyou', '121212',\n '000000', 'qazwsx', '123qwe', 'killer', 'trustno1', 'jordan', 'jennifer', 'zxcvbnm',\n 'asdfgh', 'hunter', 'buster', 'soccer', 'harley', 'batman', 'andrew', 'tigger',\n 'sunshine', 'iloveyou', 'fuckme', '2000', 'charlie', 'robert', 'thomas', 'hockey',\n 'ranger', 'daniel', 'starwars', 'klaster', '112233', 'george', 'asshole', 'computer',\n 'michelle', 'jessica', 'pepper', '1111', 'zxcvbn', '555555', '11111111', '131313',\n 'freedom', '777777', 'pass', 'fuck', 'maggie', '159753', 'aaaaaa', 'ginger',\n 'princess', 'joshua', 'cheese', 'amanda', 'summer', 'love', 'ashley', '6969',\n 'nicole', 'chelsea', 'biteme', 'matthew', 'access', 'yankees', '987654321', 'dallas',\n 'austin', 'thunder', 'taylor', 'matrix', 'william', 'corvette', 'hello', 'martin',\n 'heather', 'secret', 'fucker', 'merlin', 'diamond', '1234qwer', 'gfhjkm', 'hammer',\n 'silver', 'richard', 'samantha', 'iceman', 'tigers', 'purple', 'andrea', 'horney',\n 'dakota', 'aaaaa', 'player', 'sucker', 'mercedes', 'whatever', 'orange', 'qweqwe',\n 'mickey', 'panties', 'hannah', 'fucking', 'paul', 'pakistan', 'patrick', 'martha',\n 'morgan', 'iwantu', 'lakers', 'rachel', 'slayer', 'scott', '2112', 'fish',\n 'porn', 'matt', 'qwert', 'cookie', 'eagle1', 'london', 'phoenix', 'chicago',\n 'badboy', 'braves', 'yankee', 'lover', 'barney', 'edward', 'raiders', 'green',\n 'maverick', 'rangers', 'joseph', 'maddog', 'mike', 'jasmine', 'arsenal', 'beach',\n 'tiger', 'snoopy', 'natasha', 'matthews', 'jackson', 'cumshot', 'chevy', 'gateway',\n 'gators', 'angel', 'junior', 'samson', '6666', 'amateur', 'gemini', 'apples',\n 'august', 'biscuit', '5150', 'apollo', 'parker', 'qwerty123', 'rabbit', 'angela',\n 'love123', 'rocket', 'scooby', 'beaver', 'oliver', 'walter', 'blowme', 'spider',\n '4321', 'cumshot1', 'lucky', 'helpme', 'jackie', 'monica',\n];\n\n/** Frozen Set for O(1) `.has()` lookups; entries are pre-lowercased. */\nexport const COMMON_PASSWORDS: ReadonlySet<string> = new Set(\n COMMON_PASSWORD_LIST.map((p) => p.toLowerCase()),\n);\n","//? Password-reset primitives. Used by the framework's own forgot-password\n//? flow when `ProjectConfig.auth.forgotPassword === 'framework'`, AND\n//? exported as building blocks for consumers who picked `'custom'` and want\n//? to wire their own UI / email layer without re-implementing crypto + Redis.\n//?\n//? Token format: 32 random bytes hex-encoded → 64-char URL-safe string.\n//? Storage: `<projectName>-pwreset:<sha256(token)> → userId` with a configurable\n//? TTL (`auth.passwordResetTtlSeconds`, default 3600 = 1 hour).\n//?\n//? HASHED AT REST (0.2.0): the raw token is NEVER stored — we persist only\n//? `sha256(token)` (the Redis key) via the `@luckystack/core` one-time-token\n//? primitive, so a leaked Redis keyspace can't be replayed to mint a reset.\n//? The raw token is returned to the caller exactly once for the emailed URL.\n\n// eslint-disable-next-line import-x/no-named-as-default -- bcryptjs ships both a default and namespace export; using the default mirrors its README\nimport bcrypt from 'bcryptjs';\n/* eslint-disable import-x/no-named-as-default-member -- accessing genSalt/hash/compare via the default import is the documented pattern */\n\nimport { getProjectConfig, issueOneTimeToken, consumeOneTimeToken, formatKey, redis, oneTimeTokenKey } from '@luckystack/core';\n\nimport { validatePassword } from './passwordPolicy';\nimport { getUserAdapter } from './userAdapter';\n\n//? Shared key namespace for the one-time-token primitive. `formatKey('-pwreset',\n//? sha256(token))` is applied inside the primitive, so the project-name prefix /\n//? multi-tenant formatter still apply — only the SUFFIX is now the hash, not the\n//? raw token (the historical `<projectName>-pwreset:<token>` shape becomes\n//? `<projectName>-pwreset:<sha256(token)>`).\nconst PWRESET_NAMESPACE = '-pwreset';\n\n//? Per-user pointer key that records the hash-key of the currently-active reset\n//? token (LOGIN-F16). On each new issue we delete the prior hash key (if any) so\n//? at most one reset link per user is redeemable at a time — multiple live links\n//? until TTL is a session-hijack / takeover risk if the first link is intercepted.\nconst pwresetUserKey = (userId: string): string => formatKey('-pwreset-user', userId);\n\n/**\n * Create a one-time password-reset token bound to a user id. Any previously\n * issued (but not yet consumed) token for the same user is invalidated first\n * (LOGIN-F16). Stored in Redis with the configured TTL — only `sha256(token)`\n * is persisted (hash-at-rest). Returns the raw token string — caller emails it\n * to the user (typically embedded in a URL).\n */\nexport const createPasswordResetToken = async (userId: string): Promise<string> => {\n const ttl = getProjectConfig().auth.passwordResetTtlSeconds;\n\n //? Invalidate the prior active token before issuing the new one (LOGIN-F16).\n //? The pointer key holds the exact Redis key of the prior token hash, so we\n //? can delete it without possessing the raw token.\n const priorKey = await redis.get(pwresetUserKey(userId));\n if (priorKey) {\n await redis.del(priorKey);\n }\n\n const handle = issueOneTimeToken(PWRESET_NAMESPACE, ttl, userId);\n await handle.store();\n\n //? Record the new hash-key so the NEXT issue can invalidate this one.\n //? TTL matches the token so the pointer never outlives what it points to.\n //? `oneTimeTokenKey(namespace, rawToken)` derives the same Redis key the\n //? primitive stored — we never persist the raw token itself.\n const newHashKey = oneTimeTokenKey(PWRESET_NAMESPACE, handle.token);\n await redis.set(pwresetUserKey(userId), newHashKey, 'EX', ttl);\n\n return handle.token;\n};\n\n/**\n * Validate and consume a password-reset token. Returns the bound userId on\n * success and removes the token (one-time use, atomic GET+DEL). Returns null\n * when the token is missing, expired, malformed, or already consumed.\n */\nexport const consumePasswordResetToken = async (token: string): Promise<string | null> => {\n return consumeOneTimeToken(PWRESET_NAMESPACE, token);\n};\n\n/**\n * Bcrypt-hash a plaintext password and write it to the user record via the\n * registered user adapter. Used by the reset flow and by the settings\n * password-change flow. Validates against the active password policy\n * (`projectConfig.auth.passwordPolicy`) before hashing — throws a `PasswordPolicyError`\n * when the plaintext fails policy. Catch and surface the error code to the\n * client; framework-mode reset/change flows already do this.\n *\n * ADR — DD-LOGIN-F17: this primitive intentionally does NOT revoke sessions.\n * Session revocation is the responsibility of each calling route because the\n * right behaviour differs per use-case:\n * - `reset-password/confirmReset_v1` revokes ALL sessions (`revokeUserSessions(userId, null)`)\n * because a forgot-password reset means the old credential is compromised.\n * - `settings/changePassword_v1` revokes every OTHER session but keeps the\n * current one active (`revokeUserSessions(userId, user.token)`) so the\n * user is not kicked from the device they changed the password on.\n * - Custom consumers may want a third behaviour (e.g. re-use the existing\n * token after an admin password-force) — keeping the primitive pure lets\n * them compose it freely.\n * If you call this function directly, YOU must call `revokeUserSessions`\n * afterwards unless you have a deliberate reason not to.\n */\nexport const updatePasswordHash = async (userId: string, plaintext: string): Promise<void> => {\n const reason = validatePassword(plaintext);\n if (reason) {\n throw new PasswordPolicyError(reason);\n }\n const salt = await bcrypt.genSalt(getProjectConfig().auth.bcryptRounds);\n const hashedPassword = await bcrypt.hash(plaintext, salt);\n const patch: Record<string, unknown> = { password: hashedPassword };\n await getUserAdapter().update(userId, patch);\n};\n\n/**\n * Thrown by `updatePasswordHash` when the supplied plaintext violates the\n * active password policy. `errorCode` matches the i18n reason key the rest\n * of the login flow uses (e.g. `login.passwordRequiresUppercase`).\n */\nexport class PasswordPolicyError extends Error {\n readonly errorCode: string;\n constructor(errorCode: string) {\n super(`Password policy violation: ${errorCode}`);\n this.name = 'PasswordPolicyError';\n this.errorCode = errorCode;\n }\n}\n\n/** Verify a plaintext password against a stored bcrypt hash. */\nexport const verifyPassword = async (plaintext: string, hash: string): Promise<boolean> => {\n return bcrypt.compare(plaintext, hash);\n};\n","//? Framework-mode forgot-password orchestration. Used only when\n//? `ProjectConfig.auth.forgotPassword === 'framework'`. Pulls in\n//? `@luckystack/email` lazily so the login package doesn't list it as a\n//? hard dep — `'custom'` and `'disabled'` modes never reach this code.\n\nimport { getProjectConfig, dispatchHook, getLogger } from '@luckystack/core';\n\nimport { loadEmailModule } from './emailModuleLoader';\nimport { createPasswordResetToken } from './passwordReset';\nimport { getUserAdapter } from './userAdapter';\n\ninterface SendResetEmailArgs {\n email: string;\n /**\n * App-facing display name used in the email greeting + subject. If omitted,\n * falls back to `projectConfig.auth.passwordResetBrand`, then to `'LuckyStack'`\n * as the absolute fallback. Real consumers should set this on either the\n * call site OR via `projectConfig.auth.passwordResetBrand`.\n */\n brand?: string;\n}\n\n/**\n * Look up the user by credentials-provider email, generate a reset token,\n * and email them a reset link. Always resolves successfully regardless of\n * whether the email matched a user (anti-enumeration). Returns the result\n * of the email send for diagnostics.\n */\nexport const sendPasswordResetEmail = async ({ email, brand }: SendResetEmailArgs): Promise<{ ok: boolean; reason?: string }> => {\n const config = getProjectConfig();\n //? Log mode but NOT the raw email at start — the email is a PII field.\n getLogger().info('[forgotPassword] start', { forgotPasswordMode: config.auth.forgotPassword });\n if (config.auth.forgotPassword !== 'framework') {\n getLogger().warn('[forgotPassword] auth.forgotPassword is not \"framework\" — nothing sent', { mode: config.auth.forgotPassword });\n return { ok: false, reason: 'forgotPassword-not-framework' };\n }\n\n const resolvedBrand = brand ?? config.auth.passwordResetBrand ?? 'LuckyStack';\n\n // Lazy import — the email package is an optional peer dep so its types\n // may not be resolvable at the framework's compile time.\n //? Mirror testEmail's robustness: catch a failed dynamic import instead of\n //? throwing (an uncaught throw here would bubble out of the anti-enumeration\n //? wrapper as a generic 500). Surfaces the real reason in the server log.\n const emailModule = await loadEmailModule().catch((error: unknown) => {\n getLogger().warn('[forgotPassword] failed to load @luckystack/email — is it installed?', { error: String(error) });\n return null;\n });\n if (!emailModule) {\n return { ok: false, reason: 'email-module-load-failed' };\n }\n const { sendEmail } = emailModule;\n\n const userAdapter = getUserAdapter();\n const user = await userAdapter.findByEmail({ email, provider: 'credentials' });\n //? Log found/userId without the raw email — logging email+found together is\n //? an enumeration oracle (any reader of the log can probe the user table).\n getLogger().info('[forgotPassword] credentials user lookup', { found: Boolean(user), userId: user?.id ?? null });\n\n // Anti-enumeration: always pretend it succeeded if the email isn't found.\n if (!user) {\n //? The client always gets \"success\" (anti-enumeration). This server-side\n //? line is the ONLY signal that the address simply isn't a credentials\n //? (email+password) account — by far the most common reason a reset email\n //? never arrives (OAuth-only signups and unknown/typo'd addresses have no\n //? credentials row, so there is nothing to reset).\n //? Do NOT include the email here — this line together with the email would\n //? let a log reader determine which addresses have credentials accounts.\n getLogger().warn('[forgotPassword] no credentials account for this email — nothing sent. Use the email+password account you registered with.');\n void dispatchHook('passwordResetRequested', {\n email,\n matched: false,\n });\n return { ok: true };\n }\n\n const token = await createPasswordResetToken(user.id);\n //? SEC: do NOT emit the raw token on the hook bus — hook subscribers are\n //? untrusted code and the token is a single-use credential. Consumers who need\n //? to deliver the email themselves should call `createPasswordResetToken`\n //? directly and build the URL without the framework exposing the secret here.\n void dispatchHook('passwordResetRequested', {\n email,\n matched: true,\n userId: user.id,\n ttlSeconds: config.auth.passwordResetTtlSeconds,\n });\n const baseUrl = (config.app.publicUrl || '').replace(/\\/+$/, '');\n //? ADR — DD-ROOTSRC-O8: the reset token is in the URL query string\n //? (`?token=<hex>`), not in the URL fragment (`#token=<hex>`).\n //? Tradeoff: query strings appear in server access logs and Referer headers\n //? if the user's mail client renders the link via a tracking proxy. Fragment\n //? identifiers (`#`) are never sent to the server and are therefore\n //? invisible to any log / proxy in the chain — they are more private.\n //? Decision: query-string is the standard email-link pattern understood by\n //? every SPA router, and tokens here are one-time + hashed at rest (Redis\n //? holds only `sha256(token)`), so the exposure window is the TTL of a\n //? single-use credential, not a replayable secret. Logging is mitigated\n //? by not logging the full URL at the point of issue (we log `userId`, not\n //? `resetUrl`). A future `auth.tokenLinkMode: 'fragment'` option can be\n //? introduced without touching this file — callers receive the raw token\n //? and can build their own URL.\n const resetUrl = `${baseUrl}/reset-password?token=${encodeURIComponent(token)}`;\n\n const ttlMinutes = Math.round(config.auth.passwordResetTtlSeconds / 60);\n\n //? Dispatch via the `'password-reset'` template rather than building the\n //? email inline. `@luckystack/email` ships a built-in for this name, so the\n //? out-of-the-box copy is unchanged — but a consumer can now\n //? `registerEmailTemplate('password-reset', …)` to translate or rebrand it\n //? without forking this flow (CFG-05). `adapterHint: 'transactional'` routes\n //? through a dedicated transactional sender when one is registered, else the\n //? default sender.\n const result = await sendEmail({\n to: user.email,\n template: 'password-reset',\n data: { resetUrl, userName: user.name, brand: resolvedBrand, ttlMinutes },\n adapterHint: 'transactional',\n });\n\n if (result.ok) {\n //? SEC: log only userId for correlation — not the email address (PII).\n getLogger().info('[forgotPassword] reset email dispatched', { userId: user.id });\n } else {\n getLogger().warn('[forgotPassword] reset email send FAILED', { userId: user.id, reason: result.reason });\n }\n\n return result.ok ? { ok: true } : { ok: false, reason: result.reason };\n};\n","//? Shared lazy-loader for the optional `@luckystack/email` peer dep. Both\n//? `forgotPassword.ts` and `emailChangeNotification.ts` need the SAME dynamic\n//? import + the SAME minimal `EmailModule` shape, so the interface + the\n//? `@ts-expect-error` import + the cast live here once (finding 37).\n//?\n//? IMPORTANT — this helper does NOT swallow a failed import. It resolves to the\n//? module or REJECTS exactly like the raw `import(...)` would. That preserves\n//? the two callers' DIFFERENT failure behavior verbatim:\n//? - `emailChangeNotification` awaits it directly (a load failure throws).\n//? - `forgotPassword` wraps it in `.catch(...)` (a load failure → null + warn).\n//? Centralizing the catch here would change one of those — do NOT.\n\n/** Minimal surface of `@luckystack/email` that the login orchestrators use. */\nexport interface EmailModule {\n sendEmail: (input: Record<string, unknown>) => Promise<{ ok: boolean; reason?: string }>;\n}\n\n/**\n * Dynamically import the optional `@luckystack/email` peer. Resolves to the\n * module, or REJECTS if the package is not installed (same as a bare\n * `import('@luckystack/email')`). Callers decide whether to `.catch(...)`.\n */\nexport const loadEmailModule = (): Promise<EmailModule> =>\n // @ts-expect-error optional peer dep — installed only when the consumer wires email sending\n import('@luckystack/email') as Promise<EmailModule>;\n","//? Email-change token primitives. Mirrors `passwordReset.ts` for the\n//? confirm-via-email-link flow: user submits a new address, framework mails\n//? a tokenized URL to the NEW address, click-through confirms and updates\n//? `User.email` + revokes all sessions.\n//?\n//? Token format: 32 random bytes hex-encoded -> 64-char URL-safe string.\n//? Storage: `<projectName>-email-change:<sha256(token)>` -> JSON payload\n//? `{ userId: string; newEmail: string }`\n//? TTL: `auth.emailChangeTtlSeconds` (default 3600 = 1 hour).\n//?\n//? HASHED AT REST (0.2.0): only `sha256(token)` is persisted (the Redis key)\n//? via the `@luckystack/core` one-time-token primitive, so a leaked Redis\n//? keyspace can't be replayed to confirm an email change. The raw token is\n//? returned to the caller exactly once for the emailed URL.\n\nimport { getProjectConfig, issueOneTimeToken, consumeOneTimeTokenJson, formatKey, redis, oneTimeTokenKey } from '@luckystack/core';\n\nconst EMAIL_CHANGE_NAMESPACE = '-email-change';\n\n//? Per-user pointer key for prior-token invalidation (LOGIN-F16), mirroring the\n//? same pattern used for password-reset tokens. Stored as a formatted key so the\n//? multi-tenant key formatter applies uniformly.\nconst emailChangeUserKey = (userId: string): string => formatKey('-email-change-user', userId);\n\nexport interface EmailChangePayload {\n userId: string;\n newEmail: string;\n}\n\n/**\n * Create a one-time email-change token bound to a user id + the new email\n * address. Any previously issued (but not yet consumed) token for the same\n * user is invalidated first (LOGIN-F16) so at most one confirmation link per\n * user is redeemable. Stored in Redis with the configured TTL — only\n * `sha256(token)` is persisted (hash-at-rest). Returns the raw token string —\n * caller emails the token (embedded in a URL) to the NEW address so the\n * confirmation click proves ownership of the new mailbox.\n */\nexport const createEmailChangeToken = async (userId: string, newEmail: string): Promise<string> => {\n const ttl = getProjectConfig().auth.emailChangeTtlSeconds;\n\n //? Invalidate the prior active email-change token (LOGIN-F16). Without this,\n //? multiple links could be outstanding concurrently — a link sent to an\n //? attacker-controlled address (via a race or a compromised session) would\n //? remain redeemable even after the user re-requests the flow.\n const priorKey = await redis.get(emailChangeUserKey(userId));\n if (priorKey) {\n await redis.del(priorKey);\n }\n\n const handle = issueOneTimeToken(\n EMAIL_CHANGE_NAMESPACE,\n ttl,\n { userId, newEmail } satisfies EmailChangePayload,\n );\n await handle.store();\n\n //? Record the new hash-key so the NEXT issue can invalidate this one.\n const newHashKey = oneTimeTokenKey(EMAIL_CHANGE_NAMESPACE, handle.token);\n await redis.set(emailChangeUserKey(userId), newHashKey, 'EX', ttl);\n\n return handle.token;\n};\n\n/**\n * Validate and consume an email-change token. Returns the bound\n * `{ userId, newEmail }` on success and removes the token (one-time use, atomic\n * GET+DEL). Returns null when the token is missing, expired, malformed, or\n * already consumed.\n */\nexport const consumeEmailChangeToken = async (token: string): Promise<EmailChangePayload | null> => {\n const parsed = await consumeOneTimeTokenJson<EmailChangePayload>(EMAIL_CHANGE_NAMESPACE, token);\n if (\n !parsed\n || typeof parsed !== 'object'\n || typeof parsed.userId !== 'string'\n || typeof parsed.newEmail !== 'string'\n ) {\n return null;\n }\n return parsed;\n};\n","//? Framework-mode \"email change confirmation\" orchestration. Renders the\n//? confirmation email + sends it to the NEW address (proves the user can\n//? receive at the new mailbox). Mirrors the lazy-import pattern from\n//? `forgotPassword.ts` so `@luckystack/email` stays an optional peer.\n\nimport { getProjectConfig } from '@luckystack/core';\n\nimport { createEmailChangeToken } from './emailChange';\nimport { loadEmailModule } from './emailModuleLoader';\n\ninterface SendEmailChangeArgs {\n userId: string;\n newEmail: string;\n /** Optional display name used in the email greeting (falls back to \"there\"). */\n userName?: string | null;\n /**\n * App-facing display name used in the email greeting + subject. Falls back\n * to `projectConfig.auth.passwordResetBrand` (the shared brand label), then\n * to `'LuckyStack'` as the absolute fallback.\n */\n brand?: string;\n}\n\n/**\n * Mint a one-shot email-change token and send a confirmation email to the\n * NEW address with the tokenized URL. Returns the send result for diagnostics\n * + the token itself (callers can audit-log it through the\n * `postEmailChangeRequested` hook).\n */\nexport const sendEmailChangeConfirmation = async (\n { userId, newEmail, userName, brand }: SendEmailChangeArgs,\n): Promise<{ ok: boolean; reason?: string; token: string }> => {\n const config = getProjectConfig();\n const resolvedBrand = brand ?? config.auth.passwordResetBrand ?? 'LuckyStack';\n\n // Lazy import — `@luckystack/email` is an optional peer dep. A failed load\n //? rejects here (unlike forgotPassword's catch); this caller's behavior is\n //? unchanged — the rejection bubbles out exactly as the inline import did.\n const { sendEmail } = await loadEmailModule();\n\n const token = await createEmailChangeToken(userId, newEmail);\n const baseUrl = (config.app.publicUrl || '').replace(/\\/+$/, '');\n //? ADR — DD-ROOTSRC-O8: same query-string tradeoff as the password-reset\n //? flow (see `forgotPassword.ts`). Tokens are one-time + hashed at rest;\n //? the raw token is never persisted. The confirm URL is only sent to the\n //? NEW address (verifying mailbox ownership), so it never appears in an\n //? access log that an attacker of the OLD address could read.\n //? #64: honor the configurable `auth.emailChangeConfirmPath` (where the\n //? consumer's confirm-email page lives) instead of a hardcoded path; the\n //? prior hardcoded value is the fallback when the key is empty.\n const confirmPath = config.auth.emailChangeConfirmPath || '/settings/confirm-email';\n const confirmUrl = `${baseUrl}${confirmPath}?token=${encodeURIComponent(token)}`;\n const ttlMinutes = Math.round(config.auth.emailChangeTtlSeconds / 60);\n\n //? Dispatch via the built-in `'email-change'` template so consumers can\n //? override the copy with `registerEmailTemplate('email-change', …)` without\n //? forking (CFG-05). Default copy is unchanged.\n const result = await sendEmail({\n to: newEmail,\n template: 'email-change',\n data: { confirmUrl, userName, brand: resolvedBrand, ttlMinutes },\n adapterHint: 'transactional',\n });\n\n return result.ok ? { ok: true, token } : { ok: false, reason: result.reason, token };\n};\n","//? 2FA flow layer (ADR 0024) on top of the totp.ts primitive. Methods:\n//? - 'totp' — authenticator apps (Google/Microsoft Authenticator,\n//? Authy, …) via the open TOTP standard; primary channel.\n//? - 'email-code' — \"send the code to my email instead\" fallback\n//? (config `auth.twoFactorEmailFallback`, needs email).\n//? - 'recovery-code' — one-time backup codes minted at enrollment.\n//?\n//? Login integration: this module registers the 2FA GATE into login.ts at\n//? module init (registerTwoFactorGate — the DI idiom that avoids a module\n//? cycle). After the first factor verifies, the gate parks the login as a\n//? short-lived Redis CHALLENGE (high-entropy token, hashed at rest) instead of\n//? minting a session; `verifyTwoFactorChallenge` completes it through the same\n//? `finalizeLogin` tail the password path uses.\n//?\n//? The challenge is deliberately NOT the core one-time-token primitive: a\n//? wrong 2FA code must not burn the whole challenge (the user gets\n//? `twoFactorMaxAttempts` tries), so reads don't consume — success or the\n//? spent attempt budget does.\n\nimport crypto from 'node:crypto';\nimport { acquireLease, dispatchHook, formatKey, getLogger, getProjectConfig, getProjectName, redis, releaseLease, tryCatch } from '@luckystack/core';\n\nimport { issueEmailCode, verifyEmailCode, clearEmailCode } from './emailOtp';\nimport { loadEmailModule } from './emailModuleLoader';\nimport { buildOtpauthUri, generateTotpSecret, verifyTotp } from './totp';\nimport { getUserAdapter, type UserRecord } from './userAdapter';\nimport {\n finalizeLogin,\n registerTwoFactorGate,\n type CredentialsLoginChallenge,\n type CredentialsLoginResult,\n type TwoFactorMethod,\n} from './login';\n\nconst sha256 = (value: string): string => crypto.createHash('sha256').update(value).digest('hex');\n\n//? ─────────────── TOTP secret at rest (AES-256-GCM, optional) ───────────────\n//? With `TOTP_ENCRYPTION_KEY` set (any string; key = sha256 of it) secrets are\n//? stored as `gcm:<iv>:<tag>:<ciphertext>`; without it they are stored as the\n//? plain base32 string (warned once). Decrypt accepts BOTH forms, so adding\n//? the env key later upgrades new writes without breaking existing users.\n\nconst ENCRYPTED_PREFIX = 'gcm:';\nlet warnedPlaintextSecret = false;\n\nconst encryptionKey = (): Buffer | null => {\n const raw = process.env.TOTP_ENCRYPTION_KEY;\n if (!raw) return null;\n return crypto.createHash('sha256').update(raw).digest();\n};\n\nexport const encryptTotpSecret = (secret: string): string => {\n const key = encryptionKey();\n if (!key) {\n if (!warnedPlaintextSecret) {\n warnedPlaintextSecret = true;\n getLogger().warn('[2fa] TOTP_ENCRYPTION_KEY is not set — TOTP secrets are stored unencrypted. Set it in .env.local to encrypt new enrollments at rest.');\n }\n return secret;\n }\n const iv = crypto.randomBytes(12);\n const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);\n const ciphertext = Buffer.concat([cipher.update(secret, 'utf8'), cipher.final()]);\n return `${ENCRYPTED_PREFIX}${iv.toString('base64')}:${cipher.getAuthTag().toString('base64')}:${ciphertext.toString('base64')}`;\n};\n\nexport const decryptTotpSecret = (stored: string): string | null => {\n if (!stored.startsWith(ENCRYPTED_PREFIX)) return stored; //? legacy/keyless plaintext\n const key = encryptionKey();\n if (!key) {\n getLogger().error('[2fa] found an encrypted TOTP secret but TOTP_ENCRYPTION_KEY is not set — cannot verify TOTP codes.');\n return null;\n }\n const [ivPart, tagPart, dataPart] = stored.slice(ENCRYPTED_PREFIX.length).split(':');\n if (!ivPart || !tagPart || !dataPart) return null;\n try {\n const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(ivPart, 'base64'));\n decipher.setAuthTag(Buffer.from(tagPart, 'base64'));\n return Buffer.concat([decipher.update(Buffer.from(dataPart, 'base64')), decipher.final()]).toString('utf8');\n } catch {\n getLogger().error('[2fa] TOTP secret decryption failed (wrong TOTP_ENCRYPTION_KEY?).');\n return null;\n }\n};\n\n//? ─────────────── challenge store ───────────────\n\ninterface ChallengeRecord {\n userId: string;\n email?: string;\n}\n\nconst challengeKey = (token: string): string => formatKey('-2fa-challenge', sha256(token));\nconst challengeAttemptsKey = (token: string): string => formatKey('-2fa-challenge-attempts', sha256(token));\n//? Replay guard: per-(user, timestep) used-marker. Set with `NX` so the\n//? claim is ATOMIC — two concurrent verifies of the same code race on the\n//? same key and exactly one wins (the earlier get-highest-step-then-set was a\n//? TOCTOU: both could read the old value and both pass). TTL covers the ±drift\n//? window; after it the timestep is already outside `now ± drift` so verifyTotp\n//? rejects it anyway.\nconst usedTimestepKey = (userId: string, timestep: number): string => formatKey('-2fa-usedstep', `${userId}:${String(timestep)}`);\n//? Cross-challenge, identity-keyed 2FA failure counter. The per-challenge\n//? budget alone is resettable — a password-holding attacker mints a fresh\n//? challenge per try — so this account-level counter locks the second factor\n//? after too many failures in the window, independent of IP (a botnet can't\n//? scale past it).\nconst accountFailureKey = (userId: string): string => formatKey('-2fa-accountfail', userId);\nconst RECOVERY_LEASE_MS = 5000;\nconst recoveryLeaseName = (userId: string): string => `2fa-recovery:${userId}`;\n//? Pending (unconfirmed) enrollment secret — only written to the user record\n//? once the user proves the app is set up by entering a first valid code.\nconst pendingSecretKey = (userId: string): string => formatKey('-2fa-pending', userId);\n\n//? Account-level 2FA lockout window/limit. Fixed (not a new config key to keep\n//? the surface lean) — 10 failed second-factor attempts per 15 min per account\n//? across ALL challenges/IPs. Generous for a fat-fingered legit user, a hard\n//? ceiling for a grind.\nconst ACCOUNT_FAIL_LIMIT = 10;\nconst ACCOUNT_FAIL_WINDOW_SECONDS = 15 * 60;\n\nconst isTwoFactorAccountLocked = async (userId: string): Promise<boolean> => {\n const count = Number((await redis.get(accountFailureKey(userId))) ?? '0');\n return count >= ACCOUNT_FAIL_LIMIT;\n};\nconst recordTwoFactorAccountFailure = async (userId: string): Promise<void> => {\n const key = accountFailureKey(userId);\n const count = await redis.incr(key);\n if (count === 1) await redis.expire(key, ACCOUNT_FAIL_WINDOW_SECONDS);\n};\nconst clearTwoFactorAccountFailures = async (userId: string): Promise<void> => {\n await redis.del(accountFailureKey(userId));\n};\n\nconst twoFactorConfig = () => getProjectConfig().auth;\n\nexport const availableTwoFactorMethods = (user: UserRecord): TwoFactorMethod[] => {\n const methods: TwoFactorMethod[] = [];\n if (user.totpSecret) methods.push('totp');\n if (twoFactorConfig().twoFactorEmailFallback && user.email) methods.push('email-code');\n if ((user.recoveryCodes ?? []).length > 0) methods.push('recovery-code');\n return methods;\n};\n\n//? The login gate: park the verified first factor as a challenge when the\n//? user enrolled 2FA (and the feature isn't globally disabled).\nexport const createTwoFactorChallengeIfRequired = async (\n user: UserRecord,\n _context: { requesterIp?: string },\n): Promise<CredentialsLoginChallenge | null> => {\n if (twoFactorConfig().twoFactor === 'disabled') return null;\n if (!user.twoFactorEnabled || !user.totpSecret) return null;\n\n const methods = availableTwoFactorMethods(user);\n const token = crypto.randomBytes(32).toString('hex');\n const record: ChallengeRecord = { userId: user.id, email: user.email ?? undefined };\n await redis.set(challengeKey(token), JSON.stringify(record), 'EX', twoFactorConfig().twoFactorChallengeTtlSeconds);\n return {\n status: true,\n reason: 'login.twoFactorRequired',\n requiresTwoFactor: true,\n challengeToken: token,\n twoFactorMethods: methods,\n };\n};\n\nconst readChallenge = async (token: string): Promise<ChallengeRecord | null> => {\n if (!/^[a-f0-9]{64}$/.test(token)) return null;\n const raw = await redis.get(challengeKey(token));\n if (!raw) return null;\n const [parseError, parsed] = await tryCatch(() => Promise.resolve(JSON.parse(raw) as ChallengeRecord));\n if (parseError || typeof parsed?.userId !== 'string') return null;\n return parsed;\n};\n\nconst burnChallenge = async (token: string): Promise<void> => {\n await redis.del(challengeKey(token));\n await redis.del(challengeAttemptsKey(token));\n};\n\n//? ─────────────── verification (completes the login) ───────────────\n\n//? `enforceSingleUse` — the timestep replay guard is a LOGIN-boundary defense:\n//? an intercepted code must not be replayable to complete a challenge. It is\n//? applied on the login path only. Session-authenticated management actions\n//? (disable / regenerate) do a plain proof-of-possession verify WITHOUT the\n//? guard — otherwise a user who just logged in with a TOTP code could not\n//? manage 2FA until their authenticator rolled to the next code (their app\n//? still shows the one they just burned). Those actions are already gated by\n//? the live session, so replaying a management code buys an attacker nothing\n//? they couldn't already do with that session.\nconst verifyTotpForUser = async (user: UserRecord, code: string, enforceSingleUse: boolean): Promise<boolean> => {\n if (!user.totpSecret) return false;\n //? Pin to the 6 digits we provision (otpauth URI declares digits=6). The\n //? generic verifyTotp accepts 6–8 for RFC completeness; the auth surface\n //? must not (a longer guess is a spec deviation with no benefit).\n if (!/^\\d{6}$/.test(code.trim())) return false;\n const secret = decryptTotpSecret(user.totpSecret);\n if (!secret) return false;\n const result = verifyTotp({ secret, code });\n if (!result.valid || result.timestep === null) return false;\n if (!enforceSingleUse) return true;\n\n //? Single-use on the login boundary: atomically CLAIM this (user, timestep)\n //? with `NX`. If the marker already exists the code was already redeemed —\n //? refuse. `NX` makes the claim race-free (fixes the earlier get-then-set\n //? TOCTOU where two concurrent verifies of the same code both passed).\n const claimed = await redis.set(usedTimestepKey(user.id, result.timestep), '1', 'EX', 60 * 2, 'NX');\n return claimed !== null;\n};\n\nconst matchRecoveryHash = (hashes: string[], code: string): number => {\n const submitted = Buffer.from(sha256(code.trim().toLowerCase()));\n return hashes.findIndex((hash) => {\n const stored = Buffer.from(hash);\n return stored.length === submitted.length && crypto.timingSafeEqual(stored, submitted);\n });\n};\n\n//? Burning a recovery code is a read-modify-write on the user record's array,\n//? which the generic UserAdapter can't do atomically. Two concurrent uses of\n//? the SAME code could both match + both persist (double-spend), and two\n//? concurrent uses of DIFFERENT codes could last-writer-wins-overwrite each\n//? other (a lost update that resurrects a burned code). Serialize the whole\n//? read→match→burn per user with a short Redis lease, and RE-READ inside the\n//? lease so the burn always writes against fresh state. If the lease can't be\n//? taken (contention), fail closed — the caller can retry.\nconst consumeRecoveryCode = async (user: UserRecord, code: string): Promise<boolean> => {\n if ((user.recoveryCodes ?? []).length === 0) return false;\n const leaseToken = await acquireLease(recoveryLeaseName(user.id), RECOVERY_LEASE_MS);\n if (!leaseToken) {\n getLogger().warn('[2fa] recovery-code burn is contended (lease busy) — refusing this attempt, retry');\n return false;\n }\n try {\n //? Re-read under the lease: `user` was fetched before the lease, so its\n //? recoveryCodes array may be stale relative to a concurrent burn.\n const fresh = await getUserAdapter().findById(user.id);\n const hashes = fresh?.recoveryCodes ?? [];\n const index = matchRecoveryHash(hashes, code);\n if (index === -1) return false;\n const remaining = hashes.filter((_, position) => position !== index);\n const [updateError] = await tryCatch(() => getUserAdapter().update(user.id, { recoveryCodes: remaining }));\n //? Fail CLOSED: if the burn can't be persisted the code stays reusable — refuse it.\n if (updateError) {\n getLogger().error('[2fa] could not persist recovery-code consumption — refusing the code', updateError);\n return false;\n }\n return true;\n } finally {\n await releaseLease(recoveryLeaseName(user.id), leaseToken);\n }\n};\n\nexport interface VerifyTwoFactorInput {\n challengeToken: string;\n code: string;\n /** Defaults to 'totp'; the client sends what the user picked. */\n method?: TwoFactorMethod;\n supersedeToken?: string;\n requesterIp?: string;\n}\n\nexport const verifyTwoFactorChallenge = async (input: VerifyTwoFactorInput): Promise<CredentialsLoginResult> => {\n const challenge = await readChallenge(input.challengeToken);\n if (!challenge) return { status: false, reason: 'login.twoFactorChallengeExpired' };\n\n //? Cross-challenge, identity-keyed lockout FIRST (non-incrementing read).\n //? The per-challenge budget below is resettable by minting a fresh challenge\n //? (a password-holder can), so this account-level ceiling is what actually\n //? stops a distributed grind of the second factor.\n if (await isTwoFactorAccountLocked(challenge.userId)) {\n return { status: false, reason: 'login.twoFactorLocked' };\n }\n\n //? Attempt budget on the CHALLENGE (all methods combined). INCR is atomic;\n //? the counter inherits the challenge TTL.\n const counter = challengeAttemptsKey(input.challengeToken);\n const attempts = await redis.incr(counter);\n const remaining = await redis.ttl(challengeKey(input.challengeToken));\n if (remaining > 0) await redis.expire(counter, remaining);\n if (attempts > twoFactorConfig().twoFactorMaxAttempts) {\n await burnChallenge(input.challengeToken);\n return { status: false, reason: 'login.twoFactorLocked' };\n }\n\n const user = await getUserAdapter().findById(challenge.userId);\n if (!user) {\n await burnChallenge(input.challengeToken);\n return { status: false, reason: 'login.twoFactorChallengeExpired' };\n }\n\n const method: TwoFactorMethod = input.method ?? 'totp';\n let verified = false;\n if (method === 'totp') {\n //? Login boundary → enforce the single-use timestep replay guard.\n verified = await verifyTotpForUser(user, input.code, true);\n } else if (method === 'email-code') {\n if (!twoFactorConfig().twoFactorEmailFallback) return { status: false, reason: 'login.twoFactorMethodUnavailable' };\n verified = (await verifyEmailCode({\n purpose: '2fa',\n identity: user.id,\n code: input.code,\n maxAttempts: twoFactorConfig().emailCodeMaxAttempts,\n })) === 'valid';\n } else {\n verified = await consumeRecoveryCode(user, input.code);\n }\n\n if (!verified) {\n await recordTwoFactorAccountFailure(user.id);\n void dispatchHook('loginFailed', { email: user.email ?? undefined, userId: user.id, provider: 'credentials', reason: 'login.twoFactorInvalidCode', stage: 'login', requesterIp: input.requesterIp });\n return { status: false, reason: 'login.twoFactorInvalidCode' };\n }\n\n await burnChallenge(input.challengeToken);\n await clearEmailCode('2fa', user.id);\n await clearTwoFactorAccountFailures(user.id);\n return finalizeLogin(user, {\n provider: 'credentials',\n email: user.email ?? undefined,\n supersedeToken: input.supersedeToken,\n requesterIp: input.requesterIp,\n });\n};\n\n//? \"Send the code to my email instead\" — bound to an ACTIVE challenge so it\n//? can't be used to spam arbitrary users. Always answers ok (the challenge\n//? token itself already proves a verified first factor).\nexport const requestTwoFactorEmailCode = async (challengeToken: string): Promise<{ ok: boolean; reason?: string }> => {\n if (!twoFactorConfig().twoFactorEmailFallback) return { ok: false, reason: 'login.twoFactorMethodUnavailable' };\n const challenge = await readChallenge(challengeToken);\n if (!challenge) return { ok: false, reason: 'login.twoFactorChallengeExpired' };\n const user = await getUserAdapter().findById(challenge.userId);\n if (!user?.email) return { ok: false, reason: 'login.twoFactorChallengeExpired' };\n\n const config = twoFactorConfig();\n const code = await issueEmailCode({\n purpose: '2fa',\n identity: user.id,\n ttlSeconds: config.emailCodeTtlSeconds,\n digits: config.emailCodeLength,\n });\n const sent = await sendCodeEmail(user.email, code, 'Your verification code', `Use this code to finish signing in to ${getProjectName()}.`);\n return sent ? { ok: true } : { ok: false, reason: 'login.emailCodeSendFailed' };\n};\n\n//? Shared minimal code email (also used by the email-code LOGIN flow). Uses a\n//? raw subject/html send so no template registration is required; the code is\n//? digits-only so no HTML escaping concerns.\nexport const sendCodeEmail = async (to: string, code: string, subject: string, intro: string): Promise<boolean> => {\n const [loadError, email] = await tryCatch(() => loadEmailModule());\n if (loadError || !email) {\n getLogger().error('[2fa/email-code] @luckystack/email is not installed — cannot send the code email.');\n return false;\n }\n //? Visual spacing between digits (\"1 2 3 4 5 6\") — codes are digits-only.\n const spaced = code.match(/\\d/g)?.join(' ') ?? code;\n const [sendError, result] = await tryCatch(() => email.sendEmail({\n to,\n subject,\n html: `<p>${intro}</p><p style=\"font-size:28px;font-weight:700;letter-spacing:6px;font-family:monospace\">${spaced}</p><p>The code expires in ${String(Math.round(twoFactorConfig().emailCodeTtlSeconds / 60))} minutes. If you didn't request it, you can ignore this email.</p>`,\n text: `${intro}\\n\\nCode: ${code}\\n\\nThe code expires in ${String(Math.round(twoFactorConfig().emailCodeTtlSeconds / 60))} minutes. If you didn't request it, you can ignore this email.`,\n adapterHint: 'transactional',\n }));\n if (sendError || !result?.ok) {\n getLogger().error('[2fa/email-code] sending the code email failed', sendError ?? new Error(result?.reason ?? 'unknown'));\n return false;\n }\n return true;\n};\n\n//? ─────────────── enrollment (authenticated user) ───────────────\n\nexport type TotpEnrollmentStart =\n | { ok: true; secret: string; otpauthUri: string }\n | { ok: false; reason: string };\n\n//? Guard shared by setup + confirm: enrolling is refused when the feature is\n//? globally disabled (would give a false sense of security — the login gate\n//? never fires) AND when 2FA is ALREADY enabled. The second is the important\n//? one: without it a hijacked (2FA-less) session could call setup+enable to\n//? OVERWRITE the victim's factor with the attacker's — strictly worse than the\n//? disable path, which already requires a current code. Re-enrolling therefore\n//? requires disabling first (which proves current possession).\nconst enrollmentBlockedReason = (user: UserRecord): string | null => {\n if (twoFactorConfig().twoFactor === 'disabled') return 'login.twoFactorDisabledByServer';\n if (user.twoFactorEnabled) return 'login.twoFactorAlreadyEnabled';\n return null;\n};\n\n/**\n * Step 1: mint a secret and park it as PENDING (Redis, 10 min) — the user\n * record is untouched until the user proves the app works via\n * `confirmTotpEnrollment`. Re-calling replaces the pending secret. Refused for\n * an already-enrolled user / a disabled feature (see enrollmentBlockedReason).\n */\nexport const beginTotpEnrollment = async (user: UserRecord): Promise<TotpEnrollmentStart> => {\n const blocked = enrollmentBlockedReason(user);\n if (blocked) return { ok: false, reason: blocked };\n const secret = generateTotpSecret();\n await redis.set(pendingSecretKey(user.id), encryptTotpSecret(secret), 'EX', 60 * 10);\n return {\n ok: true,\n secret,\n otpauthUri: buildOtpauthUri({ secret, accountName: user.email ?? user.id, issuer: getProjectName() }),\n };\n};\n\n//? 80 bits of entropy per code (10 random bytes → 20 hex, grouped 5×4 for\n//? readability). 40 bits (the earlier 5 bytes) is offline-crackable against an\n//? unsalted sha256 store if the user table leaks; 80 bits is not.\nconst generateRecoveryCodes = (): { raw: string[]; hashes: string[] } => {\n const raw = Array.from({ length: 10 }, () => {\n const hex = crypto.randomBytes(10).toString('hex');\n return `${hex.slice(0, 5)}-${hex.slice(5, 10)}-${hex.slice(10, 15)}-${hex.slice(15, 20)}`;\n });\n return { raw, hashes: raw.map((code) => sha256(code.toLowerCase())) };\n};\n\nexport type ConfirmTotpEnrollmentResult =\n | { ok: true; recoveryCodes: string[] }\n | { ok: false; reason: string };\n\n/**\n * Step 2: the user enters the first code from their app. On success the\n * secret + recovery-code hashes land on the user record and 2FA is ON.\n * The RAW recovery codes are returned exactly once — show + let them save.\n */\nexport const confirmTotpEnrollment = async (user: UserRecord, code: string): Promise<ConfirmTotpEnrollmentResult> => {\n //? Re-check the block here too (defense in depth — a stale pending secret\n //? could otherwise be confirmed after 2FA was enabled by another path).\n const blocked = enrollmentBlockedReason(user);\n if (blocked) return { ok: false, reason: blocked };\n const pendingStored = await redis.get(pendingSecretKey(user.id));\n if (!pendingStored) return { ok: false, reason: 'login.twoFactorEnrollmentExpired' };\n const secret = decryptTotpSecret(pendingStored);\n if (!secret) return { ok: false, reason: 'login.twoFactorEnrollmentExpired' };\n\n const result = verifyTotp({ secret, code });\n if (!result.valid) return { ok: false, reason: 'login.twoFactorInvalidCode' };\n\n const { raw, hashes } = generateRecoveryCodes();\n const [updateError] = await tryCatch(() => getUserAdapter().update(user.id, {\n totpSecret: encryptTotpSecret(secret),\n twoFactorEnabled: true,\n recoveryCodes: hashes,\n }));\n if (updateError) {\n getLogger().error('[2fa] enrollment persist failed — is your UserAdapter/schema missing the 2FA fields? (twoFactorEnabled, totpSecret, recoveryCodes)', updateError);\n return { ok: false, reason: 'login.twoFactorPersistFailed' };\n }\n await redis.del(pendingSecretKey(user.id));\n return { ok: true, recoveryCodes: raw };\n};\n\n/**\n * Disable 2FA. Requires a currently-valid TOTP code OR an unused recovery\n * code, so a hijacked (but 2FA-less) session can't silently strip the factor.\n */\nexport const disableTwoFactor = async (user: UserRecord, code: string): Promise<{ ok: boolean; reason?: string }> => {\n //? Management action (session-authenticated) → plain proof-of-possession, no\n //? single-use guard, so a just-logged-in user isn't blocked for 30s.\n const viaTotp = await verifyTotpForUser(user, code, false);\n const viaRecovery = viaTotp ? false : await consumeRecoveryCode(user, code);\n if (!viaTotp && !viaRecovery) return { ok: false, reason: 'login.twoFactorInvalidCode' };\n\n const [updateError] = await tryCatch(() => getUserAdapter().update(user.id, {\n totpSecret: null,\n twoFactorEnabled: false,\n recoveryCodes: [],\n }));\n if (updateError) {\n getLogger().error('[2fa] disable persist failed', updateError);\n return { ok: false, reason: 'login.twoFactorPersistFailed' };\n }\n //? Clear any lingering account-lockout counter so re-enrolling later starts clean.\n await clearTwoFactorAccountFailures(user.id);\n return { ok: true };\n};\n\n/** Replace the recovery-code set (requires a valid current TOTP code). */\nexport const regenerateRecoveryCodes = async (user: UserRecord, code: string): Promise<ConfirmTotpEnrollmentResult> => {\n //? Management action → plain verify (see disableTwoFactor).\n if (!(await verifyTotpForUser(user, code, false))) return { ok: false, reason: 'login.twoFactorInvalidCode' };\n const { raw, hashes } = generateRecoveryCodes();\n const [updateError] = await tryCatch(() => getUserAdapter().update(user.id, { recoveryCodes: hashes }));\n if (updateError) {\n getLogger().error('[2fa] recovery-code regenerate persist failed', updateError);\n return { ok: false, reason: 'login.twoFactorPersistFailed' };\n }\n return { ok: true, recoveryCodes: raw };\n};\n\n//? Register the login gate (see header). Import of this module — via the\n//? package index — is what arms 2FA; nothing changes for code paths that\n//? never import it.\nregisterTwoFactorGate(createTwoFactorChallengeIfRequired);\n","//? Email-OTP primitive: short numeric codes typed over from an email —\n//? passwordless login (\"email-code\") and the 2FA email fallback both build on\n//? this. It deliberately does NOT reuse the core one-time-token primitive:\n//? that one is keyed BY sha256(token), which is only safe for high-entropy\n//? (256-bit) tokens. A 6-digit code has 10^6 possibilities — hashing it as the\n//? KEY would be brute-forceable from a Redis dump and unfindable at verify\n//? time. So codes are keyed by PURPOSE + IDENTITY instead, with the code hash\n//? in the VALUE, a server-side attempt counter, and at most ONE active code\n//? per (purpose, identity): re-issuing overwrites the previous code.\n//?\n//? Consume is at-most-once: the final DEL doubles as the winner-take-all lock\n//? (mirrors consumeOneTimeToken's atomic GET+DEL semantics for parallel\n//? verifies with the correct code).\n\nimport crypto from 'node:crypto';\nimport { formatKey, redis } from '@luckystack/core';\n\nexport type EmailOtpPurpose = 'login' | '2fa';\n\n//? Identity is an email (login) or userId (2fa). Normalize emails the same way\n//? the lockout does so `Foo@Bar.com` and `foo@bar.com` share one code slot.\nconst normalizeIdentity = (identity: string): string => identity.trim().toLowerCase();\n\nconst codeKey = (purpose: EmailOtpPurpose, identity: string): string =>\n formatKey('-emailcode', `${purpose}:${normalizeIdentity(identity)}`);\nconst attemptsKey = (purpose: EmailOtpPurpose, identity: string): string =>\n formatKey('-emailcode-attempts', `${purpose}:${normalizeIdentity(identity)}`);\n\nconst hashCode = (code: string): string => crypto.createHash('sha256').update(code).digest('hex');\n\n//? Uniform random numeric code. `crypto.randomInt` does rejection sampling\n//? internally — NO modulo bias (a plain `randomBytes % 10^n` would skew low\n//? digits), and unlike `hotp()` it is not derived from any counter/secret.\nexport const generateNumericCode = (digits: number): string =>\n String(crypto.randomInt(0, 10 ** digits)).padStart(digits, '0');\n\nexport interface IssueEmailCodeInput {\n purpose: EmailOtpPurpose;\n /** Email address (login) or userId (2fa). */\n identity: string;\n ttlSeconds: number;\n digits: number;\n}\n\n/**\n * Mint + store a fresh code for (purpose, identity). Any previously active\n * code for the same slot is replaced, and its attempt counter reset. Returns\n * the RAW code exactly once — the caller emails it; only sha256(code) is\n * stored (hash-at-rest, ADR 0010 spirit).\n */\nexport const issueEmailCode = async ({ purpose, identity, ttlSeconds, digits }: IssueEmailCodeInput): Promise<string> => {\n const code = generateNumericCode(digits);\n await redis.set(codeKey(purpose, identity), hashCode(code), 'EX', ttlSeconds);\n await redis.del(attemptsKey(purpose, identity));\n return code;\n};\n\nexport type EmailCodeVerdict = 'valid' | 'invalid' | 'expired' | 'locked';\n\nexport interface VerifyEmailCodeInput {\n purpose: EmailOtpPurpose;\n identity: string;\n code: string;\n /** Wrong-code attempts before the code is burned (counter is server-side). */\n maxAttempts: number;\n}\n\n/**\n * Verify + consume. Verdicts: 'valid' (code matched, slot consumed),\n * 'invalid' (wrong code, attempt recorded), 'locked' (attempt budget spent —\n * the active code is burned so guessing can't continue), 'expired' (no active\n * code: TTL passed, never issued, or already consumed).\n */\nexport const verifyEmailCode = async ({ purpose, identity, code, maxAttempts }: VerifyEmailCodeInput): Promise<EmailCodeVerdict> => {\n const key = codeKey(purpose, identity);\n const storedHash = await redis.get(key);\n if (!storedHash) return 'expired';\n\n //? Count the attempt FIRST (INCR is atomic across parallel verifies), and\n //? give the counter the code's own remaining TTL so it can't outlive it.\n //? If the code expired between the GET above and this TTL read (`remaining`\n //? <= 0), still bound the counter with a short fallback so a just-INCR'd\n //? counter can never linger without a TTL.\n const counter = attemptsKey(purpose, identity);\n const attempts = await redis.incr(counter);\n const remaining = await redis.ttl(key);\n await redis.expire(counter, remaining > 0 ? remaining : 600);\n if (attempts > maxAttempts) {\n await redis.del(key);\n await redis.del(counter);\n return 'locked';\n }\n\n const submitted = Buffer.from(hashCode(code.trim()));\n const expected = Buffer.from(storedHash);\n const matches = submitted.length === expected.length && crypto.timingSafeEqual(submitted, expected);\n if (!matches) return 'invalid';\n\n //? Winner-take-all consume: DEL returns how many keys were removed — 0 means\n //? a parallel verify with the same correct code beat us to it.\n const deleted = await redis.del(key);\n await redis.del(counter);\n return deleted > 0 ? 'valid' : 'expired';\n};\n\n/** Drop any active code + counter for the slot (e.g. after a completed login). */\nexport const clearEmailCode = async (purpose: EmailOtpPurpose, identity: string): Promise<void> => {\n await redis.del(codeKey(purpose, identity));\n await redis.del(attemptsKey(purpose, identity));\n};\n","//? TOTP (RFC 6238) + HOTP (RFC 4226) — hand-rolled on node:crypto, zero deps.\n//? Powers authenticator-app 2FA (Google/Microsoft Authenticator, Authy,\n//? 1Password, …): they all speak this open standard via an otpauth:// URI.\n//? Verification is timing-safe; replay protection is the CALLER's job — verify\n//? returns the matched timestep so the flow layer can persist \"last accepted\n//? timestep\" and reject codes at or before it (see twoFactor.ts).\n\nimport crypto from 'node:crypto';\n\n//? RFC 4648 base32 alphabet — authenticator apps expect the shared secret in\n//? base32 (both in the otpauth:// URI and for manual entry).\nconst BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n\nexport const base32Encode = (input: Buffer): string => {\n let bits = 0;\n let value = 0;\n let output = '';\n for (const byte of input) {\n value = (value << 8) | byte;\n bits += 8;\n while (bits >= 5) {\n output += BASE32_ALPHABET[(value >>> (bits - 5)) & 31];\n bits -= 5;\n }\n }\n if (bits > 0) output += BASE32_ALPHABET[(value << (5 - bits)) & 31];\n return output;\n};\n\nexport const base32Decode = (input: string): Buffer | null => {\n //? Tolerant of the formats users paste: lowercase, spaces, `=` padding.\n const normalized = input.toUpperCase().replaceAll(/[\\s=]/g, '');\n if (normalized.length === 0) return null;\n let bits = 0;\n let value = 0;\n const bytes: number[] = [];\n for (const char of normalized) {\n const index = BASE32_ALPHABET.indexOf(char);\n if (index === -1) return null;\n value = (value << 5) | index;\n bits += 5;\n if (bits >= 8) {\n bytes.push((value >>> (bits - 8)) & 0xFF);\n bits -= 8;\n }\n }\n return Buffer.from(bytes);\n};\n\n//? HOTP (RFC 4226 §5.3): HMAC-SHA1 over the big-endian counter, dynamic\n//? truncation, modulo 10^digits. Exported for the RFC test vectors.\nexport const hotp = (key: Buffer, counter: number, digits = 6): string => {\n const counterBuffer = Buffer.alloc(8);\n //? JS bitwise ops are 32-bit; write the 64-bit counter via BigInt.\n counterBuffer.writeBigUInt64BE(BigInt(counter));\n const digest = crypto.createHmac('sha1', key).update(counterBuffer).digest();\n const offset = digest.readUInt8(digest.length - 1) & 0x0F;\n const binary = digest.readUInt32BE(offset) & 0x7F_FF_FF_FF;\n return String(binary % 10 ** digits).padStart(digits, '0');\n};\n\nexport interface TotpVerifyInput {\n /** base32-encoded shared secret (as provisioned to the authenticator app). */\n secret: string;\n /** The user-entered code (digits; surrounding whitespace tolerated). */\n code: string;\n /**\n * Accepted clock drift in 30s steps on EITHER side of \"now\" (default 1 —\n * i.e. the previous, current and next code are accepted; RFC 6238 §5.2\n * recommends at most one backward step).\n */\n window?: number;\n /** Injection point for tests; defaults to the real clock. */\n timestampMs?: number;\n}\n\nexport interface TotpVerifyResult {\n valid: boolean;\n /**\n * The timestep the code matched (only when valid). Persist the highest\n * accepted value per user and refuse `timestep <= lastAccepted` to make\n * every code single-use (replay protection across the drift window).\n */\n timestep: number | null;\n}\n\nconst TOTP_STEP_SECONDS = 30;\n\nexport const verifyTotp = ({ secret, code, window = 1, timestampMs }: TotpVerifyInput): TotpVerifyResult => {\n const key = base32Decode(secret);\n const normalizedCode = code.trim();\n if (!key || key.length === 0 || !/^\\d{6,8}$/.test(normalizedCode)) return { valid: false, timestep: null };\n\n const now = timestampMs ?? Date.now();\n const currentStep = Math.floor(now / 1000 / TOTP_STEP_SECONDS);\n const drift = Math.max(0, Math.min(window, 4)); //? cap: a huge window would defeat the point of TOTP\n\n //? Check EVERY step in the window (no early return) with a timing-safe\n //? comparison, so response time never leaks which step (if any) matched.\n let matched: number | null = null;\n for (let step = currentStep - drift; step <= currentStep + drift; step++) {\n const expected = hotp(key, step, normalizedCode.length);\n const expectedBuffer = Buffer.from(expected);\n const actualBuffer = Buffer.from(normalizedCode);\n if (expectedBuffer.length === actualBuffer.length && crypto.timingSafeEqual(expectedBuffer, actualBuffer)) {\n matched ??= step;\n }\n }\n return matched === null ? { valid: false, timestep: null } : { valid: true, timestep: matched };\n};\n\n/** 20 random bytes (RFC 4226 §4 minimum: 128 bits; 160 recommended), base32. */\nexport const generateTotpSecret = (): string => base32Encode(crypto.randomBytes(20));\n\nexport interface OtpauthUriInput {\n /** base32 secret from generateTotpSecret(). */\n secret: string;\n /** What the authenticator app shows as the account (usually the user's email). */\n accountName: string;\n /** App/brand name shown above the account (defaults to the project title). */\n issuer: string;\n}\n\n//? The provisioning URI encoded into the enrollment QR code. Every\n//? authenticator app understands this format (Google's key-uri spec):\n//? otpauth://totp/<issuer>:<account>?secret=…&issuer=…&algorithm=SHA1&digits=6&period=30\nexport const buildOtpauthUri = ({ secret, accountName, issuer }: OtpauthUriInput): string => {\n const label = `${encodeURIComponent(issuer)}:${encodeURIComponent(accountName)}`;\n const query = new URLSearchParams({\n secret,\n issuer,\n algorithm: 'SHA1',\n digits: '6',\n period: String(TOTP_STEP_SECONDS),\n });\n return `otpauth://totp/${label}?${query.toString()}`;\n};\n","//? Passwordless email-code login (ADR 0024): the user enters their email,\n//? receives a short numeric code, and signs in by typing it. Opt-in via\n//? `auth.emailCodeLogin` (default false); needs @luckystack/email.\n//?\n//? Anti-enumeration mirrors forgotPassword.ts: the REQUEST endpoint always\n//? answers \"sent\" whether or not the address has an account — a code is only\n//? actually issued (and an email only actually sent) when a credentials user\n//? exists. Request throttling is done here (per-email + per-IP) because the\n//? send costs real email quota.\n//?\n//? 2FA interplay: possession of the mailbox is itself an email factor, but an\n//? account that enrolled an authenticator app expects the STRONGER factor to\n//? gate sign-in — so an enrolled user still gets the 2FA challenge after the\n//? email code verifies (same gate as the password path).\n\nimport { checkRateLimit, getLogger, getProjectConfig, getProjectName, tryCatch } from '@luckystack/core';\n\nimport { resolveUserByEmail } from './accountStrategy';\nimport { clearEmailCode, issueEmailCode, verifyEmailCode } from './emailOtp';\nimport { finalizeLogin, type CredentialsLoginResult } from './login';\nimport { sendCodeEmail, createTwoFactorChallengeIfRequired } from './twoFactor';\nimport { getUserAdapter } from './userAdapter';\n\nconst emailCodeConfig = () => getProjectConfig().auth;\n\nconst normalizeEmail = (email: string): string => email.trim().toLowerCase();\n\n//? Same window the auth lockout uses; request budgets are deliberately tight —\n//? each request is an outbound email.\nconst REQUEST_WINDOW_MS = 15 * 60 * 1000;\nconst REQUESTS_PER_EMAIL = 3;\nconst REQUESTS_PER_IP = 10;\n\nexport interface RequestEmailLoginCodeInput {\n email: string;\n requesterIp?: string;\n}\n\nexport type RequestEmailLoginCodeResult =\n | { ok: true }\n | { ok: false; reason: string };\n\nexport const requestEmailLoginCode = async ({ email, requesterIp }: RequestEmailLoginCodeInput): Promise<RequestEmailLoginCodeResult> => {\n const config = emailCodeConfig();\n if (!config.emailCodeLogin) return { ok: false, reason: 'login.emailCodeDisabled' };\n const normalized = normalizeEmail(email);\n if (!normalized || normalized.length > config.emailMaxLength) return { ok: false, reason: 'login.invalidEmailFormat' };\n\n //? Throttle BEFORE any user lookup so the limiter can't be used as an\n //? enumeration oracle either (both outcomes hit the same budget).\n const perEmail = await checkRateLimit({ key: `email-code:req:${normalized}`, limit: REQUESTS_PER_EMAIL, windowMs: REQUEST_WINDOW_MS });\n const perIp = requesterIp\n ? await checkRateLimit({ key: `email-code:req-ip:${requesterIp}`, limit: REQUESTS_PER_IP, windowMs: REQUEST_WINDOW_MS })\n : { allowed: true };\n if (!perEmail.allowed || !perIp.allowed) return { ok: false, reason: 'api.rateLimitExceeded' };\n\n //? Email-code login signs into the CREDENTIALS account for the address\n //? (consistent with forgot-password); under `providerAccountStrategy:\n //? 'unified'` the resolver already collapses providers by email.\n const [findError, user] = await tryCatch(() =>\n resolveUserByEmail(getUserAdapter(), { email: normalized, provider: 'credentials' }),\n );\n if (findError) {\n getLogger().error('email-code login: user lookup failed', findError);\n //? Still answer ok — a backend blip must not become an enumeration signal.\n return { ok: true };\n }\n if (!user) {\n //? Anti-enumeration: identical response; only a server-side log.\n getLogger().info('[email-code] request for unknown address (not sent)');\n return { ok: true };\n }\n\n const code = await issueEmailCode({\n purpose: 'login',\n identity: normalized,\n ttlSeconds: config.emailCodeTtlSeconds,\n digits: config.emailCodeLength,\n });\n //? Fire-and-forget the send. Awaiting it would (a) make the existing-account\n //? path measurably slower than the immediate ghost-account return above — a\n //? timing oracle — and (b) leak existence via a send-failure reason. So we\n //? ALWAYS return the same `{ ok: true }` (identical to the unknown-address\n //? path) and log a send failure server-side only. The user simply requests a\n //? new code if it never arrives.\n void sendCodeEmail(\n normalized,\n code,\n 'Your sign-in code',\n `Use this code to sign in to ${getProjectName()}.`,\n ).then((sent) => {\n if (!sent) getLogger().error('[email-code] login code send failed (account exists) — check the email adapter');\n });\n return { ok: true };\n};\n\nexport interface VerifyEmailLoginCodeInput {\n email: string;\n code: string;\n supersedeToken?: string;\n requesterIp?: string;\n}\n\nexport const verifyEmailLoginCode = async (input: VerifyEmailLoginCodeInput): Promise<CredentialsLoginResult> => {\n const config = emailCodeConfig();\n if (!config.emailCodeLogin) return { status: false, reason: 'login.emailCodeDisabled' };\n const normalized = normalizeEmail(input.email);\n\n const verdict = await verifyEmailCode({\n purpose: 'login',\n identity: normalized,\n code: input.code,\n maxAttempts: config.emailCodeMaxAttempts,\n });\n if (verdict !== 'valid') {\n const reason = verdict === 'invalid' ? 'login.emailCodeInvalid' : (verdict === 'locked' ? 'login.emailCodeLocked' : 'login.emailCodeExpired');\n return { status: false, reason };\n }\n\n const [findError, user] = await tryCatch(() =>\n resolveUserByEmail(getUserAdapter(), { email: normalized, provider: 'credentials' }),\n );\n if (findError || !user) {\n //? The code was valid, so the account existed moments ago — a vanished\n //? user (deleted mid-flow) fails closed.\n getLogger().error('email-code login: user vanished between issue and verify', findError ?? undefined);\n return { status: false, reason: 'login.emailCodeExpired' };\n }\n\n await clearEmailCode('login', normalized);\n\n //? Authenticator-enrolled accounts still answer the stronger factor (see header).\n const challenge = await createTwoFactorChallengeIfRequired(user, { requesterIp: input.requesterIp });\n if (challenge) return challenge;\n\n return finalizeLogin(user, {\n provider: 'credentials',\n email: normalized,\n supersedeToken: input.supersedeToken,\n requesterIp: input.requesterIp,\n });\n};\n"],"mappings":"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","names":["URLSearchParams","tryCatch","getLogger","getProjectConfig","getProjectConfig","getLogger","getProjectConfig","getProjectConfig","getProjectConfig","tryCatch","getLogger","validationError","response","URLSearchParams","bcrypt","getProjectConfig","formatKey","formatKey","getProjectConfig","bcrypt","hash","getProjectConfig","dispatchHook","getLogger","getProjectConfig","getLogger","dispatchHook","getProjectConfig","issueOneTimeToken","formatKey","redis","oneTimeTokenKey","getProjectConfig","getProjectConfig","crypto","dispatchHook","formatKey","getLogger","getProjectConfig","redis","tryCatch","formatKey","redis","crypto","crypto","getLogger","formatKey","redis","getProjectConfig","tryCatch","hash","dispatchHook","checkRateLimit","getLogger","getProjectConfig","getProjectName","tryCatch","getProjectConfig","checkRateLimit","tryCatch","getLogger","getProjectName"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luckystack/login",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -59,7 +59,7 @@
59
59
  "test": "vitest run"
60
60
  },
61
61
  "dependencies": {
62
- "@luckystack/core": "^0.7.1",
62
+ "@luckystack/core": "^0.7.3",
63
63
  "bcryptjs": "^3.0.0",
64
64
  "dotenv": "^17.0.0",
65
65
  "validator": "^13.15.35"
@@ -67,7 +67,7 @@
67
67
  "peerDependencies": {
68
68
  "@prisma/client": "^6.19.0",
69
69
  "socket.io": "^4.8.0",
70
- "@luckystack/email": "^0.7.1"
70
+ "@luckystack/email": "^0.7.3"
71
71
  },
72
72
  "peerDependenciesMeta": {
73
73
  "@prisma/client": {