@puzzle-ui/auth 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +301 -2
- package/dist/LICENSE +21 -0
- package/dist/auth.lifecycle.manager.cjs +835 -0
- package/dist/auth.lifecycle.manager.cjs.map +1 -0
- package/dist/auth.lifecycle.manager.d.cts +115 -0
- package/dist/auth.lifecycle.manager.d.ts +115 -0
- package/dist/auth.lifecycle.manager.js +835 -0
- package/dist/auth.lifecycle.manager.js.map +1 -0
- package/dist/auth.manager.cjs +454 -0
- package/dist/auth.manager.cjs.map +1 -0
- package/dist/auth.manager.d.cts +54 -0
- package/dist/auth.manager.d.ts +54 -0
- package/dist/auth.manager.js +454 -0
- package/dist/auth.manager.js.map +1 -0
- package/dist/auth.mfa.manager.cjs +1488 -0
- package/dist/auth.mfa.manager.cjs.map +1 -0
- package/dist/auth.mfa.manager.d.cts +100 -0
- package/dist/auth.mfa.manager.d.ts +100 -0
- package/dist/auth.mfa.manager.js +1488 -0
- package/dist/auth.mfa.manager.js.map +1 -0
- package/dist/auth.rate-limit.cjs +78 -0
- package/dist/auth.rate-limit.cjs.map +1 -0
- package/dist/auth.rate-limit.d.cts +24 -0
- package/dist/auth.rate-limit.d.ts +24 -0
- package/dist/auth.rate-limit.js +78 -0
- package/dist/auth.rate-limit.js.map +1 -0
- package/dist/auth.step-up.manager.cjs +269 -0
- package/dist/auth.step-up.manager.cjs.map +1 -0
- package/dist/auth.step-up.manager.d.cts +74 -0
- package/dist/auth.step-up.manager.d.ts +74 -0
- package/dist/auth.step-up.manager.js +268 -0
- package/dist/auth.step-up.manager.js.map +1 -0
- package/dist/auth.storage.cjs +164 -0
- package/dist/auth.storage.cjs.map +1 -0
- package/dist/auth.storage.d.cts +29 -0
- package/dist/auth.storage.d.ts +29 -0
- package/dist/auth.storage.js +148 -0
- package/dist/auth.storage.js.map +1 -0
- package/dist/errors/auth.errors.cjs +101 -0
- package/dist/errors/auth.errors.cjs.map +1 -0
- package/dist/errors/auth.errors.d.cts +62 -0
- package/dist/errors/auth.errors.d.ts +62 -0
- package/dist/errors/auth.errors.js +90 -0
- package/dist/errors/auth.errors.js.map +1 -0
- package/dist/index.cjs +54 -0
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +13 -0
- package/dist/providers/mfa-provider.types.d.cts +66 -0
- package/dist/providers/mfa-provider.types.d.ts +66 -0
- package/dist/providers/provider.types.d.cts +34 -0
- package/dist/providers/provider.types.d.ts +34 -0
- package/dist/providers/simplewebauthn-provider.cjs +185 -0
- package/dist/providers/simplewebauthn-provider.cjs.map +1 -0
- package/dist/providers/simplewebauthn-provider.d.cts +35 -0
- package/dist/providers/simplewebauthn-provider.d.ts +35 -0
- package/dist/providers/simplewebauthn-provider.js +185 -0
- package/dist/providers/simplewebauthn-provider.js.map +1 -0
- package/dist/providers/webcrypto-secret-protector.cjs +118 -0
- package/dist/providers/webcrypto-secret-protector.cjs.map +1 -0
- package/dist/providers/webcrypto-secret-protector.d.cts +14 -0
- package/dist/providers/webcrypto-secret-protector.d.ts +14 -0
- package/dist/providers/webcrypto-secret-protector.js +118 -0
- package/dist/providers/webcrypto-secret-protector.js.map +1 -0
- package/dist/types/auth-lifecycle.types.d.cts +53 -0
- package/dist/types/auth-lifecycle.types.d.ts +53 -0
- package/dist/types/auth-manager.types.d.cts +106 -0
- package/dist/types/auth-manager.types.d.ts +106 -0
- package/dist/types/credential.types.d.cts +9 -0
- package/dist/types/credential.types.d.ts +9 -0
- package/dist/types/mfa.types.d.cts +244 -0
- package/dist/types/mfa.types.d.ts +244 -0
- package/dist/types/state.types.d.cts +33 -0
- package/dist/types/state.types.d.ts +33 -0
- package/dist/types/user.types.d.cts +9 -0
- package/dist/types/user.types.d.ts +9 -0
- package/dist/utils/auth-crypto.utils.cjs +38 -0
- package/dist/utils/auth-crypto.utils.cjs.map +1 -0
- package/dist/utils/auth-crypto.utils.d.cts +4 -0
- package/dist/utils/auth-crypto.utils.d.ts +4 -0
- package/dist/utils/auth-crypto.utils.js +35 -0
- package/dist/utils/auth-crypto.utils.js.map +1 -0
- package/dist/utils/email.utils.cjs +20 -0
- package/dist/utils/email.utils.cjs.map +1 -0
- package/dist/utils/email.utils.d.cts +2 -0
- package/dist/utils/email.utils.d.ts +2 -0
- package/dist/utils/email.utils.js +19 -0
- package/dist/utils/email.utils.js.map +1 -0
- package/dist/utils/totp.utils.cjs +145 -0
- package/dist/utils/totp.utils.cjs.map +1 -0
- package/dist/utils/totp.utils.d.cts +26 -0
- package/dist/utils/totp.utils.d.ts +26 -0
- package/dist/utils/totp.utils.js +140 -0
- package/dist/utils/totp.utils.js.map +1 -0
- package/package.json +45 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.lifecycle.manager.js","names":[],"sources":["../src/auth.lifecycle.manager.ts"],"sourcesContent":["import {\n StorageConflictError,\n StorageDuplicateError,\n type StorageAdapter,\n type StorageBatchOperation,\n} from \"@puzzle-ui/storage\";\n\nimport type { SessionManager, SessionRecord } from \"@puzzle-ui/session\";\n\nimport type {\n EmailMessage,\n EmailProvider,\n PasswordHash,\n PasswordHasher,\n RateLimitProvider,\n} from \"./providers/provider.types\";\n\nimport {\n AuthAccountConflictError,\n AuthConfigurationError,\n AuthProviderError,\n AuthRateLimitError,\n AuthSessionError,\n AuthTokenError,\n AuthValidationError,\n AuthEmailVerificationRequiredError,\n} from \"./errors/auth.errors\";\n\nimport {\n AUTH_ACTION_TOKEN_COLLECTION,\n AUTH_AUDIT_COLLECTION,\n AUTH_CREDENTIAL_COLLECTION,\n AUTH_RATE_LIMIT_COLLECTION,\n AUTH_USER_COLLECTION,\n initializeAuthStorage,\n type AuthUserStorageRecord,\n} from \"./auth.storage\";\n\nimport type {\n AuthActionTokenStorageRecord,\n AuthAuditAction,\n AuthAuditEventStorageRecord,\n AuthLifecycleManagerOptions,\n AuthLifecycleRateLimitAction,\n} from \"./types/auth-lifecycle.types\";\n\nimport type { PasswordCredentialStorageRecord } from \"./types/credential.types\";\nimport type { AuthUser } from \"./types/user.types\";\n\nimport {\n generateAuthId,\n generateSecureAuthToken,\n hashAuthToken,\n createRateLimitKey,\n} from \"./utils/auth-crypto.utils\";\n\nimport { normalizeDisplayName, normalizeEmail } from \"./utils/email.utils\";\n\nimport { StorageRateLimitProvider } from \"./auth.rate-limit\";\n\nexport type StorageAuthLifecycleManagerDependencies = {\n readonly storage: StorageAdapter;\n readonly passwordHasher: PasswordHasher;\n readonly emailProvider: EmailProvider;\n readonly sessions: SessionManager;\n readonly rateLimitProvider?: RateLimitProvider;\n};\n\nexport type RequestEmailVerificationInput = {\n readonly userId: string;\n readonly verificationUrl: string;\n readonly ipAddress: string;\n};\n\nexport type RequestPasswordResetInput = {\n readonly email: string;\n readonly resetUrl: string;\n readonly ipAddress: string;\n};\n\nexport type ResetPasswordInput = {\n readonly token: string;\n readonly newPassword: string;\n};\n\nexport type ChangePasswordInput = {\n readonly sessionToken: string;\n readonly currentPassword: string;\n readonly newPassword: string;\n};\n\nexport type UpdateAuthProfileInput = {\n readonly sessionToken: string;\n readonly name?: string | null;\n readonly image?: string | null;\n};\n\nexport type RequestEmailChangeInput = {\n readonly sessionToken: string;\n readonly currentPassword: string;\n readonly email: string;\n readonly verificationUrl: string;\n readonly ipAddress: string;\n};\n\nexport type DeleteAuthAccountInput = {\n readonly sessionToken: string;\n readonly password: string;\n};\n\nexport type DeactivateAuthAccountInput = {\n readonly sessionToken: string;\n readonly password: string;\n};\n\ntype LifecycleRecord =\n | AuthActionTokenStorageRecord\n | AuthAuditEventStorageRecord\n | AuthUserStorageRecord\n | PasswordCredentialStorageRecord;\n\ntype LifecycleBatchOperation = StorageBatchOperation<LifecycleRecord>;\n\ntype AuthLifecycleUserContext = {\n readonly session: SessionRecord;\n readonly user: AuthUserStorageRecord;\n};\n\ntype AuthActionTokenResult = {\n readonly token: string;\n readonly record: AuthActionTokenStorageRecord;\n};\n\ntype RateLimitRule = {\n readonly limit: number;\n readonly windowSeconds: number;\n};\n\nconst DEFAULT_TOKEN_LIFETIMES = {\n emailVerification: 60 * 60,\n passwordReset: 60 * 60,\n emailChange: 60 * 60,\n} as const;\n\nconst DEFAULT_RATE_LIMITS = {\n emailVerification: {\n email: { limit: 3, windowSeconds: 60 * 60 },\n ip: { limit: 10, windowSeconds: 60 * 60 },\n },\n passwordReset: {\n email: { limit: 3, windowSeconds: 60 * 60 },\n ip: { limit: 10, windowSeconds: 60 * 60 },\n },\n emailChange: {\n email: { limit: 3, windowSeconds: 60 * 60 },\n ip: { limit: 10, windowSeconds: 60 * 60 },\n },\n} satisfies Record<\n AuthLifecycleRateLimitAction,\n {\n readonly email: RateLimitRule;\n readonly ip: RateLimitRule;\n }\n>;\n\nconst toPublicUser = (record: AuthUserStorageRecord): AuthUser => ({\n id: record.id,\n email: record.email,\n emailVerifiedAt: record.emailVerifiedAt,\n name: record.name,\n image: record.image,\n createdAt: record.createdAt,\n updatedAt: record.updatedAt,\n});\n\nconst validateIpAddress = (ipAddress: string): string => {\n if (\n typeof ipAddress !== \"string\" ||\n ipAddress.trim().length === 0 ||\n ipAddress.trim().length > 128\n ) {\n throw new AuthValidationError(\"A valid client IP address is required.\");\n }\n\n return ipAddress.trim();\n};\n\nconst validatePassword = (\n password: string,\n minimumLength: number,\n maximumLength: number,\n): void => {\n if (typeof password !== \"string\") {\n throw new AuthValidationError(\"A password is required.\");\n }\n\n const length = Array.from(password).length;\n const byteLength = new TextEncoder().encode(password).byteLength;\n\n if (length < minimumLength) {\n throw new AuthValidationError(\n `Password must contain at least ${minimumLength} characters.`,\n );\n }\n\n if (length > maximumLength || byteLength > 4096) {\n throw new AuthValidationError(\n `Password must contain no more than ${maximumLength} characters.`,\n );\n }\n};\n\nconst validateImageUrl = (image: string | null | undefined): string | null => {\n if (image === undefined || image === null || image === \"\") {\n return null;\n }\n\n if (typeof image !== \"string\" || image.length > 2048) {\n throw new AuthValidationError(\"The profile image URL is invalid.\");\n }\n\n let url: URL;\n\n try {\n url = new URL(image);\n } catch {\n throw new AuthValidationError(\"The profile image URL is invalid.\");\n }\n\n if (url.protocol !== \"https:\" || url.username || url.password) {\n throw new AuthValidationError(\n \"Profile image URLs must use HTTPS and cannot contain credentials.\",\n );\n }\n\n return url.toString();\n};\n\nconst createTokenUrl = (baseUrl: string, token: string): string => {\n let url: URL;\n\n try {\n url = new URL(baseUrl);\n } catch {\n throw new AuthValidationError(\n \"The configured authentication callback URL is invalid.\",\n );\n }\n\n const isLocalhost =\n url.hostname === \"localhost\" ||\n url.hostname === \"127.0.0.1\" ||\n url.hostname === \"[::1]\";\n\n if (\n (url.protocol !== \"https:\" && !(isLocalhost && url.protocol === \"http:\")) ||\n url.username ||\n url.password\n ) {\n throw new AuthValidationError(\n \"Authentication callback URLs must use HTTPS.\",\n );\n }\n\n url.searchParams.set(\"token\", token);\n\n return url.toString();\n};\n\nconst validateRateLimitRule = (\n name: string,\n rule: RateLimitRule | undefined,\n): void => {\n if (\n !rule ||\n !Number.isInteger(rule.limit) ||\n rule.limit < 1 ||\n !Number.isFinite(rule.windowSeconds) ||\n rule.windowSeconds <= 0\n ) {\n throw new AuthConfigurationError(\n `${name} must have a positive integer limit and a positive window.`,\n );\n }\n};\n\nexport class StorageAuthLifecycleManager {\n private readonly storage: StorageAdapter;\n private readonly passwordHasher: PasswordHasher;\n private readonly emailProvider: EmailProvider;\n private readonly sessions: SessionManager;\n private readonly rateLimitProvider: RateLimitProvider;\n\n private readonly rateLimitKeySecret: string;\n private readonly usersCollection: string;\n private readonly credentialsCollection: string;\n private readonly actionTokensCollection: string;\n private readonly auditCollection: string;\n private readonly rateLimitsCollection: string;\n\n private readonly requireVerifiedEmail: boolean;\n private readonly onSessionRevocationError?: (error: unknown) => void;\n\n private readonly emailTemplates: {\n readonly emailVerification: string;\n readonly passwordReset: string;\n readonly emailChange: string;\n };\n\n private readonly emailSubjects: {\n readonly emailVerification: string;\n readonly passwordReset: string;\n readonly emailChange: string;\n };\n\n private readonly minimumPasswordLength: number;\n private readonly maximumPasswordLength: number;\n\n private readonly tokenLifetimes: typeof DEFAULT_TOKEN_LIFETIMES;\n private readonly rateLimits: typeof DEFAULT_RATE_LIMITS;\n private readonly clock: () => Date;\n private readonly onEmailDeliveryError?: (error: unknown) => void;\n\n private initialized = false;\n private initializationPromise: Promise<void> | null = null;\n\n constructor(\n dependencies: StorageAuthLifecycleManagerDependencies,\n options: AuthLifecycleManagerOptions,\n ) {\n this.storage = dependencies.storage;\n this.passwordHasher = dependencies.passwordHasher;\n this.emailProvider = dependencies.emailProvider;\n this.sessions = dependencies.sessions;\n\n if (\n typeof options.rateLimitKeySecret !== \"string\" ||\n options.rateLimitKeySecret.length < 32\n ) {\n throw new AuthConfigurationError(\n \"rateLimitKeySecret must contain at least 32 characters.\",\n );\n }\n\n this.rateLimitKeySecret = options.rateLimitKeySecret;\n\n this.usersCollection = options.usersCollection ?? AUTH_USER_COLLECTION;\n\n this.credentialsCollection =\n options.credentialsCollection ?? AUTH_CREDENTIAL_COLLECTION;\n\n this.actionTokensCollection =\n options.actionTokensCollection ?? AUTH_ACTION_TOKEN_COLLECTION;\n\n this.auditCollection = options.auditCollection ?? AUTH_AUDIT_COLLECTION;\n\n this.rateLimitsCollection =\n options.rateLimitsCollection ?? AUTH_RATE_LIMIT_COLLECTION;\n\n this.requireVerifiedEmail = options.requireVerifiedEmail ?? false;\n this.onSessionRevocationError = options.onSessionRevocationError;\n\n this.emailTemplates = {\n emailVerification:\n options.emailTemplates?.emailVerification ?? \"auth/email-verification\",\n passwordReset:\n options.emailTemplates?.passwordReset ?? \"auth/password-reset\",\n emailChange:\n options.emailTemplates?.emailChange ?? \"auth/email-change-verification\",\n };\n\n this.emailSubjects = {\n emailVerification:\n options.emailSubjects?.emailVerification ?? \"Verify your email address\",\n passwordReset:\n options.emailSubjects?.passwordReset ?? \"Reset your password\",\n emailChange:\n options.emailSubjects?.emailChange ?? \"Confirm your new email address\",\n };\n\n this.minimumPasswordLength = options.minimumPasswordLength ?? 8;\n this.maximumPasswordLength = options.maximumPasswordLength ?? 1024;\n\n if (\n !Number.isInteger(this.minimumPasswordLength) ||\n !Number.isInteger(this.maximumPasswordLength) ||\n this.minimumPasswordLength < 1 ||\n this.maximumPasswordLength < this.minimumPasswordLength\n ) {\n throw new AuthConfigurationError(\n \"Password length configuration is invalid.\",\n );\n }\n\n this.tokenLifetimes = {\n emailVerification:\n options.emailVerificationTokenLifetimeSeconds ??\n DEFAULT_TOKEN_LIFETIMES.emailVerification,\n passwordReset:\n options.passwordResetTokenLifetimeSeconds ??\n DEFAULT_TOKEN_LIFETIMES.passwordReset,\n emailChange:\n options.emailChangeTokenLifetimeSeconds ??\n DEFAULT_TOKEN_LIFETIMES.emailChange,\n };\n\n for (const [name, lifetime] of Object.entries(this.tokenLifetimes)) {\n if (!Number.isFinite(lifetime) || lifetime <= 0) {\n throw new AuthConfigurationError(\n `${name} token lifetime must be positive.`,\n );\n }\n }\n\n this.rateLimits = {\n emailVerification: {\n ...DEFAULT_RATE_LIMITS.emailVerification,\n ...options.rateLimits?.emailVerification,\n },\n passwordReset: {\n ...DEFAULT_RATE_LIMITS.passwordReset,\n ...options.rateLimits?.passwordReset,\n },\n emailChange: {\n ...DEFAULT_RATE_LIMITS.emailChange,\n ...options.rateLimits?.emailChange,\n },\n };\n\n for (const [action, policy] of Object.entries(this.rateLimits)) {\n validateRateLimitRule(`${action} email rate limit`, policy.email);\n validateRateLimitRule(`${action} IP rate limit`, policy.ip);\n }\n\n this.clock = options.clock ?? (() => new Date());\n this.onEmailDeliveryError = options.onEmailDeliveryError;\n\n if (\n !this.storage.capabilities.conditionalWrites ||\n !this.storage.capabilities.atomicBatch ||\n !this.storage.capabilities.uniqueConstraints\n ) {\n throw new AuthConfigurationError(\n \"Auth lifecycle workflows require conditional writes, unique constraints and atomic batches.\",\n );\n }\n\n this.rateLimitProvider =\n dependencies.rateLimitProvider ??\n new StorageRateLimitProvider(this.storage, {\n collectionName: this.rateLimitsCollection,\n clock: this.clock,\n });\n }\n\n async initialize(): Promise<void> {\n if (!this.initializationPromise) {\n const initialization = (async () => {\n await initializeAuthStorage(this.storage, {\n usersCollection: this.usersCollection,\n credentialsCollection: this.credentialsCollection,\n actionTokensCollection: this.actionTokensCollection,\n auditCollection: this.auditCollection,\n rateLimitsCollection: this.rateLimitsCollection,\n });\n\n if (this.rateLimitProvider instanceof StorageRateLimitProvider) {\n await this.rateLimitProvider.initialize();\n }\n\n this.initialized = true;\n })();\n\n this.initializationPromise = initialization.catch((error) => {\n this.initializationPromise = null;\n throw error;\n });\n }\n\n return this.initializationPromise;\n }\n\n private assertInitialized(): void {\n if (!this.initialized) {\n throw new AuthConfigurationError(\n \"Auth lifecycle manager must be initialized before use.\",\n );\n }\n }\n\n private now(): Date {\n const now = this.clock();\n\n if (!(now instanceof Date) || !Number.isFinite(now.getTime())) {\n throw new AuthConfigurationError(\n \"The configured clock returned an invalid date.\",\n );\n }\n\n return now;\n }\n\n private async consumeRateLimit(\n action: AuthLifecycleRateLimitAction,\n scope: \"email\" | \"ip\",\n identifier: string,\n ): Promise<void> {\n const rule = this.rateLimits[action][scope];\n\n const key = await createRateLimitKey(\n this.rateLimitKeySecret,\n `auth:${action}:${scope}:${identifier}`,\n );\n\n let decision: Awaited<ReturnType<RateLimitProvider[\"consume\"]>>;\n\n try {\n decision = await this.rateLimitProvider.consume({\n key,\n limit: rule.limit,\n windowSeconds: rule.windowSeconds,\n });\n } catch (error) {\n throw new AuthProviderError(\n \"The authentication rate limiter is unavailable.\",\n { cause: error },\n );\n }\n\n if (\n !decision ||\n typeof decision.allowed !== \"boolean\" ||\n !(decision.resetAt instanceof Date) ||\n !Number.isFinite(decision.resetAt.getTime())\n ) {\n throw new AuthProviderError(\n \"The rate limiter returned an invalid response.\",\n );\n }\n\n if (!decision.allowed) {\n throw new AuthRateLimitError(decision.resetAt);\n }\n }\n\n private async enforceRateLimits(\n action: AuthLifecycleRateLimitAction,\n email: string,\n ipAddress: string,\n ): Promise<void> {\n await this.consumeRateLimit(action, \"email\", email);\n await this.consumeRateLimit(action, \"ip\", ipAddress);\n }\n\n private async createActionToken(input: {\n readonly userId: string;\n readonly purpose: AuthActionTokenStorageRecord[\"purpose\"];\n readonly lifetimeSeconds: number;\n readonly targetEmail?: string | null;\n }): Promise<AuthActionTokenResult> {\n const now = this.now();\n const token = generateSecureAuthToken();\n\n const record: AuthActionTokenStorageRecord = {\n id: generateAuthId(\"action-token\"),\n userId: input.userId,\n purpose: input.purpose,\n tokenHash: await hashAuthToken(token),\n expiresAt: new Date(now.getTime() + input.lifetimeSeconds * 1000),\n consumedAt: null,\n targetEmail: input.targetEmail ?? null,\n createdAt: now,\n updatedAt: now,\n revision: 1,\n revokedAt: null,\n };\n\n return { token, record };\n }\n\n private createAuditRecord(\n action: AuthAuditAction,\n userId: string | null,\n sessionId: string | null = null,\n metadata?: AuthAuditEventStorageRecord[\"metadata\"],\n ): AuthAuditEventStorageRecord {\n const now = this.now();\n\n return {\n id: generateAuthId(\"auth-audit\"),\n action,\n userId,\n sessionId,\n metadata,\n createdAt: now,\n updatedAt: now,\n revision: 1,\n };\n }\n\n /**\n * All operations in a lifecycle batch must succeed.\n *\n * Storage adapters may report a missing conditional update or delete\n * through the batch result rather than by throwing an error. Treat those\n * results as conflicts so a token cannot be consumed without its related\n * account or credential mutation.\n */\n private async atomic(\n operations: readonly LifecycleBatchOperation[],\n ): Promise<void> {\n try {\n const result =\n await this.storage.atomicBatch<LifecycleRecord>(operations);\n\n for (const [index, operation] of operations.entries()) {\n const operationResult = result.results[index];\n\n if (operation.type === \"update\") {\n if (\n !operationResult ||\n operationResult.type !== \"update\" ||\n !operationResult.record\n ) {\n throw new StorageConflictError(\n \"A conditional authentication update did not match its record.\",\n );\n }\n }\n\n if (operation.type === \"delete\") {\n if (\n !operationResult ||\n operationResult.type !== \"delete\" ||\n !operationResult.deleted\n ) {\n throw new StorageConflictError(\n \"A conditional authentication delete did not match its record.\",\n );\n }\n }\n\n if (operation.type === \"insert\") {\n if (!operationResult || operationResult.type !== \"insert\") {\n throw new StorageConflictError(\n \"An authentication insert did not return its record.\",\n );\n }\n }\n }\n } catch (error) {\n if (error instanceof StorageDuplicateError) {\n throw new AuthAccountConflictError({ cause: error });\n }\n\n throw error;\n }\n }\n\n private async deliverEmail(\n message: EmailMessage,\n suppressFailure: boolean,\n ): Promise<void> {\n try {\n await this.emailProvider.send(message);\n } catch (error) {\n try {\n this.onEmailDeliveryError?.(error);\n } catch {\n // Operational logging must not change the public auth response.\n }\n\n if (!suppressFailure) {\n throw new AuthProviderError(\"Authentication email delivery failed.\", {\n cause: error,\n });\n }\n }\n }\n\n private async getSessionContext(\n token: string,\n ): Promise<AuthLifecycleUserContext> {\n if (typeof token !== \"string\" || token.length === 0) {\n throw new AuthSessionError(\"An authenticated session is required.\");\n }\n\n let session: SessionRecord | null;\n\n try {\n session = await this.sessions.validate(token);\n } catch (error) {\n throw new AuthSessionError(\n \"Could not validate the authentication session.\",\n { cause: error },\n );\n }\n\n if (!session || session.kind !== \"authenticated\" || !session.userId) {\n throw new AuthSessionError(\"An authenticated session is required.\");\n }\n\n const user = await this.storage.findById<AuthUserStorageRecord>(\n this.usersCollection,\n session.userId,\n );\n\n if (!user || user.deactivatedAt != null) {\n throw new AuthSessionError(\"The authenticated account is unavailable.\");\n }\n\n if (\n user.sessionsInvalidatedAt &&\n session.createdAt.getTime() <= user.sessionsInvalidatedAt.getTime()\n ) {\n throw new AuthSessionError(\n \"The authentication session is no longer valid.\",\n );\n }\n\n if (this.requireVerifiedEmail && !user.emailVerifiedAt) {\n throw new AuthEmailVerificationRequiredError();\n }\n\n return { session, user };\n }\n\n private async findActionToken(\n token: string,\n purpose: AuthActionTokenStorageRecord[\"purpose\"],\n ): Promise<AuthActionTokenStorageRecord> {\n if (typeof token !== \"string\" || token.length < 32 || token.length > 256) {\n throw new AuthTokenError();\n }\n\n const tokenHash = await hashAuthToken(token);\n\n const record = await this.storage.findOne<AuthActionTokenStorageRecord>(\n this.actionTokensCollection,\n {\n where: {\n tokenHash: { operator: \"eq\", value: tokenHash },\n },\n },\n );\n\n const now = this.now();\n\n if (\n !record ||\n record.purpose !== purpose ||\n record.consumedAt !== null ||\n record.revokedAt != null ||\n record.expiresAt.getTime() <= now.getTime()\n ) {\n throw new AuthTokenError();\n }\n\n return record;\n }\n\n private async findCredential(\n userId: string,\n ): Promise<PasswordCredentialStorageRecord | null> {\n return this.storage.findOne<PasswordCredentialStorageRecord>(\n this.credentialsCollection,\n {\n where: {\n userId: { operator: \"eq\", value: userId },\n kind: { operator: \"eq\", value: \"password\" },\n },\n },\n );\n }\n\n private async verifyPassword(\n password: string,\n stored: PasswordHash,\n ): Promise<boolean> {\n try {\n return await this.passwordHasher.verify(password, stored);\n } catch (error) {\n throw new AuthProviderError(\"Password verification failed.\", {\n cause: error,\n });\n }\n }\n\n private async hashPassword(password: string): Promise<PasswordHash> {\n try {\n const result = await this.passwordHasher.hash(password);\n\n if (\n !result ||\n result.algorithm !== \"argon2id\" ||\n typeof result.hash !== \"string\" ||\n result.hash.length === 0\n ) {\n throw new Error(\"PasswordHasher returned an invalid Argon2id hash.\");\n }\n\n return result;\n } catch (error) {\n throw new AuthProviderError(\"Password hashing failed.\", { cause: error });\n }\n }\n\n private async revokeAllSessions(userId: string): Promise<void> {\n try {\n await this.sessions.revokeAll(userId);\n } catch (error) {\n try {\n this.onSessionRevocationError?.(error);\n } catch {\n // Operational logging must not change the authentication result.\n }\n\n // The storage-level sessionsInvalidatedAt check already blocks old\n // sessions. Physical revocation is cleanup, not the security boundary.\n }\n }\n\n async sendEmailVerification(\n input: RequestEmailVerificationInput,\n ): Promise<void> {\n this.assertInitialized();\n\n const ipAddress = validateIpAddress(input.ipAddress);\n\n const initialUser = await this.storage.findById<AuthUserStorageRecord>(\n this.usersCollection,\n input.userId,\n );\n\n if (!initialUser) {\n throw new AuthValidationError(\"The account does not exist.\");\n }\n\n if (initialUser.emailVerifiedAt) return;\n\n await this.enforceRateLimits(\n \"emailVerification\",\n initialUser.normalizedEmail,\n ipAddress,\n );\n\n const maximumAttempts = 5;\n\n for (let attempt = 0; attempt < maximumAttempts; attempt += 1) {\n // Always reload the user after a conflict. Another request may have\n // successfully issued a newer verification token in the meantime.\n const user = await this.storage.findById<AuthUserStorageRecord>(\n this.usersCollection,\n input.userId,\n );\n\n if (!user) {\n throw new AuthValidationError(\"The account does not exist.\");\n }\n\n if (user.emailVerifiedAt) return;\n\n const created = await this.createActionToken({\n userId: user.id,\n purpose: \"email-verification\",\n lifetimeSeconds: this.tokenLifetimes.emailVerification,\n });\n\n const outstandingTokens =\n await this.storage.findMany<AuthActionTokenStorageRecord>(\n this.actionTokensCollection,\n {\n where: {\n userId: { operator: \"eq\", value: user.id },\n purpose: { operator: \"eq\", value: \"email-verification\" },\n consumedAt: { operator: \"eq\", value: null },\n },\n },\n );\n\n const operations: LifecycleBatchOperation[] = [\n {\n // This conditional update acts as the serialization point.\n // Only one concurrent request can commit against this revision.\n type: \"update\",\n collection: this.usersCollection,\n id: user.id,\n condition: {\n expectedRevision: user.revision,\n },\n changes: {\n updatedAt: created.record.createdAt,\n },\n },\n {\n type: \"insert\",\n collection: this.actionTokensCollection,\n record: created.record,\n },\n {\n type: \"insert\",\n collection: this.auditCollection,\n record: this.createAuditRecord(\n \"email-verification-requested\",\n user.id,\n ),\n },\n ];\n\n for (const outstanding of outstandingTokens) {\n if (outstanding.revokedAt != null) continue;\n\n operations.push({\n type: \"update\",\n collection: this.actionTokensCollection,\n id: outstanding.id,\n condition: {\n expectedRevision: outstanding.revision,\n },\n changes: {\n revokedAt: created.record.createdAt,\n },\n });\n }\n\n try {\n await this.atomic(operations);\n } catch (error) {\n if (error instanceof StorageConflictError) {\n if (attempt < maximumAttempts - 1) {\n continue;\n }\n\n throw new AuthProviderError(\n \"Could not safely issue an email verification token due to concurrent account updates.\",\n { cause: error },\n );\n }\n\n throw error;\n }\n\n const verificationUrl = createTokenUrl(\n input.verificationUrl,\n created.token,\n );\n\n await this.deliverEmail(\n {\n to: user.email,\n subject: this.emailSubjects.emailVerification,\n template: this.emailTemplates.emailVerification,\n variables: {\n url: verificationUrl,\n expiresAt: created.record.expiresAt.toISOString(),\n },\n },\n false,\n );\n\n return;\n }\n }\n\n async verifyEmail(token: string): Promise<AuthUser> {\n this.assertInitialized();\n\n const actionToken = await this.findActionToken(token, \"email-verification\");\n\n const user = await this.storage.findById<AuthUserStorageRecord>(\n this.usersCollection,\n actionToken.userId,\n );\n\n if (!user) throw new AuthTokenError();\n\n const now = this.now();\n\n try {\n await this.atomic([\n {\n type: \"update\",\n collection: this.actionTokensCollection,\n id: actionToken.id,\n condition: { expectedRevision: actionToken.revision },\n changes: { consumedAt: now },\n },\n {\n type: \"update\",\n collection: this.usersCollection,\n id: user.id,\n condition: { expectedRevision: user.revision },\n changes: {\n emailVerifiedAt: now,\n updatedAt: now,\n },\n },\n {\n type: \"insert\",\n collection: this.auditCollection,\n record: this.createAuditRecord(\"email-verified\", user.id),\n },\n ]);\n } catch (error) {\n if (error instanceof StorageConflictError) {\n throw new AuthTokenError();\n }\n\n throw error;\n }\n\n const updated = await this.storage.findById<AuthUserStorageRecord>(\n this.usersCollection,\n user.id,\n );\n\n if (!updated) {\n throw new AuthProviderError(\n \"The verified account could not be retrieved.\",\n );\n }\n\n return toPublicUser(updated);\n }\n\n /**\n * Always resolves with the same public result, whether or not the email\n * address belongs to an account. Delivery errors are reported only through\n * onEmailDeliveryError.\n */\n async requestPasswordReset(input: RequestPasswordResetInput): Promise<void> {\n this.assertInitialized();\n\n const email = normalizeEmail(input.email);\n const ipAddress = validateIpAddress(input.ipAddress);\n\n await this.enforceRateLimits(\"passwordReset\", email, ipAddress);\n\n const user = await this.storage.findOne<AuthUserStorageRecord>(\n this.usersCollection,\n {\n where: {\n normalizedEmail: { operator: \"eq\", value: email },\n },\n },\n );\n\n if (!user) {\n await this.storage.insert<AuthAuditEventStorageRecord>(\n this.auditCollection,\n this.createAuditRecord(\"password-reset-requested\", null),\n );\n\n return;\n }\n\n const created = await this.createActionToken({\n userId: user.id,\n purpose: \"password-reset\",\n lifetimeSeconds: this.tokenLifetimes.passwordReset,\n });\n\n await this.atomic([\n {\n type: \"insert\",\n collection: this.actionTokensCollection,\n record: created.record,\n },\n {\n type: \"insert\",\n collection: this.auditCollection,\n record: this.createAuditRecord(\"password-reset-requested\", user.id),\n },\n ]);\n\n const resetUrl = createTokenUrl(input.resetUrl, created.token);\n\n await this.deliverEmail(\n {\n to: user.email,\n subject: this.emailSubjects.passwordReset,\n template: this.emailTemplates.passwordReset,\n variables: {\n url: resetUrl,\n expiresAt: created.record.expiresAt.toISOString(),\n },\n },\n true,\n );\n }\n\n async resetPassword(input: ResetPasswordInput): Promise<void> {\n this.assertInitialized();\n\n validatePassword(\n input.newPassword,\n this.minimumPasswordLength,\n this.maximumPasswordLength,\n );\n\n const actionToken = await this.findActionToken(\n input.token,\n \"password-reset\",\n );\n\n const user = await this.storage.findById<AuthUserStorageRecord>(\n this.usersCollection,\n actionToken.userId,\n );\n\n if (!user || user.deactivatedAt != null) {\n throw new AuthTokenError();\n }\n\n const credential = await this.findCredential(actionToken.userId);\n\n if (!credential || credential.revokedAt !== null) {\n throw new AuthTokenError();\n }\n\n const passwordHash = await this.hashPassword(input.newPassword);\n const now = this.now();\n\n const otherTokens =\n await this.storage.findMany<AuthActionTokenStorageRecord>(\n this.actionTokensCollection,\n {\n where: {\n userId: { operator: \"eq\", value: actionToken.userId },\n purpose: { operator: \"eq\", value: \"password-reset\" },\n consumedAt: { operator: \"eq\", value: null },\n },\n },\n );\n\n const operations: LifecycleBatchOperation[] = [\n {\n type: \"update\",\n collection: this.actionTokensCollection,\n id: actionToken.id,\n condition: { expectedRevision: actionToken.revision },\n changes: { consumedAt: now },\n },\n {\n type: \"update\",\n collection: this.credentialsCollection,\n id: credential.id,\n condition: { expectedRevision: credential.revision },\n changes: {\n passwordHash,\n passwordUpdatedAt: now,\n updatedAt: now,\n revokedAt: null,\n },\n },\n {\n type: \"update\",\n collection: this.usersCollection,\n id: user.id,\n condition: { expectedRevision: user.revision },\n changes: {\n sessionsInvalidatedAt: now,\n updatedAt: now,\n },\n },\n {\n type: \"insert\",\n collection: this.auditCollection,\n record: this.createAuditRecord(\n \"password-reset-completed\",\n actionToken.userId,\n ),\n },\n ];\n\n for (const other of otherTokens) {\n if (other.id === actionToken.id || other.revokedAt != null) {\n continue;\n }\n\n operations.push({\n type: \"update\",\n collection: this.actionTokensCollection,\n id: other.id,\n condition: { expectedRevision: other.revision },\n changes: { revokedAt: now },\n });\n }\n\n try {\n await this.atomic(operations);\n } catch (error) {\n if (\n error instanceof StorageConflictError ||\n error instanceof AuthAccountConflictError\n ) {\n throw new AuthTokenError();\n }\n\n throw error;\n }\n\n await this.revokeAllSessions(actionToken.userId);\n }\n\n async changePassword(input: ChangePasswordInput): Promise<void> {\n this.assertInitialized();\n\n validatePassword(\n input.newPassword,\n this.minimumPasswordLength,\n this.maximumPasswordLength,\n );\n\n const { session, user } = await this.getSessionContext(input.sessionToken);\n\n const credential = await this.findCredential(user.id);\n\n if (\n !credential ||\n credential.revokedAt !== null ||\n !(await this.verifyPassword(\n input.currentPassword,\n credential.passwordHash,\n ))\n ) {\n throw new AuthValidationError(\"The current password is incorrect.\");\n }\n\n const passwordHash = await this.hashPassword(input.newPassword);\n const now = this.now();\n\n const resetTokens =\n await this.storage.findMany<AuthActionTokenStorageRecord>(\n this.actionTokensCollection,\n {\n where: {\n userId: { operator: \"eq\", value: user.id },\n purpose: { operator: \"eq\", value: \"password-reset\" },\n consumedAt: { operator: \"eq\", value: null },\n },\n },\n );\n\n const operations: LifecycleBatchOperation[] = [\n {\n type: \"update\",\n collection: this.credentialsCollection,\n id: credential.id,\n condition: { expectedRevision: credential.revision },\n changes: {\n passwordHash,\n passwordUpdatedAt: now,\n updatedAt: now,\n },\n },\n {\n type: \"update\",\n collection: this.usersCollection,\n id: user.id,\n condition: { expectedRevision: user.revision },\n changes: {\n sessionsInvalidatedAt: now,\n updatedAt: now,\n },\n },\n {\n type: \"insert\",\n collection: this.auditCollection,\n record: this.createAuditRecord(\"password-changed\", user.id, session.id),\n },\n ];\n\n for (const token of resetTokens) {\n if (token.revokedAt != null) continue;\n\n operations.push({\n type: \"update\",\n collection: this.actionTokensCollection,\n id: token.id,\n condition: { expectedRevision: token.revision },\n changes: { revokedAt: now },\n });\n }\n\n await this.atomic(operations);\n\n await this.revokeAllSessions(user.id);\n }\n\n async updateProfile(input: UpdateAuthProfileInput): Promise<AuthUser> {\n this.assertInitialized();\n\n const { session, user } = await this.getSessionContext(input.sessionToken);\n\n const changes: {\n name?: string | null;\n image?: string | null;\n updatedAt: Date;\n } = {\n updatedAt: this.now(),\n };\n\n if (input.name !== undefined) {\n changes.name = normalizeDisplayName(input.name);\n }\n\n if (input.image !== undefined) {\n changes.image = validateImageUrl(input.image);\n }\n\n await this.atomic([\n {\n type: \"update\",\n collection: this.usersCollection,\n id: user.id,\n condition: { expectedRevision: user.revision },\n changes,\n },\n {\n type: \"insert\",\n collection: this.auditCollection,\n record: this.createAuditRecord(\"profile-updated\", user.id, session.id),\n },\n ]);\n\n const updated = await this.storage.findById<AuthUserStorageRecord>(\n this.usersCollection,\n user.id,\n );\n\n if (!updated) {\n throw new AuthProviderError(\n \"The updated account could not be retrieved.\",\n );\n }\n\n return toPublicUser(updated);\n }\n\n async requestEmailChange(input: RequestEmailChangeInput): Promise<void> {\n this.assertInitialized();\n\n const { session, user } = await this.getSessionContext(input.sessionToken);\n\n const credential = await this.findCredential(user.id);\n\n if (\n !credential ||\n credential.revokedAt !== null ||\n !(await this.verifyPassword(\n input.currentPassword,\n credential.passwordHash,\n ))\n ) {\n throw new AuthValidationError(\"The current password is incorrect.\");\n }\n\n const newEmail = normalizeEmail(input.email);\n\n if (newEmail === user.normalizedEmail) {\n throw new AuthValidationError(\n \"The new email address is already the current email address.\",\n );\n }\n\n const ipAddress = validateIpAddress(input.ipAddress);\n\n await this.enforceRateLimits(\"emailChange\", newEmail, ipAddress);\n\n const existing = await this.storage.findOne<AuthUserStorageRecord>(\n this.usersCollection,\n {\n where: {\n normalizedEmail: { operator: \"eq\", value: newEmail },\n },\n },\n );\n\n if (existing) {\n throw new AuthAccountConflictError();\n }\n\n const created = await this.createActionToken({\n userId: user.id,\n purpose: \"email-change\",\n targetEmail: newEmail,\n lifetimeSeconds: this.tokenLifetimes.emailChange,\n });\n\n await this.atomic([\n {\n type: \"insert\",\n collection: this.actionTokensCollection,\n record: created.record,\n },\n {\n type: \"insert\",\n collection: this.auditCollection,\n record: this.createAuditRecord(\n \"email-change-requested\",\n user.id,\n session.id,\n ),\n },\n ]);\n\n const verificationUrl = createTokenUrl(\n input.verificationUrl,\n created.token,\n );\n\n await this.deliverEmail(\n {\n to: newEmail,\n subject: this.emailSubjects.emailChange,\n template: this.emailTemplates.emailChange,\n variables: {\n url: verificationUrl,\n expiresAt: created.record.expiresAt.toISOString(),\n },\n },\n false,\n );\n }\n\n async verifyEmailChange(token: string): Promise<AuthUser> {\n this.assertInitialized();\n\n const actionToken = await this.findActionToken(token, \"email-change\");\n\n if (!actionToken.targetEmail) {\n throw new AuthTokenError();\n }\n\n const user = await this.storage.findById<AuthUserStorageRecord>(\n this.usersCollection,\n actionToken.userId,\n );\n\n if (!user) throw new AuthTokenError();\n\n const newEmail = normalizeEmail(actionToken.targetEmail);\n const now = this.now();\n\n try {\n await this.atomic([\n {\n type: \"update\",\n collection: this.actionTokensCollection,\n id: actionToken.id,\n condition: { expectedRevision: actionToken.revision },\n changes: { consumedAt: now },\n },\n {\n type: \"update\",\n collection: this.usersCollection,\n id: user.id,\n condition: { expectedRevision: user.revision },\n changes: {\n email: newEmail,\n normalizedEmail: newEmail,\n emailVerifiedAt: now,\n sessionsInvalidatedAt: now,\n updatedAt: now,\n },\n },\n {\n type: \"insert\",\n collection: this.auditCollection,\n record: this.createAuditRecord(\"email-changed\", user.id),\n },\n ]);\n } catch (error) {\n if (error instanceof AuthAccountConflictError) {\n throw error;\n }\n\n if (error instanceof StorageConflictError) {\n throw new AuthTokenError();\n }\n\n throw error;\n }\n\n await this.revokeAllSessions(user.id);\n\n const updated = await this.storage.findById<AuthUserStorageRecord>(\n this.usersCollection,\n user.id,\n );\n\n if (!updated) {\n throw new AuthProviderError(\n \"The updated account could not be retrieved.\",\n );\n }\n\n return toPublicUser(updated);\n }\n\n async deactivateAccount(input: DeactivateAuthAccountInput): Promise<void> {\n this.assertInitialized();\n\n const { session, user } = await this.getSessionContext(input.sessionToken);\n\n const credential = await this.findCredential(user.id);\n\n if (\n !credential ||\n credential.revokedAt !== null ||\n !(await this.verifyPassword(input.password, credential.passwordHash))\n ) {\n throw new AuthValidationError(\"The password confirmation is incorrect.\");\n }\n\n const now = this.now();\n\n await this.atomic([\n {\n type: \"update\",\n collection: this.usersCollection,\n id: user.id,\n condition: { expectedRevision: user.revision },\n changes: {\n deactivatedAt: now,\n sessionsInvalidatedAt: now,\n updatedAt: now,\n },\n },\n {\n type: \"insert\",\n collection: this.auditCollection,\n record: this.createAuditRecord(\n \"account-deactivated\",\n user.id,\n session.id,\n ),\n },\n ]);\n\n await this.revokeAllSessions(user.id);\n }\n\n async deleteAccount(input: DeleteAuthAccountInput): Promise<void> {\n this.assertInitialized();\n\n const { session, user } = await this.getSessionContext(input.sessionToken);\n\n const credential = await this.findCredential(user.id);\n\n if (\n !credential ||\n credential.revokedAt !== null ||\n !(await this.verifyPassword(input.password, credential.passwordHash))\n ) {\n throw new AuthValidationError(\"The password confirmation is incorrect.\");\n }\n\n const credentials =\n await this.storage.findMany<PasswordCredentialStorageRecord>(\n this.credentialsCollection,\n {\n where: {\n userId: { operator: \"eq\", value: user.id },\n },\n },\n );\n\n const actionTokens =\n await this.storage.findMany<AuthActionTokenStorageRecord>(\n this.actionTokensCollection,\n {\n where: {\n userId: { operator: \"eq\", value: user.id },\n },\n },\n );\n\n const operations: LifecycleBatchOperation[] = [\n {\n type: \"delete\",\n collection: this.usersCollection,\n id: user.id,\n condition: { expectedRevision: user.revision },\n },\n {\n type: \"insert\",\n collection: this.auditCollection,\n record: this.createAuditRecord(\"account-deleted\", user.id, session.id),\n },\n ];\n\n for (const item of credentials) {\n operations.push({\n type: \"delete\",\n collection: this.credentialsCollection,\n id: item.id,\n condition: { expectedRevision: item.revision },\n });\n }\n\n for (const item of actionTokens) {\n operations.push({\n type: \"delete\",\n collection: this.actionTokensCollection,\n id: item.id,\n condition: { expectedRevision: item.revision },\n });\n }\n\n await this.atomic(operations);\n\n await this.revokeAllSessions(user.id);\n }\n}\n"],"mappings":";;;;;;;AA0IA,MAAM,0BAA0B;CAC9B,mBAAmB;CACnB,eAAe;CACf,aAAa;AACf;AAEA,MAAM,sBAAsB;CAC1B,mBAAmB;EACjB,OAAO;GAAE,OAAO;GAAG,eAAe;EAAQ;EAC1C,IAAI;GAAE,OAAO;GAAI,eAAe;EAAQ;CAC1C;CACA,eAAe;EACb,OAAO;GAAE,OAAO;GAAG,eAAe;EAAQ;EAC1C,IAAI;GAAE,OAAO;GAAI,eAAe;EAAQ;CAC1C;CACA,aAAa;EACX,OAAO;GAAE,OAAO;GAAG,eAAe;EAAQ;EAC1C,IAAI;GAAE,OAAO;GAAI,eAAe;EAAQ;CAC1C;AACF;AAQA,MAAM,gBAAgB,YAA6C;CACjE,IAAI,OAAO;CACX,OAAO,OAAO;CACd,iBAAiB,OAAO;CACxB,MAAM,OAAO;CACb,OAAO,OAAO;CACd,WAAW,OAAO;CAClB,WAAW,OAAO;AACpB;AAEA,MAAM,qBAAqB,cAA8B;CACvD,IACE,OAAO,cAAc,YACrB,UAAU,KAAK,CAAC,CAAC,WAAW,KAC5B,UAAU,KAAK,CAAC,CAAC,SAAS,KAE1B,MAAM,IAAI,oBAAoB,wCAAwC;CAGxE,OAAO,UAAU,KAAK;AACxB;AAEA,MAAM,oBACJ,UACA,eACA,kBACS;CACT,IAAI,OAAO,aAAa,UACtB,MAAM,IAAI,oBAAoB,yBAAyB;CAGzD,MAAM,SAAS,MAAM,KAAK,QAAQ,CAAC,CAAC;CACpC,MAAM,aAAa,IAAI,YAAY,CAAC,CAAC,OAAO,QAAQ,CAAC,CAAC;CAEtD,IAAI,SAAS,eACX,MAAM,IAAI,oBACR,kCAAkC,cAAc,aAClD;CAGF,IAAI,SAAS,iBAAiB,aAAa,MACzC,MAAM,IAAI,oBACR,sCAAsC,cAAc,aACtD;AAEJ;AAEA,MAAM,oBAAoB,UAAoD;CAC5E,IAAI,UAAU,KAAA,KAAa,UAAU,QAAQ,UAAU,IACrD,OAAO;CAGT,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,MAC9C,MAAM,IAAI,oBAAoB,mCAAmC;CAGnE,IAAI;CAEJ,IAAI;EACF,MAAM,IAAI,IAAI,KAAK;CACrB,QAAQ;EACN,MAAM,IAAI,oBAAoB,mCAAmC;CACnE;CAEA,IAAI,IAAI,aAAa,YAAY,IAAI,YAAY,IAAI,UACnD,MAAM,IAAI,oBACR,mEACF;CAGF,OAAO,IAAI,SAAS;AACtB;AAEA,MAAM,kBAAkB,SAAiB,UAA0B;CACjE,IAAI;CAEJ,IAAI;EACF,MAAM,IAAI,IAAI,OAAO;CACvB,QAAQ;EACN,MAAM,IAAI,oBACR,wDACF;CACF;CAEA,MAAM,cACJ,IAAI,aAAa,eACjB,IAAI,aAAa,eACjB,IAAI,aAAa;CAEnB,IACG,IAAI,aAAa,YAAY,EAAE,eAAe,IAAI,aAAa,YAChE,IAAI,YACJ,IAAI,UAEJ,MAAM,IAAI,oBACR,8CACF;CAGF,IAAI,aAAa,IAAI,SAAS,KAAK;CAEnC,OAAO,IAAI,SAAS;AACtB;AAEA,MAAM,yBACJ,MACA,SACS;CACT,IACE,CAAC,QACD,CAAC,OAAO,UAAU,KAAK,KAAK,KAC5B,KAAK,QAAQ,KACb,CAAC,OAAO,SAAS,KAAK,aAAa,KACnC,KAAK,iBAAiB,GAEtB,MAAM,IAAI,uBACR,GAAG,KAAK,2DACV;AAEJ;AAEA,IAAa,8BAAb,MAAyC;CACvC;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CAEA;CAMA;CAMA;CACA;CAEA;CACA;CACA;CACA;CAEA,cAAsB;CACtB,wBAAsD;CAEtD,YACE,cACA,SACA;EACA,KAAK,UAAU,aAAa;EAC5B,KAAK,iBAAiB,aAAa;EACnC,KAAK,gBAAgB,aAAa;EAClC,KAAK,WAAW,aAAa;EAE7B,IACE,OAAO,QAAQ,uBAAuB,YACtC,QAAQ,mBAAmB,SAAS,IAEpC,MAAM,IAAI,uBACR,yDACF;EAGF,KAAK,qBAAqB,QAAQ;EAElC,KAAK,kBAAkB,QAAQ,mBAAA;EAE/B,KAAK,wBACH,QAAQ,yBAAA;EAEV,KAAK,yBACH,QAAQ,0BAAA;EAEV,KAAK,kBAAkB,QAAQ,mBAAA;EAE/B,KAAK,uBACH,QAAQ,wBAAA;EAEV,KAAK,uBAAuB,QAAQ,wBAAwB;EAC5D,KAAK,2BAA2B,QAAQ;EAExC,KAAK,iBAAiB;GACpB,mBACE,QAAQ,gBAAgB,qBAAqB;GAC/C,eACE,QAAQ,gBAAgB,iBAAiB;GAC3C,aACE,QAAQ,gBAAgB,eAAe;EAC3C;EAEA,KAAK,gBAAgB;GACnB,mBACE,QAAQ,eAAe,qBAAqB;GAC9C,eACE,QAAQ,eAAe,iBAAiB;GAC1C,aACE,QAAQ,eAAe,eAAe;EAC1C;EAEA,KAAK,wBAAwB,QAAQ,yBAAyB;EAC9D,KAAK,wBAAwB,QAAQ,yBAAyB;EAE9D,IACE,CAAC,OAAO,UAAU,KAAK,qBAAqB,KAC5C,CAAC,OAAO,UAAU,KAAK,qBAAqB,KAC5C,KAAK,wBAAwB,KAC7B,KAAK,wBAAwB,KAAK,uBAElC,MAAM,IAAI,uBACR,2CACF;EAGF,KAAK,iBAAiB;GACpB,mBACE,QAAQ,yCACR,wBAAwB;GAC1B,eACE,QAAQ,qCACR,wBAAwB;GAC1B,aACE,QAAQ,mCACR,wBAAwB;EAC5B;EAEA,KAAK,MAAM,CAAC,MAAM,aAAa,OAAO,QAAQ,KAAK,cAAc,GAC/D,IAAI,CAAC,OAAO,SAAS,QAAQ,KAAK,YAAY,GAC5C,MAAM,IAAI,uBACR,GAAG,KAAK,kCACV;EAIJ,KAAK,aAAa;GAChB,mBAAmB;IACjB,GAAG,oBAAoB;IACvB,GAAG,QAAQ,YAAY;GACzB;GACA,eAAe;IACb,GAAG,oBAAoB;IACvB,GAAG,QAAQ,YAAY;GACzB;GACA,aAAa;IACX,GAAG,oBAAoB;IACvB,GAAG,QAAQ,YAAY;GACzB;EACF;EAEA,KAAK,MAAM,CAAC,QAAQ,WAAW,OAAO,QAAQ,KAAK,UAAU,GAAG;GAC9D,sBAAsB,GAAG,OAAO,oBAAoB,OAAO,KAAK;GAChE,sBAAsB,GAAG,OAAO,iBAAiB,OAAO,EAAE;EAC5D;EAEA,KAAK,QAAQ,QAAQ,gCAAgB,IAAI,KAAK;EAC9C,KAAK,uBAAuB,QAAQ;EAEpC,IACE,CAAC,KAAK,QAAQ,aAAa,qBAC3B,CAAC,KAAK,QAAQ,aAAa,eAC3B,CAAC,KAAK,QAAQ,aAAa,mBAE3B,MAAM,IAAI,uBACR,6FACF;EAGF,KAAK,oBACH,aAAa,qBACb,IAAI,yBAAyB,KAAK,SAAS;GACzC,gBAAgB,KAAK;GACrB,OAAO,KAAK;EACd,CAAC;CACL;CAEA,MAAM,aAA4B;EAChC,IAAI,CAAC,KAAK,uBAAuB;GAC/B,MAAM,kBAAkB,YAAY;IAClC,MAAM,sBAAsB,KAAK,SAAS;KACxC,iBAAiB,KAAK;KACtB,uBAAuB,KAAK;KAC5B,wBAAwB,KAAK;KAC7B,iBAAiB,KAAK;KACtB,sBAAsB,KAAK;IAC7B,CAAC;IAED,IAAI,KAAK,6BAA6B,0BACpC,MAAM,KAAK,kBAAkB,WAAW;IAG1C,KAAK,cAAc;GACrB,EAAA,CAAG;GAEH,KAAK,wBAAwB,eAAe,OAAO,UAAU;IAC3D,KAAK,wBAAwB;IAC7B,MAAM;GACR,CAAC;EACH;EAEA,OAAO,KAAK;CACd;CAEA,oBAAkC;EAChC,IAAI,CAAC,KAAK,aACR,MAAM,IAAI,uBACR,wDACF;CAEJ;CAEA,MAAoB;EAClB,MAAM,MAAM,KAAK,MAAM;EAEvB,IAAI,EAAE,eAAe,SAAS,CAAC,OAAO,SAAS,IAAI,QAAQ,CAAC,GAC1D,MAAM,IAAI,uBACR,gDACF;EAGF,OAAO;CACT;CAEA,MAAc,iBACZ,QACA,OACA,YACe;EACf,MAAM,OAAO,KAAK,WAAW,OAAO,CAAC;EAErC,MAAM,MAAM,MAAM,mBAChB,KAAK,oBACL,QAAQ,OAAO,GAAG,MAAM,GAAG,YAC7B;EAEA,IAAI;EAEJ,IAAI;GACF,WAAW,MAAM,KAAK,kBAAkB,QAAQ;IAC9C;IACA,OAAO,KAAK;IACZ,eAAe,KAAK;GACtB,CAAC;EACH,SAAS,OAAO;GACd,MAAM,IAAI,kBACR,mDACA,EAAE,OAAO,MAAM,CACjB;EACF;EAEA,IACE,CAAC,YACD,OAAO,SAAS,YAAY,aAC5B,EAAE,SAAS,mBAAmB,SAC9B,CAAC,OAAO,SAAS,SAAS,QAAQ,QAAQ,CAAC,GAE3C,MAAM,IAAI,kBACR,gDACF;EAGF,IAAI,CAAC,SAAS,SACZ,MAAM,IAAI,mBAAmB,SAAS,OAAO;CAEjD;CAEA,MAAc,kBACZ,QACA,OACA,WACe;EACf,MAAM,KAAK,iBAAiB,QAAQ,SAAS,KAAK;EAClD,MAAM,KAAK,iBAAiB,QAAQ,MAAM,SAAS;CACrD;CAEA,MAAc,kBAAkB,OAKG;EACjC,MAAM,MAAM,KAAK,IAAI;EACrB,MAAM,QAAQ,wBAAwB;EAgBtC,OAAO;GAAE;GAAO,QAAA;IAbd,IAAI,eAAe,cAAc;IACjC,QAAQ,MAAM;IACd,SAAS,MAAM;IACf,WAAW,MAAM,cAAc,KAAK;IACpC,WAAW,IAAI,KAAK,IAAI,QAAQ,IAAI,MAAM,kBAAkB,GAAI;IAChE,YAAY;IACZ,aAAa,MAAM,eAAe;IAClC,WAAW;IACX,WAAW;IACX,UAAU;IACV,WAAW;GAGQ;EAAE;CACzB;CAEA,kBACE,QACA,QACA,YAA2B,MAC3B,UAC6B;EAC7B,MAAM,MAAM,KAAK,IAAI;EAErB,OAAO;GACL,IAAI,eAAe,YAAY;GAC/B;GACA;GACA;GACA;GACA,WAAW;GACX,WAAW;GACX,UAAU;EACZ;CACF;;;;;;;;;CAUA,MAAc,OACZ,YACe;EACf,IAAI;GACF,MAAM,SACJ,MAAM,KAAK,QAAQ,YAA6B,UAAU;GAE5D,KAAK,MAAM,CAAC,OAAO,cAAc,WAAW,QAAQ,GAAG;IACrD,MAAM,kBAAkB,OAAO,QAAQ;IAEvC,IAAI,UAAU,SAAS,UAEnB;SAAA,CAAC,mBACD,gBAAgB,SAAS,YACzB,CAAC,gBAAgB,QAEjB,MAAM,IAAI,qBACR,+DACF;IAAA;IAIJ,IAAI,UAAU,SAAS,UAEnB;SAAA,CAAC,mBACD,gBAAgB,SAAS,YACzB,CAAC,gBAAgB,SAEjB,MAAM,IAAI,qBACR,+DACF;IAAA;IAIJ,IAAI,UAAU,SAAS,UACjB;SAAA,CAAC,mBAAmB,gBAAgB,SAAS,UAC/C,MAAM,IAAI,qBACR,qDACF;IAAA;GAGN;EACF,SAAS,OAAO;GACd,IAAI,iBAAiB,uBACnB,MAAM,IAAI,yBAAyB,EAAE,OAAO,MAAM,CAAC;GAGrD,MAAM;EACR;CACF;CAEA,MAAc,aACZ,SACA,iBACe;EACf,IAAI;GACF,MAAM,KAAK,cAAc,KAAK,OAAO;EACvC,SAAS,OAAO;GACd,IAAI;IACF,KAAK,uBAAuB,KAAK;GACnC,QAAQ,CAER;GAEA,IAAI,CAAC,iBACH,MAAM,IAAI,kBAAkB,yCAAyC,EACnE,OAAO,MACT,CAAC;EAEL;CACF;CAEA,MAAc,kBACZ,OACmC;EACnC,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAChD,MAAM,IAAI,iBAAiB,uCAAuC;EAGpE,IAAI;EAEJ,IAAI;GACF,UAAU,MAAM,KAAK,SAAS,SAAS,KAAK;EAC9C,SAAS,OAAO;GACd,MAAM,IAAI,iBACR,kDACA,EAAE,OAAO,MAAM,CACjB;EACF;EAEA,IAAI,CAAC,WAAW,QAAQ,SAAS,mBAAmB,CAAC,QAAQ,QAC3D,MAAM,IAAI,iBAAiB,uCAAuC;EAGpE,MAAM,OAAO,MAAM,KAAK,QAAQ,SAC9B,KAAK,iBACL,QAAQ,MACV;EAEA,IAAI,CAAC,QAAQ,KAAK,iBAAiB,MACjC,MAAM,IAAI,iBAAiB,2CAA2C;EAGxE,IACE,KAAK,yBACL,QAAQ,UAAU,QAAQ,KAAK,KAAK,sBAAsB,QAAQ,GAElE,MAAM,IAAI,iBACR,gDACF;EAGF,IAAI,KAAK,wBAAwB,CAAC,KAAK,iBACrC,MAAM,IAAI,mCAAmC;EAG/C,OAAO;GAAE;GAAS;EAAK;CACzB;CAEA,MAAc,gBACZ,OACA,SACuC;EACvC,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,MAAM,MAAM,SAAS,KACnE,MAAM,IAAI,eAAe;EAG3B,MAAM,YAAY,MAAM,cAAc,KAAK;EAE3C,MAAM,SAAS,MAAM,KAAK,QAAQ,QAChC,KAAK,wBACL,EACE,OAAO,EACL,WAAW;GAAE,UAAU;GAAM,OAAO;EAAU,EAChD,EACF,CACF;EAEA,MAAM,MAAM,KAAK,IAAI;EAErB,IACE,CAAC,UACD,OAAO,YAAY,WACnB,OAAO,eAAe,QACtB,OAAO,aAAa,QACpB,OAAO,UAAU,QAAQ,KAAK,IAAI,QAAQ,GAE1C,MAAM,IAAI,eAAe;EAG3B,OAAO;CACT;CAEA,MAAc,eACZ,QACiD;EACjD,OAAO,KAAK,QAAQ,QAClB,KAAK,uBACL,EACE,OAAO;GACL,QAAQ;IAAE,UAAU;IAAM,OAAO;GAAO;GACxC,MAAM;IAAE,UAAU;IAAM,OAAO;GAAW;EAC5C,EACF,CACF;CACF;CAEA,MAAc,eACZ,UACA,QACkB;EAClB,IAAI;GACF,OAAO,MAAM,KAAK,eAAe,OAAO,UAAU,MAAM;EAC1D,SAAS,OAAO;GACd,MAAM,IAAI,kBAAkB,iCAAiC,EAC3D,OAAO,MACT,CAAC;EACH;CACF;CAEA,MAAc,aAAa,UAAyC;EAClE,IAAI;GACF,MAAM,SAAS,MAAM,KAAK,eAAe,KAAK,QAAQ;GAEtD,IACE,CAAC,UACD,OAAO,cAAc,cACrB,OAAO,OAAO,SAAS,YACvB,OAAO,KAAK,WAAW,GAEvB,MAAM,IAAI,MAAM,mDAAmD;GAGrE,OAAO;EACT,SAAS,OAAO;GACd,MAAM,IAAI,kBAAkB,4BAA4B,EAAE,OAAO,MAAM,CAAC;EAC1E;CACF;CAEA,MAAc,kBAAkB,QAA+B;EAC7D,IAAI;GACF,MAAM,KAAK,SAAS,UAAU,MAAM;EACtC,SAAS,OAAO;GACd,IAAI;IACF,KAAK,2BAA2B,KAAK;GACvC,QAAQ,CAER;EAIF;CACF;CAEA,MAAM,sBACJ,OACe;EACf,KAAK,kBAAkB;EAEvB,MAAM,YAAY,kBAAkB,MAAM,SAAS;EAEnD,MAAM,cAAc,MAAM,KAAK,QAAQ,SACrC,KAAK,iBACL,MAAM,MACR;EAEA,IAAI,CAAC,aACH,MAAM,IAAI,oBAAoB,6BAA6B;EAG7D,IAAI,YAAY,iBAAiB;EAEjC,MAAM,KAAK,kBACT,qBACA,YAAY,iBACZ,SACF;EAEA,MAAM,kBAAkB;EAExB,KAAK,IAAI,UAAU,GAAG,UAAU,iBAAiB,WAAW,GAAG;GAG7D,MAAM,OAAO,MAAM,KAAK,QAAQ,SAC9B,KAAK,iBACL,MAAM,MACR;GAEA,IAAI,CAAC,MACH,MAAM,IAAI,oBAAoB,6BAA6B;GAG7D,IAAI,KAAK,iBAAiB;GAE1B,MAAM,UAAU,MAAM,KAAK,kBAAkB;IAC3C,QAAQ,KAAK;IACb,SAAS;IACT,iBAAiB,KAAK,eAAe;GACvC,CAAC;GAED,MAAM,oBACJ,MAAM,KAAK,QAAQ,SACjB,KAAK,wBACL,EACE,OAAO;IACL,QAAQ;KAAE,UAAU;KAAM,OAAO,KAAK;IAAG;IACzC,SAAS;KAAE,UAAU;KAAM,OAAO;IAAqB;IACvD,YAAY;KAAE,UAAU;KAAM,OAAO;IAAK;GAC5C,EACF,CACF;GAEF,MAAM,aAAwC;IAC5C;KAGE,MAAM;KACN,YAAY,KAAK;KACjB,IAAI,KAAK;KACT,WAAW,EACT,kBAAkB,KAAK,SACzB;KACA,SAAS,EACP,WAAW,QAAQ,OAAO,UAC5B;IACF;IACA;KACE,MAAM;KACN,YAAY,KAAK;KACjB,QAAQ,QAAQ;IAClB;IACA;KACE,MAAM;KACN,YAAY,KAAK;KACjB,QAAQ,KAAK,kBACX,gCACA,KAAK,EACP;IACF;GACF;GAEA,KAAK,MAAM,eAAe,mBAAmB;IAC3C,IAAI,YAAY,aAAa,MAAM;IAEnC,WAAW,KAAK;KACd,MAAM;KACN,YAAY,KAAK;KACjB,IAAI,YAAY;KAChB,WAAW,EACT,kBAAkB,YAAY,SAChC;KACA,SAAS,EACP,WAAW,QAAQ,OAAO,UAC5B;IACF,CAAC;GACH;GAEA,IAAI;IACF,MAAM,KAAK,OAAO,UAAU;GAC9B,SAAS,OAAO;IACd,IAAI,iBAAiB,sBAAsB;KACzC,IAAI,UAAU,GACZ;KAGF,MAAM,IAAI,kBACR,yFACA,EAAE,OAAO,MAAM,CACjB;IACF;IAEA,MAAM;GACR;GAEA,MAAM,kBAAkB,eACtB,MAAM,iBACN,QAAQ,KACV;GAEA,MAAM,KAAK,aACT;IACE,IAAI,KAAK;IACT,SAAS,KAAK,cAAc;IAC5B,UAAU,KAAK,eAAe;IAC9B,WAAW;KACT,KAAK;KACL,WAAW,QAAQ,OAAO,UAAU,YAAY;IAClD;GACF,GACA,KACF;GAEA;EACF;CACF;CAEA,MAAM,YAAY,OAAkC;EAClD,KAAK,kBAAkB;EAEvB,MAAM,cAAc,MAAM,KAAK,gBAAgB,OAAO,oBAAoB;EAE1E,MAAM,OAAO,MAAM,KAAK,QAAQ,SAC9B,KAAK,iBACL,YAAY,MACd;EAEA,IAAI,CAAC,MAAM,MAAM,IAAI,eAAe;EAEpC,MAAM,MAAM,KAAK,IAAI;EAErB,IAAI;GACF,MAAM,KAAK,OAAO;IAChB;KACE,MAAM;KACN,YAAY,KAAK;KACjB,IAAI,YAAY;KAChB,WAAW,EAAE,kBAAkB,YAAY,SAAS;KACpD,SAAS,EAAE,YAAY,IAAI;IAC7B;IACA;KACE,MAAM;KACN,YAAY,KAAK;KACjB,IAAI,KAAK;KACT,WAAW,EAAE,kBAAkB,KAAK,SAAS;KAC7C,SAAS;MACP,iBAAiB;MACjB,WAAW;KACb;IACF;IACA;KACE,MAAM;KACN,YAAY,KAAK;KACjB,QAAQ,KAAK,kBAAkB,kBAAkB,KAAK,EAAE;IAC1D;GACF,CAAC;EACH,SAAS,OAAO;GACd,IAAI,iBAAiB,sBACnB,MAAM,IAAI,eAAe;GAG3B,MAAM;EACR;EAEA,MAAM,UAAU,MAAM,KAAK,QAAQ,SACjC,KAAK,iBACL,KAAK,EACP;EAEA,IAAI,CAAC,SACH,MAAM,IAAI,kBACR,8CACF;EAGF,OAAO,aAAa,OAAO;CAC7B;;;;;;CAOA,MAAM,qBAAqB,OAAiD;EAC1E,KAAK,kBAAkB;EAEvB,MAAM,QAAQ,eAAe,MAAM,KAAK;EACxC,MAAM,YAAY,kBAAkB,MAAM,SAAS;EAEnD,MAAM,KAAK,kBAAkB,iBAAiB,OAAO,SAAS;EAE9D,MAAM,OAAO,MAAM,KAAK,QAAQ,QAC9B,KAAK,iBACL,EACE,OAAO,EACL,iBAAiB;GAAE,UAAU;GAAM,OAAO;EAAM,EAClD,EACF,CACF;EAEA,IAAI,CAAC,MAAM;GACT,MAAM,KAAK,QAAQ,OACjB,KAAK,iBACL,KAAK,kBAAkB,4BAA4B,IAAI,CACzD;GAEA;EACF;EAEA,MAAM,UAAU,MAAM,KAAK,kBAAkB;GAC3C,QAAQ,KAAK;GACb,SAAS;GACT,iBAAiB,KAAK,eAAe;EACvC,CAAC;EAED,MAAM,KAAK,OAAO,CAChB;GACE,MAAM;GACN,YAAY,KAAK;GACjB,QAAQ,QAAQ;EAClB,GACA;GACE,MAAM;GACN,YAAY,KAAK;GACjB,QAAQ,KAAK,kBAAkB,4BAA4B,KAAK,EAAE;EACpE,CACF,CAAC;EAED,MAAM,WAAW,eAAe,MAAM,UAAU,QAAQ,KAAK;EAE7D,MAAM,KAAK,aACT;GACE,IAAI,KAAK;GACT,SAAS,KAAK,cAAc;GAC5B,UAAU,KAAK,eAAe;GAC9B,WAAW;IACT,KAAK;IACL,WAAW,QAAQ,OAAO,UAAU,YAAY;GAClD;EACF,GACA,IACF;CACF;CAEA,MAAM,cAAc,OAA0C;EAC5D,KAAK,kBAAkB;EAEvB,iBACE,MAAM,aACN,KAAK,uBACL,KAAK,qBACP;EAEA,MAAM,cAAc,MAAM,KAAK,gBAC7B,MAAM,OACN,gBACF;EAEA,MAAM,OAAO,MAAM,KAAK,QAAQ,SAC9B,KAAK,iBACL,YAAY,MACd;EAEA,IAAI,CAAC,QAAQ,KAAK,iBAAiB,MACjC,MAAM,IAAI,eAAe;EAG3B,MAAM,aAAa,MAAM,KAAK,eAAe,YAAY,MAAM;EAE/D,IAAI,CAAC,cAAc,WAAW,cAAc,MAC1C,MAAM,IAAI,eAAe;EAG3B,MAAM,eAAe,MAAM,KAAK,aAAa,MAAM,WAAW;EAC9D,MAAM,MAAM,KAAK,IAAI;EAErB,MAAM,cACJ,MAAM,KAAK,QAAQ,SACjB,KAAK,wBACL,EACE,OAAO;GACL,QAAQ;IAAE,UAAU;IAAM,OAAO,YAAY;GAAO;GACpD,SAAS;IAAE,UAAU;IAAM,OAAO;GAAiB;GACnD,YAAY;IAAE,UAAU;IAAM,OAAO;GAAK;EAC5C,EACF,CACF;EAEF,MAAM,aAAwC;GAC5C;IACE,MAAM;IACN,YAAY,KAAK;IACjB,IAAI,YAAY;IAChB,WAAW,EAAE,kBAAkB,YAAY,SAAS;IACpD,SAAS,EAAE,YAAY,IAAI;GAC7B;GACA;IACE,MAAM;IACN,YAAY,KAAK;IACjB,IAAI,WAAW;IACf,WAAW,EAAE,kBAAkB,WAAW,SAAS;IACnD,SAAS;KACP;KACA,mBAAmB;KACnB,WAAW;KACX,WAAW;IACb;GACF;GACA;IACE,MAAM;IACN,YAAY,KAAK;IACjB,IAAI,KAAK;IACT,WAAW,EAAE,kBAAkB,KAAK,SAAS;IAC7C,SAAS;KACP,uBAAuB;KACvB,WAAW;IACb;GACF;GACA;IACE,MAAM;IACN,YAAY,KAAK;IACjB,QAAQ,KAAK,kBACX,4BACA,YAAY,MACd;GACF;EACF;EAEA,KAAK,MAAM,SAAS,aAAa;GAC/B,IAAI,MAAM,OAAO,YAAY,MAAM,MAAM,aAAa,MACpD;GAGF,WAAW,KAAK;IACd,MAAM;IACN,YAAY,KAAK;IACjB,IAAI,MAAM;IACV,WAAW,EAAE,kBAAkB,MAAM,SAAS;IAC9C,SAAS,EAAE,WAAW,IAAI;GAC5B,CAAC;EACH;EAEA,IAAI;GACF,MAAM,KAAK,OAAO,UAAU;EAC9B,SAAS,OAAO;GACd,IACE,iBAAiB,wBACjB,iBAAiB,0BAEjB,MAAM,IAAI,eAAe;GAG3B,MAAM;EACR;EAEA,MAAM,KAAK,kBAAkB,YAAY,MAAM;CACjD;CAEA,MAAM,eAAe,OAA2C;EAC9D,KAAK,kBAAkB;EAEvB,iBACE,MAAM,aACN,KAAK,uBACL,KAAK,qBACP;EAEA,MAAM,EAAE,SAAS,SAAS,MAAM,KAAK,kBAAkB,MAAM,YAAY;EAEzE,MAAM,aAAa,MAAM,KAAK,eAAe,KAAK,EAAE;EAEpD,IACE,CAAC,cACD,WAAW,cAAc,QACzB,CAAE,MAAM,KAAK,eACX,MAAM,iBACN,WAAW,YACb,GAEA,MAAM,IAAI,oBAAoB,oCAAoC;EAGpE,MAAM,eAAe,MAAM,KAAK,aAAa,MAAM,WAAW;EAC9D,MAAM,MAAM,KAAK,IAAI;EAErB,MAAM,cACJ,MAAM,KAAK,QAAQ,SACjB,KAAK,wBACL,EACE,OAAO;GACL,QAAQ;IAAE,UAAU;IAAM,OAAO,KAAK;GAAG;GACzC,SAAS;IAAE,UAAU;IAAM,OAAO;GAAiB;GACnD,YAAY;IAAE,UAAU;IAAM,OAAO;GAAK;EAC5C,EACF,CACF;EAEF,MAAM,aAAwC;GAC5C;IACE,MAAM;IACN,YAAY,KAAK;IACjB,IAAI,WAAW;IACf,WAAW,EAAE,kBAAkB,WAAW,SAAS;IACnD,SAAS;KACP;KACA,mBAAmB;KACnB,WAAW;IACb;GACF;GACA;IACE,MAAM;IACN,YAAY,KAAK;IACjB,IAAI,KAAK;IACT,WAAW,EAAE,kBAAkB,KAAK,SAAS;IAC7C,SAAS;KACP,uBAAuB;KACvB,WAAW;IACb;GACF;GACA;IACE,MAAM;IACN,YAAY,KAAK;IACjB,QAAQ,KAAK,kBAAkB,oBAAoB,KAAK,IAAI,QAAQ,EAAE;GACxE;EACF;EAEA,KAAK,MAAM,SAAS,aAAa;GAC/B,IAAI,MAAM,aAAa,MAAM;GAE7B,WAAW,KAAK;IACd,MAAM;IACN,YAAY,KAAK;IACjB,IAAI,MAAM;IACV,WAAW,EAAE,kBAAkB,MAAM,SAAS;IAC9C,SAAS,EAAE,WAAW,IAAI;GAC5B,CAAC;EACH;EAEA,MAAM,KAAK,OAAO,UAAU;EAE5B,MAAM,KAAK,kBAAkB,KAAK,EAAE;CACtC;CAEA,MAAM,cAAc,OAAkD;EACpE,KAAK,kBAAkB;EAEvB,MAAM,EAAE,SAAS,SAAS,MAAM,KAAK,kBAAkB,MAAM,YAAY;EAEzE,MAAM,UAIF,EACF,WAAW,KAAK,IAAI,EACtB;EAEA,IAAI,MAAM,SAAS,KAAA,GACjB,QAAQ,OAAO,qBAAqB,MAAM,IAAI;EAGhD,IAAI,MAAM,UAAU,KAAA,GAClB,QAAQ,QAAQ,iBAAiB,MAAM,KAAK;EAG9C,MAAM,KAAK,OAAO,CAChB;GACE,MAAM;GACN,YAAY,KAAK;GACjB,IAAI,KAAK;GACT,WAAW,EAAE,kBAAkB,KAAK,SAAS;GAC7C;EACF,GACA;GACE,MAAM;GACN,YAAY,KAAK;GACjB,QAAQ,KAAK,kBAAkB,mBAAmB,KAAK,IAAI,QAAQ,EAAE;EACvE,CACF,CAAC;EAED,MAAM,UAAU,MAAM,KAAK,QAAQ,SACjC,KAAK,iBACL,KAAK,EACP;EAEA,IAAI,CAAC,SACH,MAAM,IAAI,kBACR,6CACF;EAGF,OAAO,aAAa,OAAO;CAC7B;CAEA,MAAM,mBAAmB,OAA+C;EACtE,KAAK,kBAAkB;EAEvB,MAAM,EAAE,SAAS,SAAS,MAAM,KAAK,kBAAkB,MAAM,YAAY;EAEzE,MAAM,aAAa,MAAM,KAAK,eAAe,KAAK,EAAE;EAEpD,IACE,CAAC,cACD,WAAW,cAAc,QACzB,CAAE,MAAM,KAAK,eACX,MAAM,iBACN,WAAW,YACb,GAEA,MAAM,IAAI,oBAAoB,oCAAoC;EAGpE,MAAM,WAAW,eAAe,MAAM,KAAK;EAE3C,IAAI,aAAa,KAAK,iBACpB,MAAM,IAAI,oBACR,6DACF;EAGF,MAAM,YAAY,kBAAkB,MAAM,SAAS;EAEnD,MAAM,KAAK,kBAAkB,eAAe,UAAU,SAAS;EAW/D,IAAI,MATmB,KAAK,QAAQ,QAClC,KAAK,iBACL,EACE,OAAO,EACL,iBAAiB;GAAE,UAAU;GAAM,OAAO;EAAS,EACrD,EACF,CACF,GAGE,MAAM,IAAI,yBAAyB;EAGrC,MAAM,UAAU,MAAM,KAAK,kBAAkB;GAC3C,QAAQ,KAAK;GACb,SAAS;GACT,aAAa;GACb,iBAAiB,KAAK,eAAe;EACvC,CAAC;EAED,MAAM,KAAK,OAAO,CAChB;GACE,MAAM;GACN,YAAY,KAAK;GACjB,QAAQ,QAAQ;EAClB,GACA;GACE,MAAM;GACN,YAAY,KAAK;GACjB,QAAQ,KAAK,kBACX,0BACA,KAAK,IACL,QAAQ,EACV;EACF,CACF,CAAC;EAED,MAAM,kBAAkB,eACtB,MAAM,iBACN,QAAQ,KACV;EAEA,MAAM,KAAK,aACT;GACE,IAAI;GACJ,SAAS,KAAK,cAAc;GAC5B,UAAU,KAAK,eAAe;GAC9B,WAAW;IACT,KAAK;IACL,WAAW,QAAQ,OAAO,UAAU,YAAY;GAClD;EACF,GACA,KACF;CACF;CAEA,MAAM,kBAAkB,OAAkC;EACxD,KAAK,kBAAkB;EAEvB,MAAM,cAAc,MAAM,KAAK,gBAAgB,OAAO,cAAc;EAEpE,IAAI,CAAC,YAAY,aACf,MAAM,IAAI,eAAe;EAG3B,MAAM,OAAO,MAAM,KAAK,QAAQ,SAC9B,KAAK,iBACL,YAAY,MACd;EAEA,IAAI,CAAC,MAAM,MAAM,IAAI,eAAe;EAEpC,MAAM,WAAW,eAAe,YAAY,WAAW;EACvD,MAAM,MAAM,KAAK,IAAI;EAErB,IAAI;GACF,MAAM,KAAK,OAAO;IAChB;KACE,MAAM;KACN,YAAY,KAAK;KACjB,IAAI,YAAY;KAChB,WAAW,EAAE,kBAAkB,YAAY,SAAS;KACpD,SAAS,EAAE,YAAY,IAAI;IAC7B;IACA;KACE,MAAM;KACN,YAAY,KAAK;KACjB,IAAI,KAAK;KACT,WAAW,EAAE,kBAAkB,KAAK,SAAS;KAC7C,SAAS;MACP,OAAO;MACP,iBAAiB;MACjB,iBAAiB;MACjB,uBAAuB;MACvB,WAAW;KACb;IACF;IACA;KACE,MAAM;KACN,YAAY,KAAK;KACjB,QAAQ,KAAK,kBAAkB,iBAAiB,KAAK,EAAE;IACzD;GACF,CAAC;EACH,SAAS,OAAO;GACd,IAAI,iBAAiB,0BACnB,MAAM;GAGR,IAAI,iBAAiB,sBACnB,MAAM,IAAI,eAAe;GAG3B,MAAM;EACR;EAEA,MAAM,KAAK,kBAAkB,KAAK,EAAE;EAEpC,MAAM,UAAU,MAAM,KAAK,QAAQ,SACjC,KAAK,iBACL,KAAK,EACP;EAEA,IAAI,CAAC,SACH,MAAM,IAAI,kBACR,6CACF;EAGF,OAAO,aAAa,OAAO;CAC7B;CAEA,MAAM,kBAAkB,OAAkD;EACxE,KAAK,kBAAkB;EAEvB,MAAM,EAAE,SAAS,SAAS,MAAM,KAAK,kBAAkB,MAAM,YAAY;EAEzE,MAAM,aAAa,MAAM,KAAK,eAAe,KAAK,EAAE;EAEpD,IACE,CAAC,cACD,WAAW,cAAc,QACzB,CAAE,MAAM,KAAK,eAAe,MAAM,UAAU,WAAW,YAAY,GAEnE,MAAM,IAAI,oBAAoB,yCAAyC;EAGzE,MAAM,MAAM,KAAK,IAAI;EAErB,MAAM,KAAK,OAAO,CAChB;GACE,MAAM;GACN,YAAY,KAAK;GACjB,IAAI,KAAK;GACT,WAAW,EAAE,kBAAkB,KAAK,SAAS;GAC7C,SAAS;IACP,eAAe;IACf,uBAAuB;IACvB,WAAW;GACb;EACF,GACA;GACE,MAAM;GACN,YAAY,KAAK;GACjB,QAAQ,KAAK,kBACX,uBACA,KAAK,IACL,QAAQ,EACV;EACF,CACF,CAAC;EAED,MAAM,KAAK,kBAAkB,KAAK,EAAE;CACtC;CAEA,MAAM,cAAc,OAA8C;EAChE,KAAK,kBAAkB;EAEvB,MAAM,EAAE,SAAS,SAAS,MAAM,KAAK,kBAAkB,MAAM,YAAY;EAEzE,MAAM,aAAa,MAAM,KAAK,eAAe,KAAK,EAAE;EAEpD,IACE,CAAC,cACD,WAAW,cAAc,QACzB,CAAE,MAAM,KAAK,eAAe,MAAM,UAAU,WAAW,YAAY,GAEnE,MAAM,IAAI,oBAAoB,yCAAyC;EAGzE,MAAM,cACJ,MAAM,KAAK,QAAQ,SACjB,KAAK,uBACL,EACE,OAAO,EACL,QAAQ;GAAE,UAAU;GAAM,OAAO,KAAK;EAAG,EAC3C,EACF,CACF;EAEF,MAAM,eACJ,MAAM,KAAK,QAAQ,SACjB,KAAK,wBACL,EACE,OAAO,EACL,QAAQ;GAAE,UAAU;GAAM,OAAO,KAAK;EAAG,EAC3C,EACF,CACF;EAEF,MAAM,aAAwC,CAC5C;GACE,MAAM;GACN,YAAY,KAAK;GACjB,IAAI,KAAK;GACT,WAAW,EAAE,kBAAkB,KAAK,SAAS;EAC/C,GACA;GACE,MAAM;GACN,YAAY,KAAK;GACjB,QAAQ,KAAK,kBAAkB,mBAAmB,KAAK,IAAI,QAAQ,EAAE;EACvE,CACF;EAEA,KAAK,MAAM,QAAQ,aACjB,WAAW,KAAK;GACd,MAAM;GACN,YAAY,KAAK;GACjB,IAAI,KAAK;GACT,WAAW,EAAE,kBAAkB,KAAK,SAAS;EAC/C,CAAC;EAGH,KAAK,MAAM,QAAQ,cACjB,WAAW,KAAK;GACd,MAAM;GACN,YAAY,KAAK;GACjB,IAAI,KAAK;GACT,WAAW,EAAE,kBAAkB,KAAK,SAAS;EAC/C,CAAC;EAGH,MAAM,KAAK,OAAO,UAAU;EAE5B,MAAM,KAAK,kBAAkB,KAAK,EAAE;CACtC;AACF"}
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
const require_auth_storage = require("./auth.storage.cjs");
|
|
2
|
+
const require_auth_errors = require("./errors/auth.errors.cjs");
|
|
3
|
+
const require_email_utils = require("./utils/email.utils.cjs");
|
|
4
|
+
const require_auth_crypto_utils = require("./utils/auth-crypto.utils.cjs");
|
|
5
|
+
let _puzzle_ui_storage = require("@puzzle-ui/storage");
|
|
6
|
+
// src/auth.manager.ts
|
|
7
|
+
const DEFAULT_RATE_LIMITS = {
|
|
8
|
+
registration: {
|
|
9
|
+
email: {
|
|
10
|
+
limit: 5,
|
|
11
|
+
windowSeconds: 3600
|
|
12
|
+
},
|
|
13
|
+
ip: {
|
|
14
|
+
limit: 10,
|
|
15
|
+
windowSeconds: 3600
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
signIn: {
|
|
19
|
+
email: {
|
|
20
|
+
limit: 10,
|
|
21
|
+
windowSeconds: 900
|
|
22
|
+
},
|
|
23
|
+
ip: {
|
|
24
|
+
limit: 30,
|
|
25
|
+
windowSeconds: 900
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const DEFAULT_MINIMUM_PASSWORD_LENGTH = 8;
|
|
30
|
+
const DEFAULT_MAXIMUM_PASSWORD_LENGTH = 1024;
|
|
31
|
+
const toPublicUser = (record) => ({
|
|
32
|
+
id: record.id,
|
|
33
|
+
email: record.email,
|
|
34
|
+
emailVerifiedAt: record.emailVerifiedAt,
|
|
35
|
+
name: record.name,
|
|
36
|
+
image: record.image,
|
|
37
|
+
createdAt: record.createdAt,
|
|
38
|
+
updatedAt: record.updatedAt
|
|
39
|
+
});
|
|
40
|
+
const toPublicSession = (record) => {
|
|
41
|
+
if (record.kind !== "authenticated" || !record.userId) throw new require_auth_errors.AuthSessionError("An authenticated session must contain a user ID.");
|
|
42
|
+
return {
|
|
43
|
+
id: record.id,
|
|
44
|
+
userId: record.userId,
|
|
45
|
+
kind: "authenticated",
|
|
46
|
+
createdAt: record.createdAt,
|
|
47
|
+
lastSeenAt: record.lastSeenAt,
|
|
48
|
+
expiresAt: record.expiresAt,
|
|
49
|
+
revokedAt: record.revokedAt
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
const validateRateLimitRule = (name, rule) => {
|
|
53
|
+
if (!Number.isInteger(rule.limit) || rule.limit < 1 || !Number.isFinite(rule.windowSeconds) || rule.windowSeconds <= 0) throw new require_auth_errors.AuthConfigurationError(`${name} must have a positive integer limit and a positive window.`);
|
|
54
|
+
};
|
|
55
|
+
const validatePassword = (password, minimumLength, maximumLength) => {
|
|
56
|
+
if (typeof password !== "string") throw new require_auth_errors.AuthValidationError("A password is required.");
|
|
57
|
+
const length = Array.from(password).length;
|
|
58
|
+
const byteLength = new TextEncoder().encode(password).byteLength;
|
|
59
|
+
if (length < minimumLength) throw new require_auth_errors.AuthValidationError(`Password must contain at least ${minimumLength} characters.`);
|
|
60
|
+
if (length > maximumLength || byteLength > 4096) throw new require_auth_errors.AuthValidationError(`Password must contain no more than ${maximumLength} characters.`);
|
|
61
|
+
};
|
|
62
|
+
const validateSignInPassword = (password, maximumLength) => {
|
|
63
|
+
if (typeof password !== "string" || password.length === 0) throw new require_auth_errors.AuthValidationError("A password is required.");
|
|
64
|
+
if (Array.from(password).length > maximumLength || new TextEncoder().encode(password).byteLength > 4096) throw new require_auth_errors.AuthValidationError("The supplied password is too long.");
|
|
65
|
+
};
|
|
66
|
+
const validateIpAddress = (ipAddress) => {
|
|
67
|
+
if (typeof ipAddress !== "string" || ipAddress.trim().length === 0 || ipAddress.trim().length > 128) throw new require_auth_errors.AuthValidationError("A valid client IP address is required.");
|
|
68
|
+
return ipAddress.trim();
|
|
69
|
+
};
|
|
70
|
+
var StorageAuthManager = class {
|
|
71
|
+
storage;
|
|
72
|
+
passwordHasher;
|
|
73
|
+
rateLimitProvider;
|
|
74
|
+
sessions;
|
|
75
|
+
mfa;
|
|
76
|
+
mfaPolicy;
|
|
77
|
+
usersCollection;
|
|
78
|
+
credentialsCollection;
|
|
79
|
+
auditCollection;
|
|
80
|
+
minimumPasswordLength;
|
|
81
|
+
maximumPasswordLength;
|
|
82
|
+
rateLimitPolicies;
|
|
83
|
+
rateLimitKeySecret;
|
|
84
|
+
requireVerifiedEmail;
|
|
85
|
+
clock;
|
|
86
|
+
initializationPromise = null;
|
|
87
|
+
dummyPasswordHash = null;
|
|
88
|
+
constructor(dependencies, options) {
|
|
89
|
+
this.storage = dependencies.storage;
|
|
90
|
+
this.passwordHasher = dependencies.passwordHasher;
|
|
91
|
+
this.rateLimitProvider = dependencies.rateLimitProvider;
|
|
92
|
+
this.sessions = dependencies.sessions;
|
|
93
|
+
this.mfa = dependencies.mfa;
|
|
94
|
+
this.mfaPolicy = options.mfaPolicy ?? {};
|
|
95
|
+
if (typeof options.rateLimitKeySecret !== "string" || options.rateLimitKeySecret.length < 32) throw new require_auth_errors.AuthConfigurationError("rateLimitKeySecret must contain at least 32 characters.");
|
|
96
|
+
this.rateLimitKeySecret = options.rateLimitKeySecret;
|
|
97
|
+
this.requireVerifiedEmail = options.requireVerifiedEmail ?? false;
|
|
98
|
+
this.usersCollection = options.usersCollection ?? "auth_users";
|
|
99
|
+
this.credentialsCollection = options.credentialsCollection ?? "auth_credentials";
|
|
100
|
+
this.auditCollection = options.auditCollection ?? "auth_audit_events";
|
|
101
|
+
this.minimumPasswordLength = options.minimumPasswordLength ?? DEFAULT_MINIMUM_PASSWORD_LENGTH;
|
|
102
|
+
this.maximumPasswordLength = options.maximumPasswordLength ?? DEFAULT_MAXIMUM_PASSWORD_LENGTH;
|
|
103
|
+
if (!Number.isInteger(this.minimumPasswordLength) || !Number.isInteger(this.maximumPasswordLength) || this.minimumPasswordLength < 1 || this.maximumPasswordLength < this.minimumPasswordLength) throw new require_auth_errors.AuthConfigurationError("Password length configuration is invalid.");
|
|
104
|
+
this.rateLimitPolicies = {
|
|
105
|
+
registration: {
|
|
106
|
+
...DEFAULT_RATE_LIMITS.registration,
|
|
107
|
+
...options.rateLimits?.registration
|
|
108
|
+
},
|
|
109
|
+
signIn: {
|
|
110
|
+
...DEFAULT_RATE_LIMITS.signIn,
|
|
111
|
+
...options.rateLimits?.signIn
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
validateRateLimitRule("Registration email rate limit", this.rateLimitPolicies.registration.email);
|
|
115
|
+
validateRateLimitRule("Registration IP rate limit", this.rateLimitPolicies.registration.ip);
|
|
116
|
+
validateRateLimitRule("Sign-in email rate limit", this.rateLimitPolicies.signIn.email);
|
|
117
|
+
validateRateLimitRule("Sign-in IP rate limit", this.rateLimitPolicies.signIn.ip);
|
|
118
|
+
this.clock = options.clock ?? (() => /* @__PURE__ */ new Date());
|
|
119
|
+
}
|
|
120
|
+
async initialize() {
|
|
121
|
+
if (!this.initializationPromise) {
|
|
122
|
+
const initialization = (async () => {
|
|
123
|
+
await require_auth_storage.initializeAuthStorage(this.storage, {
|
|
124
|
+
usersCollection: this.usersCollection,
|
|
125
|
+
credentialsCollection: this.credentialsCollection,
|
|
126
|
+
auditCollection: this.auditCollection
|
|
127
|
+
});
|
|
128
|
+
await this.mfa?.initialize();
|
|
129
|
+
this.dummyPasswordHash = await this.hashPassword(require_auth_crypto_utils.generateAuthId("auth-dummy"));
|
|
130
|
+
})();
|
|
131
|
+
this.initializationPromise = initialization.catch((error) => {
|
|
132
|
+
this.initializationPromise = null;
|
|
133
|
+
throw error;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return this.initializationPromise;
|
|
137
|
+
}
|
|
138
|
+
assertInitialized() {
|
|
139
|
+
if (!this.dummyPasswordHash) throw new require_auth_errors.AuthConfigurationError("AuthManager must be initialized before it can be used.");
|
|
140
|
+
}
|
|
141
|
+
now() {
|
|
142
|
+
const now = this.clock();
|
|
143
|
+
if (!(now instanceof Date) || !Number.isFinite(now.getTime())) throw new require_auth_errors.AuthConfigurationError("The configured clock returned an invalid date.");
|
|
144
|
+
return now;
|
|
145
|
+
}
|
|
146
|
+
createAuditRecord(action, userId, sessionId = null, metadata) {
|
|
147
|
+
const now = this.now();
|
|
148
|
+
return {
|
|
149
|
+
id: require_auth_crypto_utils.generateAuthId("auth-audit"),
|
|
150
|
+
action,
|
|
151
|
+
userId,
|
|
152
|
+
sessionId,
|
|
153
|
+
metadata,
|
|
154
|
+
createdAt: now,
|
|
155
|
+
updatedAt: now,
|
|
156
|
+
revision: 1
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
async recordAudit(action, userId, sessionId = null, metadata) {
|
|
160
|
+
try {
|
|
161
|
+
await this.storage.insert(this.auditCollection, this.createAuditRecord(action, userId, sessionId, metadata));
|
|
162
|
+
} catch (error) {
|
|
163
|
+
throw new require_auth_errors.AuthProviderError("Could not record the authentication audit event.", { cause: error });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async hashPassword(password) {
|
|
167
|
+
try {
|
|
168
|
+
const result = await this.passwordHasher.hash(password);
|
|
169
|
+
if (!result || result.algorithm !== "argon2id" || typeof result.hash !== "string" || result.hash.length === 0) throw new Error("PasswordHasher returned an invalid Argon2id hash.");
|
|
170
|
+
return result;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw new require_auth_errors.AuthProviderError("Password hashing failed.", { cause: error });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async verifyPassword(password, stored) {
|
|
176
|
+
try {
|
|
177
|
+
return await this.passwordHasher.verify(password, stored);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
throw new require_auth_errors.AuthProviderError("Password verification failed.", { cause: error });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async enforceRateLimits(action, email, ipAddress) {
|
|
183
|
+
const policy = this.rateLimitPolicies[action];
|
|
184
|
+
const checks = [{
|
|
185
|
+
scope: "email",
|
|
186
|
+
identifier: email,
|
|
187
|
+
rule: policy.email
|
|
188
|
+
}, {
|
|
189
|
+
scope: "ip",
|
|
190
|
+
identifier: ipAddress,
|
|
191
|
+
rule: policy.ip
|
|
192
|
+
}];
|
|
193
|
+
for (const check of checks) {
|
|
194
|
+
const key = await require_auth_crypto_utils.createRateLimitKey(this.rateLimitKeySecret, `auth:${action}:${check.scope}:${check.identifier}`);
|
|
195
|
+
let decision;
|
|
196
|
+
try {
|
|
197
|
+
decision = await this.rateLimitProvider.consume({
|
|
198
|
+
key,
|
|
199
|
+
limit: check.rule.limit,
|
|
200
|
+
windowSeconds: check.rule.windowSeconds
|
|
201
|
+
});
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw new require_auth_errors.AuthProviderError("The authentication rate limiter is unavailable.", { cause: error });
|
|
204
|
+
}
|
|
205
|
+
if (!decision || typeof decision.allowed !== "boolean" || !(decision.resetAt instanceof Date) || !Number.isFinite(decision.resetAt.getTime())) throw new require_auth_errors.AuthProviderError("The rate limiter returned an invalid response.");
|
|
206
|
+
if (!decision.allowed) throw new require_auth_errors.AuthRateLimitError(decision.resetAt);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async findUserByEmail(normalizedEmail) {
|
|
210
|
+
return this.storage.findOne(this.usersCollection, { where: { normalizedEmail: {
|
|
211
|
+
operator: "eq",
|
|
212
|
+
value: normalizedEmail
|
|
213
|
+
} } });
|
|
214
|
+
}
|
|
215
|
+
isSessionValidForUser(session, user) {
|
|
216
|
+
if (user.deactivatedAt != null) return false;
|
|
217
|
+
if (user.sessionsInvalidatedAt && session.createdAt.getTime() <= user.sessionsInvalidatedAt.getTime()) return false;
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
async isMfaRequired(user) {
|
|
221
|
+
if (this.mfaPolicy.required === true) return true;
|
|
222
|
+
if (this.mfaPolicy.requiredFor) return Boolean(await this.mfaPolicy.requiredFor(user));
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
async hasSatisfiedMfaPolicy(user) {
|
|
226
|
+
if (!await this.isMfaRequired(user)) return true;
|
|
227
|
+
if (!this.mfa) throw new require_auth_errors.AuthConfigurationError("MFA is required by policy, but no MFA manager is configured.");
|
|
228
|
+
return this.mfa.hasActiveFactor(user.id);
|
|
229
|
+
}
|
|
230
|
+
async createAuthenticatedSession(userId, metadata) {
|
|
231
|
+
try {
|
|
232
|
+
const result = await this.sessions.create({
|
|
233
|
+
userId,
|
|
234
|
+
kind: "authenticated",
|
|
235
|
+
metadata
|
|
236
|
+
});
|
|
237
|
+
if (!result.token || result.session.userId !== userId || result.session.kind !== "authenticated") throw new Error("SessionManager returned an invalid authenticated session.");
|
|
238
|
+
return {
|
|
239
|
+
session: toPublicSession(result.session),
|
|
240
|
+
token: result.token
|
|
241
|
+
};
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error instanceof require_auth_errors.AuthSessionError) throw error;
|
|
244
|
+
throw new require_auth_errors.AuthSessionError("Could not create an authenticated session.", { cause: error });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async register(input) {
|
|
248
|
+
this.assertInitialized();
|
|
249
|
+
const normalizedEmail = require_email_utils.normalizeEmail(input.email);
|
|
250
|
+
const name = require_email_utils.normalizeDisplayName(input.name);
|
|
251
|
+
const ipAddress = validateIpAddress(input.ipAddress);
|
|
252
|
+
validatePassword(input.password, this.minimumPasswordLength, this.maximumPasswordLength);
|
|
253
|
+
await this.enforceRateLimits("registration", normalizedEmail, ipAddress);
|
|
254
|
+
const passwordHash = await this.hashPassword(input.password);
|
|
255
|
+
const now = this.now();
|
|
256
|
+
const userId = require_auth_crypto_utils.generateAuthId("user");
|
|
257
|
+
const credentialId = require_auth_crypto_utils.generateAuthId("credential");
|
|
258
|
+
const userRecord = {
|
|
259
|
+
id: userId,
|
|
260
|
+
email: normalizedEmail,
|
|
261
|
+
normalizedEmail,
|
|
262
|
+
emailVerifiedAt: null,
|
|
263
|
+
name,
|
|
264
|
+
image: null,
|
|
265
|
+
deactivatedAt: null,
|
|
266
|
+
sessionsInvalidatedAt: null,
|
|
267
|
+
createdAt: now,
|
|
268
|
+
updatedAt: now,
|
|
269
|
+
revision: 1
|
|
270
|
+
};
|
|
271
|
+
const credentialRecord = {
|
|
272
|
+
id: credentialId,
|
|
273
|
+
userId,
|
|
274
|
+
kind: "password",
|
|
275
|
+
passwordHash,
|
|
276
|
+
passwordUpdatedAt: now,
|
|
277
|
+
createdAt: now,
|
|
278
|
+
updatedAt: now,
|
|
279
|
+
revokedAt: null,
|
|
280
|
+
revision: 1
|
|
281
|
+
};
|
|
282
|
+
const publicUser = toPublicUser(userRecord);
|
|
283
|
+
const mfaRequired = await this.isMfaRequired(publicUser);
|
|
284
|
+
if (mfaRequired && !this.mfa) throw new require_auth_errors.AuthConfigurationError("MFA is required by policy, but no MFA manager is configured.");
|
|
285
|
+
const auditRecord = this.createAuditRecord("account-registered", userId);
|
|
286
|
+
const operations = [
|
|
287
|
+
{
|
|
288
|
+
type: "insert",
|
|
289
|
+
collection: this.usersCollection,
|
|
290
|
+
record: userRecord
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
type: "insert",
|
|
294
|
+
collection: this.credentialsCollection,
|
|
295
|
+
record: credentialRecord
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
type: "insert",
|
|
299
|
+
collection: this.auditCollection,
|
|
300
|
+
record: auditRecord
|
|
301
|
+
}
|
|
302
|
+
];
|
|
303
|
+
try {
|
|
304
|
+
await this.storage.atomicBatch(operations);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (error instanceof _puzzle_ui_storage.StorageDuplicateError) throw new require_auth_errors.AuthAccountConflictError({ cause: error });
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
if (mfaRequired) return this.mfa.createEnrollmentChallenge({
|
|
310
|
+
userId,
|
|
311
|
+
ipAddress
|
|
312
|
+
});
|
|
313
|
+
const authenticated = await this.createAuthenticatedSession(userId, input.sessionMetadata);
|
|
314
|
+
return {
|
|
315
|
+
user: publicUser,
|
|
316
|
+
session: authenticated.session,
|
|
317
|
+
token: authenticated.token
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
async signIn(input) {
|
|
321
|
+
this.assertInitialized();
|
|
322
|
+
const normalizedEmail = require_email_utils.normalizeEmail(input.email);
|
|
323
|
+
const ipAddress = validateIpAddress(input.ipAddress);
|
|
324
|
+
validateSignInPassword(input.password, this.maximumPasswordLength);
|
|
325
|
+
await this.enforceRateLimits("signIn", normalizedEmail, ipAddress);
|
|
326
|
+
const userRecord = await this.findUserByEmail(normalizedEmail);
|
|
327
|
+
const credential = userRecord ? await this.storage.findOne(this.credentialsCollection, { where: {
|
|
328
|
+
userId: {
|
|
329
|
+
operator: "eq",
|
|
330
|
+
value: userRecord.id
|
|
331
|
+
},
|
|
332
|
+
kind: {
|
|
333
|
+
operator: "eq",
|
|
334
|
+
value: "password"
|
|
335
|
+
}
|
|
336
|
+
} }) : null;
|
|
337
|
+
const activeCredential = credential && credential.revokedAt === null ? credential : null;
|
|
338
|
+
const passwordHash = activeCredential?.passwordHash ?? this.dummyPasswordHash;
|
|
339
|
+
const passwordMatches = await this.verifyPassword(input.password, passwordHash);
|
|
340
|
+
if (!userRecord || !activeCredential || !passwordMatches) {
|
|
341
|
+
await this.recordAudit("sign-in-failed", userRecord?.id ?? null, null, { reason: "invalid-credentials" });
|
|
342
|
+
throw new require_auth_errors.AuthInvalidCredentialsError();
|
|
343
|
+
}
|
|
344
|
+
if (userRecord.deactivatedAt != null) {
|
|
345
|
+
await this.recordAudit("sign-in-failed", userRecord.id, null, { reason: "account-deactivated" });
|
|
346
|
+
throw new require_auth_errors.AuthInvalidCredentialsError();
|
|
347
|
+
}
|
|
348
|
+
if (this.requireVerifiedEmail && !userRecord.emailVerifiedAt) {
|
|
349
|
+
await this.recordAudit("sign-in-failed", userRecord.id, null, { reason: "email-not-verified" });
|
|
350
|
+
throw new require_auth_errors.AuthEmailVerificationRequiredError();
|
|
351
|
+
}
|
|
352
|
+
const publicUser = toPublicUser(userRecord);
|
|
353
|
+
const mfaRequired = await this.isMfaRequired(publicUser);
|
|
354
|
+
const mfaPending = await this.mfa?.createSignInChallenge({
|
|
355
|
+
userId: userRecord.id,
|
|
356
|
+
ipAddress
|
|
357
|
+
});
|
|
358
|
+
if (mfaPending) return mfaPending;
|
|
359
|
+
if (mfaRequired) {
|
|
360
|
+
if (!this.mfa) throw new require_auth_errors.AuthConfigurationError("MFA is required by policy, but no MFA manager is configured.");
|
|
361
|
+
return this.mfa.createEnrollmentChallenge({
|
|
362
|
+
userId: userRecord.id,
|
|
363
|
+
ipAddress
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
const authenticated = await this.createAuthenticatedSession(userRecord.id, input.sessionMetadata);
|
|
367
|
+
try {
|
|
368
|
+
await this.recordAudit("sign-in", userRecord.id, authenticated.session.id);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
try {
|
|
371
|
+
await this.sessions.revoke(authenticated.session.id);
|
|
372
|
+
} catch (revokeError) {
|
|
373
|
+
throw new require_auth_errors.AuthSessionError("Could not revoke a session after an audit failure.", { cause: revokeError });
|
|
374
|
+
}
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
user: toPublicUser(userRecord),
|
|
379
|
+
session: authenticated.session,
|
|
380
|
+
token: authenticated.token
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
async validateSession(token) {
|
|
384
|
+
if (typeof token !== "string" || token.length === 0) return null;
|
|
385
|
+
try {
|
|
386
|
+
return await this.sessions.validate(token);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
throw new require_auth_errors.AuthSessionError("Could not validate the authentication session.", { cause: error });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async getCurrentUser(token) {
|
|
392
|
+
this.assertInitialized();
|
|
393
|
+
const session = await this.validateSession(token);
|
|
394
|
+
if (!session || session.kind !== "authenticated" || !session.userId) return null;
|
|
395
|
+
const user = await this.storage.findById(this.usersCollection, session.userId);
|
|
396
|
+
if (!user) return null;
|
|
397
|
+
if (!this.isSessionValidForUser(session, user)) return null;
|
|
398
|
+
if (this.requireVerifiedEmail && !user.emailVerifiedAt) return null;
|
|
399
|
+
const publicUser = toPublicUser(user);
|
|
400
|
+
if (!await this.hasSatisfiedMfaPolicy(publicUser)) return null;
|
|
401
|
+
return publicUser;
|
|
402
|
+
}
|
|
403
|
+
async getAuthState(token) {
|
|
404
|
+
this.assertInitialized();
|
|
405
|
+
const pendingMfa = await this.mfa?.getPendingAuthState(token);
|
|
406
|
+
if (pendingMfa) return pendingMfa;
|
|
407
|
+
const session = await this.validateSession(token);
|
|
408
|
+
if (!session || session.kind !== "authenticated" || !session.userId) return { status: "unauthenticated" };
|
|
409
|
+
const user = await this.storage.findById(this.usersCollection, session.userId);
|
|
410
|
+
if (!user) return { status: "unauthenticated" };
|
|
411
|
+
if (!this.isSessionValidForUser(session, user)) return { status: "unauthenticated" };
|
|
412
|
+
if (this.requireVerifiedEmail && !user.emailVerifiedAt) return { status: "unauthenticated" };
|
|
413
|
+
const publicUser = toPublicUser(user);
|
|
414
|
+
if (!await this.hasSatisfiedMfaPolicy(publicUser)) return {
|
|
415
|
+
status: "mfa-enrollment-required",
|
|
416
|
+
userId: user.id,
|
|
417
|
+
sessionId: session.id
|
|
418
|
+
};
|
|
419
|
+
return {
|
|
420
|
+
status: "authenticated",
|
|
421
|
+
userId: user.id,
|
|
422
|
+
sessionId: session.id
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
async signOut(token) {
|
|
426
|
+
this.assertInitialized();
|
|
427
|
+
const session = await this.validateSession(token);
|
|
428
|
+
if (!session) return;
|
|
429
|
+
try {
|
|
430
|
+
await this.sessions.revoke(session.id);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
throw new require_auth_errors.AuthSessionError("Could not revoke the authentication session.", { cause: error });
|
|
433
|
+
}
|
|
434
|
+
await this.recordAudit("sign-out", session.userId, session.id);
|
|
435
|
+
}
|
|
436
|
+
async signOutEverywhere(userId) {
|
|
437
|
+
this.assertInitialized();
|
|
438
|
+
if (typeof userId !== "string" || userId.trim().length === 0) throw new require_auth_errors.AuthValidationError("A valid user ID is required.");
|
|
439
|
+
try {
|
|
440
|
+
await this.sessions.revokeAll(userId);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
throw new require_auth_errors.AuthSessionError("Could not revoke the user's sessions.", { cause: error });
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
await this.mfa?.revokeChallenges(userId);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
throw new require_auth_errors.AuthProviderError("Could not revoke pending MFA challenges.", { cause: error });
|
|
448
|
+
}
|
|
449
|
+
await this.recordAudit("sign-out-everywhere", userId);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
exports.StorageAuthManager = StorageAuthManager;
|
|
453
|
+
|
|
454
|
+
//# sourceMappingURL=auth.manager.cjs.map
|