@luckystack/login 0.1.9 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sessionAdapter.ts","../src/logout.ts","../src/session.ts","../src/index.ts","../src/oauthProviders.ts","../src/redirectResolver.ts","../src/login.ts","../src/userAdapter.ts","../src/passwordPolicy.ts","../src/data/commonPasswords.ts","../src/passwordReset.ts","../src/forgotPassword.ts","../src/emailChange.ts","../src/emailChangeNotification.ts"],"sourcesContent":["//? Session storage DI surface. The default adapter persists session\r\n//? records in Redis (same behaviour as before extraction). Consumers can\r\n//? plug in DynamoDB / Memcached / Postgres-table / signed-JWT-stateless\r\n//? adapters by implementing `SessionAdapter` and calling\r\n//? `registerSessionAdapter(...)` at boot.\r\n//?\r\n//? The adapter owns ONLY storage primitives. Framework-level concerns\r\n//? (CSRF minting, hook dispatch, socket emission, single-session\r\n//? enforcement) stay in session.ts and call the adapter to read/write.\r\n\r\nimport { redis, tryCatch, formatKey } from '@luckystack/core';\r\n\r\nexport interface SessionAdapter {\r\n /** Human-readable identifier (used in logs/diagnostics). */\r\n name: string;\r\n\r\n /** Read the raw stored session string for `token`. Null when missing. */\r\n getRaw(token: string): Promise<string | null>;\r\n\r\n /**\r\n * Write the raw session string for `token` with a TTL. Implementations\r\n * MUST honour the TTL — sessions without one would never expire.\r\n */\r\n setRaw(token: string, value: string, ttlSeconds: number): Promise<void>;\r\n\r\n /** Delete the session record for `token`. Idempotent. */\r\n delete(token: string): Promise<void>;\r\n\r\n /**\r\n * Refresh the TTL on an existing session record (sliding-window\r\n * expiration). Returns true when the underlying record existed and the\r\n * TTL was applied, false otherwise. Implementations must not create a\r\n * record on miss.\r\n */\r\n expire(token: string, ttlSeconds: number): Promise<boolean>;\r\n\r\n /** Remaining TTL in seconds (or null when the record is missing). */\r\n ttl(token: string): Promise<number | null>;\r\n\r\n // ---- Active-tokens-per-user (for single-session enforcement and\r\n // revokeUserSessions). Implementations need a set-like structure keyed\r\n // by userId. Memcached/edge-KV adapters can serialise as a comma-list\r\n // if no native set type is available. ----\r\n\r\n trackActive(userId: string, token: string, ttlSeconds: number): Promise<void>;\r\n untrackActive(userId: string, token: string): Promise<void>;\r\n listActive(userId: string): Promise<string[]>;\r\n\r\n /**\r\n * Admin walk: yield every active session record. Optional because\r\n * non-scannable backends (signed-JWT-stateless, log-only) cannot\r\n * enumerate. Callers fall back to a per-user listActive when omitted.\r\n */\r\n listAll?(): Promise<{ token: string; raw: string }[]>;\r\n}\r\n\r\n/**\r\n * Default adapter: Redis-backed. Key layout matches what the framework\r\n * has used since the first release, so registering this explicitly is\r\n * equivalent to not registering anything.\r\n *\r\n * Imports `redis` and `tryCatch` from `@luckystack/core`. Project name\r\n * + prefix are resolved at call time so projectConfig changes take\r\n * effect without reloading this module.\r\n */\r\n\r\nconst sessionKey = (token: string): string => formatKey('-session', token);\r\nconst activeUsersKey = (userId: string): string => formatKey('-activeUsers', userId);\r\n\r\nexport const redisSessionAdapter: SessionAdapter = {\r\n name: 'redis',\r\n\r\n async getRaw(token) {\r\n const [error, value] = await tryCatch(async () => await redis.get(sessionKey(token)));\r\n if (error) return null;\r\n return value ?? null;\r\n },\r\n\r\n async setRaw(token, value, ttlSeconds) {\r\n const key = sessionKey(token);\r\n await redis.set(key, value);\r\n await redis.expire(key, ttlSeconds);\r\n },\r\n\r\n async delete(token) {\r\n await redis.del(sessionKey(token));\r\n },\r\n\r\n async expire(token, ttlSeconds) {\r\n const result = await redis.expire(sessionKey(token), ttlSeconds);\r\n return result === 1;\r\n },\r\n\r\n async ttl(token) {\r\n const [error, value] = await tryCatch(async () => await redis.ttl(sessionKey(token)));\r\n if (error) return null;\r\n return typeof value === 'number' ? value : null;\r\n },\r\n\r\n async trackActive(userId, token, ttlSeconds) {\r\n const key = activeUsersKey(userId);\r\n await redis.sadd(key, token);\r\n await redis.expire(key, ttlSeconds);\r\n },\r\n\r\n async untrackActive(userId, token) {\r\n await redis.srem(activeUsersKey(userId), token);\r\n },\r\n\r\n async listActive(userId) {\r\n const [error, value] = await tryCatch(async () => await redis.smembers(activeUsersKey(userId)));\r\n if (error || !Array.isArray(value)) return [];\r\n return value;\r\n },\r\n\r\n async listAll() {\r\n const pattern = `${formatKey('-session', '')}:*`;\r\n const collected: { token: string; raw: string }[] = [];\r\n let cursor = '0';\r\n\r\n do {\r\n const [scanError, scanResponse] = await tryCatch(\r\n async () => await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100),\r\n );\r\n if (scanError || !scanResponse) break;\r\n\r\n cursor = scanResponse[0];\r\n const keys = scanResponse[1];\r\n\r\n if (Array.isArray(keys) && keys.length > 0) {\r\n const values = await Promise.all(keys.map((k) => redis.get(k)));\r\n for (const [idx, k] of keys.entries()) {\r\n const raw = values[idx];\r\n if (raw) {\r\n collected.push({\r\n token: k.replace(`${formatKey('-session', '')}:`, ''),\r\n raw,\r\n });\r\n }\r\n }\r\n }\r\n } while (cursor !== '0');\r\n\r\n return collected;\r\n },\r\n};\r\n\r\nlet activeAdapter: SessionAdapter = redisSessionAdapter;\r\n\r\n/**\r\n * Replace the active session storage backend. Call once at boot from\r\n * `luckystack/server/index.ts`, BEFORE the first login request. Common\r\n * use cases:\r\n *\r\n * - DynamoDB / Cosmos DB for serverless / edge deployments\r\n * - Postgres `sessions` table when you already operate Postgres\r\n * - signed-JWT-stateless (the adapter writes nothing, reads from the\r\n * JWT, and stores activeUsers per token in a small Redis hash)\r\n * - in-memory mock for integration tests\r\n *\r\n * Last-write-wins: subsequent calls overwrite the active adapter.\r\n */\r\nexport const registerSessionAdapter = (adapter: SessionAdapter): void => {\r\n activeAdapter = adapter;\r\n};\r\n\r\n/** Read the currently-active session adapter (defaults to Redis). */\r\nexport const getSessionAdapter = (): SessionAdapter => activeAdapter;\r\n","/* eslint-disable unicorn/no-abusive-eslint-disable */\r\n/* eslint-disable */\r\nimport { Socket } from \"socket.io\";\r\nimport { redis as redisClient, tryCatch, socketEventNames, dispatchHook, getLogger } from '@luckystack/core';\r\nimport { deleteSession, activeUsersKeyFor } from \"./session\";\r\n\r\nexport const logout = async ({ token, socket, userId, skipSessionDelete }: {\r\n token: string | null,\r\n socket: Socket | undefined,\r\n userId: string | null,\r\n skipSessionDelete?: boolean,\r\n}) => {\r\n const [error, result] = await tryCatch(async () => {\r\n if (!socket) {\r\n getLogger().warn('logout: invalid socket');\r\n return;\r\n }\r\n if (!token) {\r\n getLogger().warn('logout: no token provided');\r\n return;\r\n }\r\n\r\n const preLogoutResult = await dispatchHook('preLogout', { userId, token });\r\n if (preLogoutResult.stopped) {\r\n getLogger().warn('logout: aborted by preLogout hook', { errorCode: preLogoutResult.signal.errorCode });\r\n socket.emit(socketEventNames.logout, \"error\");\r\n return;\r\n }\r\n\r\n getLogger().debug(`logout: user ${userId ?? '?'}`, { token });\r\n\r\n if (!skipSessionDelete) {\r\n await deleteSession(token);\r\n }\r\n if (userId) {\r\n await redisClient.srem(activeUsersKeyFor(userId), token);\r\n }\r\n socket.leave(token);\r\n\r\n // Presence state (disconnectTimers, tempDisconnectedSockets) is cleaned up\r\n // by `@luckystack/presence`'s `postLogout` hook handler, registered at\r\n // server startup via `registerPresenceHooks()`. Keeps login → presence\r\n // a one-way dep: presence knows about login (uses getSession/deleteSession),\r\n // login does not know about presence.\r\n await dispatchHook('postLogout', { userId, token });\r\n return true;\r\n });\r\n if (error) {\r\n if (socket) {\r\n socket.emit(socketEventNames.logout, \"error\");\r\n }\r\n } else if (result && socket) {\r\n //? Opt-in logout tracing. This fires on EVERY successful logout, so an\r\n //? unconditional warn+stacktrace spams the server log. Set\r\n //? LUCKYSTACK_TRACE_SESSION_DELETES=1 when hunting a \"why was I logged out?\"\r\n //? bug to print the originating caller's stack; silent otherwise. The\r\n //? client still receives the success-emit and clears its session regardless.\r\n if (process.env.LUCKYSTACK_TRACE_SESSION_DELETES === '1') {\r\n getLogger().warn('[session] logout success — emitting logout to socket', {\r\n tokenPrefix: token ? token.slice(0, 8) : null,\r\n userId,\r\n socketId: socket.id,\r\n stack: new Error('logout invoked').stack,\r\n });\r\n }\r\n socket.emit(socketEventNames.logout, \"success\");\r\n }\r\n}\r\n","import type { BaseSessionLayout as SessionLayout } from \"./sessionLayout\";\r\nimport { randomBytes } from 'node:crypto';\r\nimport {\r\n socketEventNames,\r\n dispatchHook,\r\n getCsrfConfig,\r\n getLogger,\r\n getProjectConfig,\r\n formatKey,\r\n tryCatch,\r\n} from '@luckystack/core';\r\n\r\nimport { getSessionAdapter } from './sessionAdapter';\r\n\r\n//? Resolved at call time so project registration order doesn't matter.\r\n//? Session TTL comes from projectConfig and is honoured by every adapter\r\n//? (Redis default, or any custom adapter the consumer registers).\r\nconst getSessionTtl = (): number => 60 * 60 * 24 * getProjectConfig().session.expiryDays;\r\n\r\nconst isSessionLayout = (value: unknown): value is SessionLayout => {\r\n return value !== null && typeof value === 'object' && 'id' in value;\r\n};\r\n\r\nconst parseSessionLayout = (value: string): SessionLayout | null => {\r\n const parsed = JSON.parse(value) as unknown;\r\n return isSessionLayout(parsed) ? parsed : null;\r\n};\r\n\r\n/**\r\n * Save or update a user session through the active session adapter.\r\n *\r\n * @param token - The session token (unique identifier)\r\n * @param data - The session data to store\r\n * @param newUser - If true, this is a new login (triggers single-session enforcement)\r\n * @param options.supersedeToken - A token belonging to the SAME browser that is\r\n * being replaced by this login (e.g. re-login while already signed in). It is\r\n * excluded from single-session enforcement so the user's current device is not\r\n * kicked by its own new session. The caller is responsible for deleting it\r\n * afterwards (use `deleteSession(supersedeToken, { skipSocketLogout: true })`).\r\n */\r\nconst saveSession = async (\r\n token: string,\r\n data: SessionLayout,\r\n newUser?: boolean,\r\n options?: { supersedeToken?: string },\r\n): Promise<{ ok: true } | { ok: false; errorCode: string }> => {\r\n const [error, outcome] = await tryCatch(async () => {\r\n const adapter = getSessionAdapter();\r\n\r\n if (newUser) {\r\n const preSessionCreateResult = await dispatchHook('preSessionCreate', {\r\n token,\r\n user: data,\r\n persistent: !getProjectConfig().session.basedToken,\r\n });\r\n if (preSessionCreateResult.stopped) {\r\n getLogger().warn(`session create aborted by preSessionCreate hook`, { errorCode: preSessionCreateResult.signal.errorCode });\r\n return { ok: false, errorCode: preSessionCreateResult.signal.errorCode || 'api.internalServerError' } as const;\r\n }\r\n }\r\n\r\n //? Mint a CSRF token on first session write. Subsequent writes preserve\r\n //? the existing token so the client doesn't have to re-fetch on every\r\n //? session update. The token is rotated on logout via `deleteSession`.\r\n //? Token length is consumer-configurable via `registerCsrfConfig({ tokenLength })`.\r\n data.csrfToken ??= randomBytes(getCsrfConfig().tokenLength).toString('hex');\r\n\r\n const ttl = getSessionTtl();\r\n await adapter.setRaw(token, JSON.stringify(data), ttl);\r\n\r\n const userId = data.id;\r\n\r\n //? Always track active tokens BEFORE the io check — `trackActive` is a\r\n //? Redis-only write, independent of any live socket. Gating it behind a\r\n //? live io meant processes without a Socket.io instance (the test harness,\r\n //? background workers, CLI tasks) persisted the session but never populated\r\n //? the active-tokens set, so `listSessions` / `revokeUserSessions` saw\r\n //? nothing for that user.\r\n if (userId) {\r\n await adapter.trackActive(userId, token, ttl);\r\n }\r\n\r\n const { getIoInstance } = await import('@luckystack/core');\r\n const io = getIoInstance();\r\n //? Socket fanout + single-session enforcement below need a live io; skip\r\n //? them when there isn't one. The session IS already persisted + tracked, so\r\n //? this is a success (background workers / CLI tasks have no io).\r\n if (!io) return { ok: true } as const;\r\n\r\n // Handle session-limit enforcement on new login. Logic:\r\n // 1. perUser = 'single' (default) OR legacy `allowMultiple: false` →\r\n // kick every prior session for this user.\r\n // 2. perUser = 'multiple' with `maxConcurrentPerUser` cap reached:\r\n // `onConflict === 'revokeOld'` kicks oldest until under cap,\r\n // `onConflict === 'rejectNew'` would refuse the new login (handled\r\n // at the login API layer, not here).\r\n // 3. perUser = 'multiple' with no cap → no kick.\r\n if (newUser && userId) {\r\n const sessionCfg = getProjectConfig().session;\r\n // eslint-disable-next-line @typescript-eslint/no-deprecated -- BC shim: legacy `allowMultiple` still honored when set\r\n const effectivePerUser = sessionCfg.allowMultiple ? 'multiple' : sessionCfg.perUser;\r\n const cap = sessionCfg.maxConcurrentPerUser;\r\n\r\n const allTokens = await adapter.listActive(userId);\r\n //? Exclude the current token — it was just added and has no socket room yet.\r\n //? Also exclude `supersedeToken`: when the same browser re-logs-in, its old\r\n //? token is being replaced (not a separate device), so kicking it would log\r\n //? the user out of the very browser performing the login.\r\n const previousTokens = allTokens.filter(\r\n (t) => t !== token && t !== options?.supersedeToken,\r\n );\r\n\r\n let tokensToKick: string[] = [];\r\n if (effectivePerUser === 'single') {\r\n tokensToKick = previousTokens;\r\n } else if (cap !== null && previousTokens.length + 1 > cap && sessionCfg.onConflict === 'revokeOld') {\r\n // Kick oldest until under cap. Without ordering metadata in the adapter,\r\n // we drop the head of the list — most adapters return insertion-order\r\n // for set-like structures, but consumers needing strict LRU should\r\n // implement a custom adapter that orders deterministically.\r\n const excess = previousTokens.length + 1 - cap;\r\n tokensToKick = previousTokens.slice(0, excess);\r\n }\r\n\r\n if (tokensToKick.length > 0) {\r\n const { logout } = await import('./logout');\r\n\r\n await Promise.all(tokensToKick.map(async (previousToken) => {\r\n const sockets = io.sockets.adapter.rooms.get(previousToken);\r\n if (sockets) {\r\n getLogger().debug(`Kicking previous session for user ${userId}`);\r\n // Optional UI notification before the disconnect lands.\r\n if (sessionCfg.notifyOldDeviceOnRevoke) {\r\n io.to(previousToken).emit(socketEventNames.sessionReplaced, JSON.stringify({\r\n reason: 'session-replaced',\r\n userId,\r\n }));\r\n }\r\n for (const socketId of sockets) {\r\n const socket = io.sockets.sockets.get(socketId);\r\n if (socket) {\r\n await logout({ token: previousToken, socket, userId });\r\n }\r\n }\r\n } else {\r\n // No active sockets, just clean up the adapter state\r\n await adapter.delete(previousToken);\r\n await adapter.untrackActive(userId, previousToken);\r\n }\r\n }));\r\n }\r\n }\r\n\r\n // Broadcast session updates to connected clients\r\n if (io.sockets.adapter.rooms.has(token)) {\r\n io.to(token).emit(socketEventNames.updateSession, JSON.stringify(data));\r\n }\r\n\r\n if (newUser) {\r\n await dispatchHook('postSessionCreate', { token, user: data, persistent: !getProjectConfig().session.basedToken });\r\n }\r\n\r\n return { ok: true } as const;\r\n }, undefined, { fn: 'saveSession', token });\r\n\r\n if (error) {\r\n getLogger().error('saveSession failed', error, { token });\r\n //? Surface the failure instead of swallowing it. Callers (credentials/OAuth\r\n //? login) MUST NOT mint a token + delete the prior session when the new\r\n //? session never persisted — a transient adapter blip would otherwise log\r\n //? the user out with no usable replacement.\r\n return { ok: false, errorCode: 'api.internalServerError' };\r\n }\r\n return outcome ?? { ok: false, errorCode: 'api.internalServerError' };\r\n};\r\n\r\n/**\r\n * Retrieve a user session through the active session adapter.\r\n *\r\n * @param token - The session token\r\n * @returns The session data or null if not found\r\n */\r\nconst getSession = async (token: string | null): Promise<SessionLayout | null> => {\r\n if (!token) return null;\r\n\r\n const [error, value] = await tryCatch(async () => {\r\n const adapter = getSessionAdapter();\r\n\r\n const raw = await adapter.getRaw(token);\r\n if (!raw) return null;\r\n\r\n const parsed = parseSessionLayout(raw);\r\n const userId = parsed?.id ?? null;\r\n\r\n // Sliding expiration: each successful authenticated access extends session lifetime.\r\n const newTtl = getSessionTtl();\r\n const oldTtl = await adapter.ttl(token);\r\n const preRefreshResult = await dispatchHook('preSessionRefresh', { token, userId, oldTtl, newTtl });\r\n\r\n //? A `preSessionRefresh` handler can stop the TTL extension (e.g. admin\r\n //? freezing a session pending review). When stopped, we skip the\r\n //? `adapter.expire` call and surface `applied: false` on the post payload\r\n //? so audit handlers see the same signal.\r\n let applied = false;\r\n if (!preRefreshResult.stopped) {\r\n applied = await adapter.expire(token, newTtl);\r\n }\r\n await dispatchHook('postSessionRefresh', {\r\n token,\r\n userId,\r\n oldTtl,\r\n newTtl,\r\n applied,\r\n });\r\n\r\n if (!parsed) return null;\r\n const merged: SessionLayout = { ...parsed, token };\r\n return merged;\r\n }, undefined, { fn: 'getSession', token });\r\n\r\n if (error) {\r\n getLogger().error('getSession failed', error, { token });\r\n return null;\r\n }\r\n return value;\r\n};\r\n\r\n/**\r\n * Delete a user session through the active adapter and notify connected\r\n * clients.\r\n *\r\n * @param token - The session token to delete\r\n * @param options.skipSocketLogout - When true, the session data + active-token\r\n * tracking are cleaned up and the pre/post delete hooks still fire, but NO\r\n * `logout` is emitted to sockets in the token's room. Use this when the SAME\r\n * browser is replacing its session (re-login): emitting a logout would bounce\r\n * that browser to the login page and null its brand-new session.\r\n * @returns true if successful\r\n */\r\nconst deleteSession = async (\r\n token: string,\r\n options?: { skipSocketLogout?: boolean },\r\n): Promise<boolean> => {\r\n //? Opt-in spurious-delete tracing. `deleteSession` runs on EVERY legitimate\r\n //? logout / revokeSession / signOutEverywhere / deleteAccount, so an\r\n //? unconditional warn+stacktrace drowns the signal it's meant to surface and\r\n //? spams dev/test output with stacktrace-shaped lines that read like errors.\r\n //? Set LUCKYSTACK_TRACE_SESSION_DELETES=1 when hunting a \"why was I logged\r\n //? out?\" bug to print the originating caller's stack; silent otherwise.\r\n if (process.env.LUCKYSTACK_TRACE_SESSION_DELETES === '1') {\r\n getLogger().warn('[session] deleteSession invoked', {\r\n tokenPrefix: token.slice(0, 8),\r\n stack: new Error('deleteSession invoked').stack,\r\n });\r\n }\r\n\r\n const [error, ok] = await tryCatch(async () => {\r\n const adapter = getSessionAdapter();\r\n\r\n const raw = await adapter.getRaw(token);\r\n let resolvedUserId: string | null = null;\r\n\r\n if (raw) {\r\n resolvedUserId = parseSessionLayout(raw)?.id ?? null;\r\n }\r\n\r\n const preSessionDeleteResult = await dispatchHook('preSessionDelete', { token, userId: resolvedUserId });\r\n if (preSessionDeleteResult.stopped) {\r\n getLogger().warn(`session delete aborted by preSessionDelete hook`, { errorCode: preSessionDeleteResult.signal.errorCode });\r\n return false;\r\n }\r\n\r\n if (raw && resolvedUserId) {\r\n const { getIoInstance } = await import('@luckystack/core');\r\n const ioInstance = getIoInstance();\r\n\r\n // Reuse the same logout flow as single-session enforcement.\r\n if (ioInstance && !options?.skipSocketLogout) {\r\n const { logout } = await import('./logout');\r\n const sockets = ioInstance.sockets.adapter.rooms.get(token);\r\n\r\n if (sockets) {\r\n await Promise.all([...sockets].map(async (socketId) => {\r\n const socket = ioInstance.sockets.sockets.get(socketId);\r\n if (!socket) return;\r\n\r\n await logout({\r\n token,\r\n socket,\r\n userId: resolvedUserId,\r\n skipSessionDelete: true,\r\n });\r\n }));\r\n }\r\n }\r\n\r\n await adapter.untrackActive(resolvedUserId, token);\r\n }\r\n\r\n await adapter.delete(token);\r\n await dispatchHook('postSessionDelete', { token, userId: resolvedUserId });\r\n return true;\r\n }, undefined, { fn: 'deleteSession', token });\r\n\r\n if (error) {\r\n getLogger().error('deleteSession failed', error, { token });\r\n return false;\r\n }\r\n return ok ?? false;\r\n};\r\n\r\n/**\r\n * Get all active sessions (admin utility). Returns empty when the active\r\n * adapter doesn't implement enumeration (signed-JWT-stateless, log-only).\r\n *\r\n * @returns Array of all session data\r\n */\r\nconst getAllSessions = async (): Promise<SessionLayout[]> => {\r\n const [error, sessions] = await tryCatch(async () => {\r\n const adapter = getSessionAdapter();\r\n if (!adapter.listAll) {\r\n getLogger().warn(`[session] getAllSessions: adapter '${adapter.name}' does not implement listAll — returning empty`);\r\n return [];\r\n }\r\n\r\n const rows = await adapter.listAll();\r\n const parsedSessions: SessionLayout[] = [];\r\n for (const { raw } of rows) {\r\n const parsed = parseSessionLayout(raw);\r\n if (parsed) parsedSessions.push(parsed);\r\n }\r\n return parsedSessions;\r\n }, undefined, { fn: 'getAllSessions' });\r\n\r\n if (error) {\r\n getLogger().error('getAllSessions failed', error);\r\n return [];\r\n }\r\n return sessions ?? [];\r\n};\r\n\r\n/**\r\n * Revoke every active session for a user, optionally keeping one alive.\r\n *\r\n * @param userId - The user whose sessions to revoke\r\n * @param exceptToken - If provided, this session is left untouched. Pass the\r\n * caller's own token when revoking after a password change so the user\r\n * isn't logged out of the device they just changed it from. Pass null/omit\r\n * to revoke everything (account deletion, \"sign out everywhere\", etc.).\r\n *\r\n * Each revocation goes through `deleteSession`, so connected sockets are\r\n * told to log out via the same flow as a normal logout. Returns the number\r\n * of sessions actually revoked.\r\n */\r\nconst revokeUserSessions = async (userId: string, exceptToken?: string | null): Promise<number> => {\r\n if (!userId) return 0;\r\n\r\n const adapter = getSessionAdapter();\r\n const tokens = await adapter.listActive(userId);\r\n const targets = exceptToken ? tokens.filter((t) => t !== exceptToken) : tokens;\r\n\r\n await Promise.all(targets.map((token) => deleteSession(token)));\r\n return targets.length;\r\n};\r\n\r\n//? Legacy key-builders preserved as exports for downstream code that\r\n//? assumed a stable Redis layout (admin tooling, dev REPL scripts). New\r\n//? callers should use the active SessionAdapter instead.\r\nconst sessionKeyFor = (token: string): string => formatKey('-session', token);\r\nconst activeUsersKeyFor = (userId: string): string => formatKey('-activeUsers', userId);\r\n\r\nexport { saveSession, getSession, deleteSession, getAllSessions, revokeUserSessions, sessionKeyFor, activeUsersKeyFor };\r\n","// Registers module augmentations on @luckystack/core for auth/session hooks.\r\nimport './hookPayloads';\r\n\r\nexport type { BaseSessionLayout, SessionLocation, AuthProps } from './sessionLayout';\r\nexport type {\r\n PreLoginPayload,\r\n PostLoginPayload,\r\n PreRegisterPayload,\r\n PostRegisterPayload,\r\n PreLogoutPayload,\r\n PostLogoutPayload,\r\n PreSessionCreatePayload,\r\n PostSessionCreatePayload,\r\n PreSessionDeletePayload,\r\n PostSessionDeletePayload,\r\n PreEmailChangePayload,\r\n PostEmailChangeRequestedPayload,\r\n PostEmailChangedPayload,\r\n} from './hookPayloads';\r\nexport { saveSession, getSession, deleteSession, getAllSessions, revokeUserSessions, sessionKeyFor, activeUsersKeyFor } from './session';\r\nexport { registerSessionAdapter, getSessionAdapter, redisSessionAdapter } from './sessionAdapter';\r\nexport type { SessionAdapter } from './sessionAdapter';\r\nexport { loginWithCredentials, loginCallback, createOAuthState, registerWithCredentials, loginWithCredentialsCore } from './login';\r\nexport { logout } from './logout';\r\n\r\n// Password-reset primitives. Used by the framework's `framework`-mode\r\n// forgot-password flow AND exported for consumers who picked `'custom'`.\r\nexport {\r\n createPasswordResetToken,\r\n consumePasswordResetToken,\r\n updatePasswordHash,\r\n verifyPassword,\r\n PasswordPolicyError,\r\n} from './passwordReset';\r\nexport { validatePassword } from './passwordPolicy';\r\nexport { sendPasswordResetEmail } from './forgotPassword';\r\n\r\n// Email-change primitives. Used by the framework's settings flow to confirm\r\n// a new email address before applying it (token mailed to NEW address proves\r\n// ownership of the new mailbox).\r\nexport {\r\n createEmailChangeToken,\r\n consumeEmailChangeToken,\r\n} from './emailChange';\r\nexport type { EmailChangePayload } from './emailChange';\r\nexport { sendEmailChangeConfirmation } from './emailChangeNotification';\r\n\r\n// OAuth provider registry + composable helpers.\r\nexport {\r\n registerOAuthProviders,\r\n getOAuthProviders,\r\n isFullOAuthProvider,\r\n credentialsProvider,\r\n googleProvider,\r\n githubProvider,\r\n discordProvider,\r\n facebookProvider,\r\n microsoftProvider,\r\n} from './oauthProviders';\r\nexport type {\r\n OAuthProvider,\r\n CredentialsProvider,\r\n FullOAuthProvider,\r\n} from './oauthProviders';\r\n\r\n// User adapter registry: lets consumers swap out the Prisma User model behind auth flows.\r\nexport {\r\n registerUserAdapter,\r\n getUserAdapter,\r\n isUserAdapterRegistered,\r\n defaultPrismaUserAdapter,\r\n} from './userAdapter';\r\nexport type {\r\n UserAdapter,\r\n UserAdapterCreateInput,\r\n UserRecord,\r\n} from './userAdapter';\r\n\r\n// Post-login redirect resolver — lets consumers compute the OAuth callback\r\n// destination dynamically (per-user, per-tenant, per-provider).\r\nexport {\r\n registerPostLoginRedirect,\r\n getPostLoginRedirect,\r\n} from './redirectResolver';\r\nexport type {\r\n PostLoginRedirectResolver,\r\n PostLoginRedirectInput,\r\n} from './redirectResolver';\r\n","//? OAuth provider registry. Replaces the previously hardcoded provider list\r\n//? in `loginConfig.ts`. Consumers register providers from their\r\n//? `luckystack/login/oauthProviders.ts` overlay file at boot.\r\n//?\r\n//? Two-layer surface:\r\n//? 1. `registerOAuthProviders([...])` — accept an array of provider definitions.\r\n//? 2. Built-in helpers (`googleProvider`, `githubProvider`, ...) so the common\r\n//? case is one function call per provider with only the secrets + callback.\r\n//?\r\n//? Custom providers (Okta, Apple, X, etc.) plug in as raw `OAuthProvider`\r\n//? objects alongside the helpers.\r\n\r\nimport { tryCatch } from '@luckystack/core';\r\n\r\ntype OAuthUserData = Record<string, unknown>;\r\n\r\nexport const asOAuthUserData = (value: unknown): OAuthUserData => {\r\n if (value && typeof value === 'object') {\r\n return value as OAuthUserData;\r\n }\r\n return {};\r\n};\r\n\r\nexport interface CredentialsProvider {\r\n name: 'credentials';\r\n}\r\n\r\nexport interface FullOAuthProvider {\r\n name: string;\r\n clientID: string;\r\n clientSecret: string;\r\n callbackURL: string;\r\n authorizationURL: string;\r\n tokenExchangeURL: string;\r\n tokenExchangeMethod: 'json' | 'form';\r\n userInfoURL: string;\r\n /**\r\n * OAuth scopes requested at the authorization step. Consumers can extend\r\n * a built-in provider's defaults to request additional permissions:\r\n *\r\n * ```ts\r\n * const customGoogle = googleProvider({...});\r\n * customGoogle.scope.push('https://www.googleapis.com/auth/calendar.readonly');\r\n * ```\r\n *\r\n * Or pass `extraScopes` to the built-in factory (see `OAuthHelperInput`).\r\n */\r\n scope: string[];\r\n nameKey: string;\r\n emailKey: string;\r\n avatarKey?: string;\r\n avatarCodeKey: string;\r\n getEmail?: (accessToken: string) => Promise<string | false | undefined>;\r\n getAvatar?: (params: { userData: OAuthUserData; avatarId?: string; accessToken: string }) => string | undefined | Promise<string | undefined>;\r\n /**\r\n * Optional per-provider hook to attach extra runtime-only data to the\r\n * session (NOT to the User row). Return key/value pairs that get merged\r\n * into `BaseSessionLayout` before `saveSession`. To get strict typing\r\n * downstream, extend `BaseSessionLayout` via module augmentation:\r\n *\r\n * ```ts\r\n * declare module '@luckystack/core' {\r\n * interface BaseSessionLayout {\r\n * googleCalendarToken?: string;\r\n * }\r\n * }\r\n * ```\r\n *\r\n * Data lives on the session in Redis and disappears on logout — no Prisma\r\n * schema change required. Don't store secrets you wouldn't want surfaced\r\n * to a user inspecting their own session.\r\n */\r\n extraSessionFields?: (params: {\r\n userData: OAuthUserData;\r\n accessToken: string;\r\n }) => Promise<Record<string, unknown>> | Record<string, unknown>;\r\n}\r\n\r\nexport type OAuthProvider = CredentialsProvider | FullOAuthProvider;\r\n\r\nexport const isFullOAuthProvider = (provider: OAuthProvider): provider is FullOAuthProvider =>\r\n provider.name !== 'credentials';\r\n\r\nexport interface OAuthEndpointOverrides {\r\n /** Override the provider's authorization URL (e.g. GitHub Enterprise host, Microsoft custom tenant). */\r\n authorizationURL?: string;\r\n /** Override the token exchange URL. */\r\n tokenExchangeURL?: string;\r\n /** Override the userInfo URL. */\r\n userInfoURL?: string;\r\n}\r\n\r\ninterface OAuthHelperInput {\r\n clientId: string | undefined;\r\n clientSecret: string | undefined;\r\n callbackUrl: string;\r\n /** Optional URL overrides for self-hosted / custom-tenant deployments. */\r\n endpoints?: OAuthEndpointOverrides;\r\n /**\r\n * Additional OAuth scopes to request alongside the built-in defaults. Use\r\n * for \"give me access to the user's calendar / drive / repos\" flows.\r\n * Merged with the helper's default scopes; duplicates are deduplicated.\r\n */\r\n extraScopes?: string[];\r\n /**\r\n * Per-provider hook for attaching runtime-only fields to the session.\r\n * See `FullOAuthProvider.extraSessionFields` for usage + module-augmentation\r\n * pattern.\r\n */\r\n extraSessionFields?: FullOAuthProvider['extraSessionFields'];\r\n}\r\n\r\nconst mergeScopes = (defaults: string[], extra: string[] | undefined): string[] => {\r\n if (!extra || extra.length === 0) return defaults;\r\n return [...new Set([...defaults, ...extra])];\r\n};\r\n\r\nconst requireString = (value: string | undefined, label: string): string => {\r\n if (!value || value.length === 0) {\r\n throw new Error(`OAuth provider configuration error: ${label} is empty.`);\r\n }\r\n return value;\r\n};\r\n\r\nexport const credentialsProvider = (): CredentialsProvider => ({ name: 'credentials' });\r\n\r\nexport const googleProvider = (input: OAuthHelperInput): FullOAuthProvider => ({\r\n name: 'google',\r\n clientID: requireString(input.clientId, 'google clientId'),\r\n clientSecret: requireString(input.clientSecret, 'google clientSecret'),\r\n callbackURL: input.callbackUrl,\r\n authorizationURL: input.endpoints?.authorizationURL ?? 'https://accounts.google.com/o/oauth2/v2/auth',\r\n tokenExchangeURL: input.endpoints?.tokenExchangeURL ?? 'https://oauth2.googleapis.com/token',\r\n tokenExchangeMethod: 'json',\r\n userInfoURL: input.endpoints?.userInfoURL ?? 'https://www.googleapis.com/oauth2/v1/userinfo',\r\n scope: mergeScopes([\r\n 'https://www.googleapis.com/auth/userinfo.profile',\r\n 'https://www.googleapis.com/auth/userinfo.email',\r\n ], input.extraScopes),\r\n nameKey: 'name',\r\n emailKey: 'email',\r\n avatarKey: 'picture',\r\n avatarCodeKey: '',\r\n extraSessionFields: input.extraSessionFields,\r\n});\r\n\r\nexport const githubProvider = (input: OAuthHelperInput): FullOAuthProvider => ({\r\n name: 'github',\r\n clientID: requireString(input.clientId, 'github clientId'),\r\n clientSecret: requireString(input.clientSecret, 'github clientSecret'),\r\n callbackURL: input.callbackUrl,\r\n authorizationURL: input.endpoints?.authorizationURL ?? 'https://github.com/login/oauth/authorize',\r\n tokenExchangeURL: input.endpoints?.tokenExchangeURL ?? 'https://github.com/login/oauth/access_token',\r\n tokenExchangeMethod: 'json',\r\n userInfoURL: input.endpoints?.userInfoURL ?? 'https://api.github.com/user',\r\n scope: mergeScopes(['read:user', 'user:email'], input.extraScopes),\r\n nameKey: 'login',\r\n emailKey: 'email',\r\n avatarKey: 'avatar_url',\r\n avatarCodeKey: '',\r\n extraSessionFields: input.extraSessionFields,\r\n getEmail: async (accessToken) => {\r\n const fetchEmails = async () => {\r\n const response = await fetch('https://api.github.com/user/emails', {\r\n method: 'GET',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n Accept: 'application/json',\r\n Authorization: `Bearer ${accessToken}`,\r\n },\r\n });\r\n if (!response.ok) return false;\r\n const emails: unknown = await response.json();\r\n if (!Array.isArray(emails)) return false;\r\n return emails as { primary?: boolean; email?: string }[];\r\n };\r\n\r\n const [error, emails] = await tryCatch(fetchEmails);\r\n if (error || !emails) return false;\r\n\r\n let mainEmail: string | undefined;\r\n for (const entry of emails) {\r\n if (entry.primary && typeof entry.email === 'string') mainEmail = entry.email;\r\n }\r\n mainEmail ??= emails[0]?.email;\r\n return mainEmail;\r\n },\r\n});\r\n\r\nexport const discordProvider = (input: OAuthHelperInput): FullOAuthProvider => ({\r\n name: 'discord',\r\n clientID: requireString(input.clientId, 'discord clientId'),\r\n clientSecret: requireString(input.clientSecret, 'discord clientSecret'),\r\n callbackURL: input.callbackUrl,\r\n authorizationURL: input.endpoints?.authorizationURL ?? 'https://discord.com/oauth2/authorize',\r\n tokenExchangeURL: input.endpoints?.tokenExchangeURL ?? 'https://discord.com/api/oauth2/token',\r\n tokenExchangeMethod: 'form',\r\n userInfoURL: input.endpoints?.userInfoURL ?? 'https://discord.com/api/users/@me',\r\n scope: mergeScopes(['identify', 'email'], input.extraScopes),\r\n nameKey: 'username',\r\n emailKey: 'email',\r\n avatarCodeKey: 'avatar',\r\n extraSessionFields: input.extraSessionFields,\r\n getAvatar: ({ userData, avatarId }) => {\r\n if (!avatarId) return;\r\n const userId = typeof userData.id === 'string' ? userData.id : '';\r\n if (!userId) return;\r\n const format = avatarId.startsWith('a_') ? 'gif' : 'png';\r\n return `https://cdn.discordapp.com/avatars/${userId}/${avatarId}.${format}`;\r\n },\r\n});\r\n\r\nexport interface FacebookProviderInput extends OAuthHelperInput {\r\n /** Graph API version segment used in the URLs. Default `'v18.0'` (current as of 2026 — bump as Meta deprecates older versions). */\r\n apiVersion?: string;\r\n}\r\n\r\nexport const facebookProvider = (input: FacebookProviderInput): FullOAuthProvider => {\r\n const v = input.apiVersion ?? 'v18.0';\r\n return {\r\n name: 'facebook',\r\n clientID: requireString(input.clientId, 'facebook clientId'),\r\n clientSecret: requireString(input.clientSecret, 'facebook clientSecret'),\r\n callbackURL: input.callbackUrl,\r\n authorizationURL: input.endpoints?.authorizationURL ?? `https://www.facebook.com/${v}/dialog/oauth`,\r\n tokenExchangeURL: input.endpoints?.tokenExchangeURL ?? `https://graph.facebook.com/${v}/oauth/access_token`,\r\n tokenExchangeMethod: 'form',\r\n userInfoURL: input.endpoints?.userInfoURL ?? 'https://graph.facebook.com/me?fields=id,name,email,picture.type(large)',\r\n scope: mergeScopes(['public_profile', 'email'], input.extraScopes),\r\n nameKey: 'name',\r\n emailKey: 'email',\r\n avatarCodeKey: '',\r\n extraSessionFields: input.extraSessionFields,\r\n getAvatar: ({ userData }) => {\r\n const picture = asOAuthUserData(userData.picture);\r\n const pictureData = asOAuthUserData(picture.data);\r\n const url = pictureData.url;\r\n return typeof url === 'string' ? url : undefined;\r\n },\r\n };\r\n};\r\n\r\nexport interface MicrosoftProviderInput extends OAuthHelperInput {\r\n /** Tenant id segment in the authorize/token URLs. Default `'common'`. */\r\n tenant?: string;\r\n /** OAuth API version segment. Default `'v2.0'`. */\r\n apiVersion?: string;\r\n /** Graph API version segment for /me. Default `'v1.0'`. */\r\n graphApiVersion?: string;\r\n}\r\n\r\n//? NOTE: Microsoft flow is implemented but has not been end-to-end verified\r\n//? against an Azure AD tenant as of 2026-05-14. The token-exchange URL, the\r\n//? Graph /me shape, and the photo data-URL pipeline are based on Microsoft\r\n//? docs and pattern-matching against Google/GitHub. First consumer to wire a\r\n//? real Azure tenant should report back so this note can be dropped.\r\nexport const microsoftProvider = (input: MicrosoftProviderInput): FullOAuthProvider => {\r\n const tenant = input.tenant ?? 'common';\r\n const oauthVersion = input.apiVersion ?? 'v2.0';\r\n const graphVersion = input.graphApiVersion ?? 'v1.0';\r\n return {\r\n name: 'microsoft',\r\n clientID: requireString(input.clientId, 'microsoft clientId'),\r\n clientSecret: requireString(input.clientSecret, 'microsoft clientSecret'),\r\n callbackURL: input.callbackUrl,\r\n authorizationURL: input.endpoints?.authorizationURL ?? `https://login.microsoftonline.com/${tenant}/oauth2/${oauthVersion}/authorize`,\r\n tokenExchangeURL: input.endpoints?.tokenExchangeURL ?? `https://login.microsoftonline.com/${tenant}/oauth2/${oauthVersion}/token`,\r\n tokenExchangeMethod: 'form',\r\n userInfoURL: input.endpoints?.userInfoURL ?? `https://graph.microsoft.com/${graphVersion}/me`,\r\n scope: mergeScopes(['openid', 'profile', 'email', 'User.Read'], input.extraScopes),\r\n nameKey: 'displayName',\r\n emailKey: 'mail',\r\n avatarCodeKey: 'id',\r\n extraSessionFields: input.extraSessionFields,\r\n //? Graph's /photo/$value requires bearer auth, so we can't store the URL —\r\n //? a browser <img> would 401. Fetch the bytes and inline as a data URL.\r\n getAvatar: async ({ avatarId, accessToken }) => {\r\n if (!avatarId) return;\r\n const fetchPhoto = async () => {\r\n const response = await fetch(\r\n `https://graph.microsoft.com/${graphVersion}/users/${avatarId}/photo/$value`,\r\n { method: 'GET', headers: { Authorization: `Bearer ${accessToken}` } },\r\n );\r\n if (!response.ok) return;\r\n const contentType = response.headers.get('content-type') ?? 'image/jpeg';\r\n const buffer = Buffer.from(await response.arrayBuffer());\r\n return `data:${contentType};base64,${buffer.toString('base64')}`;\r\n };\r\n const [error, dataUrl] = await tryCatch(fetchPhoto);\r\n if (error) return;\r\n return dataUrl ?? undefined;\r\n },\r\n getEmail: async (accessToken) => {\r\n const fetchProfile = async () => {\r\n const response = await fetch(`https://graph.microsoft.com/${graphVersion}/me`, {\r\n method: 'GET',\r\n headers: {\r\n Authorization: `Bearer ${accessToken}`,\r\n 'Content-Type': 'application/json',\r\n },\r\n });\r\n if (!response.ok) return false;\r\n const data: unknown = await response.json();\r\n if (!data || typeof data !== 'object') return false;\r\n return data as Record<string, unknown>;\r\n };\r\n\r\n const [error, profile] = await tryCatch(fetchProfile);\r\n if (error || !profile) return false;\r\n\r\n const record = asOAuthUserData(profile);\r\n const mail = typeof record.mail === 'string' ? record.mail : '';\r\n const userPrincipalName = typeof record.userPrincipalName === 'string' ? record.userPrincipalName : '';\r\n return (mail || userPrincipalName) || false;\r\n },\r\n };\r\n};\r\n\r\nlet registeredProviders: OAuthProvider[] = [{ name: 'credentials' }];\r\n\r\n//? Always returns the active list. The default value contains only the\r\n//? `credentials` entry so calls into `loginWithCredentials` keep working in\r\n//? environments that never register OAuth providers (tests, CLI, etc.).\r\nexport const getOAuthProviders = (): OAuthProvider[] => registeredProviders;\r\n\r\nexport const registerOAuthProviders = (providers: OAuthProvider[]): OAuthProvider[] => {\r\n registeredProviders = providers;\r\n return registeredProviders;\r\n};\r\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 localhost + the bound\n//? SERVER_IP) before being used. An invalid URL falls back to\n//? `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","/* eslint-disable unicorn/no-abusive-eslint-disable */\r\n/* eslint-disable */\r\nimport { asOAuthUserData, getOAuthProviders, isFullOAuthProvider, type FullOAuthProvider } from './oauthProviders';\r\nimport { getPostLoginRedirect } from './redirectResolver';\r\nimport { IncomingMessage, ServerResponse } from 'node:http';\r\nimport { URLSearchParams } from 'node:url';\r\nimport { tryCatch, redis as redisClient, getUploadsDir, dispatchHook, getLogger, formatKey } from '@luckystack/core';\r\nimport bcrypt from 'bcryptjs';\r\nimport { randomBytes } from 'node:crypto';\r\nimport { saveSession } from \"./session\"\r\nimport validator from 'validator';\r\nimport type { BaseSessionLayout as SessionLayout } from './sessionLayout';\r\nimport { getProjectConfig } from '@luckystack/core';\r\nimport { getUserAdapter } from './userAdapter';\r\nimport { validatePassword } from './passwordPolicy';\r\nimport path from 'node:path';\r\nimport { existsSync } from 'node:fs';\r\n\r\ninterface paramsType {\r\n email?: string,\r\n password?: string,\r\n name?: string,\r\n confirmPassword?: string,\r\n}\r\n\r\n//? Resolved at call time via getUploadsDir() so consumer path overrides win.\r\nconst uploadsFolder = (): string => getUploadsDir();\r\n//? Resolve at call time so dotenv/test-setup timing can't capture a stale\r\n//? value at module load. Routes through `projectConfig.logging.devLogs`\r\n//? which is the standard predicate everywhere else in the codebase.\r\nconst isDevMode = (): boolean => getProjectConfig().logging.devLogs;\r\nconst { compare, genSalt, hash } = bcrypt;\r\nconst { escape, isEmail } = validator;\r\n\r\n//? Use the shared `getProjectName()` helper so the OAuth state Redis key\r\n//? prefix matches the namespace used everywhere else (sessions, activeUsers,\r\n//? rate-limit, password-reset). Previously this used a separate\r\n//? `auth.oauthStateProjectNameFallback` config field which could silently\r\n//? drift from `session.projectName`.\r\nconst getOAuthStateKey = (providerName: string, state: string): string => {\r\n return formatKey('-oauth-state', `${providerName}:${state}`);\r\n};\r\n\r\nexport const createOAuthState = async (providerName: string): Promise<string | null> => {\r\n const state = randomBytes(32).toString('hex');\r\n const key = getOAuthStateKey(providerName, state);\r\n const result = await redisClient.set(key, '1', 'EX', getProjectConfig().auth.oauthStateTtlSeconds, 'NX');\r\n\r\n if (result !== 'OK') {\r\n return null;\r\n }\r\n\r\n return state;\r\n};\r\n\r\nconst consumeOAuthState = async (providerName: string, state: string): Promise<boolean> => {\r\n if (!state) {\r\n return false;\r\n }\r\n\r\n const key = getOAuthStateKey(providerName, state);\r\n const txResult = await redisClient.multi().get(key).del(key).exec();\r\n if (!txResult || txResult.length < 2) {\r\n return false;\r\n }\r\n\r\n const getResult = txResult[0];\r\n if (!getResult || getResult[0]) {\r\n return false;\r\n }\r\n\r\n return getResult[1] === '1';\r\n};\r\n\r\nconst asRecord = (value: unknown): Record<string, unknown> => {\r\n if (value && typeof value === 'object') {\r\n return value as Record<string, unknown>;\r\n }\r\n return {};\r\n};\r\n\r\nconst sanitizeUserForSession = <T extends { password?: unknown }>(user: T): Omit<T, 'password'> => {\r\n const { password: _password, ...safeUser } = user;\r\n return safeUser;\r\n};\r\n\r\nconst toReasonKey = (error: unknown, fallback = 'api.internalServerError'): string => {\r\n if (typeof error === 'string' && error.length > 0) {\r\n return error;\r\n }\r\n\r\n if (error instanceof Error && error.message) {\r\n const message = error.message.toLowerCase();\r\n if (message.includes('authentication failed') || message.includes('scram failure')) {\r\n return 'api.internalServerError';\r\n }\r\n }\r\n\r\n return fallback;\r\n};\r\n\r\ninterface NormalizedCredentials {\r\n email: string;\r\n password: string;\r\n name: string | undefined;\r\n confirmPassword: string | undefined;\r\n}\r\n\r\nconst normalizeCredentials = (params: paramsType): NormalizedCredentials => ({\r\n //? Don't HTML-escape password OR confirmPassword before bcrypt — neither\r\n //? reaches HTML, and they're memory-compared for equality during register.\r\n //? Escaping confirmPassword while leaving password raw made any password\r\n //? containing `& < > \" '` fail the `password !== confirmPassword` check\r\n //? (regression introduced in pass-1 fix #9 which only un-escaped password).\r\n //? Email is normalized (lowercase + trim) and validated via `isEmail`;\r\n //? HTML-escaping it would make legacy raw rows unfindable.\r\n email: (params.email || '').trim().toLowerCase(),\r\n password: params.password || '',\r\n name: params.name ? escape(params.name) : undefined,\r\n confirmPassword: params.confirmPassword || undefined,\r\n});\r\n\r\nconst validateCredentialsShape = (creds: NormalizedCredentials): { status: false; reason: string } | null => {\r\n const authLimits = getProjectConfig().auth;\r\n if (!creds.email || !creds.password) return { status: false, reason: 'login.empty' };\r\n if (creds.email.length > authLimits.emailMaxLength) return { status: false, reason: 'login.emailCharacterLimit' };\r\n if (creds.name && creds.name.length > authLimits.nameMaxLength) return { status: false, reason: 'login.nameCharacterLimit' };\r\n if (!isEmail(creds.email)) return { status: false, reason: 'login.invalidEmailFormat' };\r\n //? Full password-policy check (length, complexity, common-list, customValidator).\r\n //? Policy lives in `projectConfig.auth.passwordPolicy`; the deprecated\r\n //? `passwordMinLength`/`passwordMaxLength` top-level fields are still read\r\n //? from the policy's `minLength`/`maxLength` defaults.\r\n const passwordReason = validatePassword(creds.password);\r\n if (passwordReason) return { status: false, reason: passwordReason };\r\n return null;\r\n};\r\n\r\nconst registerWithCredentials = async ({\r\n email,\r\n password,\r\n name,\r\n confirmPassword,\r\n}: {\r\n email: string;\r\n password: string;\r\n name: string;\r\n confirmPassword: string;\r\n}) => {\r\n if (password !== confirmPassword) {\r\n return { status: false, reason: 'login.passwordNotMatch' };\r\n }\r\n\r\n const preRegisterResult = await dispatchHook('preRegister', { email, provider: 'credentials', name });\r\n if (preRegisterResult.stopped) {\r\n return { status: false, reason: preRegisterResult.signal.errorCode };\r\n }\r\n\r\n const userAdapter = getUserAdapter();\r\n const [checkEmailError, checkEmailResponse] = await tryCatch(() =>\r\n userAdapter.findByEmail({ email, provider: 'credentials' })\r\n );\r\n if (checkEmailError) {\r\n getLogger().error('login: findByEmail failed during register', checkEmailError);\r\n return { status: false, reason: toReasonKey(checkEmailError) };\r\n }\r\n if (checkEmailResponse) return { status: false, reason: 'login.emailExists' };\r\n\r\n const createNewUser = async () => {\r\n const salt = await genSalt(getProjectConfig().auth.bcryptRounds);\r\n const hashedPassword = await hash(password, salt);\r\n return await userAdapter.create({\r\n email,\r\n provider: 'credentials',\r\n name,\r\n password: hashedPassword,\r\n avatar: '',\r\n avatarFallback: `#${Math.floor(Math.random() * 0xFF_FF_FF).toString(16).padStart(6, \"0\")}`,\r\n language: getProjectConfig().defaultLanguage,\r\n });\r\n };\r\n\r\n const [createNewUserError, createNewUserResponse] = await tryCatch(createNewUser);\r\n if (createNewUserError) return { status: false, reason: toReasonKey(createNewUserError) };\r\n if (!createNewUserResponse) return { status: false, reason: 'login.createUserFailed' };\r\n\r\n await dispatchHook('postRegister', { userId: createNewUserResponse.id, provider: 'credentials' });\r\n return {\r\n status: true,\r\n reason: 'login.userCreated',\r\n session: sanitizeUserForSession(createNewUserResponse),\r\n };\r\n};\r\n\r\nconst loginWithCredentialsCore = async (\r\n { email, password }: { email: string; password: string },\r\n options?: { supersedeToken?: string },\r\n) => {\r\n const preLoginResult = await dispatchHook('preLogin', { email, provider: 'credentials' });\r\n if (preLoginResult.stopped) {\r\n return { status: false, reason: preLoginResult.signal.errorCode };\r\n }\r\n\r\n const userAdapter = getUserAdapter();\r\n const [findUserError, findUserResponse] = await tryCatch(() =>\r\n userAdapter.findByEmail({ email, provider: 'credentials' })\r\n );\r\n if (findUserError) {\r\n getLogger().error('login: findByEmail failed', findUserError);\r\n return { status: false, reason: toReasonKey(findUserError) };\r\n }\r\n if (!findUserResponse) return { status: false, reason: 'login.userNotFound' };\r\n\r\n const [checkPasswordError, checkPasswordResponse] = await tryCatch(() =>\r\n compare(password, findUserResponse.password!)\r\n );\r\n if (checkPasswordError) {\r\n getLogger().error('login: bcrypt compare failed', checkPasswordError);\r\n return { status: false, reason: checkPasswordError };\r\n }\r\n if (!checkPasswordResponse) return { status: false, reason: 'login.wrongPassword' };\r\n\r\n const newToken = randomBytes(32).toString('hex');\r\n const previousLogin = (findUserResponse as { lastLogin?: Date | null }).lastLogin ?? null;\r\n const nowLogin = new Date();\r\n\r\n // Best-effort lastLogin update — silently no-ops if the user adapter\r\n // (or User schema) doesn't accept the field.\r\n await tryCatch(() => userAdapter.update(findUserResponse.id, { lastLogin: nowLogin } as never));\r\n\r\n const newUser: SessionLayout = {\r\n ...sanitizeUserForSession(findUserResponse),\r\n token: newToken,\r\n lastLogin: nowLogin,\r\n previousLogin,\r\n };\r\n\r\n const filePath = path.join(uploadsFolder(), `${newUser.id}.webp`);\r\n if (existsSync(filePath)) {\r\n newUser.avatar = `${newUser.id}.webp`;\r\n }\r\n\r\n const saved = await saveSession(newToken, newUser, true, { supersedeToken: options?.supersedeToken });\r\n if (!saved.ok) {\r\n //? Session never persisted (adapter blip / preSessionCreate veto). Fail the\r\n //? login so the route does NOT set a cookie or delete the prior session for\r\n //? a token that getSession() can't resolve.\r\n return { status: false, reason: saved.errorCode };\r\n }\r\n await dispatchHook('postLogin', { userId: newUser.id, provider: 'credentials', isNewUser: false, token: newToken });\r\n if (isDevMode()) {\r\n getLogger().debug(`credentials login success for user ${newUser.id}`);\r\n }\r\n return { status: true, reason: 'login.loggedIn', newToken, session: newUser };\r\n};\r\n\r\n//? Thin dispatcher: keeps the existing single-entry HTTP surface but forwards\r\n//? to the dedicated register / login functions. `name && confirmPassword`\r\n//? present in the body means the client is registering; otherwise it's a\r\n//? login.\r\nconst loginWithCredentials = async (params: paramsType, options?: { supersedeToken?: string }) => {\r\n const creds = normalizeCredentials(params);\r\n\r\n if (isDevMode()) {\r\n getLogger().debug(`credentials auth attempt for ${creds.email || 'unknown-email'}`);\r\n }\r\n\r\n const validationError = validateCredentialsShape(creds);\r\n if (validationError) return validationError;\r\n\r\n if (creds.name && creds.confirmPassword) {\r\n return registerWithCredentials({\r\n email: creds.email,\r\n password: creds.password,\r\n name: creds.name,\r\n confirmPassword: creds.confirmPassword,\r\n });\r\n }\r\n return loginWithCredentialsCore({ email: creds.email, password: creds.password }, options);\r\n};\r\n\r\nexport interface OAuthCallbackResult {\r\n token: string;\r\n redirectUrl: string;\r\n userId: string;\r\n provider: string;\r\n isNewUser: boolean;\r\n}\r\n\r\nconst isAllowedRedirectUrl = (url: string): boolean => {\r\n if (!URL.canParse(url, 'http://placeholder')) return false;\r\n const parsed = new URL(url, 'http://placeholder');\r\n if (parsed.origin === 'http://placeholder') {\r\n // relative URL — same-origin, always safe\r\n return true;\r\n }\r\n //? `allowedOrigins` is `string[] | (origin: string) => boolean` since we\r\n //? added the function-resolver variant. Branch on the shape.\r\n const allowed = getProjectConfig().http.cors.allowedOrigins ?? [];\r\n if (typeof allowed === 'function') {\r\n return allowed(parsed.origin);\r\n }\r\n return allowed.some((origin) => {\r\n if (!URL.canParse(origin)) return false;\r\n return new URL(origin).origin === parsed.origin;\r\n });\r\n};\r\n\r\nconst exchangeOAuthToken = async (provider: FullOAuthProvider, code: string): Promise<string | null> => {\r\n const values = {\r\n code,\r\n client_id: provider.clientID,\r\n client_secret: provider.clientSecret,\r\n redirect_uri: provider.callbackURL,\r\n grant_type: 'authorization_code',\r\n };\r\n\r\n const getToken = async () => {\r\n if (provider.tokenExchangeMethod === 'json') {\r\n const response = await fetch(provider.tokenExchangeURL, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },\r\n body: JSON.stringify(values),\r\n });\r\n return await response.json();\r\n }\r\n if (provider.tokenExchangeMethod === 'form') {\r\n const formParams = new URLSearchParams();\r\n formParams.append('client_id', provider.clientID);\r\n formParams.append('client_secret', provider.clientSecret);\r\n formParams.append('code', values.code);\r\n formParams.append('grant_type', 'authorization_code');\r\n formParams.append('redirect_uri', provider.callbackURL);\r\n\r\n if (isDevMode()) {\r\n getLogger().debug('oauth: token-exchange form params', { params: formParams.toString() });\r\n }\r\n const response = await fetch(provider.tokenExchangeURL, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' },\r\n body: formParams.toString(),\r\n });\r\n return await response.json();\r\n }\r\n return null;\r\n };\r\n\r\n const [error, response] = await tryCatch(getToken);\r\n if (error) {\r\n getLogger().error('oauth: token exchange failed', error);\r\n return null;\r\n }\r\n const tokenData = asRecord(response);\r\n const accessToken = typeof tokenData.access_token === 'string' ? tokenData.access_token : '';\r\n if (!accessToken) {\r\n getLogger().warn('oauth: no access_token in token response', { provider: provider.name });\r\n return null;\r\n }\r\n return accessToken;\r\n};\r\n\r\ninterface OAuthProfile {\r\n email: string | undefined;\r\n name: string;\r\n avatar: string;\r\n}\r\n\r\nconst fetchOAuthProfile = async (\r\n provider: FullOAuthProvider,\r\n accessToken: string,\r\n): Promise<OAuthProfile | null> => {\r\n const getUserData = async () => {\r\n const response = await fetch(provider.userInfoURL, {\r\n method: 'GET',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'Accept': 'application/json',\r\n 'Authorization': `Bearer ${accessToken}`,\r\n },\r\n });\r\n return await response.json();\r\n };\r\n\r\n const [error, response] = await tryCatch(getUserData);\r\n if (error) {\r\n getLogger().error('oauth: userInfo fetch failed', error);\r\n return null;\r\n }\r\n\r\n const userData = asRecord(response);\r\n const name = String(userData[provider.nameKey] || 'didnt find a name');\r\n const emailValue = userData[provider.emailKey];\r\n let email: string | undefined = typeof emailValue === 'string' ? emailValue : undefined;\r\n\r\n const avatarId = provider.avatarCodeKey ? userData[provider.avatarCodeKey] : undefined;\r\n const avatarValue = provider?.avatarKey\r\n ? String(userData[provider.avatarKey] || '')\r\n : (provider.getAvatar\r\n ? await provider.getAvatar({ userData, avatarId: typeof avatarId === 'string' ? avatarId : '', accessToken })\r\n : '');\r\n const avatar = typeof avatarValue === 'string' ? avatarValue : '';\r\n\r\n //? Some providers (GitHub) don't return email in /userinfo — fall back to the\r\n //? provider's `getEmail` accessor.\r\n if (!email && provider.getEmail) {\r\n const selectedEmail = await provider.getEmail(accessToken);\r\n if (!selectedEmail) {\r\n getLogger().warn('oauth: no email found via provider.getEmail', { provider: provider.name });\r\n return null;\r\n }\r\n email = selectedEmail;\r\n }\r\n\r\n return { email, name, avatar };\r\n};\r\n\r\ninterface ResolvedOAuthUser {\r\n user: SessionLayout;\r\n isNewUser: boolean;\r\n}\r\n\r\nconst findOrCreateOAuthUser = async (\r\n provider: FullOAuthProvider,\r\n profile: OAuthProfile,\r\n): Promise<ResolvedOAuthUser | null> => {\r\n const { email, name, avatar } = profile;\r\n if (!email) return null;\r\n\r\n const preLoginResult = await dispatchHook('preLogin', { email, provider: provider.name });\r\n if (preLoginResult.stopped) {\r\n getLogger().warn(`oauth login aborted by preLogin hook`, { errorCode: preLoginResult.signal.errorCode });\r\n return null;\r\n }\r\n\r\n const userAdapter = getUserAdapter();\r\n const [findError, findResponse] = await tryCatch(() =>\r\n userAdapter.findByEmail({ email, provider: provider.name })\r\n );\r\n if (findError) {\r\n getLogger().error('oauth: findByEmail failed', findError);\r\n return null;\r\n }\r\n\r\n if (findResponse?.id) {\r\n const filePath = path.join(uploadsFolder(), `${findResponse.id}.webp`);\r\n if (existsSync(filePath)) {\r\n findResponse.avatar = `${findResponse.id}.webp`;\r\n }\r\n\r\n const previousLogin = (findResponse as { lastLogin?: Date | null }).lastLogin ?? null;\r\n const nowLogin = new Date();\r\n await tryCatch(() => userAdapter.update(findResponse.id, { lastLogin: nowLogin } as never));\r\n\r\n return {\r\n user: {\r\n ...sanitizeUserForSession(findResponse),\r\n token: '',\r\n lastLogin: nowLogin,\r\n previousLogin,\r\n },\r\n isNewUser: false,\r\n };\r\n }\r\n\r\n const preRegisterResult = await dispatchHook('preRegister', { email, provider: provider.name, name });\r\n if (preRegisterResult.stopped) {\r\n getLogger().warn(`oauth register aborted by preRegister hook`, { errorCode: preRegisterResult.signal.errorCode });\r\n return null;\r\n }\r\n\r\n const createNewUser = async () => userAdapter.create({\r\n email,\r\n provider: provider.name,\r\n name,\r\n avatar,\r\n avatarFallback: `#${Math.floor(Math.random() * 0xFF_FF_FF).toString(16).padStart(6, \"0\")}`,\r\n language: getProjectConfig().defaultLanguage,\r\n });\r\n const [createError, createResponse] = await tryCatch(createNewUser);\r\n if (createError) {\r\n getLogger().error('oauth: user create failed', createError);\r\n return null;\r\n }\r\n if (!createResponse) return null;\r\n\r\n return {\r\n user: { ...sanitizeUserForSession(createResponse), token: '' },\r\n isNewUser: true,\r\n };\r\n};\r\n\r\nconst resolvePostLoginRedirect = async ({\r\n fallbackUrl,\r\n userId,\r\n providerName,\r\n isNewUser,\r\n}: {\r\n fallbackUrl: string;\r\n userId: string;\r\n providerName: string;\r\n isNewUser: boolean;\r\n}): Promise<string> => {\r\n const resolver = getPostLoginRedirect();\r\n if (!resolver) return fallbackUrl;\r\n\r\n const [resolverError, resolved] = await tryCatch(() => resolver({\r\n userId,\r\n provider: providerName,\r\n isNewUser,\r\n defaultUrl: fallbackUrl,\r\n }));\r\n if (resolverError) {\r\n getLogger().warn(`[oauth] postLoginRedirect resolver threw`, { message: (resolverError as Error).message });\r\n return fallbackUrl;\r\n }\r\n if (resolved && isAllowedRedirectUrl(resolved)) return resolved;\r\n if (resolved) {\r\n getLogger().warn(\r\n `[oauth] postLoginRedirect returned a URL not in allowed origins — falling back`,\r\n { resolved, fallbackUrl },\r\n );\r\n }\r\n return fallbackUrl;\r\n};\r\n\r\n// Route that handles the callback from the OAuth provider\r\nconst loginCallback = async (\r\n pathname: string,\r\n req: IncomingMessage,\r\n _res: ServerResponse,\r\n options: { defaultRedirectUrl?: string; supersedeToken?: string } = {},\r\n): Promise<OAuthCallbackResult | false> => {\r\n const providerName = pathname.split('/')[3]; // google/github/etc.\r\n if (!providerName) return false;\r\n const provider = getOAuthProviders().find(p => p.name === providerName);\r\n if (!provider || !req.url) return false;\r\n if (!isFullOAuthProvider(provider)) return false;\r\n\r\n const queryString = req.url.split('?')[1] ?? '';\r\n const params = new URLSearchParams(queryString);\r\n const code = params.get('code');\r\n const state = params.get('state');\r\n\r\n const stateIsValid = await consumeOAuthState(provider.name, state || '');\r\n if (!stateIsValid) {\r\n getLogger().warn('oauth: invalid or missing state', { provider: provider.name });\r\n return false;\r\n }\r\n\r\n if (!code) {\r\n getLogger().warn('oauth: no code provided in callback url', { provider: provider.name });\r\n return false;\r\n }\r\n\r\n const accessToken = await exchangeOAuthToken(provider, code);\r\n if (!accessToken) return false;\r\n\r\n const profile = await fetchOAuthProfile(provider, accessToken);\r\n if (!profile) return false;\r\n\r\n const resolved = await findOrCreateOAuthUser(provider, profile);\r\n if (!resolved) return false;\r\n\r\n const newToken = randomBytes(32).toString('hex');\r\n resolved.user.token = newToken;\r\n\r\n //? Per-provider runtime extras (calendar tokens, tenant ids, etc.) are\r\n //? merged into the session BEFORE save so saveSession + the resulting\r\n //? sessionStorage broadcast see the final shape. Errors are logged but\r\n //? do not block login — a missing extra is not worth keeping the user\r\n //? from signing in.\r\n const extraSessionFields = provider.extraSessionFields;\r\n if (extraSessionFields) {\r\n const [extraErr, extra] = await tryCatch(async () =>\r\n extraSessionFields({ userData: asOAuthUserData(profile), accessToken }),\r\n );\r\n if (extraErr) {\r\n getLogger().warn(`[oauth:${providerName}] extraSessionFields hook threw — continuing without extras`, { err: extraErr });\r\n } else if (extra) {\r\n Object.assign(resolved.user, extra);\r\n }\r\n }\r\n\r\n const saved = await saveSession(newToken, resolved.user, true, { supersedeToken: options.supersedeToken });\r\n if (!saved.ok) {\r\n //? Session never persisted — abort the OAuth callback so authCallbackRoute\r\n //? returns \"Login failed\" instead of redirecting with a dead token.\r\n getLogger().error(`[oauth:${providerName}] saveSession failed — aborting callback`, { errorCode: saved.errorCode });\r\n return false;\r\n }\r\n\r\n if (resolved.isNewUser) {\r\n await dispatchHook('postRegister', { userId: resolved.user.id, provider: providerName });\r\n }\r\n await dispatchHook('postLogin', {\r\n userId: resolved.user.id,\r\n provider: providerName,\r\n isNewUser: resolved.isNewUser,\r\n token: newToken,\r\n });\r\n\r\n if (isDevMode()) {\r\n getLogger().debug(`oauth login success for user ${resolved.user.id}`);\r\n }\r\n\r\n const fallbackUrl =\r\n options.defaultRedirectUrl\r\n ?? getProjectConfig().loginRedirectUrl\r\n ?? '/';\r\n\r\n const redirectUrl = await resolvePostLoginRedirect({\r\n fallbackUrl,\r\n userId: resolved.user.id,\r\n providerName,\r\n isNewUser: resolved.isNewUser,\r\n });\r\n\r\n return {\r\n token: newToken,\r\n redirectUrl,\r\n userId: resolved.user.id,\r\n provider: providerName,\r\n isNewUser: resolved.isNewUser,\r\n };\r\n};\r\n\r\nexport { loginWithCredentials, loginCallback, registerWithCredentials, loginWithCredentialsCore }\r\n","//? User adapter registry. Decouples auth flows from a specific Prisma User\r\n//? schema so consumers with different column names, additional fields, or a\r\n//? non-Prisma data layer can plug their own implementation.\r\n//?\r\n//? `defaultPrismaUserAdapter()` implements the framework's recommended schema\r\n//? (id, email, password?, provider, name, avatar, avatarFallback, admin,\r\n//? language) and is used automatically when no adapter has been registered.\r\n\r\nimport { prisma, type BaseSessionLayout } from '@luckystack/core';\r\n\r\nexport interface UserAdapterCreateInput {\r\n email: string;\r\n provider: string;\r\n name?: string | null;\r\n password?: string | null;\r\n avatar?: string | null;\r\n avatarFallback?: string | null;\r\n language?: string | null;\r\n}\r\n\r\nexport interface UserRecord extends BaseSessionLayout {\r\n password?: string | null;\r\n provider?: string | null;\r\n /** Most recent successful login. Mirrors the optional `lastLogin` column on User. */\r\n lastLogin?: Date | null;\r\n}\r\n\r\nexport interface UserAdapter {\r\n findByEmail: (params: { email: string; provider: string }) => Promise<UserRecord | null>;\r\n findById: (id: string) => Promise<UserRecord | null>;\r\n create: (input: UserAdapterCreateInput) => Promise<UserRecord>;\r\n update: (id: string, patch: Partial<UserRecord>) => Promise<UserRecord>;\r\n}\r\n\r\nlet registeredAdapter: UserAdapter | null = null;\r\n\r\nexport const registerUserAdapter = (adapter: UserAdapter): UserAdapter => {\r\n registeredAdapter = adapter;\r\n return registeredAdapter;\r\n};\r\n\r\nexport const isUserAdapterRegistered = (): boolean => registeredAdapter !== null;\r\n\r\n//? Default adapter: backed by the Prisma client exposed from `@luckystack/core`.\r\n//?\r\n//? The single `as unknown as PrismaUserDelegate` below is intentional and\r\n//? load-bearing — it is the abstraction boundary between the framework's\r\n//? loose `UserAdapterCreateInput` (string-typed fields) and the consumer's\r\n//? strict Prisma model (which may declare enums for `provider`, `language`,\r\n//? etc.). The framework cannot know those enum types statically, so it\r\n//? type-erases once at this seam and assumes the runtime `prisma.user`\r\n//? satisfies the documented `PrismaUserDelegate` shape. Consumers whose User\r\n//? schema diverges (different column names, mandatory extra fields) should\r\n//? register their own `UserAdapter` via `registerUserAdapter(...)` rather\r\n//? than relying on this default.\r\ninterface PrismaUserDelegate {\r\n findFirst: (args: { where: { email: string; provider: string } }) => Promise<UserRecord | null>;\r\n findUnique: (args: { where: { id: string } }) => Promise<UserRecord | null>;\r\n create: (args: { data: Record<string, unknown> }) => Promise<UserRecord>;\r\n update: (args: { where: { id: string }; data: Record<string, unknown> }) => Promise<UserRecord>;\r\n}\r\n\r\n//? Bridge between Prisma's generated client surface and the adapter contract.\r\n//? Structural cast required because Prisma's auto-generated delegate types\r\n//? don't structurally match our trimmed `PrismaUserDelegate` shape; this is a\r\n//? known framework boundary (see /docs/ARCHITECTURE_AUTH.md).\r\nconst getPrismaUser = (): PrismaUserDelegate =>\r\n // eslint-disable-next-line no-restricted-syntax -- structural Prisma boundary\r\n prisma.user as unknown as PrismaUserDelegate;\r\n\r\nexport const defaultPrismaUserAdapter = (): UserAdapter => {\r\n return {\r\n findByEmail: async ({ email, provider }) =>\r\n getPrismaUser().findFirst({ where: { email, provider } }),\r\n findById: async (id) =>\r\n getPrismaUser().findUnique({ where: { id } }),\r\n create: async (input) =>\r\n getPrismaUser().create({\r\n data: {\r\n email: input.email,\r\n provider: input.provider,\r\n name: input.name,\r\n password: input.password ?? null,\r\n avatar: input.avatar ?? '',\r\n avatarFallback: input.avatarFallback,\r\n language: input.language,\r\n },\r\n }),\r\n update: async (id, patch) =>\r\n getPrismaUser().update({ where: { id }, data: patch }),\r\n };\r\n};\r\n\r\nlet cachedDefaultAdapter: UserAdapter | null = null;\r\n\r\nexport const getUserAdapter = (): UserAdapter => {\r\n if (registeredAdapter) return registeredAdapter;\r\n cachedDefaultAdapter ??= defaultPrismaUserAdapter();\r\n return cachedDefaultAdapter;\r\n};\r\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/**\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 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\r\n//? flow when `ProjectConfig.auth.forgotPassword === 'framework'`, AND\r\n//? exported as building blocks for consumers who picked `'custom'` and want\r\n//? to wire their own UI / email layer without re-implementing crypto + Redis.\r\n//?\r\n//? Token format: 32 random bytes hex-encoded → 64-char URL-safe string.\r\n//? Storage: `<projectName>-pwreset:<token> → userId` with a configurable TTL\r\n//? (`auth.passwordResetTtlSeconds`, default 3600 = 1 hour).\r\n\r\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\r\nimport bcrypt from 'bcryptjs';\r\n/* eslint-disable import-x/no-named-as-default-member -- accessing genSalt/hash/compare via the default import is the documented pattern */\r\nimport { randomBytes } from 'node:crypto';\r\n\r\nimport { getProjectConfig, formatKey, redis } from '@luckystack/core';\r\n\r\nimport { validatePassword } from './passwordPolicy';\r\nimport { getUserAdapter } from './userAdapter';\r\n\r\n//? Resolve at call time via the shared `getProjectName()` helper (single\r\n//? source of truth — see also session.ts, rateLimiter.ts, login.ts). Avoids\r\n//? the module-load capture bug where dotenv timing could put tokens under\r\n//? the wrong namespace.\r\nconst tokenKey = (token: string): string => formatKey('-pwreset', token);\r\n\r\n/**\r\n * Create a one-time password-reset token bound to a user id. Stored in Redis\r\n * with the configured TTL. Returns the token string — caller emails it to\r\n * the user (typically embedded in a URL).\r\n */\r\nexport const createPasswordResetToken = async (userId: string): Promise<string> => {\r\n const token = randomBytes(32).toString('hex');\r\n const ttl = getProjectConfig().auth.passwordResetTtlSeconds;\r\n await redis.set(tokenKey(token), userId, 'EX', ttl);\r\n return token;\r\n};\r\n\r\n/**\r\n * Validate and consume a password-reset token. Returns the bound userId on\r\n * success and removes the token (one-time use). Returns null when the token\r\n * is missing, expired, or malformed.\r\n */\r\nexport const consumePasswordResetToken = async (token: string): Promise<string | null> => {\r\n if (!token || typeof token !== 'string') return null;\r\n const key = tokenKey(token);\r\n const txResult = await redis.multi().get(key).del(key).exec();\r\n if (!txResult || txResult.length === 0) return null;\r\n const first = txResult[0];\r\n if (!first) return null;\r\n const [getErr, value] = first;\r\n if (getErr) return null;\r\n return typeof value === 'string' && value.length > 0 ? value : null;\r\n};\r\n\r\n/**\r\n * Bcrypt-hash a plaintext password and write it to the user record via the\r\n * registered user adapter. Used by the reset flow and by the settings\r\n * password-change flow. Validates against the active password policy\r\n * (`projectConfig.auth.passwordPolicy`) before hashing — throws a `PasswordPolicyError`\r\n * when the plaintext fails policy. Catch and surface the error code to the\r\n * client; framework-mode reset/change flows already do this.\r\n */\r\nexport const updatePasswordHash = async (userId: string, plaintext: string): Promise<void> => {\r\n const reason = validatePassword(plaintext);\r\n if (reason) {\r\n throw new PasswordPolicyError(reason);\r\n }\r\n const salt = await bcrypt.genSalt(getProjectConfig().auth.bcryptRounds);\r\n const hashedPassword = await bcrypt.hash(plaintext, salt);\r\n const patch: Record<string, unknown> = { password: hashedPassword };\r\n await getUserAdapter().update(userId, patch);\r\n};\r\n\r\n/**\r\n * Thrown by `updatePasswordHash` when the supplied plaintext violates the\r\n * active password policy. `errorCode` matches the i18n reason key the rest\r\n * of the login flow uses (e.g. `login.passwordRequiresUppercase`).\r\n */\r\nexport class PasswordPolicyError extends Error {\r\n readonly errorCode: string;\r\n constructor(errorCode: string) {\r\n super(`Password policy violation: ${errorCode}`);\r\n this.name = 'PasswordPolicyError';\r\n this.errorCode = errorCode;\r\n }\r\n}\r\n\r\n/** Verify a plaintext password against a stored bcrypt hash. */\r\nexport const verifyPassword = async (plaintext: string, hash: string): Promise<boolean> => {\r\n return bcrypt.compare(plaintext, hash);\r\n};\r\n","//? Framework-mode forgot-password orchestration. Used only when\r\n//? `ProjectConfig.auth.forgotPassword === 'framework'`. Pulls in\r\n//? `@luckystack/email` lazily so the login package doesn't list it as a\r\n//? hard dep — `'custom'` and `'disabled'` modes never reach this code.\r\n\r\nimport { getProjectConfig, dispatchHook, getLogger } from '@luckystack/core';\r\n\r\nimport { createPasswordResetToken } from './passwordReset';\r\nimport { getUserAdapter } from './userAdapter';\r\n\r\ninterface SendResetEmailArgs {\r\n email: string;\r\n /**\r\n * App-facing display name used in the email greeting + subject. If omitted,\r\n * falls back to `projectConfig.auth.passwordResetBrand`, then to `'LuckyStack'`\r\n * as the absolute fallback. Real consumers should set this on either the\r\n * call site OR via `projectConfig.auth.passwordResetBrand`.\r\n */\r\n brand?: string;\r\n}\r\n\r\n/**\r\n * Look up the user by credentials-provider email, generate a reset token,\r\n * and email them a reset link. Always resolves successfully regardless of\r\n * whether the email matched a user (anti-enumeration). Returns the result\r\n * of the email send for diagnostics.\r\n */\r\nexport const sendPasswordResetEmail = async ({ email, brand }: SendResetEmailArgs): Promise<{ ok: boolean; reason?: string }> => {\r\n const config = getProjectConfig();\r\n getLogger().info('[forgotPassword] start', { email, forgotPasswordMode: config.auth.forgotPassword });\r\n if (config.auth.forgotPassword !== 'framework') {\r\n getLogger().warn('[forgotPassword] auth.forgotPassword is not \"framework\" — nothing sent', { mode: config.auth.forgotPassword });\r\n return { ok: false, reason: 'forgotPassword-not-framework' };\r\n }\r\n\r\n const resolvedBrand = brand ?? config.auth.passwordResetBrand ?? 'LuckyStack';\r\n\r\n // Lazy import — the email package is an optional peer dep so its types\r\n // may not be resolvable at the framework's compile time.\r\n interface EmailModule {\r\n sendEmail: (input: Record<string, unknown>) => Promise<{ ok: boolean; reason?: string }>;\r\n renderEmailLayout: (input: Record<string, unknown>) => { html: string; text: string };\r\n }\r\n //? Mirror testEmail's robustness: catch a failed dynamic import instead of\r\n //? throwing (an uncaught throw here would bubble out of the anti-enumeration\r\n //? wrapper as a generic 500). Surfaces the real reason in the server log.\r\n const emailModule = await (\r\n // @ts-expect-error optional peer dep — installed only when forgotPassword === 'framework'\r\n import('@luckystack/email') as Promise<EmailModule>\r\n ).catch((error: unknown) => {\r\n getLogger().warn('[forgotPassword] failed to load @luckystack/email — is it installed?', { error: String(error) });\r\n return null;\r\n });\r\n if (!emailModule) {\r\n return { ok: false, reason: 'email-module-load-failed' };\r\n }\r\n const { sendEmail, renderEmailLayout } = emailModule;\r\n\r\n const userAdapter = getUserAdapter();\r\n const user = await userAdapter.findByEmail({ email, provider: 'credentials' });\r\n getLogger().info('[forgotPassword] credentials user lookup', { email, found: Boolean(user), userId: user?.id ?? null });\r\n\r\n // Anti-enumeration: always pretend it succeeded if the email isn't found.\r\n if (!user) {\r\n //? The client always gets \"success\" (anti-enumeration). This server-side\r\n //? line is the ONLY signal that the address simply isn't a credentials\r\n //? (email+password) account — by far the most common reason a reset email\r\n //? never arrives (OAuth-only signups and unknown/typo'd addresses have no\r\n //? credentials row, so there is nothing to reset).\r\n getLogger().warn('[forgotPassword] no credentials account for this email — nothing sent. Use the email+password account you registered with.', { email });\r\n void dispatchHook('passwordResetRequested', {\r\n email,\r\n matched: false,\r\n });\r\n return { ok: true };\r\n }\r\n\r\n const token = await createPasswordResetToken(user.id);\r\n void dispatchHook('passwordResetRequested', {\r\n email,\r\n matched: true,\r\n userId: user.id,\r\n token,\r\n ttlSeconds: config.auth.passwordResetTtlSeconds,\r\n });\r\n const baseUrl = (config.app.publicUrl || '').replace(/\\/+$/, '');\r\n const resetUrl = `${baseUrl}/reset-password?token=${encodeURIComponent(token)}`;\r\n\r\n const ttlMinutes = Math.round(config.auth.passwordResetTtlSeconds / 60);\r\n const { html, text } = renderEmailLayout({\r\n brand: resolvedBrand,\r\n title: 'Reset your password',\r\n intro: `Hi ${user.name ?? 'there'}, we received a request to reset the password on your ${resolvedBrand} account. Click the button below to choose a new one. The link expires in ${String(ttlMinutes)} minutes.`,\r\n ctaLabel: 'Reset password',\r\n ctaUrl: resetUrl,\r\n outro: `If you didn't request this, you can safely ignore this email — your password will stay the same. The link: ${resetUrl}`,\r\n footer: `Sent by ${resolvedBrand}. If you have questions, reply to this email.`,\r\n });\r\n\r\n //? `adapterHint: 'transactional'` lets consumers who registered separate\r\n //? marketing + transactional senders via `registerEmailSenders({...})`\r\n //? route this through the transactional adapter automatically. Falls\r\n //? back to the default sender when only one is registered.\r\n const result = await sendEmail({\r\n to: user.email,\r\n subject: `Reset your ${resolvedBrand} password`,\r\n html,\r\n text,\r\n adapterHint: 'transactional',\r\n });\r\n\r\n if (result.ok) {\r\n getLogger().info('[forgotPassword] reset email dispatched', { userId: user.id, to: user.email });\r\n } else {\r\n getLogger().warn('[forgotPassword] reset email send FAILED', { userId: user.id, reason: result.reason });\r\n }\r\n\r\n return result.ok ? { ok: true } : { ok: false, reason: result.reason };\r\n};\r\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:<token>` -> JSON payload\n//? `{ userId: string; newEmail: string }`\n//? TTL: `auth.emailChangeTtlSeconds` (default 3600 = 1 hour).\n\nimport { randomBytes } from 'node:crypto';\n\nimport { getProjectConfig, formatKey, redis } from '@luckystack/core';\n\nconst tokenKey = (token: string): string => formatKey('-email-change', token);\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. Stored in Redis with the configured TTL. Returns the 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 token = randomBytes(32).toString('hex');\n const ttl = getProjectConfig().auth.emailChangeTtlSeconds;\n const payload = JSON.stringify({ userId, newEmail } satisfies EmailChangePayload);\n await redis.set(tokenKey(token), payload, 'EX', ttl);\n return 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).\n * Returns null when the token is missing, expired, or malformed.\n */\nexport const consumeEmailChangeToken = async (token: string): Promise<EmailChangePayload | null> => {\n if (!token || typeof token !== 'string') return null;\n const key = tokenKey(token);\n const txResult = await redis.multi().get(key).del(key).exec();\n if (!txResult || txResult.length === 0) return null;\n const first = txResult[0];\n if (!first) return null;\n const [getErr, value] = first;\n if (getErr) return null;\n if (typeof value !== 'string' || value.length === 0) return null;\n try {\n const parsed = JSON.parse(value) as unknown;\n if (\n !parsed\n || typeof parsed !== 'object'\n || typeof (parsed as EmailChangePayload).userId !== 'string'\n || typeof (parsed as EmailChangePayload).newEmail !== 'string'\n ) {\n return null;\n }\n return parsed as EmailChangePayload;\n } catch {\n return null;\n }\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';\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.\n interface EmailModule {\n sendEmail: (input: Record<string, unknown>) => Promise<{ ok: boolean; reason?: string }>;\n renderEmailLayout: (input: Record<string, unknown>) => { html: string; text: string };\n }\n const { sendEmail, renderEmailLayout } = await (\n // @ts-expect-error optional peer dep — installed only when the consumer wires email sending\n import('@luckystack/email') as Promise<EmailModule>\n );\n\n const token = await createEmailChangeToken(userId, newEmail);\n const baseUrl = (config.app.publicUrl || '').replace(/\\/+$/, '');\n const confirmUrl = `${baseUrl}/settings/confirm-email?token=${encodeURIComponent(token)}`;\n const ttlMinutes = Math.round(config.auth.emailChangeTtlSeconds / 60);\n\n const { html, text } = renderEmailLayout({\n brand: resolvedBrand,\n title: 'Confirm your new email address',\n intro: `Hi ${userName ?? 'there'}, you (or someone using your ${resolvedBrand} account) asked to change the account email to this address. Click the button below to confirm — the link expires in ${String(ttlMinutes)} minutes. If you confirm, all of your active sessions will be signed out as a security precaution.`,\n ctaLabel: 'Confirm new email',\n ctaUrl: confirmUrl,\n outro: `If you didn't request this, you can safely ignore this email — nothing will change. The link: ${confirmUrl}`,\n footer: `Sent by ${resolvedBrand}. If you have questions, reply to this email.`,\n });\n\n const result = await sendEmail({\n to: newEmail,\n subject: `Confirm your new ${resolvedBrand} email address`,\n html,\n text,\n adapterHint: 'transactional',\n });\n\n return result.ok ? { ok: true, token } : { ok: false, reason: result.reason, token };\n};\n"],"mappings":"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","names":["tryCatch","socketEventNames","dispatchHook","getLogger","formatKey","tryCatch","logout","tryCatch","emails","tryCatch","redisClient","dispatchHook","getLogger","formatKey","randomBytes","getProjectConfig","getProjectConfig","getProjectConfig","getProjectConfig","formatKey","randomBytes","redisClient","dispatchHook","tryCatch","getLogger","response","bcrypt","randomBytes","getProjectConfig","formatKey","redis","formatKey","randomBytes","getProjectConfig","redis","bcrypt","hash","getProjectConfig","dispatchHook","getLogger","getProjectConfig","getLogger","dispatchHook","randomBytes","getProjectConfig","formatKey","redis","tokenKey","getProjectConfig","getProjectConfig"]}
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"],"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 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 } from './login';\nexport type {\n CredentialsLoginResult,\n CredentialsLoginSuccess,\n CredentialsLoginFailure,\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// 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} 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/** Read the active auth-lockout config (returns null when the feature is off). */\nconst getAuthLockoutConfig = (): { maxAttempts: number; windowMs: number } | null => {\n const auth = getProjectConfig().rateLimiting.auth;\n if (!auth.enabled) return null;\n return { maxAttempts: auth.maxAttempts, windowMs: auth.windowMs };\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 const { remaining } = await getRateLimitStatus(lockoutKey(accountKey, requesterIp), cfg.maxAttempts);\n return remaining <= 0;\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 const { allowed, remaining } = await checkRateLimit({\n key: lockoutKey(accountKey, requesterIp),\n limit: cfg.maxAttempts,\n windowMs: cfg.windowMs,\n });\n if (!allowed) {\n getLogger().warn('[authLockout] account temporarily locked after repeated failures', {\n remaining,\n maxAttempts: cfg.maxAttempts,\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 await clearRateLimit(lockoutKey(accountKey, requesterIp));\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 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 localhost + the bound\n//? SERVER_IP) before being used. An invalid URL falls back to\n//? `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 } 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 `status: true`,\n//? `newToken` + `session` are always present; on `status: false` only `reason`\n//? is meaningful (the failure branch never mints a token).\nexport interface CredentialsLoginSuccess {\n status: true;\n reason: string;\n newToken: string;\n session: SessionLayout;\n}\nexport interface CredentialsLoginFailure {\n status: false;\n reason: string;\n}\nexport type CredentialsLoginResult = CredentialsLoginSuccess | 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 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\nconst sanitizeUserForSession = <T extends { password?: unknown }>(user: T): Omit<T, 'password'> => {\n const { password: _password, ...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\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 emitLoginFailed({ 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 emitLoginFailed({ email, userId: findUserResponse.id, provider: 'credentials', reason: 'login.wrongPassword', stage: 'login', requesterIp });\n return { status: false, reason: 'login.wrongPassword' };\n }\n\n const newToken = randomBytes(32).toString('hex');\n const previousLogin = findUserResponse.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(() => userAdapter.update(findUserResponse.id, { lastLogin: nowLogin }));\n\n const newUser: SessionLayout = {\n ...sanitizeUserForSession(findUserResponse),\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, userId: newUser.id, provider: 'credentials', reason: saved.errorCode, stage: 'login', requesterIp });\n return { status: false, reason: saved.errorCode };\n }\n await dispatchHook('postLogin', { userId: newUser.id, provider: 'credentials', 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 //? Pass `requesterIp` so the composite key (DD-LOGIN-F5) is cleared when\n //? an IP was threaded in; falls back to the bare account key otherwise.\n void clearAuthFailures(email, requesterIp);\n if (isDevMode()) {\n getLogger().debug(`credentials login success for user ${newUser.id}`);\n }\n return { status: true, reason: 'login.loggedIn', newToken, session: newUser };\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 if (verifiedFlag === false) {\n getLogger().warn('oauth: provider email is not verified — rejecting', { provider: provider.name });\n return null;\n }\n //? Key present-and-not-false ⇒ provider positively confirmed verification.\n if (verifiedFlag !== undefined) 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 const isCrossProviderLink = existingProvider !== null && 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 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 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","//? 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 void dispatchHook('passwordResetRequested', {\n email,\n matched: true,\n userId: user.id,\n token,\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 getLogger().info('[forgotPassword] reset email dispatched', { userId: user.id, to: user.email });\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 const confirmUrl = `${baseUrl}/settings/confirm-email?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"],"mappings":"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","names":["getLogger","getProjectConfig","getProjectConfig","getLogger","getProjectConfig","getProjectConfig","getProjectConfig","getLogger","validationError","response","bcrypt","getProjectConfig","formatKey","formatKey","getProjectConfig","bcrypt","hash","getProjectConfig","dispatchHook","getLogger","getProjectConfig","getLogger","dispatchHook","getProjectConfig","issueOneTimeToken","formatKey","redis","oneTimeTokenKey","getProjectConfig","getProjectConfig"]}