@azlib/identity 0.2.1 → 0.3.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.
Files changed (70) hide show
  1. package/README.md +119 -54
  2. package/dist/cookie-header-DdTShpuc.d.cts +83 -0
  3. package/dist/cookie-header-DdTShpuc.d.cts.map +1 -0
  4. package/dist/cookie-header-DdTShpuc.d.mts +83 -0
  5. package/dist/cookie-header-DdTShpuc.d.mts.map +1 -0
  6. package/dist/cookie-header-DptIxRmW.mjs +76 -0
  7. package/dist/cookie-header-DptIxRmW.mjs.map +1 -0
  8. package/dist/cookie-header-Dqd5k5BU.cjs +115 -0
  9. package/dist/{errors-CBbiRO2n.d.cts → errors-0Zh7EG06.d.cts} +4 -2
  10. package/dist/errors-0Zh7EG06.d.cts.map +1 -0
  11. package/dist/{errors-_OcodtdV.d.mts → errors-DVDEUk9-.d.mts} +4 -2
  12. package/dist/errors-DVDEUk9-.d.mts.map +1 -0
  13. package/dist/express.d.cts +3 -3
  14. package/dist/express.d.cts.map +1 -1
  15. package/dist/express.d.mts +3 -3
  16. package/dist/express.d.mts.map +1 -1
  17. package/dist/express.mjs.map +1 -1
  18. package/dist/{identity-Bz9RDOvT.mjs → identity-D0qyca_c.mjs} +37 -3
  19. package/dist/identity-D0qyca_c.mjs.map +1 -0
  20. package/dist/{identity-4eP45YIP.cjs → identity-Yz1BVkrM.cjs} +46 -0
  21. package/dist/{identity-router-D1xJ5H7y.d.cts → identity-router-BiratpJR.d.mts} +3 -3
  22. package/dist/identity-router-BiratpJR.d.mts.map +1 -0
  23. package/dist/{identity-router-tj3-teBI.d.mts → identity-router-D4oqYI60.d.cts} +3 -3
  24. package/dist/identity-router-D4oqYI60.d.cts.map +1 -0
  25. package/dist/{identity-service---OTann1.d.cts → identity-service-DE0Ffguz.d.cts} +3 -3
  26. package/dist/{identity-service---OTann1.d.cts.map → identity-service-DE0Ffguz.d.cts.map} +1 -1
  27. package/dist/{identity-service-WpOKb0MK.d.mts → identity-service-JtsoA6lv.d.mts} +3 -3
  28. package/dist/{identity-service-WpOKb0MK.d.mts.map → identity-service-JtsoA6lv.d.mts.map} +1 -1
  29. package/dist/identity-store-BRRahxcS.d.cts.map +1 -1
  30. package/dist/identity-store-BRRahxcS.d.mts.map +1 -1
  31. package/dist/index-CsYqHPvK.d.cts +54 -0
  32. package/dist/index-CsYqHPvK.d.cts.map +1 -0
  33. package/dist/index-LhSdoWEP.d.mts +54 -0
  34. package/dist/index-LhSdoWEP.d.mts.map +1 -0
  35. package/dist/index.cjs +11 -1
  36. package/dist/index.d.cts +4 -3
  37. package/dist/index.d.mts +4 -3
  38. package/dist/index.mjs +3 -2
  39. package/dist/logger-CcCHJVVe.mjs.map +1 -1
  40. package/dist/nestjs.d.cts +3 -3
  41. package/dist/nestjs.d.cts.map +1 -1
  42. package/dist/nestjs.d.mts +3 -3
  43. package/dist/nestjs.d.mts.map +1 -1
  44. package/dist/nestjs.mjs.map +1 -1
  45. package/dist/next.cjs +280 -0
  46. package/dist/next.d.cts +58 -0
  47. package/dist/next.d.cts.map +1 -0
  48. package/dist/next.d.mts +58 -0
  49. package/dist/next.d.mts.map +1 -0
  50. package/dist/next.mjs +269 -0
  51. package/dist/next.mjs.map +1 -0
  52. package/dist/node.cjs +18 -19
  53. package/dist/node.d.cts +5 -4
  54. package/dist/node.d.cts.map +1 -1
  55. package/dist/node.d.mts +5 -4
  56. package/dist/node.d.mts.map +1 -1
  57. package/dist/node.mjs +10 -20
  58. package/dist/node.mjs.map +1 -1
  59. package/dist/test-utils.mjs.map +1 -1
  60. package/package.json +7 -7
  61. package/schema/model.ts +110 -22
  62. package/dist/errors-CBbiRO2n.d.cts.map +0 -1
  63. package/dist/errors-_OcodtdV.d.mts.map +0 -1
  64. package/dist/identity-Bz9RDOvT.mjs.map +0 -1
  65. package/dist/identity-router-D1xJ5H7y.d.cts.map +0 -1
  66. package/dist/identity-router-tj3-teBI.d.mts.map +0 -1
  67. package/dist/index-CpufYgyn.d.cts +0 -30
  68. package/dist/index-CpufYgyn.d.cts.map +0 -1
  69. package/dist/index-CpufYgyn.d.mts +0 -30
  70. package/dist/index-CpufYgyn.d.mts.map +0 -1
package/dist/node.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"node.mjs","names":["MIN_PASSWORD_LENGTH","DEFAULT_TTL_SECONDS","DEFAULT_SCOPES"],"sources":["../core/password-hasher.ts","../core/token-service.ts","../core/audit.ts","../core/register.ts","../core/lockout.ts","../core/totp.ts","../core/two-factor.ts","../core/login.ts","../core/refresh-service.ts","../core/verification-token.ts","../core/email-verification.ts","../core/password-reset.ts","../core/account-admin.ts","../core/identity-service.ts","../core/oauth/providers/google.ts","../core/oauth/providers/microsoft.ts"],"sourcesContent":["import { randomBytes, scrypt, timingSafeEqual } from \"node:crypto\";\n\n/**\n * Password hashing using Node's scrypt. Hashes are self-describing:\n * `scrypt$<N>$<saltHex>$<derivedKeyHex>` so parameters can evolve without breaking\n * existing records.\n */\n\nconst KEY_LENGTH = 64;\n\nconst scryptAsync = (\n password: string,\n salt: Buffer,\n cost: number,\n): Promise<Buffer> =>\n new Promise((resolve, reject) => {\n // maxmem must accommodate the chosen cost parameter.\n scrypt(password, salt, KEY_LENGTH, { N: cost, maxmem: 256 * cost * 8 }, (err, derived) => {\n if (err) reject(err);\n else resolve(derived);\n });\n });\n\nexport interface PasswordHasher {\n hash(password: string): Promise<string>;\n verify(password: string, storedHash: string): Promise<boolean>;\n}\n\n/** Creates a scrypt-based {@link PasswordHasher}. */\nexport function createPasswordHasher(cost: number): PasswordHasher {\n return {\n async hash(password: string): Promise<string> {\n const salt = randomBytes(16);\n const derived = await scryptAsync(password, salt, cost);\n return `scrypt$${cost}$${salt.toString(\"hex\")}$${derived.toString(\"hex\")}`;\n },\n\n async verify(password: string, storedHash: string): Promise<boolean> {\n const parts = storedHash.split(\"$\");\n if (parts.length !== 4 || parts[0] !== \"scrypt\") {\n return false;\n }\n const cost = Number.parseInt(parts[1]!, 10);\n const salt = Buffer.from(parts[2]!, \"hex\");\n const expected = Buffer.from(parts[3]!, \"hex\");\n if (!Number.isInteger(cost) || salt.length === 0 || expected.length === 0) {\n return false;\n }\n const actual = await scryptAsync(password, salt, cost);\n if (actual.length !== expected.length) {\n return false;\n }\n return timingSafeEqual(actual, expected);\n },\n };\n}\n","import { createHash, randomBytes } from \"node:crypto\";\nimport { jwtVerify, SignJWT } from \"jose\";\n\nimport type { IdentityConfig } from \"./config\";\nimport { InvalidTokenError } from \"./errors\";\nimport type { AccessTokenClaims } from \"./types\";\n\n/**\n * Issues and verifies short-lived HS256 access tokens (via `jose`) and generates the\n * opaque refresh tokens whose hashes are stored server-side.\n */\nexport interface TokenService {\n issueAccessToken(userId: string, authVersion: number): Promise<{ token: string; expiresAt: Date }>;\n verifyAccessToken(token: string): Promise<AccessTokenClaims>;\n /**\n * Creates a refresh token bound to a session id. The id prefix lets the server look up\n * the session, while only the hash of the full token is stored.\n */\n createRefreshToken(sessionId: string): { token: string; hash: string };\n /** Extracts the session id encoded in a refresh token, or null if malformed. */\n parseSessionId(token: string): string | null;\n /** Hashes a presented refresh token for comparison with the stored hash. */\n hashRefreshToken(token: string): string;\n}\n\nconst hashToken = (token: string): string =>\n createHash(\"sha256\").update(token).digest(\"hex\");\n\nexport function createTokenService(config: IdentityConfig): TokenService {\n const secret = new TextEncoder().encode(config.accessTokenSecret);\n\n return {\n async issueAccessToken(userId, authVersion) {\n const issuedAt = Math.floor(config.now().getTime() / 1000);\n const expiresAtSeconds = issuedAt + config.accessTokenTtlSeconds;\n const builder = new SignJWT({ authVersion })\n .setProtectedHeader({ alg: \"HS256\" })\n .setSubject(userId)\n .setIssuedAt(issuedAt)\n .setIssuer(config.issuer)\n .setExpirationTime(expiresAtSeconds);\n if (config.audience) {\n builder.setAudience(config.audience);\n }\n const token = await builder.sign(secret);\n return { token, expiresAt: new Date(expiresAtSeconds * 1000) };\n },\n\n async verifyAccessToken(token) {\n try {\n const { payload } = await jwtVerify(token, secret, {\n issuer: config.issuer,\n audience: config.audience,\n });\n if (typeof payload.sub !== \"string\" || typeof payload.authVersion !== \"number\") {\n throw new InvalidTokenError();\n }\n return {\n sub: payload.sub,\n authVersion: payload.authVersion,\n iat: payload.iat ?? 0,\n exp: payload.exp ?? 0,\n };\n } catch (error) {\n if (error instanceof InvalidTokenError) throw error;\n throw new InvalidTokenError();\n }\n },\n\n createRefreshToken(sessionId) {\n const token = `${sessionId}.${randomBytes(32).toString(\"base64url\")}`;\n return { token, hash: hashToken(token) };\n },\n\n parseSessionId(token) {\n const separator = token.indexOf(\".\");\n if (separator <= 0) return null;\n return token.slice(0, separator);\n },\n\n hashRefreshToken(token) {\n return hashToken(token);\n },\n };\n}\n","import type { IdentityStore } from \"./identity-store\";\nimport type { IdentityEvent, IdentityEventType } from \"./types\";\n\n/**\n * Thin auditing helper. Persists events through the store's optional `recordEvent` hook\n * when present and never throws — auditing must not break the primary auth flow.\n *\n * Metadata is restricted to non-sensitive scalars; never pass passwords or raw tokens.\n */\nexport interface AuditLogger {\n record(\n type: IdentityEventType,\n userId: string | null,\n metadata?: IdentityEvent[\"metadata\"],\n ): Promise<void>;\n}\n\nexport function createAuditLogger(\n store: IdentityStore,\n now: () => Date,\n onEvent?: (event: IdentityEvent) => void | Promise<void>,\n): AuditLogger {\n return {\n async record(type, userId, metadata = {}) {\n const event: IdentityEvent = { type, userId, metadata, occurredAt: now() };\n try {\n await store.recordEvent?.(event);\n } catch {\n // Auditing failures are swallowed intentionally.\n }\n try {\n await onEvent?.(event);\n } catch {\n // Lifecycle-hook failures must not break auth flows.\n }\n },\n };\n}\n","import type { AuditLogger } from \"./audit\";\nimport { EmailAlreadyRegisteredError, IdentityError } from \"./errors\";\nimport type { PasswordHasher } from \"./password-hasher\";\nimport { issueSession, type SessionDeps } from \"./session-state\";\nimport type { AuthResult } from \"./types\";\n\n/** Dependencies for the register flow. */\nexport interface RegisterDeps extends SessionDeps {\n hasher: PasswordHasher;\n audit: AuditLogger;\n}\n\n/** Input for {@link registerUser}. */\nexport interface RegisterInput {\n email: string;\n password: string;\n displayName?: string | null;\n}\n\n/** Raised when registration input fails validation. */\nexport class RegistrationValidationError extends IdentityError {\n constructor(message: string) {\n super(\"identity/registration-invalid\", message, 400);\n }\n}\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst MIN_PASSWORD_LENGTH = 8;\n\n/** Normalizes an email for storage and lookup (trim + lowercase). */\nexport function normalizeEmail(email: string): string {\n return email.trim().toLowerCase();\n}\n\n/**\n * Registers a new user: validates input, enforces unique email, hashes the password,\n * persists the user and credential, and returns an authenticated session.\n */\nexport async function registerUser(\n deps: RegisterDeps,\n input: RegisterInput,\n): Promise<AuthResult> {\n const { config, store, hasher, audit } = deps;\n\n const email = normalizeEmail(input.email);\n if (!EMAIL_PATTERN.test(email)) {\n throw new RegistrationValidationError(\"A valid email address is required.\");\n }\n if (input.password.length < MIN_PASSWORD_LENGTH) {\n throw new RegistrationValidationError(\n `Password must be at least ${MIN_PASSWORD_LENGTH} characters.`,\n );\n }\n\n const existing = await store.findUserByEmail(email);\n if (existing) {\n throw new EmailAlreadyRegisteredError();\n }\n\n const now = config.now();\n const user = await store.createUser({\n userId: config.generateId(),\n email,\n displayName: input.displayName ?? null,\n status: \"active\",\n authVersion: 0,\n createdAt: now,\n updatedAt: now,\n });\n\n const passwordHash = await hasher.hash(input.password);\n await store.upsertCredential({ userId: user.userId, passwordHash, updatedAt: now });\n\n await audit.record(\"user.registered\", user.userId, { email });\n\n return issueSession(deps, user);\n}\n","import type { LockoutConfig } from \"./config\";\nimport { AccountUnavailableError } from \"./errors\";\nimport type { IdentityStore } from \"./identity-store\";\nimport type { IdentityUser } from \"./types\";\n\n/**\n * Checks whether the user account is currently locked. Throws {@link AccountUnavailableError}\n * when the lockout is active (i.e. `lockedUntil` is in the future).\n *\n * Note: if `lockedUntil` is in the past the lock has expired — the user may proceed and\n * the stale lock will be cleared on successful login.\n */\nexport function checkLockout(\n user: IdentityUser,\n config: LockoutConfig,\n now: () => Date,\n): void {\n if (config.maxFailedAttempts === 0) return; // Lockout disabled\n if (user.lockedUntil !== null && user.lockedUntil > now()) {\n throw new AccountUnavailableError();\n }\n}\n\n/**\n * Records a failed login attempt. When the threshold is reached the account is locked\n * for `config.durationSeconds` seconds (or permanently when `durationSeconds` is 0).\n */\nexport async function recordFailedAttempt(\n store: IdentityStore,\n user: IdentityUser,\n config: LockoutConfig,\n now: () => Date,\n): Promise<void> {\n if (config.maxFailedAttempts === 0) return; // Lockout disabled\n\n const attempts = user.failedLoginAttempts + 1;\n const exceeded = attempts >= config.maxFailedAttempts;\n\n const lockedUntil = exceeded\n ? config.durationSeconds > 0\n ? new Date(now().getTime() + config.durationSeconds * 1000)\n : new Date(8_640_000_000_000_000) // max date = permanent lock\n : null;\n\n const patch: Parameters<IdentityStore[\"updateUser\"]>[1] = {\n failedLoginAttempts: attempts,\n ...(exceeded ? { status: \"locked\" as const, lockedUntil } : {}),\n };\n\n await store.updateUser(user.userId, patch);\n}\n\n/**\n * Clears lockout state after a successful login. Resets the failure counter and any\n * active lockout so the account returns to `active` status.\n */\nexport async function clearLockout(\n store: IdentityStore,\n user: IdentityUser,\n): Promise<void> {\n if (user.failedLoginAttempts === 0 && user.lockedUntil === null) return; // Nothing to clear\n await store.updateUser(user.userId, {\n failedLoginAttempts: 0,\n lockedUntil: null,\n // Restore active status only if the account was locked (not disabled by an admin)\n ...(user.status === \"locked\" ? { status: \"active\" as const } : {}),\n });\n}\n","import { createHmac, randomBytes } from \"node:crypto\";\n\n// ---------------------------------------------------------------------------\n// Base32 (RFC 4648) — required for TOTP secret encoding / OTP Auth URI\n// ---------------------------------------------------------------------------\n\nconst BASE32_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567\";\n\n/** Encodes a Buffer to a base32 string (no padding). */\nexport function base32Encode(buffer: Buffer): string {\n let result = \"\";\n let bits = 0;\n let accumulator = 0;\n\n for (const byte of buffer) {\n accumulator = (accumulator << 8) | byte;\n bits += 8;\n while (bits >= 5) {\n bits -= 5;\n result += BASE32_ALPHABET[(accumulator >> bits) & 0x1f] ?? \"\";\n }\n }\n if (bits > 0) {\n result += BASE32_ALPHABET[(accumulator << (5 - bits)) & 0x1f] ?? \"\";\n }\n return result;\n}\n\n/** Decodes a base32 string (case-insensitive, padding stripped) to a Buffer. */\nexport function base32Decode(encoded: string): Buffer {\n const str = encoded.toUpperCase().replace(/=+$/, \"\");\n const bytes: number[] = [];\n let bits = 0;\n let accumulator = 0;\n\n for (const char of str) {\n const value = BASE32_ALPHABET.indexOf(char);\n if (value === -1) continue; // skip non-alphabet chars\n accumulator = (accumulator << 5) | value;\n bits += 5;\n if (bits >= 8) {\n bits -= 8;\n bytes.push((accumulator >> bits) & 0xff);\n }\n }\n return Buffer.from(bytes);\n}\n\n// ---------------------------------------------------------------------------\n// HOTP (RFC 4226) + TOTP (RFC 6238)\n// ---------------------------------------------------------------------------\n\nconst TOTP_STEP_SECONDS = 30;\nconst TOTP_DIGITS = 6;\n\n/**\n * Computes an HOTP value for the given base32 key and integer counter (RFC 4226).\n * Returns a zero-padded 6-digit string.\n */\nfunction hotp(secretBase32: string, counter: bigint): string {\n const key = base32Decode(secretBase32);\n const counterBuf = Buffer.alloc(8);\n counterBuf.writeBigUInt64BE(counter);\n const hmac = createHmac(\"sha1\", key).update(counterBuf).digest();\n const offset = hmac[19]! & 0x0f;\n const code =\n ((hmac[offset]! & 0x7f) << 24) |\n ((hmac[offset + 1]! & 0xff) << 16) |\n ((hmac[offset + 2]! & 0xff) << 8) |\n (hmac[offset + 3]! & 0xff);\n return (code % Math.pow(10, TOTP_DIGITS)).toString().padStart(TOTP_DIGITS, \"0\");\n}\n\n/**\n * Generates a 160-bit random TOTP secret and returns it base32-encoded.\n * 160 bits (20 bytes) aligns with the SHA-1 block size and is standard for TOTP.\n */\nexport function generateTotpSecret(): string {\n return base32Encode(randomBytes(20));\n}\n\n/**\n * Generates the current TOTP code for the given base32 secret (RFC 6238).\n */\nexport function generateTotp(secretBase32: string, now: Date = new Date()): string {\n const counter = BigInt(Math.floor(now.getTime() / 1000 / TOTP_STEP_SECONDS));\n return hotp(secretBase32, counter);\n}\n\n/**\n * Verifies a 6-digit TOTP code.\n * Accepts codes from the previous, current, or next time step (±1 window) to\n * tolerate clock skew between server and authenticator app.\n */\nexport function verifyTotp(code: string, secretBase32: string, now: Date = new Date()): boolean {\n if (code.length !== TOTP_DIGITS) return false;\n const counter = BigInt(Math.floor(now.getTime() / 1000 / TOTP_STEP_SECONDS));\n return [-1n, 0n, 1n].some((delta) => hotp(secretBase32, counter + delta) === code);\n}\n\n/**\n * Builds an `otpauth://totp/` URI suitable for QR code scanning in authenticator apps\n * (Google Authenticator, Authy, 1Password, etc.).\n */\nexport function buildOtpAuthUri(params: {\n secret: string;\n accountName: string;\n issuer: string;\n}): string {\n const enc = encodeURIComponent;\n return (\n `otpauth://totp/${enc(params.issuer)}:${enc(params.accountName)}` +\n `?secret=${params.secret}&issuer=${enc(params.issuer)}&algorithm=SHA1&digits=${TOTP_DIGITS}&period=${TOTP_STEP_SECONDS}`\n );\n}\n","import { jwtVerify, SignJWT } from \"jose\";\n\nimport type { AuditLogger } from \"./audit\";\nimport type { IdentityConfig } from \"./config\";\nimport { IdentityError, InvalidTokenError } from \"./errors\";\nimport type { IdentityStore } from \"./identity-store\";\nimport { issueSession, type SessionDeps } from \"./session-state\";\nimport { buildOtpAuthUri, generateTotpSecret, verifyTotp } from \"./totp\";\nimport type { AuthResult, TotpSetupResult } from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/** Raised when a TOTP code is absent or incorrect. */\nexport class TwoFactorError extends IdentityError {\n constructor(message = \"Invalid two-factor authentication code.\") {\n super(\"identity/2fa-invalid\", message, 401);\n }\n}\n\n/**\n * Raised during login when the user has 2FA enabled and the TOTP challenge has not yet\n * been completed. HTTP 403 signals \"authenticated but additional factor required\".\n */\nexport class TwoFactorRequiredError extends IdentityError {\n constructor() {\n super(\"identity/2fa-required\", \"Two-factor authentication is required to complete sign-in.\", 403);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Deps\n// ---------------------------------------------------------------------------\n\nexport interface TwoFactorDeps extends SessionDeps {\n audit: AuditLogger;\n}\n\n// ---------------------------------------------------------------------------\n// Setup\n// ---------------------------------------------------------------------------\n\n/**\n * Begins the 2FA setup flow. Generates a new TOTP secret (stored as pending, not yet\n * active) and returns the secret + QR URI for display in an authenticator app.\n *\n * The caller must then call {@link enable2FA} with a valid TOTP code to activate 2FA.\n */\nexport async function setup2FA(\n deps: TwoFactorDeps,\n userId: string,\n): Promise<TotpSetupResult> {\n const { config, store, audit } = deps;\n\n const user = await store.findUserById(userId);\n if (!user) throw new TwoFactorError(\"User not found.\");\n\n const secretBase32 = generateTotpSecret();\n const now = config.now();\n\n await store.upsertTotpSecret?.({\n userId,\n secretBase32,\n enabledAt: null, // pending confirmation\n createdAt: now,\n });\n\n await audit.record(\"user.2fa.setup.started\", userId, {});\n\n return {\n secret: secretBase32,\n otpAuthUri: buildOtpAuthUri({\n secret: secretBase32,\n accountName: user.email,\n issuer: config.issuer,\n }),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Enable\n// ---------------------------------------------------------------------------\n\n/**\n * Confirms and enables 2FA. A valid TOTP code from the authenticator app is required.\n * Idempotent when 2FA is already enabled for the user.\n */\nexport async function enable2FA(\n deps: TwoFactorDeps,\n userId: string,\n totpCode: string,\n): Promise<void> {\n const { config, store, audit } = deps;\n\n const record = await store.findTotpSecret?.(userId);\n if (!record) {\n throw new TwoFactorError(\"2FA setup has not been started. Call setup2FA first.\");\n }\n if (record.enabledAt) return; // already enabled — idempotent\n\n if (!verifyTotp(totpCode, record.secretBase32, config.now())) {\n throw new TwoFactorError();\n }\n\n await store.upsertTotpSecret?.({ ...record, enabledAt: config.now() });\n await store.updateUser(userId, { twoFactorEnabled: true });\n\n await audit.record(\"user.2fa.enabled\", userId, {});\n}\n\n// ---------------------------------------------------------------------------\n// Disable\n// ---------------------------------------------------------------------------\n\n/**\n * Disables 2FA for the user. Requires a valid TOTP code as confirmation.\n * After disabling, login flows no longer issue MFA challenges for the account.\n */\nexport async function disable2FA(\n deps: TwoFactorDeps,\n userId: string,\n totpCode: string,\n): Promise<void> {\n const { config, store, audit } = deps;\n\n const record = await store.findTotpSecret?.(userId);\n if (!record || !record.enabledAt) {\n throw new TwoFactorError(\"2FA is not enabled for this account.\");\n }\n\n if (!verifyTotp(totpCode, record.secretBase32, config.now())) {\n throw new TwoFactorError();\n }\n\n await store.deleteTotpSecret?.(userId);\n await store.updateUser(userId, { twoFactorEnabled: false });\n\n await audit.record(\"user.2fa.disabled\", userId, {});\n}\n\n// ---------------------------------------------------------------------------\n// MFA challenge — issued during login when 2FA is active\n// ---------------------------------------------------------------------------\n\nconst MFA_CHALLENGE_TTL_SECONDS = 5 * 60; // 5 minutes\nconst MFA_CHALLENGE_TYPE = \"mfa_challenge\";\n\n/**\n * Issues a short-lived signed JWT that encodes a pending MFA session.\n * The client must present this token together with a TOTP code to complete login.\n */\nexport async function issueMfaChallengeToken(\n config: IdentityConfig,\n userId: string,\n): Promise<string> {\n const secret = new TextEncoder().encode(config.accessTokenSecret);\n const now = Math.floor(config.now().getTime() / 1000);\n return new SignJWT({ type: MFA_CHALLENGE_TYPE })\n .setProtectedHeader({ alg: \"HS256\" })\n .setSubject(userId)\n .setIssuedAt(now)\n .setExpirationTime(now + MFA_CHALLENGE_TTL_SECONDS)\n .sign(secret);\n}\n\n// ---------------------------------------------------------------------------\n// Complete MFA challenge\n// ---------------------------------------------------------------------------\n\n/**\n * Verifies an MFA challenge token and TOTP code, then issues a full auth session.\n *\n * Throws {@link InvalidTokenError} for an expired/invalid challenge token and\n * {@link TwoFactorError} for a wrong TOTP code.\n */\nexport async function verifyMfaChallenge(\n deps: TwoFactorDeps,\n mfaToken: string,\n totpCode: string,\n): Promise<AuthResult> {\n const { config, store, audit } = deps;\n const secret = new TextEncoder().encode(config.accessTokenSecret);\n\n let userId: string;\n try {\n const { payload } = await jwtVerify(mfaToken, secret);\n if (payload[\"type\"] !== MFA_CHALLENGE_TYPE || typeof payload.sub !== \"string\") {\n throw new InvalidTokenError(\"Invalid MFA challenge token.\");\n }\n userId = payload.sub;\n } catch {\n throw new InvalidTokenError(\"MFA challenge token is invalid or expired.\");\n }\n\n const record = await store.findTotpSecret?.(userId);\n if (!record || !record.enabledAt) {\n throw new TwoFactorError(\"2FA is not configured for this account.\");\n }\n\n if (!verifyTotp(totpCode, record.secretBase32, config.now())) {\n await audit.record(\"user.login.failed\", userId, { reason: \"invalid_totp\" });\n throw new TwoFactorError();\n }\n\n const user = await store.findUserById(userId);\n if (!user) throw new InvalidTokenError();\n\n await audit.record(\"user.login.succeeded\", userId, { via: \"mfa\" });\n return issueSession(deps, user);\n}\n","import type { AuditLogger } from \"./audit\";\nimport { AccountUnavailableError, InvalidCredentialsError } from \"./errors\";\nimport { checkLockout, clearLockout, recordFailedAttempt } from \"./lockout\";\nimport type { PasswordHasher } from \"./password-hasher\";\nimport { normalizeEmail } from \"./register\";\nimport { issueSession, type SessionDeps } from \"./session-state\";\nimport { issueMfaChallengeToken } from \"./two-factor\";\nimport type { LoginResult } from \"./types\";\n\n/** Dependencies for the login flow. */\nexport interface LoginDeps extends SessionDeps {\n hasher: PasswordHasher;\n audit: AuditLogger;\n}\n\n/** Input for {@link loginUser}. */\nexport interface LoginInput {\n email: string;\n password: string;\n}\n\n/**\n * Authenticates a user with email + password. Uses a single generic failure for both\n * \"unknown user\" and \"wrong password\" to avoid account enumeration, and performs a hash\n * comparison even when the user is unknown to reduce timing side-channels.\n *\n * When account lockout is enabled (config.lockout.maxFailedAttempts > 0), consecutive\n * failures increment a counter and temporarily lock the account.\n */\nexport async function loginUser(deps: LoginDeps, input: LoginInput): Promise<LoginResult> {\n const { config, store, hasher, audit } = deps;\n const email = normalizeEmail(input.email);\n\n const user = await store.findUserByEmail(email);\n const credential = user ? await store.findCredential(user.userId) : null;\n\n const storedHash =\n credential?.passwordHash ??\n // Dummy hash so verification cost is paid even for unknown users.\n \"scrypt$16384$00$00\";\n const passwordMatches = await hasher.verify(input.password, storedHash);\n\n if (!user || !credential || !passwordMatches) {\n // Record failed attempt for known users (unknown users have no counter to update).\n if (user) {\n await recordFailedAttempt(store, user, config.lockout, config.now);\n if (user.failedLoginAttempts + 1 >= config.lockout.maxFailedAttempts && config.lockout.maxFailedAttempts > 0) {\n await audit.record(\"user.account.locked\", user.userId, { email, reason: \"too_many_failures\" });\n }\n }\n await audit.record(\"user.login.failed\", user?.userId ?? null, { email });\n throw new InvalidCredentialsError();\n }\n\n // Check time-based lockout (throws when lockedUntil is in the future).\n checkLockout(user, config.lockout, config.now);\n\n // Check admin-imposed status: disabled (permanent) or locked with no expiry (manual lock).\n if (user.status === \"disabled\" || (user.status === \"locked\" && user.lockedUntil === null)) {\n await audit.record(\"user.login.failed\", user.userId, { email, status: user.status });\n throw new AccountUnavailableError();\n }\n\n // Successful login: clear any stale lockout state.\n await clearLockout(store, user);\n\n // If 2FA is enabled, return an MFA challenge instead of a full session.\n if (user.twoFactorEnabled) {\n await audit.record(\"user.login.mfa.challenged\", user.userId, { email });\n const mfaToken = await issueMfaChallengeToken(config, user.userId);\n return { kind: \"mfa_required\", mfaToken };\n }\n\n await audit.record(\"user.login.succeeded\", user.userId, { email });\n return issueSession(deps, user);\n}\n","import type { AuditLogger } from \"./audit\";\nimport type { IdentityConfig } from \"./config\";\nimport { AccountUnavailableError, InvalidTokenError } from \"./errors\";\nimport type { IdentityStore } from \"./identity-store\";\nimport { hydratePrincipal } from \"./session-state\";\nimport type { TokenService } from \"./token-service\";\nimport type { AuthResult } from \"./types\";\n\n/** Dependencies for the refresh flow. */\nexport interface RefreshDeps {\n config: IdentityConfig;\n store: IdentityStore;\n tokenService: TokenService;\n audit: AuditLogger;\n}\n\n/**\n * Rotates a refresh session: validates the presented refresh token against the stored\n * hash, issues a new access token, and replaces the stored refresh-token hash.\n *\n * Detects refresh-token reuse (a token whose session exists but whose hash no longer\n * matches) and revokes the session as a precaution.\n */\nexport async function refreshSession(\n deps: RefreshDeps,\n presentedToken: string,\n): Promise<AuthResult> {\n const { config, store, tokenService, audit } = deps;\n\n const sessionId = tokenService.parseSessionId(presentedToken);\n if (!sessionId) {\n throw new InvalidTokenError();\n }\n\n const session = await store.findSessionById(sessionId);\n const now = config.now();\n\n if (!session || session.revokedAt || session.expiresAt.getTime() <= now.getTime()) {\n throw new InvalidTokenError();\n }\n\n const presentedHash = tokenService.hashRefreshToken(presentedToken);\n if (presentedHash !== session.refreshTokenHash) {\n // Token reuse or tampering — revoke the session defensively.\n await store.revokeSession(sessionId, now);\n await audit.record(\"session.revoked\", session.userId, { reason: \"refresh-reuse\" });\n throw new InvalidTokenError();\n }\n\n const user = await store.findUserById(session.userId);\n if (!user) {\n throw new InvalidTokenError();\n }\n if (user.status !== \"active\") {\n await store.revokeSession(sessionId, now);\n throw new AccountUnavailableError();\n }\n\n const principal = await hydratePrincipal(store, user);\n const access = await tokenService.issueAccessToken(user.userId, user.authVersion);\n const refresh = tokenService.createRefreshToken(sessionId);\n const refreshExpiresAt = new Date(now.getTime() + config.refreshTokenTtlSeconds * 1000);\n\n await store.rotateSession(sessionId, refresh.hash, refreshExpiresAt);\n await audit.record(\"session.refreshed\", user.userId, {});\n\n return {\n user: principal,\n tokens: {\n accessToken: access.token,\n refreshToken: refresh.token,\n accessTokenExpiresAt: access.expiresAt,\n refreshTokenExpiresAt: refreshExpiresAt,\n },\n };\n}\n\n/** Revokes a single session (logout). */\nexport async function revokeSession(\n deps: Pick<RefreshDeps, \"config\" | \"store\" | \"tokenService\" | \"audit\">,\n presentedToken: string,\n): Promise<void> {\n const { config, store, tokenService, audit } = deps;\n const sessionId = tokenService.parseSessionId(presentedToken);\n if (!sessionId) return;\n const session = await store.findSessionById(sessionId);\n if (!session || session.revokedAt) return;\n await store.revokeSession(sessionId, config.now());\n await audit.record(\"session.revoked\", session.userId, { reason: \"logout\" });\n}\n","import { createHash, randomBytes } from \"node:crypto\";\n\nimport type { VerificationToken, VerificationTokenType } from \"./types\";\n\nexport type { VerificationToken, VerificationTokenType };\n\n/** Input for persisting a new verification token. */\nexport interface CreateVerificationTokenInput {\n /** SHA-256 hash of the raw token (never store the raw token). */\n tokenHash: string;\n userId: string;\n type: VerificationTokenType;\n expiresAt: Date;\n}\n\n/**\n * Generates a cryptographically random opaque token and its SHA-256 hash.\n * Returns both: the raw token is sent to the user; only the hash is persisted.\n */\nexport function generateVerificationToken(): { token: string; tokenHash: string } {\n const token = randomBytes(32).toString(\"hex\");\n const tokenHash = createHash(\"sha256\").update(token).digest(\"hex\");\n return { token, tokenHash };\n}\n\n/** Hashes a token presented by the user for secure lookup against stored hashes. */\nexport function hashVerificationToken(token: string): string {\n return createHash(\"sha256\").update(token).digest(\"hex\");\n}\n","import type { AuditLogger } from \"./audit\";\nimport type { IdentityConfig } from \"./config\";\nimport { IdentityError } from \"./errors\";\nimport type { IdentityStore } from \"./identity-store\";\nimport type { NotificationService } from \"./notification\";\nimport { generateVerificationToken, hashVerificationToken } from \"./verification-token\";\n\n/** Default token lifetime for email verification links. */\nconst DEFAULT_TTL_SECONDS = 24 * 60 * 60; // 24 hours\n\n/** Raised when an email verification token is missing, expired, or already used. */\nexport class EmailVerificationError extends IdentityError {\n constructor(message = \"Email verification token is invalid or expired.\") {\n super(\"identity/email-verification-invalid\", message, 400);\n }\n}\n\nexport interface EmailVerificationDeps {\n config: IdentityConfig;\n store: IdentityStore;\n audit: AuditLogger;\n}\n\n/**\n * Generates and persists a verification token, then calls the notification service.\n *\n * Safe to call after registration or when the user explicitly requests a resend.\n * Returns silently when the email is already verified.\n */\nexport async function requestEmailVerification(\n deps: EmailVerificationDeps,\n userId: string,\n): Promise<void> {\n const { config, store, audit } = deps;\n const notifications: NotificationService | undefined = config.notifications;\n\n const user = await store.findUserById(userId);\n if (!user) throw new EmailVerificationError(\"User not found.\");\n if (user.emailVerifiedAt) return; // already verified — no-op\n\n const { token, tokenHash } = generateVerificationToken();\n const now = config.now();\n const expiresAt = new Date(now.getTime() + DEFAULT_TTL_SECONDS * 1000);\n\n await store.createVerificationToken?.({\n tokenHash,\n userId,\n type: \"email_verification\",\n expiresAt,\n });\n\n await notifications?.sendEmailVerification?.({\n email: user.email,\n displayName: user.displayName,\n token,\n });\n\n await audit.record(\"user.email.verification.requested\", userId, { email: user.email });\n}\n\n/**\n * Verifies the user's email address using the token from the verification link.\n *\n * Throws {@link EmailVerificationError} for unknown, expired, or already-used tokens.\n */\nexport async function verifyEmail(\n deps: EmailVerificationDeps,\n rawToken: string,\n): Promise<void> {\n const { config, store, audit } = deps;\n\n const tokenHash = hashVerificationToken(rawToken);\n const record = await store.findVerificationToken?.(tokenHash);\n\n if (!record || record.type !== \"email_verification\") {\n throw new EmailVerificationError();\n }\n if (record.usedAt) {\n throw new EmailVerificationError(\"This verification link has already been used.\");\n }\n\n const now = config.now();\n if (record.expiresAt <= now) {\n throw new EmailVerificationError(\"This verification link has expired.\");\n }\n\n await store.markVerificationTokenUsed?.(tokenHash, now);\n await store.updateUser(record.userId, { emailVerifiedAt: now });\n\n await audit.record(\"user.email.verified\", record.userId, {});\n}\n","import type { AuditLogger } from \"./audit\";\nimport type { IdentityConfig } from \"./config\";\nimport { IdentityError } from \"./errors\";\nimport type { IdentityStore } from \"./identity-store\";\nimport type { NotificationService } from \"./notification\";\nimport type { PasswordHasher } from \"./password-hasher\";\nimport { generateVerificationToken, hashVerificationToken } from \"./verification-token\";\nimport { normalizeEmail } from \"./register\";\n\n/** Default token lifetime for password reset links. */\nconst DEFAULT_TTL_SECONDS = 60 * 60; // 1 hour\n\nconst MIN_PASSWORD_LENGTH = 8;\n\n/** Raised when a password reset token is missing, expired, or already used. */\nexport class PasswordResetError extends IdentityError {\n constructor(message = \"Password reset token is invalid or expired.\") {\n super(\"identity/password-reset-invalid\", message, 400);\n }\n}\n\nexport interface PasswordResetDeps {\n config: IdentityConfig;\n store: IdentityStore;\n hasher: PasswordHasher;\n audit: AuditLogger;\n}\n\n/**\n * Generates a password reset token and dispatches the notification.\n *\n * Never reveals whether the email address exists in the system (returns silently for\n * unknown addresses) to prevent account enumeration.\n */\nexport async function requestPasswordReset(\n deps: PasswordResetDeps,\n email: string,\n): Promise<void> {\n const { config, store, notifications, audit } = deps as PasswordResetDeps & {\n notifications?: NotificationService;\n };\n\n const normalized = normalizeEmail(email);\n const user = await store.findUserByEmail(normalized);\n if (!user) return; // silent — do not reveal whether the address exists\n\n const { token, tokenHash } = generateVerificationToken();\n const now = config.now();\n const expiresAt = new Date(now.getTime() + DEFAULT_TTL_SECONDS * 1000);\n\n await store.createVerificationToken?.({\n tokenHash,\n userId: user.userId,\n type: \"password_reset\",\n expiresAt,\n });\n\n const resolvedNotifications: NotificationService | undefined =\n notifications ?? config.notifications;\n\n await resolvedNotifications?.sendPasswordReset?.({\n email: user.email,\n displayName: user.displayName,\n token,\n });\n\n await audit.record(\"user.password.reset.requested\", user.userId, { email: user.email });\n}\n\n/**\n * Validates the reset token and sets a new password.\n *\n * On success, bumps `authVersion` and revokes all existing sessions so outstanding\n * refresh tokens are immediately invalidated.\n */\nexport async function resetPassword(\n deps: PasswordResetDeps,\n rawToken: string,\n newPassword: string,\n): Promise<void> {\n const { config, store, hasher, audit } = deps;\n\n if (newPassword.length < MIN_PASSWORD_LENGTH) {\n throw new PasswordResetError(\n `Password must be at least ${MIN_PASSWORD_LENGTH} characters.`,\n );\n }\n\n const tokenHash = hashVerificationToken(rawToken);\n const record = await store.findVerificationToken?.(tokenHash);\n\n if (!record || record.type !== \"password_reset\") {\n throw new PasswordResetError();\n }\n if (record.usedAt) {\n throw new PasswordResetError(\"This reset link has already been used.\");\n }\n\n const now = config.now();\n if (record.expiresAt <= now) {\n throw new PasswordResetError(\"This reset link has expired.\");\n }\n\n await store.markVerificationTokenUsed?.(tokenHash, now);\n\n const passwordHash = await hasher.hash(newPassword);\n await store.upsertCredential({ userId: record.userId, passwordHash, updatedAt: now });\n\n // Bump authVersion so all existing access tokens are immediately invalidated.\n const user = await store.findUserById(record.userId);\n if (user) {\n await store.updateUser(record.userId, { authVersion: user.authVersion + 1 });\n await store.revokeAllSessions(record.userId, now);\n }\n\n await audit.record(\"user.password.reset.completed\", record.userId, {});\n}\n","import type { AuditLogger } from \"./audit\";\nimport type { IdentityConfig } from \"./config\";\nimport { IdentityError } from \"./errors\";\nimport type { IdentityStore } from \"./identity-store\";\n\n/** Raised when the target user does not exist. */\nexport class AccountAdminError extends IdentityError {\n constructor(message: string) {\n super(\"identity/account-not-found\", message, 404);\n }\n}\n\nexport interface AccountAdminDeps {\n config: IdentityConfig;\n store: IdentityStore;\n audit: AuditLogger;\n}\n\n/**\n * Locks a user account. Sets `status` to `\"locked\"` and immediately revokes all active\n * sessions so existing refresh tokens stop working.\n *\n * Typically called by an admin. The `AccountUnavailableError` is thrown during any\n * subsequent login or token verification attempt for the account.\n */\nexport async function lockAccount(deps: AccountAdminDeps, userId: string): Promise<void> {\n const { config, store, audit } = deps;\n\n const user = await store.findUserById(userId);\n if (!user) throw new AccountAdminError(`User ${userId} not found.`);\n\n const now = config.now();\n await store.updateUser(userId, { status: \"locked\" });\n await store.revokeAllSessions(userId, now);\n\n await audit.record(\"user.account.locked\", userId, { reason: \"admin\" });\n}\n\n/**\n * Unlocks a user account. Resets `status` to `\"active\"` and clears any stale lockout\n * counters so the user can log in immediately.\n */\nexport async function unlockAccount(deps: AccountAdminDeps, userId: string): Promise<void> {\n const { config, store, audit } = deps;\n\n const user = await store.findUserById(userId);\n if (!user) throw new AccountAdminError(`User ${userId} not found.`);\n\n await store.updateUser(userId, {\n status: \"active\",\n failedLoginAttempts: 0,\n lockedUntil: null,\n });\n\n await audit.record(\"user.account.unlocked\", userId, {});\n}\n\n/**\n * Disables a user account. Sets `status` to `\"disabled\"` and revokes all sessions.\n * Unlike `lockAccount`, disabled accounts are not automatically unlockable by the\n * lockout timer — an explicit call to `unlockAccount` is required.\n */\nexport async function disableAccount(deps: AccountAdminDeps, userId: string): Promise<void> {\n const { config, store, audit } = deps;\n\n const user = await store.findUserById(userId);\n if (!user) throw new AccountAdminError(`User ${userId} not found.`);\n\n const now = config.now();\n await store.updateUser(userId, { status: \"disabled\" });\n await store.revokeAllSessions(userId, now);\n\n await audit.record(\"user.account.locked\", userId, { reason: \"admin_disabled\" });\n}\n","import { createAuditLogger } from \"./audit\";\nimport {\n type AuthorizationContext,\n type AuthorizationDecision,\n type AuthorizationRequirement,\n evaluateAuthorization,\n} from \"./authorization\";\nimport { type IdentityConfigInput, resolveIdentityConfig } from \"./config\";\nimport { AccountUnavailableError, InvalidTokenError } from \"./errors\";\nimport {\n requestEmailVerification,\n verifyEmail,\n type EmailVerificationDeps,\n} from \"./email-verification\";\nimport type { IdentityStore } from \"./identity-store\";\nimport { type LoginInput, loginUser } from \"./login\";\nimport { createOAuthService, type OAuthService } from \"./oauth/oauth-service\";\nimport type { OAuthProvider } from \"./oauth/oauth-provider\";\nimport { createPasswordHasher } from \"./password-hasher\";\nimport { requestPasswordReset, resetPassword } from \"./password-reset\";\nimport { type RegisterInput, registerUser } from \"./register\";\nimport { refreshSession, revokeSession } from \"./refresh-service\";\nimport { hydratePrincipal } from \"./session-state\";\nimport { createTokenService } from \"./token-service\";\nimport {\n disable2FA,\n enable2FA,\n setup2FA,\n verifyMfaChallenge,\n type TwoFactorDeps,\n} from \"./two-factor\";\nimport { lockAccount, unlockAccount, disableAccount } from \"./account-admin\";\nimport type {\n AuthResult,\n AuthenticatedIdentity,\n LoginResult,\n TotpSetupResult,\n} from \"./types\";\n\n/** The wired runtime API returned by {@link createIdentityService}. */\nexport interface IdentityService {\n register(input: RegisterInput): Promise<AuthResult>;\n /**\n * Authenticates with email + password. Returns a full {@link AuthResult} normally, or\n * an {@link MfaRequiredResult} (containing an `mfaToken`) when the account has 2FA\n * enabled. In the latter case call {@link verifyMfaChallenge} to complete the login.\n */\n login(input: LoginInput): Promise<LoginResult>;\n refresh(refreshToken: string): Promise<AuthResult>;\n logout(refreshToken: string): Promise<void>;\n /**\n * Verifies an access token and hydrates the current principal from the store.\n * Throws {@link InvalidTokenError} for invalid/stale tokens.\n */\n authenticate(accessToken: string): Promise<AuthenticatedIdentity>;\n authorize<TResource = unknown>(\n requirement: AuthorizationRequirement<TResource>,\n context: AuthorizationContext<TResource>,\n ): Promise<AuthorizationDecision>;\n\n // --- Email verification --------------------------------------------------\n /** Sends (or resends) an email verification link for the given user id. */\n requestEmailVerification(userId: string): Promise<void>;\n /** Marks the user's email as verified using the token from the verification link. */\n verifyEmail(token: string): Promise<void>;\n\n // --- Password reset -------------------------------------------------------\n /** Sends a password reset link to the given email address. Silent for unknown addresses. */\n requestPasswordReset(email: string): Promise<void>;\n /** Resets the password using the token from the reset link. */\n resetPassword(token: string, newPassword: string): Promise<void>;\n\n // --- Two-factor authentication -------------------------------------------\n /**\n * Begins the 2FA setup flow. Returns the TOTP secret and an OTP Auth URI for QR code\n * display. Call {@link enable2FA} with a valid code to activate.\n */\n setup2FA(userId: string): Promise<TotpSetupResult>;\n /** Confirms and enables 2FA. Requires a valid TOTP code from the authenticator app. */\n enable2FA(userId: string, totpCode: string): Promise<void>;\n /** Disables 2FA. Requires a valid TOTP code as confirmation. */\n disable2FA(userId: string, totpCode: string): Promise<void>;\n /**\n * Completes an MFA challenge. Present the `mfaToken` returned by `login` together\n * with the TOTP code from the authenticator app.\n */\n verifyMfaChallenge(mfaToken: string, totpCode: string): Promise<AuthResult>;\n\n // --- Account management (admin) ------------------------------------------\n /** Locks the account and revokes all active sessions. */\n lockAccount(userId: string): Promise<void>;\n /** Unlocks the account and clears lockout state. */\n unlockAccount(userId: string): Promise<void>;\n /** Permanently disables the account (requires manual admin unlock). */\n disableAccount(userId: string): Promise<void>;\n\n /**\n * The OAuth 2.0 / OpenID Connect surface. Present only when at least one provider is\n * configured via `IdentityConfigInput.oauth.providers`.\n */\n oauth?: OAuthService;\n}\n\n/** Extended config input with optional OAuth provider list. */\nexport interface IdentityServiceConfig extends IdentityConfigInput {\n /** Optional OAuth 2.0 / OIDC configuration. */\n oauth?: {\n /**\n * The list of external authentication providers to enable. Each item is an\n * {@link OAuthProvider} instance, e.g. `createGoogleOAuthProvider(...)` or\n * `createMicrosoftOAuthProvider(...)`.\n */\n providers: OAuthProvider[];\n };\n}\n\n/**\n * Wires the framework-agnostic identity runtime with Node defaults (scrypt hashing,\n * `jose` HS256 tokens). Express and NestJS adapters build on top of this service.\n */\nexport function createIdentityService(\n configInput: IdentityServiceConfig,\n store: IdentityStore,\n): IdentityService {\n const config = resolveIdentityConfig(configInput);\n const tokenService = createTokenService(config);\n const hasher = createPasswordHasher(config.passwordScryptCost);\n const audit = createAuditLogger(store, config.now, config.onEvent);\n\n const deps = { config, store, tokenService, hasher, audit };\n const mfaDeps: TwoFactorDeps = deps;\n const emailDeps: EmailVerificationDeps = { config, store, audit };\n\n const oauthProviders = configInput.oauth?.providers;\n const oauthService: OAuthService | undefined =\n oauthProviders && oauthProviders.length > 0\n ? createOAuthService({ providers: oauthProviders, config, store, sessionDeps: deps, audit })\n : undefined;\n\n return {\n register: (input) => registerUser(deps, input),\n login: (input) => loginUser(deps, input),\n refresh: (refreshToken) => refreshSession(deps, refreshToken),\n logout: (refreshToken) => revokeSession(deps, refreshToken),\n\n async authenticate(accessToken) {\n const claims = await tokenService.verifyAccessToken(accessToken);\n const user = await store.findUserById(claims.sub);\n if (!user || user.authVersion !== claims.authVersion) {\n throw new InvalidTokenError();\n }\n if (user.status !== \"active\") {\n throw new AccountUnavailableError();\n }\n return hydratePrincipal(store, user);\n },\n\n authorize: (requirement, context) => evaluateAuthorization(requirement, context),\n\n // Email verification\n requestEmailVerification: (userId) => requestEmailVerification(emailDeps, userId),\n verifyEmail: (token) => verifyEmail(emailDeps, token),\n\n // Password reset\n requestPasswordReset: (email) => requestPasswordReset(deps, email),\n resetPassword: (token, newPassword) => resetPassword(deps, token, newPassword),\n\n // Two-factor authentication\n setup2FA: (userId) => setup2FA(mfaDeps, userId),\n enable2FA: (userId, code) => enable2FA(mfaDeps, userId, code),\n disable2FA: (userId, code) => disable2FA(mfaDeps, userId, code),\n verifyMfaChallenge: (mfaToken, code) => verifyMfaChallenge(mfaDeps, mfaToken, code),\n\n // Account management\n lockAccount: (userId) => lockAccount(deps, userId),\n unlockAccount: (userId) => unlockAccount(deps, userId),\n disableAccount: (userId) => disableAccount(deps, userId),\n\n oauth: oauthService,\n };\n}\n","/**\n * Google OAuth 2.0 / OpenID Connect provider for `@azlib/identity`.\n *\n * Uses the Google OAuth2 v2 endpoints and calls the userinfo endpoint to retrieve user\n * details. Requires a project in Google Cloud Console with an OAuth 2.0 client credential.\n *\n * @example\n * ```ts\n * import { createGoogleOAuthProvider } from \"@azlib/identity/node\";\n *\n * const google = createGoogleOAuthProvider({\n * clientId: process.env.GOOGLE_CLIENT_ID!,\n * clientSecret: process.env.GOOGLE_CLIENT_SECRET!,\n * });\n * ```\n */\n\nimport type {\n BuildAuthUrlParams,\n ExchangeCodeParams,\n OAuthProvider,\n OAuthTokens,\n OAuthUserInfo,\n} from \"../oauth-provider\";\n\nconst GOOGLE_AUTH_URL = \"https://accounts.google.com/o/oauth2/v2/auth\";\nconst GOOGLE_TOKEN_URL = \"https://oauth2.googleapis.com/token\";\nconst GOOGLE_USERINFO_URL = \"https://www.googleapis.com/oauth2/v3/userinfo\";\n\nconst DEFAULT_SCOPES = [\"openid\", \"email\", \"profile\"];\n\n/** Configuration for {@link createGoogleOAuthProvider}. */\nexport interface GoogleOAuthConfig {\n /** OAuth 2.0 Client ID from Google Cloud Console. */\n clientId: string;\n /**\n * OAuth 2.0 Client Secret. Must be kept server-side — never expose in client code or\n * commit to source control.\n */\n clientSecret: string;\n /**\n * Override the default OpenID Connect scopes. Defaults to `[\"openid\", \"email\", \"profile\"]`.\n */\n scopes?: string[];\n}\n\ninterface GoogleTokenResponse {\n access_token: string;\n id_token?: string;\n token_type: string;\n expires_in: number;\n refresh_token?: string;\n scope?: string;\n error?: string;\n error_description?: string;\n}\n\ninterface GoogleUserInfoResponse {\n sub: string;\n email?: string;\n email_verified?: boolean;\n name?: string;\n error?: { message?: string };\n}\n\n/**\n * Creates an {@link OAuthProvider} configured for Google OAuth 2.0 / OpenID Connect.\n *\n * Security notes:\n * - The client secret is used only during server-side code exchange.\n * - The userinfo endpoint is called with the access token to fetch user data, providing\n * a verified source of truth over the network (HTTPS).\n * - The `state` parameter is required and must be verified by the caller to prevent CSRF.\n */\nexport function createGoogleOAuthProvider(config: GoogleOAuthConfig): OAuthProvider {\n const defaultScopes = config.scopes ?? DEFAULT_SCOPES;\n\n return {\n name: \"google\",\n\n buildAuthorizationUrl({ redirectUri, state, scopes }: BuildAuthUrlParams): string {\n const url = new URL(GOOGLE_AUTH_URL);\n url.searchParams.set(\"client_id\", config.clientId);\n url.searchParams.set(\"redirect_uri\", redirectUri);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"scope\", (scopes ?? defaultScopes).join(\" \"));\n url.searchParams.set(\"state\", state);\n // Request offline access to get a refresh token.\n url.searchParams.set(\"access_type\", \"offline\");\n return url.toString();\n },\n\n async exchangeCode({ code, redirectUri }: ExchangeCodeParams): Promise<OAuthTokens> {\n const body = new URLSearchParams({\n code,\n client_id: config.clientId,\n client_secret: config.clientSecret,\n redirect_uri: redirectUri,\n grant_type: \"authorization_code\",\n });\n\n const response = await fetch(GOOGLE_TOKEN_URL, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n });\n\n const data = (await response.json()) as GoogleTokenResponse;\n if (!response.ok || data.error) {\n throw new Error(\n `Google token exchange failed: ${data.error_description ?? data.error ?? response.status}`,\n );\n }\n\n return {\n accessToken: data.access_token,\n idToken: data.id_token,\n tokenType: data.token_type,\n expiresIn: data.expires_in,\n refreshToken: data.refresh_token,\n scope: data.scope,\n };\n },\n\n async fetchUserInfo(tokens: OAuthTokens): Promise<OAuthUserInfo> {\n const response = await fetch(GOOGLE_USERINFO_URL, {\n headers: { authorization: `Bearer ${tokens.accessToken}` },\n });\n\n const data = (await response.json()) as GoogleUserInfoResponse;\n if (!response.ok) {\n throw new Error(\n `Google userinfo request failed: ${data.error?.message ?? response.status}`,\n );\n }\n\n return {\n providerUserId: data.sub,\n email: data.email ?? null,\n emailVerified: data.email_verified === true,\n displayName: data.name ?? null,\n };\n },\n };\n}\n","/**\n * Microsoft Entra ID (Azure AD) OAuth 2.0 / OpenID Connect provider for `@azlib/identity`.\n *\n * Uses the Microsoft identity platform v2.0 endpoints and calls the Microsoft Graph\n * `/v1.0/me` endpoint to retrieve user details.\n *\n * @example\n * ```ts\n * import { createMicrosoftOAuthProvider } from \"@azlib/identity/node\";\n *\n * const microsoft = createMicrosoftOAuthProvider({\n * clientId: process.env.MICROSOFT_CLIENT_ID!,\n * clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,\n * // tenantId defaults to \"common\" (all accounts). Set to your tenant id for\n * // single-tenant apps, or \"organizations\" / \"consumers\" to restrict account types.\n * tenantId: process.env.MICROSOFT_TENANT_ID,\n * });\n * ```\n */\n\nimport type {\n BuildAuthUrlParams,\n ExchangeCodeParams,\n OAuthProvider,\n OAuthTokens,\n OAuthUserInfo,\n} from \"../oauth-provider\";\n\nconst DEFAULT_SCOPES = [\"openid\", \"email\", \"profile\", \"User.Read\"];\n\n/** Configuration for {@link createMicrosoftOAuthProvider}. */\nexport interface MicrosoftOAuthConfig {\n /** Application (client) ID from the Azure / Entra app registration. */\n clientId: string;\n /**\n * Client secret from the Azure / Entra app registration. Must be kept server-side —\n * never expose in client code or commit to source control.\n */\n clientSecret: string;\n /**\n * Tenant identifier. Use `\"common\"` (default) to accept personal and work/school\n * accounts, `\"organizations\"` for work/school only, `\"consumers\"` for personal\n * accounts only, or your specific tenant GUID / domain.\n */\n tenantId?: string;\n /**\n * Override the default Microsoft Graph scopes. Defaults to\n * `[\"openid\", \"email\", \"profile\", \"User.Read\"]`.\n */\n scopes?: string[];\n}\n\ninterface MicrosoftTokenResponse {\n access_token: string;\n id_token?: string;\n token_type: string;\n expires_in: number;\n refresh_token?: string;\n scope?: string;\n error?: string;\n error_description?: string;\n}\n\ninterface MicrosoftGraphMeResponse {\n id: string;\n mail?: string;\n userPrincipalName?: string;\n displayName?: string;\n error?: { message?: string };\n}\n\n/**\n * Creates an {@link OAuthProvider} configured for Microsoft Entra ID OAuth 2.0 / OIDC.\n *\n * Security notes:\n * - The client secret is used only during server-side code exchange.\n * - User data is fetched from the Microsoft Graph API using the access token, providing\n * a verified source of truth over HTTPS.\n * - The `state` parameter is required and must be verified by the caller to prevent CSRF.\n */\nexport function createMicrosoftOAuthProvider(config: MicrosoftOAuthConfig): OAuthProvider {\n const tenantId = config.tenantId ?? \"common\";\n const defaultScopes = config.scopes ?? DEFAULT_SCOPES;\n\n const authBase = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0`;\n\n return {\n name: \"microsoft\",\n\n buildAuthorizationUrl({ redirectUri, state, scopes }: BuildAuthUrlParams): string {\n const url = new URL(`${authBase}/authorize`);\n url.searchParams.set(\"client_id\", config.clientId);\n url.searchParams.set(\"redirect_uri\", redirectUri);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"scope\", (scopes ?? defaultScopes).join(\" \"));\n url.searchParams.set(\"state\", state);\n url.searchParams.set(\"response_mode\", \"query\");\n return url.toString();\n },\n\n async exchangeCode({ code, redirectUri }: ExchangeCodeParams): Promise<OAuthTokens> {\n const body = new URLSearchParams({\n code,\n client_id: config.clientId,\n client_secret: config.clientSecret,\n redirect_uri: redirectUri,\n grant_type: \"authorization_code\",\n scope: defaultScopes.join(\" \"),\n });\n\n const response = await fetch(`${authBase}/token`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n });\n\n const data = (await response.json()) as MicrosoftTokenResponse;\n if (!response.ok || data.error) {\n throw new Error(\n `Microsoft token exchange failed: ${data.error_description ?? data.error ?? response.status}`,\n );\n }\n\n return {\n accessToken: data.access_token,\n idToken: data.id_token,\n tokenType: data.token_type,\n expiresIn: data.expires_in,\n refreshToken: data.refresh_token,\n scope: data.scope,\n };\n },\n\n async fetchUserInfo(tokens: OAuthTokens): Promise<OAuthUserInfo> {\n const response = await fetch(\"https://graph.microsoft.com/v1.0/me\", {\n headers: { authorization: `Bearer ${tokens.accessToken}` },\n });\n\n const data = (await response.json()) as MicrosoftGraphMeResponse;\n if (!response.ok) {\n throw new Error(\n `Microsoft Graph /me request failed: ${data.error?.message ?? response.status}`,\n );\n }\n\n // Use `mail` (for work accounts) or `userPrincipalName` as email fallback.\n const email = data.mail ?? data.userPrincipalName ?? null;\n\n return {\n providerUserId: data.id,\n email,\n // Microsoft does not expose email_verified via Graph; treat work/school accounts\n // as verified since Entra manages the identity.\n emailVerified: true,\n displayName: data.displayName ?? null,\n };\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"node.mjs","names":["MIN_PASSWORD_LENGTH","DEFAULT_TTL_SECONDS","DEFAULT_SCOPES"],"sources":["../core/password-hasher.ts","../core/token-service.ts","../core/audit.ts","../core/register.ts","../core/lockout.ts","../core/totp.ts","../core/two-factor.ts","../core/login.ts","../core/refresh-service.ts","../core/verification-token.ts","../core/email-verification.ts","../core/password-reset.ts","../core/account-admin.ts","../core/identity-service.ts","../core/oauth/providers/google.ts","../core/oauth/providers/microsoft.ts"],"sourcesContent":["import { randomBytes, scrypt, timingSafeEqual } from \"node:crypto\";\n\n/**\n * Password hashing using Node's scrypt. Hashes are self-describing:\n * `scrypt$<N>$<saltHex>$<derivedKeyHex>` so parameters can evolve without breaking\n * existing records.\n */\n\nconst KEY_LENGTH = 64;\n\nconst scryptAsync = (\n password: string,\n salt: Buffer,\n cost: number,\n): Promise<Buffer> =>\n new Promise((resolve, reject) => {\n // maxmem must accommodate the chosen cost parameter.\n scrypt(\n password,\n salt,\n KEY_LENGTH,\n { N: cost, maxmem: 256 * cost * 8 },\n (err, derived) => {\n if (err) reject(err);\n else resolve(derived);\n },\n );\n });\n\nexport interface PasswordHasher {\n hash(password: string): Promise<string>;\n verify(password: string, storedHash: string): Promise<boolean>;\n}\n\n/** Creates a scrypt-based {@link PasswordHasher}. */\nexport function createPasswordHasher(cost: number): PasswordHasher {\n return {\n async hash(password: string): Promise<string> {\n const salt = randomBytes(16);\n const derived = await scryptAsync(password, salt, cost);\n return `scrypt$${cost}$${salt.toString(\"hex\")}$${derived.toString(\"hex\")}`;\n },\n\n async verify(password: string, storedHash: string): Promise<boolean> {\n const parts = storedHash.split(\"$\");\n if (parts.length !== 4 || parts[0] !== \"scrypt\") {\n return false;\n }\n const cost = Number.parseInt(parts[1]!, 10);\n const salt = Buffer.from(parts[2]!, \"hex\");\n const expected = Buffer.from(parts[3]!, \"hex\");\n if (\n !Number.isInteger(cost) ||\n salt.length === 0 ||\n expected.length === 0\n ) {\n return false;\n }\n const actual = await scryptAsync(password, salt, cost);\n if (actual.length !== expected.length) {\n return false;\n }\n return timingSafeEqual(actual, expected);\n },\n };\n}\n","import { createHash, randomBytes } from \"node:crypto\";\nimport { SignJWT } from \"jose\";\n\nimport { verifyAccessToken } from \"./access-token\";\nimport type { IdentityConfig } from \"./config\";\nimport type { AccessTokenClaims } from \"./types\";\n\n/**\n * Issues and verifies short-lived HS256 access tokens (via `jose`) and generates the\n * opaque refresh tokens whose hashes are stored server-side.\n */\nexport interface TokenService {\n issueAccessToken(\n userId: string,\n authVersion: number,\n ): Promise<{ token: string; expiresAt: Date }>;\n verifyAccessToken(token: string): Promise<AccessTokenClaims>;\n /**\n * Creates a refresh token bound to a session id. The id prefix lets the server look up\n * the session, while only the hash of the full token is stored.\n */\n createRefreshToken(sessionId: string): { token: string; hash: string };\n /** Extracts the session id encoded in a refresh token, or null if malformed. */\n parseSessionId(token: string): string | null;\n /** Hashes a presented refresh token for comparison with the stored hash. */\n hashRefreshToken(token: string): string;\n}\n\nconst hashToken = (token: string): string =>\n createHash(\"sha256\").update(token).digest(\"hex\");\n\nexport function createTokenService(config: IdentityConfig): TokenService {\n const secret = new TextEncoder().encode(config.accessTokenSecret);\n\n return {\n async issueAccessToken(userId, authVersion) {\n const issuedAt = Math.floor(config.now().getTime() / 1000);\n const expiresAtSeconds = issuedAt + config.accessTokenTtlSeconds;\n const builder = new SignJWT({ authVersion })\n .setProtectedHeader({ alg: \"HS256\" })\n .setSubject(userId)\n .setIssuedAt(issuedAt)\n .setIssuer(config.issuer)\n .setExpirationTime(expiresAtSeconds);\n if (config.audience) {\n builder.setAudience(config.audience);\n }\n const token = await builder.sign(secret);\n return { token, expiresAt: new Date(expiresAtSeconds * 1000) };\n },\n\n async verifyAccessToken(token) {\n return verifyAccessToken(token, {\n secret: config.accessTokenSecret,\n issuer: config.issuer,\n audience: config.audience,\n });\n },\n\n createRefreshToken(sessionId) {\n const token = `${sessionId}.${randomBytes(32).toString(\"base64url\")}`;\n return { token, hash: hashToken(token) };\n },\n\n parseSessionId(token) {\n const separator = token.indexOf(\".\");\n if (separator <= 0) return null;\n return token.slice(0, separator);\n },\n\n hashRefreshToken(token) {\n return hashToken(token);\n },\n };\n}\n","import type { IdentityStore } from \"./identity-store\";\nimport type { IdentityEvent, IdentityEventType } from \"./types\";\n\n/**\n * Thin auditing helper. Persists events through the store's optional `recordEvent` hook\n * when present and never throws — auditing must not break the primary auth flow.\n *\n * Metadata is restricted to non-sensitive scalars; never pass passwords or raw tokens.\n */\nexport interface AuditLogger {\n record(\n type: IdentityEventType,\n userId: string | null,\n metadata?: IdentityEvent[\"metadata\"],\n ): Promise<void>;\n}\n\nexport function createAuditLogger(\n store: IdentityStore,\n now: () => Date,\n onEvent?: (event: IdentityEvent) => void | Promise<void>,\n): AuditLogger {\n return {\n async record(type, userId, metadata = {}) {\n const event: IdentityEvent = {\n type,\n userId,\n metadata,\n occurredAt: now(),\n };\n try {\n await store.recordEvent?.(event);\n } catch {\n // Auditing failures are swallowed intentionally.\n }\n try {\n await onEvent?.(event);\n } catch {\n // Lifecycle-hook failures must not break auth flows.\n }\n },\n };\n}\n","import type { AuditLogger } from \"./audit\";\nimport { EmailAlreadyRegisteredError, IdentityError } from \"./errors\";\nimport type { PasswordHasher } from \"./password-hasher\";\nimport { issueSession, type SessionDeps } from \"./session-state\";\nimport type { AuthResult } from \"./types\";\n\n/** Dependencies for the register flow. */\nexport interface RegisterDeps extends SessionDeps {\n hasher: PasswordHasher;\n audit: AuditLogger;\n}\n\n/** Input for {@link registerUser}. */\nexport interface RegisterInput {\n email: string;\n password: string;\n displayName?: string | null;\n}\n\n/** Raised when registration input fails validation. */\nexport class RegistrationValidationError extends IdentityError {\n constructor(message: string) {\n super(\"identity/registration-invalid\", message, 400);\n }\n}\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst MIN_PASSWORD_LENGTH = 8;\n\n/** Normalizes an email for storage and lookup (trim + lowercase). */\nexport function normalizeEmail(email: string): string {\n return email.trim().toLowerCase();\n}\n\n/**\n * Registers a new user: validates input, enforces unique email, hashes the password,\n * persists the user and credential, and returns an authenticated session.\n */\nexport async function registerUser(\n deps: RegisterDeps,\n input: RegisterInput,\n): Promise<AuthResult> {\n const { config, store, hasher, audit } = deps;\n\n const email = normalizeEmail(input.email);\n if (!EMAIL_PATTERN.test(email)) {\n throw new RegistrationValidationError(\"A valid email address is required.\");\n }\n if (input.password.length < MIN_PASSWORD_LENGTH) {\n throw new RegistrationValidationError(\n `Password must be at least ${MIN_PASSWORD_LENGTH} characters.`,\n );\n }\n\n const existing = await store.findUserByEmail(email);\n if (existing) {\n throw new EmailAlreadyRegisteredError();\n }\n\n const now = config.now();\n const user = await store.createUser({\n userId: config.generateId(),\n email,\n displayName: input.displayName ?? null,\n status: \"active\",\n authVersion: 0,\n createdAt: now,\n updatedAt: now,\n });\n\n const passwordHash = await hasher.hash(input.password);\n await store.upsertCredential({\n userId: user.userId,\n passwordHash,\n updatedAt: now,\n });\n\n await audit.record(\"user.registered\", user.userId, { email });\n\n return issueSession(deps, user);\n}\n","import type { LockoutConfig } from \"./config\";\nimport { AccountUnavailableError } from \"./errors\";\nimport type { IdentityStore } from \"./identity-store\";\nimport type { IdentityUser } from \"./types\";\n\n/**\n * Checks whether the user account is currently locked. Throws {@link AccountUnavailableError}\n * when the lockout is active (i.e. `lockedUntil` is in the future).\n *\n * Note: if `lockedUntil` is in the past the lock has expired — the user may proceed and\n * the stale lock will be cleared on successful login.\n */\nexport function checkLockout(\n user: IdentityUser,\n config: LockoutConfig,\n now: () => Date,\n): void {\n if (config.maxFailedAttempts === 0) return; // Lockout disabled\n if (user.lockedUntil !== null && user.lockedUntil > now()) {\n throw new AccountUnavailableError();\n }\n}\n\n/**\n * Records a failed login attempt. When the threshold is reached the account is locked\n * for `config.durationSeconds` seconds (or permanently when `durationSeconds` is 0).\n */\nexport async function recordFailedAttempt(\n store: IdentityStore,\n user: IdentityUser,\n config: LockoutConfig,\n now: () => Date,\n): Promise<void> {\n if (config.maxFailedAttempts === 0) return; // Lockout disabled\n\n const attempts = user.failedLoginAttempts + 1;\n const exceeded = attempts >= config.maxFailedAttempts;\n\n const lockedUntil = exceeded\n ? config.durationSeconds > 0\n ? new Date(now().getTime() + config.durationSeconds * 1000)\n : new Date(8_640_000_000_000_000) // max date = permanent lock\n : null;\n\n const patch: Parameters<IdentityStore[\"updateUser\"]>[1] = {\n failedLoginAttempts: attempts,\n ...(exceeded ? { status: \"locked\" as const, lockedUntil } : {}),\n };\n\n await store.updateUser(user.userId, patch);\n}\n\n/**\n * Clears lockout state after a successful login. Resets the failure counter and any\n * active lockout so the account returns to `active` status.\n */\nexport async function clearLockout(\n store: IdentityStore,\n user: IdentityUser,\n): Promise<void> {\n if (user.failedLoginAttempts === 0 && user.lockedUntil === null) return; // Nothing to clear\n await store.updateUser(user.userId, {\n failedLoginAttempts: 0,\n lockedUntil: null,\n // Restore active status only if the account was locked (not disabled by an admin)\n ...(user.status === \"locked\" ? { status: \"active\" as const } : {}),\n });\n}\n","import { createHmac, randomBytes } from \"node:crypto\";\n\n// ---------------------------------------------------------------------------\n// Base32 (RFC 4648) — required for TOTP secret encoding / OTP Auth URI\n// ---------------------------------------------------------------------------\n\nconst BASE32_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567\";\n\n/** Encodes a Buffer to a base32 string (no padding). */\nexport function base32Encode(buffer: Buffer): string {\n let result = \"\";\n let bits = 0;\n let accumulator = 0;\n\n for (const byte of buffer) {\n accumulator = (accumulator << 8) | byte;\n bits += 8;\n while (bits >= 5) {\n bits -= 5;\n result += BASE32_ALPHABET[(accumulator >> bits) & 0x1f] ?? \"\";\n }\n }\n if (bits > 0) {\n result += BASE32_ALPHABET[(accumulator << (5 - bits)) & 0x1f] ?? \"\";\n }\n return result;\n}\n\n/** Decodes a base32 string (case-insensitive, padding stripped) to a Buffer. */\nexport function base32Decode(encoded: string): Buffer {\n const str = encoded.toUpperCase().replace(/=+$/, \"\");\n const bytes: number[] = [];\n let bits = 0;\n let accumulator = 0;\n\n for (const char of str) {\n const value = BASE32_ALPHABET.indexOf(char);\n if (value === -1) continue; // skip non-alphabet chars\n accumulator = (accumulator << 5) | value;\n bits += 5;\n if (bits >= 8) {\n bits -= 8;\n bytes.push((accumulator >> bits) & 0xff);\n }\n }\n return Buffer.from(bytes);\n}\n\n// ---------------------------------------------------------------------------\n// HOTP (RFC 4226) + TOTP (RFC 6238)\n// ---------------------------------------------------------------------------\n\nconst TOTP_STEP_SECONDS = 30;\nconst TOTP_DIGITS = 6;\n\n/**\n * Computes an HOTP value for the given base32 key and integer counter (RFC 4226).\n * Returns a zero-padded 6-digit string.\n */\nfunction hotp(secretBase32: string, counter: bigint): string {\n const key = base32Decode(secretBase32);\n const counterBuf = Buffer.alloc(8);\n counterBuf.writeBigUInt64BE(counter);\n const hmac = createHmac(\"sha1\", key).update(counterBuf).digest();\n const offset = hmac[19]! & 0x0f;\n const code =\n ((hmac[offset]! & 0x7f) << 24) |\n ((hmac[offset + 1]! & 0xff) << 16) |\n ((hmac[offset + 2]! & 0xff) << 8) |\n (hmac[offset + 3]! & 0xff);\n return (code % Math.pow(10, TOTP_DIGITS))\n .toString()\n .padStart(TOTP_DIGITS, \"0\");\n}\n\n/**\n * Generates a 160-bit random TOTP secret and returns it base32-encoded.\n * 160 bits (20 bytes) aligns with the SHA-1 block size and is standard for TOTP.\n */\nexport function generateTotpSecret(): string {\n return base32Encode(randomBytes(20));\n}\n\n/**\n * Generates the current TOTP code for the given base32 secret (RFC 6238).\n */\nexport function generateTotp(\n secretBase32: string,\n now: Date = new Date(),\n): string {\n const counter = BigInt(Math.floor(now.getTime() / 1000 / TOTP_STEP_SECONDS));\n return hotp(secretBase32, counter);\n}\n\n/**\n * Verifies a 6-digit TOTP code.\n * Accepts codes from the previous, current, or next time step (±1 window) to\n * tolerate clock skew between server and authenticator app.\n */\nexport function verifyTotp(\n code: string,\n secretBase32: string,\n now: Date = new Date(),\n): boolean {\n if (code.length !== TOTP_DIGITS) return false;\n const counter = BigInt(Math.floor(now.getTime() / 1000 / TOTP_STEP_SECONDS));\n return [-1n, 0n, 1n].some(\n (delta) => hotp(secretBase32, counter + delta) === code,\n );\n}\n\n/**\n * Builds an `otpauth://totp/` URI suitable for QR code scanning in authenticator apps\n * (Google Authenticator, Authy, 1Password, etc.).\n */\nexport function buildOtpAuthUri(params: {\n secret: string;\n accountName: string;\n issuer: string;\n}): string {\n const enc = encodeURIComponent;\n return (\n `otpauth://totp/${enc(params.issuer)}:${enc(params.accountName)}` +\n `?secret=${params.secret}&issuer=${enc(params.issuer)}&algorithm=SHA1&digits=${TOTP_DIGITS}&period=${TOTP_STEP_SECONDS}`\n );\n}\n","import { jwtVerify, SignJWT } from \"jose\";\n\nimport type { AuditLogger } from \"./audit\";\nimport type { IdentityConfig } from \"./config\";\nimport { IdentityError, InvalidTokenError } from \"./errors\";\nimport type { IdentityStore } from \"./identity-store\";\nimport { issueSession, type SessionDeps } from \"./session-state\";\nimport { buildOtpAuthUri, generateTotpSecret, verifyTotp } from \"./totp\";\nimport type { AuthResult, TotpSetupResult } from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/** Raised when a TOTP code is absent or incorrect. */\nexport class TwoFactorError extends IdentityError {\n constructor(message = \"Invalid two-factor authentication code.\") {\n super(\"identity/2fa-invalid\", message, 401);\n }\n}\n\n/**\n * Raised during login when the user has 2FA enabled and the TOTP challenge has not yet\n * been completed. HTTP 403 signals \"authenticated but additional factor required\".\n */\nexport class TwoFactorRequiredError extends IdentityError {\n constructor() {\n super(\n \"identity/2fa-required\",\n \"Two-factor authentication is required to complete sign-in.\",\n 403,\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Deps\n// ---------------------------------------------------------------------------\n\nexport interface TwoFactorDeps extends SessionDeps {\n audit: AuditLogger;\n}\n\n// ---------------------------------------------------------------------------\n// Setup\n// ---------------------------------------------------------------------------\n\n/**\n * Begins the 2FA setup flow. Generates a new TOTP secret (stored as pending, not yet\n * active) and returns the secret + QR URI for display in an authenticator app.\n *\n * The caller must then call {@link enable2FA} with a valid TOTP code to activate 2FA.\n */\nexport async function setup2FA(\n deps: TwoFactorDeps,\n userId: string,\n): Promise<TotpSetupResult> {\n const { config, store, audit } = deps;\n\n const user = await store.findUserById(userId);\n if (!user) throw new TwoFactorError(\"User not found.\");\n\n const secretBase32 = generateTotpSecret();\n const now = config.now();\n\n await store.upsertTotpSecret?.({\n userId,\n secretBase32,\n enabledAt: null, // pending confirmation\n createdAt: now,\n });\n\n await audit.record(\"user.2fa.setup.started\", userId, {});\n\n return {\n secret: secretBase32,\n otpAuthUri: buildOtpAuthUri({\n secret: secretBase32,\n accountName: user.email,\n issuer: config.issuer,\n }),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Enable\n// ---------------------------------------------------------------------------\n\n/**\n * Confirms and enables 2FA. A valid TOTP code from the authenticator app is required.\n * Idempotent when 2FA is already enabled for the user.\n */\nexport async function enable2FA(\n deps: TwoFactorDeps,\n userId: string,\n totpCode: string,\n): Promise<void> {\n const { config, store, audit } = deps;\n\n const record = await store.findTotpSecret?.(userId);\n if (!record) {\n throw new TwoFactorError(\n \"2FA setup has not been started. Call setup2FA first.\",\n );\n }\n if (record.enabledAt) return; // already enabled — idempotent\n\n if (!verifyTotp(totpCode, record.secretBase32, config.now())) {\n throw new TwoFactorError();\n }\n\n await store.upsertTotpSecret?.({ ...record, enabledAt: config.now() });\n await store.updateUser(userId, { twoFactorEnabled: true });\n\n await audit.record(\"user.2fa.enabled\", userId, {});\n}\n\n// ---------------------------------------------------------------------------\n// Disable\n// ---------------------------------------------------------------------------\n\n/**\n * Disables 2FA for the user. Requires a valid TOTP code as confirmation.\n * After disabling, login flows no longer issue MFA challenges for the account.\n */\nexport async function disable2FA(\n deps: TwoFactorDeps,\n userId: string,\n totpCode: string,\n): Promise<void> {\n const { config, store, audit } = deps;\n\n const record = await store.findTotpSecret?.(userId);\n if (!record || !record.enabledAt) {\n throw new TwoFactorError(\"2FA is not enabled for this account.\");\n }\n\n if (!verifyTotp(totpCode, record.secretBase32, config.now())) {\n throw new TwoFactorError();\n }\n\n await store.deleteTotpSecret?.(userId);\n await store.updateUser(userId, { twoFactorEnabled: false });\n\n await audit.record(\"user.2fa.disabled\", userId, {});\n}\n\n// ---------------------------------------------------------------------------\n// MFA challenge — issued during login when 2FA is active\n// ---------------------------------------------------------------------------\n\nconst MFA_CHALLENGE_TTL_SECONDS = 5 * 60; // 5 minutes\nconst MFA_CHALLENGE_TYPE = \"mfa_challenge\";\n\n/**\n * Issues a short-lived signed JWT that encodes a pending MFA session.\n * The client must present this token together with a TOTP code to complete login.\n */\nexport async function issueMfaChallengeToken(\n config: IdentityConfig,\n userId: string,\n): Promise<string> {\n const secret = new TextEncoder().encode(config.accessTokenSecret);\n const now = Math.floor(config.now().getTime() / 1000);\n return new SignJWT({ type: MFA_CHALLENGE_TYPE })\n .setProtectedHeader({ alg: \"HS256\" })\n .setSubject(userId)\n .setIssuedAt(now)\n .setExpirationTime(now + MFA_CHALLENGE_TTL_SECONDS)\n .sign(secret);\n}\n\n// ---------------------------------------------------------------------------\n// Complete MFA challenge\n// ---------------------------------------------------------------------------\n\n/**\n * Verifies an MFA challenge token and TOTP code, then issues a full auth session.\n *\n * Throws {@link InvalidTokenError} for an expired/invalid challenge token and\n * {@link TwoFactorError} for a wrong TOTP code.\n */\nexport async function verifyMfaChallenge(\n deps: TwoFactorDeps,\n mfaToken: string,\n totpCode: string,\n): Promise<AuthResult> {\n const { config, store, audit } = deps;\n const secret = new TextEncoder().encode(config.accessTokenSecret);\n\n let userId: string;\n try {\n const { payload } = await jwtVerify(mfaToken, secret);\n if (\n payload[\"type\"] !== MFA_CHALLENGE_TYPE ||\n typeof payload.sub !== \"string\"\n ) {\n throw new InvalidTokenError(\"Invalid MFA challenge token.\");\n }\n userId = payload.sub;\n } catch {\n throw new InvalidTokenError(\"MFA challenge token is invalid or expired.\");\n }\n\n const record = await store.findTotpSecret?.(userId);\n if (!record || !record.enabledAt) {\n throw new TwoFactorError(\"2FA is not configured for this account.\");\n }\n\n if (!verifyTotp(totpCode, record.secretBase32, config.now())) {\n await audit.record(\"user.login.failed\", userId, { reason: \"invalid_totp\" });\n throw new TwoFactorError();\n }\n\n const user = await store.findUserById(userId);\n if (!user) throw new InvalidTokenError();\n\n await audit.record(\"user.login.succeeded\", userId, { via: \"mfa\" });\n return issueSession(deps, user);\n}\n","import type { AuditLogger } from \"./audit\";\nimport { AccountUnavailableError, InvalidCredentialsError } from \"./errors\";\nimport { checkLockout, clearLockout, recordFailedAttempt } from \"./lockout\";\nimport type { PasswordHasher } from \"./password-hasher\";\nimport { normalizeEmail } from \"./register\";\nimport { issueSession, type SessionDeps } from \"./session-state\";\nimport { issueMfaChallengeToken } from \"./two-factor\";\nimport type { LoginResult } from \"./types\";\n\n/** Dependencies for the login flow. */\nexport interface LoginDeps extends SessionDeps {\n hasher: PasswordHasher;\n audit: AuditLogger;\n}\n\n/** Input for {@link loginUser}. */\nexport interface LoginInput {\n email: string;\n password: string;\n}\n\n/**\n * Authenticates a user with email + password. Uses a single generic failure for both\n * \"unknown user\" and \"wrong password\" to avoid account enumeration, and performs a hash\n * comparison even when the user is unknown to reduce timing side-channels.\n *\n * When account lockout is enabled (config.lockout.maxFailedAttempts > 0), consecutive\n * failures increment a counter and temporarily lock the account.\n */\nexport async function loginUser(\n deps: LoginDeps,\n input: LoginInput,\n): Promise<LoginResult> {\n const { config, store, hasher, audit } = deps;\n const email = normalizeEmail(input.email);\n\n const user = await store.findUserByEmail(email);\n const credential = user ? await store.findCredential(user.userId) : null;\n\n const storedHash =\n credential?.passwordHash ??\n // Dummy hash so verification cost is paid even for unknown users.\n \"scrypt$16384$00$00\";\n const passwordMatches = await hasher.verify(input.password, storedHash);\n\n if (!user || !credential || !passwordMatches) {\n // Record failed attempt for known users (unknown users have no counter to update).\n if (user) {\n await recordFailedAttempt(store, user, config.lockout, config.now);\n if (\n user.failedLoginAttempts + 1 >= config.lockout.maxFailedAttempts &&\n config.lockout.maxFailedAttempts > 0\n ) {\n await audit.record(\"user.account.locked\", user.userId, {\n email,\n reason: \"too_many_failures\",\n });\n }\n }\n await audit.record(\"user.login.failed\", user?.userId ?? null, { email });\n throw new InvalidCredentialsError();\n }\n\n // Check time-based lockout (throws when lockedUntil is in the future).\n checkLockout(user, config.lockout, config.now);\n\n // Check admin-imposed status: disabled (permanent) or locked with no expiry (manual lock).\n if (\n user.status === \"disabled\" ||\n (user.status === \"locked\" && user.lockedUntil === null)\n ) {\n await audit.record(\"user.login.failed\", user.userId, {\n email,\n status: user.status,\n });\n throw new AccountUnavailableError();\n }\n\n // Successful login: clear any stale lockout state.\n await clearLockout(store, user);\n\n // If 2FA is enabled, return an MFA challenge instead of a full session.\n if (user.twoFactorEnabled) {\n await audit.record(\"user.login.mfa.challenged\", user.userId, { email });\n const mfaToken = await issueMfaChallengeToken(config, user.userId);\n return { kind: \"mfa_required\", mfaToken };\n }\n\n await audit.record(\"user.login.succeeded\", user.userId, { email });\n return issueSession(deps, user);\n}\n","import type { AuditLogger } from \"./audit\";\nimport type { IdentityConfig } from \"./config\";\nimport { AccountUnavailableError, InvalidTokenError } from \"./errors\";\nimport type { IdentityStore } from \"./identity-store\";\nimport { hydratePrincipal } from \"./session-state\";\nimport type { TokenService } from \"./token-service\";\nimport type { AuthResult } from \"./types\";\n\n/** Dependencies for the refresh flow. */\nexport interface RefreshDeps {\n config: IdentityConfig;\n store: IdentityStore;\n tokenService: TokenService;\n audit: AuditLogger;\n}\n\n/**\n * Rotates a refresh session: validates the presented refresh token against the stored\n * hash, issues a new access token, and replaces the stored refresh-token hash.\n *\n * Detects refresh-token reuse (a token whose session exists but whose hash no longer\n * matches) and revokes the session as a precaution.\n */\nexport async function refreshSession(\n deps: RefreshDeps,\n presentedToken: string,\n): Promise<AuthResult> {\n const { config, store, tokenService, audit } = deps;\n\n const sessionId = tokenService.parseSessionId(presentedToken);\n if (!sessionId) {\n throw new InvalidTokenError();\n }\n\n const session = await store.findSessionById(sessionId);\n const now = config.now();\n\n if (\n !session ||\n session.revokedAt ||\n session.expiresAt.getTime() <= now.getTime()\n ) {\n throw new InvalidTokenError();\n }\n\n const presentedHash = tokenService.hashRefreshToken(presentedToken);\n if (presentedHash !== session.refreshTokenHash) {\n // Token reuse or tampering — revoke the session defensively.\n await store.revokeSession(sessionId, now);\n await audit.record(\"session.revoked\", session.userId, {\n reason: \"refresh-reuse\",\n });\n throw new InvalidTokenError();\n }\n\n const user = await store.findUserById(session.userId);\n if (!user) {\n throw new InvalidTokenError();\n }\n if (user.status !== \"active\") {\n await store.revokeSession(sessionId, now);\n throw new AccountUnavailableError();\n }\n\n const principal = await hydratePrincipal(store, user);\n const access = await tokenService.issueAccessToken(\n user.userId,\n user.authVersion,\n );\n const refresh = tokenService.createRefreshToken(sessionId);\n const refreshExpiresAt = new Date(\n now.getTime() + config.refreshTokenTtlSeconds * 1000,\n );\n\n await store.rotateSession(sessionId, refresh.hash, refreshExpiresAt);\n await audit.record(\"session.refreshed\", user.userId, {});\n\n return {\n user: principal,\n tokens: {\n accessToken: access.token,\n refreshToken: refresh.token,\n accessTokenExpiresAt: access.expiresAt,\n refreshTokenExpiresAt: refreshExpiresAt,\n },\n };\n}\n\n/** Revokes a single session (logout). */\nexport async function revokeSession(\n deps: Pick<RefreshDeps, \"config\" | \"store\" | \"tokenService\" | \"audit\">,\n presentedToken: string,\n): Promise<void> {\n const { config, store, tokenService, audit } = deps;\n const sessionId = tokenService.parseSessionId(presentedToken);\n if (!sessionId) return;\n const session = await store.findSessionById(sessionId);\n if (!session || session.revokedAt) return;\n await store.revokeSession(sessionId, config.now());\n await audit.record(\"session.revoked\", session.userId, { reason: \"logout\" });\n}\n","import { createHash, randomBytes } from \"node:crypto\";\n\nimport type { VerificationToken, VerificationTokenType } from \"./types\";\n\nexport type { VerificationToken, VerificationTokenType };\n\n/** Input for persisting a new verification token. */\nexport interface CreateVerificationTokenInput {\n /** SHA-256 hash of the raw token (never store the raw token). */\n tokenHash: string;\n userId: string;\n type: VerificationTokenType;\n expiresAt: Date;\n}\n\n/**\n * Generates a cryptographically random opaque token and its SHA-256 hash.\n * Returns both: the raw token is sent to the user; only the hash is persisted.\n */\nexport function generateVerificationToken(): {\n token: string;\n tokenHash: string;\n} {\n const token = randomBytes(32).toString(\"hex\");\n const tokenHash = createHash(\"sha256\").update(token).digest(\"hex\");\n return { token, tokenHash };\n}\n\n/** Hashes a token presented by the user for secure lookup against stored hashes. */\nexport function hashVerificationToken(token: string): string {\n return createHash(\"sha256\").update(token).digest(\"hex\");\n}\n","import type { AuditLogger } from \"./audit\";\nimport type { IdentityConfig } from \"./config\";\nimport { IdentityError } from \"./errors\";\nimport type { IdentityStore } from \"./identity-store\";\nimport type { NotificationService } from \"./notification\";\nimport {\n generateVerificationToken,\n hashVerificationToken,\n} from \"./verification-token\";\n\n/** Default token lifetime for email verification links. */\nconst DEFAULT_TTL_SECONDS = 24 * 60 * 60; // 24 hours\n\n/** Raised when an email verification token is missing, expired, or already used. */\nexport class EmailVerificationError extends IdentityError {\n constructor(message = \"Email verification token is invalid or expired.\") {\n super(\"identity/email-verification-invalid\", message, 400);\n }\n}\n\nexport interface EmailVerificationDeps {\n config: IdentityConfig;\n store: IdentityStore;\n audit: AuditLogger;\n}\n\n/**\n * Generates and persists a verification token, then calls the notification service.\n *\n * Safe to call after registration or when the user explicitly requests a resend.\n * Returns silently when the email is already verified.\n */\nexport async function requestEmailVerification(\n deps: EmailVerificationDeps,\n userId: string,\n): Promise<void> {\n const { config, store, audit } = deps;\n const notifications: NotificationService | undefined = config.notifications;\n\n const user = await store.findUserById(userId);\n if (!user) throw new EmailVerificationError(\"User not found.\");\n if (user.emailVerifiedAt) return; // already verified — no-op\n\n const { token, tokenHash } = generateVerificationToken();\n const now = config.now();\n const expiresAt = new Date(now.getTime() + DEFAULT_TTL_SECONDS * 1000);\n\n await store.createVerificationToken?.({\n tokenHash,\n userId,\n type: \"email_verification\",\n expiresAt,\n });\n\n await notifications?.sendEmailVerification?.({\n email: user.email,\n displayName: user.displayName,\n token,\n });\n\n await audit.record(\"user.email.verification.requested\", userId, {\n email: user.email,\n });\n}\n\n/**\n * Verifies the user's email address using the token from the verification link.\n *\n * Throws {@link EmailVerificationError} for unknown, expired, or already-used tokens.\n */\nexport async function verifyEmail(\n deps: EmailVerificationDeps,\n rawToken: string,\n): Promise<void> {\n const { config, store, audit } = deps;\n\n const tokenHash = hashVerificationToken(rawToken);\n const record = await store.findVerificationToken?.(tokenHash);\n\n if (!record || record.type !== \"email_verification\") {\n throw new EmailVerificationError();\n }\n if (record.usedAt) {\n throw new EmailVerificationError(\n \"This verification link has already been used.\",\n );\n }\n\n const now = config.now();\n if (record.expiresAt <= now) {\n throw new EmailVerificationError(\"This verification link has expired.\");\n }\n\n await store.markVerificationTokenUsed?.(tokenHash, now);\n await store.updateUser(record.userId, { emailVerifiedAt: now });\n\n await audit.record(\"user.email.verified\", record.userId, {});\n}\n","import type { AuditLogger } from \"./audit\";\nimport type { IdentityConfig } from \"./config\";\nimport { IdentityError } from \"./errors\";\nimport type { IdentityStore } from \"./identity-store\";\nimport type { NotificationService } from \"./notification\";\nimport type { PasswordHasher } from \"./password-hasher\";\nimport {\n generateVerificationToken,\n hashVerificationToken,\n} from \"./verification-token\";\nimport { normalizeEmail } from \"./register\";\n\n/** Default token lifetime for password reset links. */\nconst DEFAULT_TTL_SECONDS = 60 * 60; // 1 hour\n\nconst MIN_PASSWORD_LENGTH = 8;\n\n/** Raised when a password reset token is missing, expired, or already used. */\nexport class PasswordResetError extends IdentityError {\n constructor(message = \"Password reset token is invalid or expired.\") {\n super(\"identity/password-reset-invalid\", message, 400);\n }\n}\n\nexport interface PasswordResetDeps {\n config: IdentityConfig;\n store: IdentityStore;\n hasher: PasswordHasher;\n audit: AuditLogger;\n}\n\n/**\n * Generates a password reset token and dispatches the notification.\n *\n * Never reveals whether the email address exists in the system (returns silently for\n * unknown addresses) to prevent account enumeration.\n */\nexport async function requestPasswordReset(\n deps: PasswordResetDeps,\n email: string,\n): Promise<void> {\n const { config, store, notifications, audit } = deps as PasswordResetDeps & {\n notifications?: NotificationService;\n };\n\n const normalized = normalizeEmail(email);\n const user = await store.findUserByEmail(normalized);\n if (!user) return; // silent — do not reveal whether the address exists\n\n const { token, tokenHash } = generateVerificationToken();\n const now = config.now();\n const expiresAt = new Date(now.getTime() + DEFAULT_TTL_SECONDS * 1000);\n\n await store.createVerificationToken?.({\n tokenHash,\n userId: user.userId,\n type: \"password_reset\",\n expiresAt,\n });\n\n const resolvedNotifications: NotificationService | undefined =\n notifications ?? config.notifications;\n\n await resolvedNotifications?.sendPasswordReset?.({\n email: user.email,\n displayName: user.displayName,\n token,\n });\n\n await audit.record(\"user.password.reset.requested\", user.userId, {\n email: user.email,\n });\n}\n\n/**\n * Validates the reset token and sets a new password.\n *\n * On success, bumps `authVersion` and revokes all existing sessions so outstanding\n * refresh tokens are immediately invalidated.\n */\nexport async function resetPassword(\n deps: PasswordResetDeps,\n rawToken: string,\n newPassword: string,\n): Promise<void> {\n const { config, store, hasher, audit } = deps;\n\n if (newPassword.length < MIN_PASSWORD_LENGTH) {\n throw new PasswordResetError(\n `Password must be at least ${MIN_PASSWORD_LENGTH} characters.`,\n );\n }\n\n const tokenHash = hashVerificationToken(rawToken);\n const record = await store.findVerificationToken?.(tokenHash);\n\n if (!record || record.type !== \"password_reset\") {\n throw new PasswordResetError();\n }\n if (record.usedAt) {\n throw new PasswordResetError(\"This reset link has already been used.\");\n }\n\n const now = config.now();\n if (record.expiresAt <= now) {\n throw new PasswordResetError(\"This reset link has expired.\");\n }\n\n await store.markVerificationTokenUsed?.(tokenHash, now);\n\n const passwordHash = await hasher.hash(newPassword);\n await store.upsertCredential({\n userId: record.userId,\n passwordHash,\n updatedAt: now,\n });\n\n // Bump authVersion so all existing access tokens are immediately invalidated.\n const user = await store.findUserById(record.userId);\n if (user) {\n await store.updateUser(record.userId, {\n authVersion: user.authVersion + 1,\n });\n await store.revokeAllSessions(record.userId, now);\n }\n\n await audit.record(\"user.password.reset.completed\", record.userId, {});\n}\n","import type { AuditLogger } from \"./audit\";\nimport type { IdentityConfig } from \"./config\";\nimport { IdentityError } from \"./errors\";\nimport type { IdentityStore } from \"./identity-store\";\n\n/** Raised when the target user does not exist. */\nexport class AccountAdminError extends IdentityError {\n constructor(message: string) {\n super(\"identity/account-not-found\", message, 404);\n }\n}\n\nexport interface AccountAdminDeps {\n config: IdentityConfig;\n store: IdentityStore;\n audit: AuditLogger;\n}\n\n/**\n * Locks a user account. Sets `status` to `\"locked\"` and immediately revokes all active\n * sessions so existing refresh tokens stop working.\n *\n * Typically called by an admin. The `AccountUnavailableError` is thrown during any\n * subsequent login or token verification attempt for the account.\n */\nexport async function lockAccount(\n deps: AccountAdminDeps,\n userId: string,\n): Promise<void> {\n const { config, store, audit } = deps;\n\n const user = await store.findUserById(userId);\n if (!user) throw new AccountAdminError(`User ${userId} not found.`);\n\n const now = config.now();\n await store.updateUser(userId, { status: \"locked\" });\n await store.revokeAllSessions(userId, now);\n\n await audit.record(\"user.account.locked\", userId, { reason: \"admin\" });\n}\n\n/**\n * Unlocks a user account. Resets `status` to `\"active\"` and clears any stale lockout\n * counters so the user can log in immediately.\n */\nexport async function unlockAccount(\n deps: AccountAdminDeps,\n userId: string,\n): Promise<void> {\n const { config, store, audit } = deps;\n\n const user = await store.findUserById(userId);\n if (!user) throw new AccountAdminError(`User ${userId} not found.`);\n\n await store.updateUser(userId, {\n status: \"active\",\n failedLoginAttempts: 0,\n lockedUntil: null,\n });\n\n await audit.record(\"user.account.unlocked\", userId, {});\n}\n\n/**\n * Disables a user account. Sets `status` to `\"disabled\"` and revokes all sessions.\n * Unlike `lockAccount`, disabled accounts are not automatically unlockable by the\n * lockout timer — an explicit call to `unlockAccount` is required.\n */\nexport async function disableAccount(\n deps: AccountAdminDeps,\n userId: string,\n): Promise<void> {\n const { config, store, audit } = deps;\n\n const user = await store.findUserById(userId);\n if (!user) throw new AccountAdminError(`User ${userId} not found.`);\n\n const now = config.now();\n await store.updateUser(userId, { status: \"disabled\" });\n await store.revokeAllSessions(userId, now);\n\n await audit.record(\"user.account.locked\", userId, {\n reason: \"admin_disabled\",\n });\n}\n","import { createAuditLogger } from \"./audit\";\nimport {\n type AuthorizationContext,\n type AuthorizationDecision,\n type AuthorizationRequirement,\n evaluateAuthorization,\n} from \"./authorization\";\nimport { type IdentityConfigInput, resolveIdentityConfig } from \"./config\";\nimport { AccountUnavailableError, InvalidTokenError } from \"./errors\";\nimport {\n requestEmailVerification,\n verifyEmail,\n type EmailVerificationDeps,\n} from \"./email-verification\";\nimport type { IdentityStore } from \"./identity-store\";\nimport { type LoginInput, loginUser } from \"./login\";\nimport { createOAuthService, type OAuthService } from \"./oauth/oauth-service\";\nimport type { OAuthProvider } from \"./oauth/oauth-provider\";\nimport { createPasswordHasher } from \"./password-hasher\";\nimport { requestPasswordReset, resetPassword } from \"./password-reset\";\nimport { type RegisterInput, registerUser } from \"./register\";\nimport { refreshSession, revokeSession } from \"./refresh-service\";\nimport { hydratePrincipal } from \"./session-state\";\nimport { createTokenService } from \"./token-service\";\nimport {\n disable2FA,\n enable2FA,\n setup2FA,\n verifyMfaChallenge,\n type TwoFactorDeps,\n} from \"./two-factor\";\nimport { lockAccount, unlockAccount, disableAccount } from \"./account-admin\";\nimport type {\n AuthResult,\n AuthenticatedIdentity,\n LoginResult,\n TotpSetupResult,\n} from \"./types\";\n\n/** The wired runtime API returned by {@link createIdentityService}. */\nexport interface IdentityService {\n register(input: RegisterInput): Promise<AuthResult>;\n /**\n * Authenticates with email + password. Returns a full {@link AuthResult} normally, or\n * an {@link MfaRequiredResult} (containing an `mfaToken`) when the account has 2FA\n * enabled. In the latter case call {@link verifyMfaChallenge} to complete the login.\n */\n login(input: LoginInput): Promise<LoginResult>;\n refresh(refreshToken: string): Promise<AuthResult>;\n logout(refreshToken: string): Promise<void>;\n /**\n * Verifies an access token and hydrates the current principal from the store.\n * Throws {@link InvalidTokenError} for invalid/stale tokens.\n */\n authenticate(accessToken: string): Promise<AuthenticatedIdentity>;\n authorize<TResource = unknown>(\n requirement: AuthorizationRequirement<TResource>,\n context: AuthorizationContext<TResource>,\n ): Promise<AuthorizationDecision>;\n\n // --- Email verification --------------------------------------------------\n /** Sends (or resends) an email verification link for the given user id. */\n requestEmailVerification(userId: string): Promise<void>;\n /** Marks the user's email as verified using the token from the verification link. */\n verifyEmail(token: string): Promise<void>;\n\n // --- Password reset -------------------------------------------------------\n /** Sends a password reset link to the given email address. Silent for unknown addresses. */\n requestPasswordReset(email: string): Promise<void>;\n /** Resets the password using the token from the reset link. */\n resetPassword(token: string, newPassword: string): Promise<void>;\n\n // --- Two-factor authentication -------------------------------------------\n /**\n * Begins the 2FA setup flow. Returns the TOTP secret and an OTP Auth URI for QR code\n * display. Call {@link enable2FA} with a valid code to activate.\n */\n setup2FA(userId: string): Promise<TotpSetupResult>;\n /** Confirms and enables 2FA. Requires a valid TOTP code from the authenticator app. */\n enable2FA(userId: string, totpCode: string): Promise<void>;\n /** Disables 2FA. Requires a valid TOTP code as confirmation. */\n disable2FA(userId: string, totpCode: string): Promise<void>;\n /**\n * Completes an MFA challenge. Present the `mfaToken` returned by `login` together\n * with the TOTP code from the authenticator app.\n */\n verifyMfaChallenge(mfaToken: string, totpCode: string): Promise<AuthResult>;\n\n // --- Account management (admin) ------------------------------------------\n /** Locks the account and revokes all active sessions. */\n lockAccount(userId: string): Promise<void>;\n /** Unlocks the account and clears lockout state. */\n unlockAccount(userId: string): Promise<void>;\n /** Permanently disables the account (requires manual admin unlock). */\n disableAccount(userId: string): Promise<void>;\n\n /**\n * The OAuth 2.0 / OpenID Connect surface. Present only when at least one provider is\n * configured via `IdentityConfigInput.oauth.providers`.\n */\n oauth?: OAuthService;\n}\n\n/** Extended config input with optional OAuth provider list. */\nexport interface IdentityServiceConfig extends IdentityConfigInput {\n /** Optional OAuth 2.0 / OIDC configuration. */\n oauth?: {\n /**\n * The list of external authentication providers to enable. Each item is an\n * {@link OAuthProvider} instance, e.g. `createGoogleOAuthProvider(...)` or\n * `createMicrosoftOAuthProvider(...)`.\n */\n providers: OAuthProvider[];\n };\n}\n\n/**\n * Wires the framework-agnostic identity runtime with Node defaults (scrypt hashing,\n * `jose` HS256 tokens). Express, NestJS, and Next.js adapters build on top of this service.\n */\nexport function createIdentityService(\n configInput: IdentityServiceConfig,\n store: IdentityStore,\n): IdentityService {\n const config = resolveIdentityConfig(configInput);\n const tokenService = createTokenService(config);\n const hasher = createPasswordHasher(config.passwordScryptCost);\n const audit = createAuditLogger(store, config.now, config.onEvent);\n\n const deps = { config, store, tokenService, hasher, audit };\n const mfaDeps: TwoFactorDeps = deps;\n const emailDeps: EmailVerificationDeps = { config, store, audit };\n\n const oauthProviders = configInput.oauth?.providers;\n const oauthService: OAuthService | undefined =\n oauthProviders && oauthProviders.length > 0\n ? createOAuthService({\n providers: oauthProviders,\n config,\n store,\n sessionDeps: deps,\n audit,\n })\n : undefined;\n\n return {\n register: (input) => registerUser(deps, input),\n login: (input) => loginUser(deps, input),\n refresh: (refreshToken) => refreshSession(deps, refreshToken),\n logout: (refreshToken) => revokeSession(deps, refreshToken),\n\n async authenticate(accessToken) {\n const claims = await tokenService.verifyAccessToken(accessToken);\n const user = await store.findUserById(claims.sub);\n if (!user || user.authVersion !== claims.authVersion) {\n throw new InvalidTokenError();\n }\n if (user.status !== \"active\") {\n throw new AccountUnavailableError();\n }\n return hydratePrincipal(store, user);\n },\n\n authorize: (requirement, context) =>\n evaluateAuthorization(requirement, context),\n\n // Email verification\n requestEmailVerification: (userId) =>\n requestEmailVerification(emailDeps, userId),\n verifyEmail: (token) => verifyEmail(emailDeps, token),\n\n // Password reset\n requestPasswordReset: (email) => requestPasswordReset(deps, email),\n resetPassword: (token, newPassword) =>\n resetPassword(deps, token, newPassword),\n\n // Two-factor authentication\n setup2FA: (userId) => setup2FA(mfaDeps, userId),\n enable2FA: (userId, code) => enable2FA(mfaDeps, userId, code),\n disable2FA: (userId, code) => disable2FA(mfaDeps, userId, code),\n verifyMfaChallenge: (mfaToken, code) =>\n verifyMfaChallenge(mfaDeps, mfaToken, code),\n\n // Account management\n lockAccount: (userId) => lockAccount(deps, userId),\n unlockAccount: (userId) => unlockAccount(deps, userId),\n disableAccount: (userId) => disableAccount(deps, userId),\n\n oauth: oauthService,\n };\n}\n","/**\n * Google OAuth 2.0 / OpenID Connect provider for `@azlib/identity`.\n *\n * Uses the Google OAuth2 v2 endpoints and calls the userinfo endpoint to retrieve user\n * details. Requires a project in Google Cloud Console with an OAuth 2.0 client credential.\n *\n * @example\n * ```ts\n * import { createGoogleOAuthProvider } from \"@azlib/identity/node\";\n *\n * const google = createGoogleOAuthProvider({\n * clientId: process.env.GOOGLE_CLIENT_ID!,\n * clientSecret: process.env.GOOGLE_CLIENT_SECRET!,\n * });\n * ```\n */\n\nimport type {\n BuildAuthUrlParams,\n ExchangeCodeParams,\n OAuthProvider,\n OAuthTokens,\n OAuthUserInfo,\n} from \"../oauth-provider\";\n\nconst GOOGLE_AUTH_URL = \"https://accounts.google.com/o/oauth2/v2/auth\";\nconst GOOGLE_TOKEN_URL = \"https://oauth2.googleapis.com/token\";\nconst GOOGLE_USERINFO_URL = \"https://www.googleapis.com/oauth2/v3/userinfo\";\n\nconst DEFAULT_SCOPES = [\"openid\", \"email\", \"profile\"];\n\n/** Configuration for {@link createGoogleOAuthProvider}. */\nexport interface GoogleOAuthConfig {\n /** OAuth 2.0 Client ID from Google Cloud Console. */\n clientId: string;\n /**\n * OAuth 2.0 Client Secret. Must be kept server-side — never expose in client code or\n * commit to source control.\n */\n clientSecret: string;\n /**\n * Override the default OpenID Connect scopes. Defaults to `[\"openid\", \"email\", \"profile\"]`.\n */\n scopes?: string[];\n}\n\ninterface GoogleTokenResponse {\n access_token: string;\n id_token?: string;\n token_type: string;\n expires_in: number;\n refresh_token?: string;\n scope?: string;\n error?: string;\n error_description?: string;\n}\n\ninterface GoogleUserInfoResponse {\n sub: string;\n email?: string;\n email_verified?: boolean;\n name?: string;\n error?: { message?: string };\n}\n\n/**\n * Creates an {@link OAuthProvider} configured for Google OAuth 2.0 / OpenID Connect.\n *\n * Security notes:\n * - The client secret is used only during server-side code exchange.\n * - The userinfo endpoint is called with the access token to fetch user data, providing\n * a verified source of truth over the network (HTTPS).\n * - The `state` parameter is required and must be verified by the caller to prevent CSRF.\n */\nexport function createGoogleOAuthProvider(\n config: GoogleOAuthConfig,\n): OAuthProvider {\n const defaultScopes = config.scopes ?? DEFAULT_SCOPES;\n\n return {\n name: \"google\",\n\n buildAuthorizationUrl({\n redirectUri,\n state,\n scopes,\n }: BuildAuthUrlParams): string {\n const url = new URL(GOOGLE_AUTH_URL);\n url.searchParams.set(\"client_id\", config.clientId);\n url.searchParams.set(\"redirect_uri\", redirectUri);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"scope\", (scopes ?? defaultScopes).join(\" \"));\n url.searchParams.set(\"state\", state);\n // Request offline access to get a refresh token.\n url.searchParams.set(\"access_type\", \"offline\");\n return url.toString();\n },\n\n async exchangeCode({\n code,\n redirectUri,\n }: ExchangeCodeParams): Promise<OAuthTokens> {\n const body = new URLSearchParams({\n code,\n client_id: config.clientId,\n client_secret: config.clientSecret,\n redirect_uri: redirectUri,\n grant_type: \"authorization_code\",\n });\n\n const response = await fetch(GOOGLE_TOKEN_URL, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n });\n\n const data = (await response.json()) as GoogleTokenResponse;\n if (!response.ok || data.error) {\n throw new Error(\n `Google token exchange failed: ${data.error_description ?? data.error ?? response.status}`,\n );\n }\n\n return {\n accessToken: data.access_token,\n idToken: data.id_token,\n tokenType: data.token_type,\n expiresIn: data.expires_in,\n refreshToken: data.refresh_token,\n scope: data.scope,\n };\n },\n\n async fetchUserInfo(tokens: OAuthTokens): Promise<OAuthUserInfo> {\n const response = await fetch(GOOGLE_USERINFO_URL, {\n headers: { authorization: `Bearer ${tokens.accessToken}` },\n });\n\n const data = (await response.json()) as GoogleUserInfoResponse;\n if (!response.ok) {\n throw new Error(\n `Google userinfo request failed: ${data.error?.message ?? response.status}`,\n );\n }\n\n return {\n providerUserId: data.sub,\n email: data.email ?? null,\n emailVerified: data.email_verified === true,\n displayName: data.name ?? null,\n };\n },\n };\n}\n","/**\n * Microsoft Entra ID (Azure AD) OAuth 2.0 / OpenID Connect provider for `@azlib/identity`.\n *\n * Uses the Microsoft identity platform v2.0 endpoints and calls the Microsoft Graph\n * `/v1.0/me` endpoint to retrieve user details.\n *\n * @example\n * ```ts\n * import { createMicrosoftOAuthProvider } from \"@azlib/identity/node\";\n *\n * const microsoft = createMicrosoftOAuthProvider({\n * clientId: process.env.MICROSOFT_CLIENT_ID!,\n * clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,\n * // tenantId defaults to \"common\" (all accounts). Set to your tenant id for\n * // single-tenant apps, or \"organizations\" / \"consumers\" to restrict account types.\n * tenantId: process.env.MICROSOFT_TENANT_ID,\n * });\n * ```\n */\n\nimport type {\n BuildAuthUrlParams,\n ExchangeCodeParams,\n OAuthProvider,\n OAuthTokens,\n OAuthUserInfo,\n} from \"../oauth-provider\";\n\nconst DEFAULT_SCOPES = [\"openid\", \"email\", \"profile\", \"User.Read\"];\n\n/** Configuration for {@link createMicrosoftOAuthProvider}. */\nexport interface MicrosoftOAuthConfig {\n /** Application (client) ID from the Azure / Entra app registration. */\n clientId: string;\n /**\n * Client secret from the Azure / Entra app registration. Must be kept server-side —\n * never expose in client code or commit to source control.\n */\n clientSecret: string;\n /**\n * Tenant identifier. Use `\"common\"` (default) to accept personal and work/school\n * accounts, `\"organizations\"` for work/school only, `\"consumers\"` for personal\n * accounts only, or your specific tenant GUID / domain.\n */\n tenantId?: string;\n /**\n * Override the default Microsoft Graph scopes. Defaults to\n * `[\"openid\", \"email\", \"profile\", \"User.Read\"]`.\n */\n scopes?: string[];\n}\n\ninterface MicrosoftTokenResponse {\n access_token: string;\n id_token?: string;\n token_type: string;\n expires_in: number;\n refresh_token?: string;\n scope?: string;\n error?: string;\n error_description?: string;\n}\n\ninterface MicrosoftGraphMeResponse {\n id: string;\n mail?: string;\n userPrincipalName?: string;\n displayName?: string;\n error?: { message?: string };\n}\n\n/**\n * Creates an {@link OAuthProvider} configured for Microsoft Entra ID OAuth 2.0 / OIDC.\n *\n * Security notes:\n * - The client secret is used only during server-side code exchange.\n * - User data is fetched from the Microsoft Graph API using the access token, providing\n * a verified source of truth over HTTPS.\n * - The `state` parameter is required and must be verified by the caller to prevent CSRF.\n */\nexport function createMicrosoftOAuthProvider(\n config: MicrosoftOAuthConfig,\n): OAuthProvider {\n const tenantId = config.tenantId ?? \"common\";\n const defaultScopes = config.scopes ?? DEFAULT_SCOPES;\n\n const authBase = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0`;\n\n return {\n name: \"microsoft\",\n\n buildAuthorizationUrl({\n redirectUri,\n state,\n scopes,\n }: BuildAuthUrlParams): string {\n const url = new URL(`${authBase}/authorize`);\n url.searchParams.set(\"client_id\", config.clientId);\n url.searchParams.set(\"redirect_uri\", redirectUri);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"scope\", (scopes ?? defaultScopes).join(\" \"));\n url.searchParams.set(\"state\", state);\n url.searchParams.set(\"response_mode\", \"query\");\n return url.toString();\n },\n\n async exchangeCode({\n code,\n redirectUri,\n }: ExchangeCodeParams): Promise<OAuthTokens> {\n const body = new URLSearchParams({\n code,\n client_id: config.clientId,\n client_secret: config.clientSecret,\n redirect_uri: redirectUri,\n grant_type: \"authorization_code\",\n scope: defaultScopes.join(\" \"),\n });\n\n const response = await fetch(`${authBase}/token`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n });\n\n const data = (await response.json()) as MicrosoftTokenResponse;\n if (!response.ok || data.error) {\n throw new Error(\n `Microsoft token exchange failed: ${data.error_description ?? data.error ?? response.status}`,\n );\n }\n\n return {\n accessToken: data.access_token,\n idToken: data.id_token,\n tokenType: data.token_type,\n expiresIn: data.expires_in,\n refreshToken: data.refresh_token,\n scope: data.scope,\n };\n },\n\n async fetchUserInfo(tokens: OAuthTokens): Promise<OAuthUserInfo> {\n const response = await fetch(\"https://graph.microsoft.com/v1.0/me\", {\n headers: { authorization: `Bearer ${tokens.accessToken}` },\n });\n\n const data = (await response.json()) as MicrosoftGraphMeResponse;\n if (!response.ok) {\n throw new Error(\n `Microsoft Graph /me request failed: ${data.error?.message ?? response.status}`,\n );\n }\n\n // Use `mail` (for work accounts) or `userPrincipalName` as email fallback.\n const email = data.mail ?? data.userPrincipalName ?? null;\n\n return {\n providerUserId: data.id,\n email,\n // Microsoft does not expose email_verified via Graph; treat work/school accounts\n // as verified since Entra manages the identity.\n emailVerified: true,\n displayName: data.displayName ?? null,\n };\n },\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"test-utils.mjs","names":[],"sources":["../test-utils.ts"],"sourcesContent":["import type {\n CreateAuthSessionInput,\n CreateIdentityUserInput,\n IdentityStore,\n} from \"./core/identity-store\";\nimport type { CreateVerificationTokenInput } from \"./core/verification-token\";\nimport type {\n AuthSession,\n CredentialRecord,\n DirectPermissionGrant,\n IdentityEvent,\n IdentityUser,\n OAuthLinkedAccount,\n Permission,\n Role,\n RolePermissionGrant,\n TotpSecret,\n UserRoleAssignment,\n VerificationToken,\n} from \"./core/types\";\n\n/** An in-memory {@link IdentityStore} for tests and examples. Not for production use. */\nexport interface InMemoryIdentityStore extends IdentityStore {\n /** All recorded audit events (for assertions). */\n readonly events: readonly IdentityEvent[];\n}\n\nexport function createInMemoryIdentityStore(): InMemoryIdentityStore {\n const users = new Map<string, IdentityUser>();\n const usersByEmail = new Map<string, string>();\n const credentials = new Map<string, CredentialRecord>();\n const sessions = new Map<string, AuthSession>();\n const roles = new Map<string, Role>();\n const userRoles = new Set<string>();\n const rolePermissions = new Set<string>();\n const userPermissions = new Set<string>();\n const events: IdentityEvent[] = [];\n const oauthLinks = new Map<string, OAuthLinkedAccount>(); // key: `provider::providerUserId`\n const oauthLinksByUser = new Map<string, OAuthLinkedAccount[]>(); // key: userId\n const verificationTokens = new Map<string, VerificationToken>(); // key: tokenHash\n const totpSecrets = new Map<string, TotpSecret>(); // key: userId\n\n const key = (a: string, b: string): string => `${a}::${b}`;\n\n return {\n events,\n\n async findUserByEmail(email) {\n const id = usersByEmail.get(email.toLowerCase());\n return id ? (users.get(id) ?? null) : null;\n },\n\n async findUserById(userId) {\n return users.get(userId) ?? null;\n },\n\n async createUser(input: CreateIdentityUserInput) {\n const user: IdentityUser = {\n ...input,\n twoFactorEnabled: false,\n emailVerifiedAt: null,\n failedLoginAttempts: 0,\n lockedUntil: null,\n };\n users.set(user.userId, user);\n usersByEmail.set(user.email.toLowerCase(), user.userId);\n return user;\n },\n\n async updateUser(userId, patch) {\n const existing = users.get(userId);\n if (!existing) throw new Error(`Unknown user ${userId}`);\n const updated: IdentityUser = { ...existing, ...patch };\n users.set(userId, updated);\n return updated;\n },\n\n async findCredential(userId) {\n return credentials.get(userId) ?? null;\n },\n\n async upsertCredential(record) {\n credentials.set(record.userId, record);\n },\n\n async createSession(input: CreateAuthSessionInput) {\n const session: AuthSession = { ...input, revokedAt: null };\n sessions.set(session.sessionId, session);\n return session;\n },\n\n async findSessionById(sessionId) {\n return sessions.get(sessionId) ?? null;\n },\n\n async revokeSession(sessionId, revokedAt) {\n const session = sessions.get(sessionId);\n if (session) sessions.set(sessionId, { ...session, revokedAt });\n },\n\n async rotateSession(sessionId, refreshTokenHash, expiresAt) {\n const session = sessions.get(sessionId);\n if (session) sessions.set(sessionId, { ...session, refreshTokenHash, expiresAt });\n },\n\n async revokeAllSessions(userId, revokedAt) {\n for (const [id, session] of sessions) {\n if (session.userId === userId && !session.revokedAt) {\n sessions.set(id, { ...session, revokedAt });\n }\n }\n },\n\n async listRolesForUser(userId) {\n const result: Role[] = [];\n for (const role of roles.values()) {\n if (userRoles.has(key(userId, role.roleId))) result.push(role);\n }\n return result;\n },\n\n async listPermissionsForUser(userId) {\n const result = new Set<Permission>();\n for (const entry of userPermissions) {\n const [uid, permission] = entry.split(\"::\");\n if (uid === userId && permission) result.add(permission);\n }\n for (const role of roles.values()) {\n if (!userRoles.has(key(userId, role.roleId))) continue;\n for (const entry of rolePermissions) {\n const [rid, permission] = entry.split(\"::\");\n if (rid === role.roleId && permission) result.add(permission);\n }\n }\n return [...result];\n },\n\n async assignRole(assignment: UserRoleAssignment) {\n if (!roles.has(assignment.roleId)) {\n roles.set(assignment.roleId, {\n roleId: assignment.roleId,\n name: assignment.roleId,\n description: null,\n });\n }\n userRoles.add(key(assignment.userId, assignment.roleId));\n },\n\n async grantRolePermission(grant: RolePermissionGrant) {\n if (!roles.has(grant.roleId)) {\n roles.set(grant.roleId, { roleId: grant.roleId, name: grant.roleId, description: null });\n }\n rolePermissions.add(key(grant.roleId, grant.permission));\n },\n\n async grantDirectPermission(grant: DirectPermissionGrant) {\n userPermissions.add(key(grant.userId, grant.permission));\n },\n\n async recordEvent(event: IdentityEvent) {\n events.push(event);\n },\n\n async findUserByOAuthId(provider: string, providerUserId: string) {\n const link = oauthLinks.get(key(provider, providerUserId));\n return link ? (users.get(link.userId) ?? null) : null;\n },\n\n async createOAuthLink(link: OAuthLinkedAccount) {\n oauthLinks.set(key(link.provider, link.providerUserId), link);\n const existing = oauthLinksByUser.get(link.userId) ?? [];\n oauthLinksByUser.set(link.userId, [...existing, link]);\n },\n\n async listOAuthLinks(userId: string) {\n return oauthLinksByUser.get(userId) ?? [];\n },\n\n // --- Verification tokens ------------------------------------------------\n async createVerificationToken(input: CreateVerificationTokenInput) {\n verificationTokens.set(input.tokenHash, { ...input, usedAt: null });\n },\n\n async findVerificationToken(tokenHash: string) {\n return verificationTokens.get(tokenHash) ?? null;\n },\n\n async markVerificationTokenUsed(tokenHash: string, usedAt: Date) {\n const existing = verificationTokens.get(tokenHash);\n if (existing) verificationTokens.set(tokenHash, { ...existing, usedAt });\n },\n\n // --- TOTP secrets -------------------------------------------------------\n async findTotpSecret(userId: string) {\n return totpSecrets.get(userId) ?? null;\n },\n\n async upsertTotpSecret(record: TotpSecret) {\n totpSecrets.set(record.userId, record);\n },\n\n async deleteTotpSecret(userId: string) {\n totpSecrets.delete(userId);\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"test-utils.mjs","names":[],"sources":["../test-utils.ts"],"sourcesContent":["import type {\n CreateAuthSessionInput,\n CreateIdentityUserInput,\n IdentityStore,\n} from \"./core/identity-store\";\nimport type { CreateVerificationTokenInput } from \"./core/verification-token\";\nimport type {\n AuthSession,\n CredentialRecord,\n DirectPermissionGrant,\n IdentityEvent,\n IdentityUser,\n OAuthLinkedAccount,\n Permission,\n Role,\n RolePermissionGrant,\n TotpSecret,\n UserRoleAssignment,\n VerificationToken,\n} from \"./core/types\";\n\n/** An in-memory {@link IdentityStore} for tests and examples. Not for production use. */\nexport interface InMemoryIdentityStore extends IdentityStore {\n /** All recorded audit events (for assertions). */\n readonly events: readonly IdentityEvent[];\n}\n\nexport function createInMemoryIdentityStore(): InMemoryIdentityStore {\n const users = new Map<string, IdentityUser>();\n const usersByEmail = new Map<string, string>();\n const credentials = new Map<string, CredentialRecord>();\n const sessions = new Map<string, AuthSession>();\n const roles = new Map<string, Role>();\n const userRoles = new Set<string>();\n const rolePermissions = new Set<string>();\n const userPermissions = new Set<string>();\n const events: IdentityEvent[] = [];\n const oauthLinks = new Map<string, OAuthLinkedAccount>(); // key: `provider::providerUserId`\n const oauthLinksByUser = new Map<string, OAuthLinkedAccount[]>(); // key: userId\n const verificationTokens = new Map<string, VerificationToken>(); // key: tokenHash\n const totpSecrets = new Map<string, TotpSecret>(); // key: userId\n\n const key = (a: string, b: string): string => `${a}::${b}`;\n\n return {\n events,\n\n async findUserByEmail(email) {\n const id = usersByEmail.get(email.toLowerCase());\n return id ? (users.get(id) ?? null) : null;\n },\n\n async findUserById(userId) {\n return users.get(userId) ?? null;\n },\n\n async createUser(input: CreateIdentityUserInput) {\n const user: IdentityUser = {\n ...input,\n twoFactorEnabled: false,\n emailVerifiedAt: null,\n failedLoginAttempts: 0,\n lockedUntil: null,\n };\n users.set(user.userId, user);\n usersByEmail.set(user.email.toLowerCase(), user.userId);\n return user;\n },\n\n async updateUser(userId, patch) {\n const existing = users.get(userId);\n if (!existing) throw new Error(`Unknown user ${userId}`);\n const updated: IdentityUser = { ...existing, ...patch };\n users.set(userId, updated);\n return updated;\n },\n\n async findCredential(userId) {\n return credentials.get(userId) ?? null;\n },\n\n async upsertCredential(record) {\n credentials.set(record.userId, record);\n },\n\n async createSession(input: CreateAuthSessionInput) {\n const session: AuthSession = { ...input, revokedAt: null };\n sessions.set(session.sessionId, session);\n return session;\n },\n\n async findSessionById(sessionId) {\n return sessions.get(sessionId) ?? null;\n },\n\n async revokeSession(sessionId, revokedAt) {\n const session = sessions.get(sessionId);\n if (session) sessions.set(sessionId, { ...session, revokedAt });\n },\n\n async rotateSession(sessionId, refreshTokenHash, expiresAt) {\n const session = sessions.get(sessionId);\n if (session)\n sessions.set(sessionId, { ...session, refreshTokenHash, expiresAt });\n },\n\n async revokeAllSessions(userId, revokedAt) {\n for (const [id, session] of sessions) {\n if (session.userId === userId && !session.revokedAt) {\n sessions.set(id, { ...session, revokedAt });\n }\n }\n },\n\n async listRolesForUser(userId) {\n const result: Role[] = [];\n for (const role of roles.values()) {\n if (userRoles.has(key(userId, role.roleId))) result.push(role);\n }\n return result;\n },\n\n async listPermissionsForUser(userId) {\n const result = new Set<Permission>();\n for (const entry of userPermissions) {\n const [uid, permission] = entry.split(\"::\");\n if (uid === userId && permission) result.add(permission);\n }\n for (const role of roles.values()) {\n if (!userRoles.has(key(userId, role.roleId))) continue;\n for (const entry of rolePermissions) {\n const [rid, permission] = entry.split(\"::\");\n if (rid === role.roleId && permission) result.add(permission);\n }\n }\n return [...result];\n },\n\n async assignRole(assignment: UserRoleAssignment) {\n if (!roles.has(assignment.roleId)) {\n roles.set(assignment.roleId, {\n roleId: assignment.roleId,\n name: assignment.roleId,\n description: null,\n });\n }\n userRoles.add(key(assignment.userId, assignment.roleId));\n },\n\n async grantRolePermission(grant: RolePermissionGrant) {\n if (!roles.has(grant.roleId)) {\n roles.set(grant.roleId, {\n roleId: grant.roleId,\n name: grant.roleId,\n description: null,\n });\n }\n rolePermissions.add(key(grant.roleId, grant.permission));\n },\n\n async grantDirectPermission(grant: DirectPermissionGrant) {\n userPermissions.add(key(grant.userId, grant.permission));\n },\n\n async recordEvent(event: IdentityEvent) {\n events.push(event);\n },\n\n async findUserByOAuthId(provider: string, providerUserId: string) {\n const link = oauthLinks.get(key(provider, providerUserId));\n return link ? (users.get(link.userId) ?? null) : null;\n },\n\n async createOAuthLink(link: OAuthLinkedAccount) {\n oauthLinks.set(key(link.provider, link.providerUserId), link);\n const existing = oauthLinksByUser.get(link.userId) ?? [];\n oauthLinksByUser.set(link.userId, [...existing, link]);\n },\n\n async listOAuthLinks(userId: string) {\n return oauthLinksByUser.get(userId) ?? [];\n },\n\n // --- Verification tokens ------------------------------------------------\n async createVerificationToken(input: CreateVerificationTokenInput) {\n verificationTokens.set(input.tokenHash, { ...input, usedAt: null });\n },\n\n async findVerificationToken(tokenHash: string) {\n return verificationTokens.get(tokenHash) ?? null;\n },\n\n async markVerificationTokenUsed(tokenHash: string, usedAt: Date) {\n const existing = verificationTokens.get(tokenHash);\n if (existing) verificationTokens.set(tokenHash, { ...existing, usedAt });\n },\n\n // --- TOTP secrets -------------------------------------------------------\n async findTotpSecret(userId: string) {\n return totpSecrets.get(userId) ?? null;\n },\n\n async upsertTotpSecret(record: TotpSecret) {\n totpSecrets.set(record.userId, record);\n },\n\n async deleteTotpSecret(userId: string) {\n totpSecrets.delete(userId);\n },\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azlib/identity",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,10 +28,10 @@
28
28
  "import": "./dist/express.mjs",
29
29
  "require": "./dist/express.cjs"
30
30
  },
31
- "./nestjs": {
32
- "types": "./dist/nestjs.d.mts",
33
- "import": "./dist/nestjs.mjs",
34
- "require": "./dist/nestjs.cjs"
31
+ "./next": {
32
+ "types": "./dist/next.d.mts",
33
+ "import": "./dist/next.mjs",
34
+ "require": "./dist/next.cjs"
35
35
  },
36
36
  "./test-utils": {
37
37
  "types": "./dist/test-utils.d.mts",
@@ -74,9 +74,9 @@
74
74
  "rxjs": "^7.8.2",
75
75
  "supertest": "^7.2.2",
76
76
  "tsdown": "^0.22.14",
77
- "typescript": "5.5.4",
77
+ "typescript": "7.0.2",
78
78
  "vitest": "^4.1.10",
79
- "@repo/typescript-config": "0.0.0"
79
+ "@azlib/ts-config": "0.0.0"
80
80
  },
81
81
  "publishConfig": {
82
82
  "access": "public",