@absolutejs/auth 0.58.0 → 0.59.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5848,6 +5848,7 @@ var generateBackupCodes = async (count) => {
5848
5848
  init_constants();
5849
5849
  var DEFAULT_BACKUP_CODE_COUNT = 10;
5850
5850
  var DEFAULT_MFA_ISSUER = "AbsoluteAuth";
5851
+ var DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS = 5 * SECONDS_IN_A_MINUTE * MILLISECONDS_IN_A_SECOND;
5851
5852
  var DEFAULT_MFA_SESSION_TTL_MS = MILLISECONDS_IN_A_DAY;
5852
5853
  var DEFAULT_SMS_CODE_LENGTH = 6;
5853
5854
  var SMS_CODE_TTL_MINUTES = 5;
@@ -5877,6 +5878,9 @@ class VerificationProviderError extends Error {
5877
5878
  }
5878
5879
  }
5879
5880
 
5881
+ // src/mfa/recentAuth.ts
5882
+ var hasRecentAuthentication = (session, maxAgeMs, now = Date.now()) => session.authenticatedAt !== undefined && now >= session.authenticatedAt && now - session.authenticatedAt <= maxAgeMs;
5883
+
5880
5884
  // src/mfa/sms.ts
5881
5885
  var DECIMAL_RADIX2 = 10;
5882
5886
  var MASK_VISIBLE_DIGITS = 4;
@@ -5892,6 +5896,46 @@ var issueSmsCode = async (codeLength, ttlMs) => {
5892
5896
  const expiresAt = Date.now() + ttlMs;
5893
5897
  return { code, expiresAt, hash };
5894
5898
  };
5899
+ var startProviderSmsChallenge = async (input) => {
5900
+ let started;
5901
+ try {
5902
+ started = await input.verificationProvider.start({
5903
+ channel: "sms",
5904
+ purpose: input.purpose,
5905
+ subject: input.userId,
5906
+ to: input.phone
5907
+ });
5908
+ } catch (error) {
5909
+ await input.mfaStore.rollbackSmsChallenge({
5910
+ challengeId: input.challengeId,
5911
+ previous: input.previousEnrollment,
5912
+ userId: input.userId
5913
+ });
5914
+ throw error;
5915
+ }
5916
+ const finalized = await input.mfaStore.finalizeSmsChallenge({
5917
+ challengeId: input.challengeId,
5918
+ expiresAt: started.expiresAt,
5919
+ providerReference: started.reference,
5920
+ userId: input.userId
5921
+ });
5922
+ if (finalized)
5923
+ return started;
5924
+ await input.verificationProvider.cancel({
5925
+ channel: "sms",
5926
+ purpose: input.purpose,
5927
+ reference: started.reference,
5928
+ subject: input.userId,
5929
+ to: input.phone
5930
+ }).catch(() => {
5931
+ return;
5932
+ });
5933
+ throw new SmsChallengeConflictError("SMS challenge claim was lost");
5934
+ };
5935
+
5936
+ class SmsChallengeConflictError extends Error {
5937
+ name = "SmsChallengeConflictError";
5938
+ }
5895
5939
  var checkWithVerificationProvider = async (provider, input) => {
5896
5940
  try {
5897
5941
  return { result: await provider.check(input) };
@@ -5908,7 +5952,9 @@ var issueAndStoreSmsCode = async ({
5908
5952
  enrollment,
5909
5953
  mfaStore,
5910
5954
  onSendSmsCode,
5955
+ previousEnrollment,
5911
5956
  purpose,
5957
+ resendCooldownMs,
5912
5958
  verificationProvider,
5913
5959
  ttlMs,
5914
5960
  userId
@@ -5916,40 +5962,62 @@ var issueAndStoreSmsCode = async ({
5916
5962
  const phone = enrollment.smsPhone;
5917
5963
  if (phone === undefined)
5918
5964
  return;
5919
- if (verificationProvider !== undefined) {
5920
- const started = await verificationProvider.start({
5921
- channel: "sms",
5922
- purpose,
5923
- subject: userId,
5924
- to: phone
5925
- });
5926
- await mfaStore.saveEnrollment({
5965
+ const challengeId = crypto.randomUUID();
5966
+ const now = Date.now();
5967
+ const claimed = await mfaStore.claimSmsChallenge({
5968
+ challengeId,
5969
+ cooldownCutoff: now - resendCooldownMs,
5970
+ enrollment: {
5927
5971
  ...enrollment,
5928
- smsCodeSentAt: Date.now(),
5972
+ smsChallengeId: challengeId,
5973
+ smsCodeSentAt: now,
5929
5974
  smsFailedAttempts: 0,
5930
- smsPendingCodeExpiresAt: started.expiresAt,
5975
+ smsPendingCodeExpiresAt: now + ttlMs,
5931
5976
  smsPendingCodeHash: undefined,
5932
5977
  smsPendingPurpose: purpose,
5933
- smsProviderReference: started.reference,
5934
- updatedAt: Date.now()
5978
+ smsProviderReference: undefined,
5979
+ updatedAt: now
5980
+ }
5981
+ });
5982
+ if (!claimed)
5983
+ throw new SmsChallengeConflictError("SMS resend cooldown active");
5984
+ if (verificationProvider !== undefined) {
5985
+ const started = await startProviderSmsChallenge({
5986
+ challengeId,
5987
+ mfaStore,
5988
+ phone,
5989
+ previousEnrollment,
5990
+ purpose,
5991
+ userId,
5992
+ verificationProvider
5935
5993
  });
5936
5994
  return started.expiresAt;
5937
5995
  }
5938
5996
  const { code, expiresAt, hash } = await issueSmsCode(codeLength, ttlMs);
5939
- await mfaStore.saveEnrollment({
5940
- ...enrollment,
5941
- smsCodeSentAt: Date.now(),
5942
- smsFailedAttempts: 0,
5943
- smsPendingCodeExpiresAt: expiresAt,
5944
- smsPendingCodeHash: hash,
5945
- smsPendingPurpose: purpose,
5946
- smsProviderReference: undefined,
5947
- updatedAt: Date.now()
5997
+ const finalized = await mfaStore.finalizeSmsChallenge({
5998
+ challengeId,
5999
+ expiresAt,
6000
+ hash,
6001
+ userId
5948
6002
  });
5949
- await onSendSmsCode?.({ code, expiresAt, phone, purpose, userId });
6003
+ if (!finalized)
6004
+ throw new SmsChallengeConflictError("SMS challenge claim was lost");
6005
+ try {
6006
+ await onSendSmsCode?.({ code, expiresAt, phone, purpose, userId });
6007
+ } catch (error) {
6008
+ await mfaStore.rollbackSmsChallenge({
6009
+ challengeId,
6010
+ previous: previousEnrollment,
6011
+ userId
6012
+ });
6013
+ throw error;
6014
+ }
5950
6015
  return expiresAt;
5951
6016
  };
5952
6017
  var mapVerificationProviderError = (error) => {
6018
+ if (error instanceof SmsChallengeConflictError) {
6019
+ return { message: error.message, status: "Too Many Requests" };
6020
+ }
5953
6021
  if (!(error instanceof VerificationProviderError))
5954
6022
  return;
5955
6023
  if (error.kind === "rate_limited") {
@@ -5978,6 +6046,7 @@ var mfaSmsRoutes = ({
5978
6046
  authSessionStore,
5979
6047
  getUserId,
5980
6048
  mfaStore,
6049
+ managementAuthMaxAgeMs = DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS,
5981
6050
  onMfaEnrolled,
5982
6051
  onSendSmsCode,
5983
6052
  verificationProvider,
@@ -6004,6 +6073,9 @@ var mfaSmsRoutes = ({
6004
6073
  if (!userSession) {
6005
6074
  return status("Unauthorized", "Authentication required");
6006
6075
  }
6076
+ if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
6077
+ return status("Unauthorized", "Recent authentication required");
6078
+ }
6007
6079
  if (!isE164Phone(phone)) {
6008
6080
  return status("Bad Request", "Phone must be in E.164 format");
6009
6081
  }
@@ -6013,48 +6085,37 @@ var mfaSmsRoutes = ({
6013
6085
  if (existing?.smsCodeSentAt !== undefined && now - existing.smsCodeSentAt < smsResendCooldownMs) {
6014
6086
  return status("Too Many Requests", "SMS resend cooldown active");
6015
6087
  }
6016
- let started;
6017
6088
  try {
6018
- started = verificationProvider ? await verificationProvider.start({
6019
- channel: "sms",
6089
+ await issueAndStoreSmsCode({
6090
+ codeLength: smsCodeLength,
6091
+ enrollment: {
6092
+ backupCodeHashes: existing?.backupCodeHashes ?? [],
6093
+ createdAt: existing?.createdAt ?? now,
6094
+ lastUsedAt: existing?.lastUsedAt,
6095
+ smsFailedAttempts: 0,
6096
+ smsPhone: phone,
6097
+ smsVerified: false,
6098
+ totpFailedAttempts: existing?.totpFailedAttempts ?? 0,
6099
+ totpSecretCiphertext: existing?.totpSecretCiphertext,
6100
+ totpVerified: existing?.totpVerified ?? false,
6101
+ updatedAt: now,
6102
+ userId
6103
+ },
6104
+ mfaStore,
6105
+ onSendSmsCode,
6106
+ previousEnrollment: existing,
6020
6107
  purpose: "mfa_enrollment",
6021
- subject: userId,
6022
- to: phone
6023
- }) : undefined;
6108
+ resendCooldownMs: smsResendCooldownMs,
6109
+ ttlMs: smsCodeTtlMs,
6110
+ userId,
6111
+ verificationProvider
6112
+ });
6024
6113
  } catch (error) {
6025
6114
  const mapped = mapVerificationProviderError(error);
6026
6115
  if (mapped === undefined)
6027
6116
  throw error;
6028
6117
  return status(mapped.status, mapped.message);
6029
6118
  }
6030
- const local = started ? undefined : await issueSmsCode(smsCodeLength, smsCodeTtlMs);
6031
- await mfaStore.saveEnrollment({
6032
- backupCodeHashes: existing?.backupCodeHashes ?? [],
6033
- createdAt: existing?.createdAt ?? now,
6034
- lastUsedAt: existing?.lastUsedAt,
6035
- smsCodeSentAt: now,
6036
- smsFailedAttempts: 0,
6037
- smsPendingCodeExpiresAt: started?.expiresAt ?? local?.expiresAt,
6038
- smsPendingCodeHash: local?.hash,
6039
- smsPendingPurpose: "mfa_enrollment",
6040
- smsPhone: phone,
6041
- smsProviderReference: started?.reference,
6042
- smsVerified: false,
6043
- totpFailedAttempts: existing?.totpFailedAttempts ?? 0,
6044
- totpSecretCiphertext: existing?.totpSecretCiphertext,
6045
- totpVerified: existing?.totpVerified ?? false,
6046
- updatedAt: now,
6047
- userId
6048
- });
6049
- if (local !== undefined && onSendSmsCode !== undefined) {
6050
- await onSendSmsCode({
6051
- code: local.code,
6052
- expiresAt: local.expiresAt,
6053
- phone,
6054
- purpose: "mfa_enrollment",
6055
- userId
6056
- });
6057
- }
6058
6119
  return status("OK", { phone: maskPhone(phone) });
6059
6120
  }, {
6060
6121
  body: t11.Object({ phone: t11.String() }),
@@ -6073,15 +6134,19 @@ var mfaSmsRoutes = ({
6073
6134
  if (!userSession) {
6074
6135
  return status("Unauthorized", "Authentication required");
6075
6136
  }
6137
+ if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
6138
+ return status("Unauthorized", "Recent authentication required");
6139
+ }
6076
6140
  const userId = getUserId(userSession.user);
6077
6141
  const enrollment = await mfaStore.getEnrollment(userId);
6078
6142
  if (!enrollment?.smsPhone) {
6079
6143
  return status("Bad Request", "No SMS enrollment in progress");
6080
6144
  }
6081
6145
  const localCodeHash = enrollment.smsPendingCodeHash;
6146
+ const challengeId = enrollment.smsChallengeId;
6082
6147
  const localCodeExpiresAt = enrollment.smsPendingCodeExpiresAt;
6083
6148
  const providerReference = enrollment.smsProviderReference;
6084
- if (enrollment.smsPendingPurpose !== "mfa_enrollment" || localCodeExpiresAt === undefined) {
6149
+ if (enrollment.smsPendingPurpose !== "mfa_enrollment" || localCodeExpiresAt === undefined || challengeId === undefined) {
6085
6150
  return status("Bad Request", "No SMS enrollment in progress");
6086
6151
  }
6087
6152
  if (verificationProvider === undefined && localCodeHash === undefined) {
@@ -6113,23 +6178,21 @@ var mfaSmsRoutes = ({
6113
6178
  }
6114
6179
  const codeValid = providerResult ? providerResult.status === "approved" : localCodeHash !== undefined && await constantTimeEqual(await hashToken(code), localCodeHash);
6115
6180
  if (!codeValid) {
6116
- await mfaStore.saveEnrollment({
6117
- ...enrollment,
6118
- smsFailedAttempts: (enrollment.smsFailedAttempts ?? 0) + 1,
6119
- updatedAt: Date.now()
6181
+ const attempts = await mfaStore.recordSmsFailure({
6182
+ challengeId,
6183
+ maxAttempts: smsMaxAttempts,
6184
+ userId
6120
6185
  });
6121
- return status(providerResult?.status === "max_attempts_reached" ? "Too Many Requests" : "Bad Request", providerResult?.status === "expired" ? "SMS code expired" : "Invalid SMS code");
6186
+ return status(providerResult?.status === "max_attempts_reached" || attempts === undefined ? "Too Many Requests" : "Bad Request", providerResult?.status === "expired" ? "SMS code expired" : "Invalid SMS code");
6122
6187
  }
6123
- await mfaStore.saveEnrollment({
6124
- ...enrollment,
6125
- smsFailedAttempts: 0,
6126
- smsPendingCodeExpiresAt: undefined,
6127
- smsPendingCodeHash: undefined,
6128
- smsPendingPurpose: undefined,
6129
- smsProviderReference: undefined,
6188
+ const completed = await mfaStore.completeSmsChallenge({
6189
+ challengeId,
6130
6190
  smsVerified: true,
6131
- updatedAt: Date.now()
6191
+ userId
6132
6192
  });
6193
+ if (!completed) {
6194
+ return status("Bad Request", "SMS challenge is no longer active");
6195
+ }
6133
6196
  await onMfaEnrolled?.({ userId });
6134
6197
  return status("OK", { status: "enrolled" });
6135
6198
  }, {
@@ -6206,7 +6269,9 @@ var mfaChallenge = ({
6206
6269
  enrollment,
6207
6270
  mfaStore,
6208
6271
  onSendSmsCode,
6272
+ previousEnrollment: enrollment,
6209
6273
  purpose: "mfa_challenge",
6274
+ resendCooldownMs: smsResendCooldownMs,
6210
6275
  ttlMs: smsCodeTtlMs,
6211
6276
  userId: getUserId(user),
6212
6277
  verificationProvider
@@ -6229,9 +6294,10 @@ var mfaChallenge = ({
6229
6294
  return status("Bad Request", "SMS code required");
6230
6295
  }
6231
6296
  const localCodeHash = enrollment.smsPendingCodeHash;
6297
+ const challengeId = enrollment.smsChallengeId;
6232
6298
  const localCodeExpiresAt = enrollment.smsPendingCodeExpiresAt;
6233
6299
  const providerReference = enrollment.smsProviderReference;
6234
- if (enrollment.smsPendingPurpose !== "mfa_challenge" || localCodeExpiresAt === undefined) {
6300
+ if (enrollment.smsPendingPurpose !== "mfa_challenge" || localCodeExpiresAt === undefined || challengeId === undefined) {
6235
6301
  return status("Bad Request", "No SMS code in progress");
6236
6302
  }
6237
6303
  if (verificationProvider === undefined && localCodeHash === undefined) {
@@ -6267,27 +6333,26 @@ var mfaChallenge = ({
6267
6333
  }
6268
6334
  const smsValid = providerResult ? providerResult.status === "approved" : localCodeHash !== undefined && await constantTimeEqual(await hashToken(code), localCodeHash);
6269
6335
  if (!smsValid) {
6270
- await mfaStore.saveEnrollment({
6271
- ...enrollment,
6272
- smsFailedAttempts: (enrollment.smsFailedAttempts ?? 0) + 1,
6273
- updatedAt: Date.now()
6336
+ const attempts = await mfaStore.recordSmsFailure({
6337
+ challengeId,
6338
+ maxAttempts: smsMaxAttempts,
6339
+ userId: getUserId(user)
6274
6340
  });
6275
6341
  await onMfaChallengeError?.({
6276
6342
  error: new Error("invalid_mfa_code"),
6277
6343
  userId: getUserId(user)
6278
6344
  });
6279
- return status(providerResult?.status === "max_attempts_reached" ? "Too Many Requests" : "Unauthorized", providerResult?.status === "expired" ? "SMS code expired" : "Invalid MFA code");
6345
+ return status(providerResult?.status === "max_attempts_reached" || attempts === undefined ? "Too Many Requests" : "Unauthorized", providerResult?.status === "expired" ? "SMS code expired" : "Invalid MFA code");
6280
6346
  }
6281
- await mfaStore.saveEnrollment({
6282
- ...enrollment,
6347
+ const completed = await mfaStore.completeSmsChallenge({
6348
+ challengeId,
6283
6349
  lastUsedAt: Date.now(),
6284
- smsFailedAttempts: 0,
6285
- smsPendingCodeExpiresAt: undefined,
6286
- smsPendingCodeHash: undefined,
6287
- smsPendingPurpose: undefined,
6288
- smsProviderReference: undefined,
6289
- updatedAt: Date.now()
6350
+ smsVerified: true,
6351
+ userId: getUserId(user)
6290
6352
  });
6353
+ if (!completed) {
6354
+ return status("Unauthorized", "SMS challenge is no longer active");
6355
+ }
6291
6356
  return promote();
6292
6357
  };
6293
6358
  if (factor === "sms")
@@ -6349,6 +6414,7 @@ var mfaManagementRoutes = ({
6349
6414
  authSessionStore,
6350
6415
  getUserId,
6351
6416
  managementRoute = "/auth/mfa",
6417
+ managementAuthMaxAgeMs = DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS,
6352
6418
  mfaStore
6353
6419
  }) => new Elysia19().use(sessionStore()).get(managementRoute, async ({
6354
6420
  cookie: { user_session_id },
@@ -6387,6 +6453,9 @@ var mfaManagementRoutes = ({
6387
6453
  if (!userSession) {
6388
6454
  return status("Unauthorized", "Authentication required");
6389
6455
  }
6456
+ if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
6457
+ return status("Unauthorized", "Recent authentication required");
6458
+ }
6390
6459
  await mfaStore.removeEnrollment(getUserId(userSession.user));
6391
6460
  return status("OK", { status: "disabled" });
6392
6461
  }, { cookie: t13.Cookie({ user_session_id: userSessionIdTypebox }) });
@@ -6400,6 +6469,7 @@ var mfaTotpRoutes = ({
6400
6469
  encryptionKey,
6401
6470
  getUserId,
6402
6471
  issuer = DEFAULT_MFA_ISSUER,
6472
+ managementAuthMaxAgeMs = DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS,
6403
6473
  mfaStore,
6404
6474
  onMfaEnrolled,
6405
6475
  totpSetupRoute = "/auth/mfa/totp/setup",
@@ -6417,6 +6487,9 @@ var mfaTotpRoutes = ({
6417
6487
  if (!userSession) {
6418
6488
  return status("Unauthorized", "Authentication required");
6419
6489
  }
6490
+ if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
6491
+ return status("Unauthorized", "Recent authentication required");
6492
+ }
6420
6493
  const userId = getUserId(userSession.user);
6421
6494
  const secret = generateTotpSecret();
6422
6495
  const existing = await mfaStore.getEnrollment(userId);
@@ -6456,6 +6529,9 @@ var mfaTotpRoutes = ({
6456
6529
  if (!userSession) {
6457
6530
  return status("Unauthorized", "Authentication required");
6458
6531
  }
6532
+ if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
6533
+ return status("Unauthorized", "Recent authentication required");
6534
+ }
6459
6535
  const userId = getUserId(userSession.user);
6460
6536
  const enrollment = await mfaStore.getEnrollment(userId);
6461
6537
  if (!enrollment?.totpSecretCiphertext) {
@@ -31994,14 +32070,85 @@ var cloneEnrollment = (value) => ({
31994
32070
  var createInMemoryMfaStore = () => {
31995
32071
  const enrollments = new Map;
31996
32072
  return {
32073
+ claimSmsChallenge: async ({
32074
+ challengeId,
32075
+ cooldownCutoff,
32076
+ enrollment
32077
+ }) => {
32078
+ const current = enrollments.get(enrollment.userId);
32079
+ if (current?.smsCodeSentAt !== undefined && current.smsCodeSentAt > cooldownCutoff) {
32080
+ return false;
32081
+ }
32082
+ enrollments.set(enrollment.userId, cloneEnrollment({
32083
+ ...enrollment,
32084
+ smsChallengeId: challengeId
32085
+ }));
32086
+ return true;
32087
+ },
32088
+ completeSmsChallenge: async ({
32089
+ challengeId,
32090
+ lastUsedAt,
32091
+ smsVerified,
32092
+ userId
32093
+ }) => {
32094
+ const current = enrollments.get(userId);
32095
+ if (current?.smsChallengeId !== challengeId)
32096
+ return false;
32097
+ enrollments.set(userId, cloneEnrollment({
32098
+ ...current,
32099
+ lastUsedAt: lastUsedAt ?? current.lastUsedAt,
32100
+ smsChallengeId: undefined,
32101
+ smsFailedAttempts: 0,
32102
+ smsPendingCodeExpiresAt: undefined,
32103
+ smsPendingCodeHash: undefined,
32104
+ smsPendingPurpose: undefined,
32105
+ smsProviderReference: undefined,
32106
+ smsVerified,
32107
+ updatedAt: Date.now()
32108
+ }));
32109
+ return true;
32110
+ },
32111
+ finalizeSmsChallenge: async (input) => {
32112
+ const current = enrollments.get(input.userId);
32113
+ if (current?.smsChallengeId !== input.challengeId)
32114
+ return false;
32115
+ enrollments.set(input.userId, cloneEnrollment({
32116
+ ...current,
32117
+ smsPendingCodeExpiresAt: input.expiresAt,
32118
+ smsPendingCodeHash: input.hash,
32119
+ smsProviderReference: input.providerReference,
32120
+ updatedAt: Date.now()
32121
+ }));
32122
+ return true;
32123
+ },
31997
32124
  getEnrollment: async (userId) => {
31998
32125
  const enrollment = enrollments.get(userId);
31999
32126
  return enrollment ? cloneEnrollment(enrollment) : undefined;
32000
32127
  },
32001
32128
  listEnrollments: async () => Array.from(enrollments.values()).map(cloneEnrollment),
32129
+ recordSmsFailure: async ({ challengeId, maxAttempts, userId }) => {
32130
+ const current = enrollments.get(userId);
32131
+ if (current?.smsChallengeId !== challengeId || (current.smsFailedAttempts ?? 0) >= maxAttempts)
32132
+ return;
32133
+ const attempts = (current.smsFailedAttempts ?? 0) + 1;
32134
+ enrollments.set(userId, cloneEnrollment({
32135
+ ...current,
32136
+ smsFailedAttempts: attempts,
32137
+ updatedAt: Date.now()
32138
+ }));
32139
+ return attempts;
32140
+ },
32002
32141
  removeEnrollment: async (userId) => {
32003
32142
  enrollments.delete(userId);
32004
32143
  },
32144
+ rollbackSmsChallenge: async ({ challengeId, previous, userId }) => {
32145
+ if (enrollments.get(userId)?.smsChallengeId !== challengeId)
32146
+ return;
32147
+ if (previous)
32148
+ enrollments.set(userId, cloneEnrollment(previous));
32149
+ else
32150
+ enrollments.delete(userId);
32151
+ },
32005
32152
  saveEnrollment: async (enrollment) => {
32006
32153
  enrollments.set(enrollment.userId, cloneEnrollment(enrollment));
32007
32154
  }
@@ -32014,6 +32161,7 @@ var mfaEnrollmentsTable = pgTable("auth_mfa_enrollments", {
32014
32161
  backup_code_hashes: jsonb("backup_code_hashes").$type().notNull().default([]),
32015
32162
  created_at_ms: bigint("created_at_ms", { mode: "number" }).notNull(),
32016
32163
  last_used_at_ms: bigint("last_used_at_ms", { mode: "number" }),
32164
+ sms_challenge_id: text("sms_challenge_id"),
32017
32165
  sms_code_sent_at_ms: bigint("sms_code_sent_at_ms", { mode: "number" }),
32018
32166
  sms_failed_attempts: smallint("sms_failed_attempts").notNull().default(0),
32019
32167
  sms_pending_code_expires_at_ms: bigint("sms_pending_code_expires_at_ms", {
@@ -32034,6 +32182,7 @@ var toEnrollment = (row) => ({
32034
32182
  backupCodeHashes: row.backup_code_hashes,
32035
32183
  createdAt: row.created_at_ms,
32036
32184
  lastUsedAt: row.last_used_at_ms ?? undefined,
32185
+ smsChallengeId: row.sms_challenge_id ?? undefined,
32037
32186
  smsCodeSentAt: row.sms_code_sent_at_ms ?? undefined,
32038
32187
  smsFailedAttempts: row.sms_failed_attempts,
32039
32188
  smsPendingCodeExpiresAt: row.sms_pending_code_expires_at_ms ?? undefined,
@@ -32049,43 +32198,118 @@ var toEnrollment = (row) => ({
32049
32198
  userId: row.user_id
32050
32199
  });
32051
32200
  var createNeonMfaStore = (databaseUrl) => createPostgresMfaStore(createNeonDatabase(databaseUrl));
32052
- var createPostgresMfaStore = (db) => ({
32053
- getEnrollment: async (userId) => {
32054
- const [row] = await db.select().from(mfaEnrollmentsTable).where(eq(mfaEnrollmentsTable.user_id, userId)).limit(1);
32055
- return row ? toEnrollment(row) : undefined;
32056
- },
32057
- listEnrollments: async () => {
32058
- const rows = await db.select().from(mfaEnrollmentsTable);
32059
- return rows.map(toEnrollment);
32060
- },
32061
- removeEnrollment: async (userId) => {
32062
- await db.delete(mfaEnrollmentsTable).where(eq(mfaEnrollmentsTable.user_id, userId));
32063
- },
32064
- saveEnrollment: async (enrollment) => {
32065
- const values = {
32066
- backup_code_hashes: enrollment.backupCodeHashes,
32067
- created_at_ms: enrollment.createdAt,
32068
- last_used_at_ms: enrollment.lastUsedAt ?? null,
32069
- sms_code_sent_at_ms: enrollment.smsCodeSentAt ?? null,
32070
- sms_failed_attempts: enrollment.smsFailedAttempts ?? 0,
32071
- sms_pending_code_expires_at_ms: enrollment.smsPendingCodeExpiresAt ?? null,
32072
- sms_pending_code_hash: enrollment.smsPendingCodeHash ?? null,
32073
- sms_pending_purpose: enrollment.smsPendingPurpose ?? null,
32074
- sms_phone: enrollment.smsPhone ?? null,
32075
- sms_provider_reference: enrollment.smsProviderReference ?? null,
32076
- sms_verified: enrollment.smsVerified,
32077
- totp_failed_attempts: enrollment.totpFailedAttempts ?? 0,
32078
- totp_secret_ciphertext: enrollment.totpSecretCiphertext ?? null,
32079
- totp_verified: enrollment.totpVerified,
32080
- updated_at_ms: enrollment.updatedAt,
32081
- user_id: enrollment.userId
32082
- };
32083
- await db.insert(mfaEnrollmentsTable).values(values).onConflictDoUpdate({
32084
- set: values,
32085
- target: mfaEnrollmentsTable.user_id
32086
- });
32087
- }
32088
- });
32201
+ var createPostgresMfaStore = (db) => {
32202
+ const toValues = (enrollment) => ({
32203
+ backup_code_hashes: enrollment.backupCodeHashes,
32204
+ created_at_ms: enrollment.createdAt,
32205
+ last_used_at_ms: enrollment.lastUsedAt ?? null,
32206
+ sms_challenge_id: enrollment.smsChallengeId ?? null,
32207
+ sms_code_sent_at_ms: enrollment.smsCodeSentAt ?? null,
32208
+ sms_failed_attempts: enrollment.smsFailedAttempts ?? 0,
32209
+ sms_pending_code_expires_at_ms: enrollment.smsPendingCodeExpiresAt ?? null,
32210
+ sms_pending_code_hash: enrollment.smsPendingCodeHash ?? null,
32211
+ sms_pending_purpose: enrollment.smsPendingPurpose ?? null,
32212
+ sms_phone: enrollment.smsPhone ?? null,
32213
+ sms_provider_reference: enrollment.smsProviderReference ?? null,
32214
+ sms_verified: enrollment.smsVerified,
32215
+ totp_failed_attempts: enrollment.totpFailedAttempts ?? 0,
32216
+ totp_secret_ciphertext: enrollment.totpSecretCiphertext ?? null,
32217
+ totp_verified: enrollment.totpVerified,
32218
+ updated_at_ms: enrollment.updatedAt,
32219
+ user_id: enrollment.userId
32220
+ });
32221
+ return {
32222
+ claimSmsChallenge: async ({
32223
+ challengeId,
32224
+ cooldownCutoff,
32225
+ enrollment
32226
+ }) => {
32227
+ const values = toValues({
32228
+ ...enrollment,
32229
+ smsChallengeId: challengeId
32230
+ });
32231
+ const rows = await db.insert(mfaEnrollmentsTable).values(values).onConflictDoUpdate({
32232
+ set: {
32233
+ sms_challenge_id: challengeId,
32234
+ sms_code_sent_at_ms: enrollment.smsCodeSentAt ?? null,
32235
+ sms_failed_attempts: 0,
32236
+ sms_pending_code_expires_at_ms: enrollment.smsPendingCodeExpiresAt ?? null,
32237
+ sms_pending_code_hash: null,
32238
+ sms_pending_purpose: enrollment.smsPendingPurpose ?? null,
32239
+ sms_phone: enrollment.smsPhone ?? null,
32240
+ sms_provider_reference: null,
32241
+ sms_verified: enrollment.smsVerified,
32242
+ updated_at_ms: enrollment.updatedAt
32243
+ },
32244
+ setWhere: or(isNull(mfaEnrollmentsTable.sms_code_sent_at_ms), lte(mfaEnrollmentsTable.sms_code_sent_at_ms, cooldownCutoff)),
32245
+ target: mfaEnrollmentsTable.user_id
32246
+ }).returning({ userId: mfaEnrollmentsTable.user_id });
32247
+ return rows.length === 1;
32248
+ },
32249
+ completeSmsChallenge: async ({
32250
+ challengeId,
32251
+ lastUsedAt,
32252
+ smsVerified,
32253
+ userId
32254
+ }) => {
32255
+ const rows = await db.update(mfaEnrollmentsTable).set({
32256
+ last_used_at_ms: lastUsedAt,
32257
+ sms_challenge_id: null,
32258
+ sms_failed_attempts: 0,
32259
+ sms_pending_code_expires_at_ms: null,
32260
+ sms_pending_code_hash: null,
32261
+ sms_pending_purpose: null,
32262
+ sms_provider_reference: null,
32263
+ sms_verified: smsVerified,
32264
+ updated_at_ms: Date.now()
32265
+ }).where(and(eq(mfaEnrollmentsTable.user_id, userId), eq(mfaEnrollmentsTable.sms_challenge_id, challengeId))).returning({ userId: mfaEnrollmentsTable.user_id });
32266
+ return rows.length === 1;
32267
+ },
32268
+ finalizeSmsChallenge: async (input) => {
32269
+ const rows = await db.update(mfaEnrollmentsTable).set({
32270
+ sms_pending_code_expires_at_ms: input.expiresAt,
32271
+ sms_pending_code_hash: input.hash ?? null,
32272
+ sms_provider_reference: input.providerReference ?? null,
32273
+ updated_at_ms: Date.now()
32274
+ }).where(and(eq(mfaEnrollmentsTable.user_id, input.userId), eq(mfaEnrollmentsTable.sms_challenge_id, input.challengeId))).returning({ userId: mfaEnrollmentsTable.user_id });
32275
+ return rows.length === 1;
32276
+ },
32277
+ getEnrollment: async (userId) => {
32278
+ const [row] = await db.select().from(mfaEnrollmentsTable).where(eq(mfaEnrollmentsTable.user_id, userId)).limit(1);
32279
+ return row ? toEnrollment(row) : undefined;
32280
+ },
32281
+ listEnrollments: async () => {
32282
+ const rows = await db.select().from(mfaEnrollmentsTable);
32283
+ return rows.map(toEnrollment);
32284
+ },
32285
+ recordSmsFailure: async ({ challengeId, maxAttempts, userId }) => {
32286
+ const rows = await db.update(mfaEnrollmentsTable).set({
32287
+ sms_failed_attempts: sql`${mfaEnrollmentsTable.sms_failed_attempts} + 1`,
32288
+ updated_at_ms: Date.now()
32289
+ }).where(and(eq(mfaEnrollmentsTable.user_id, userId), eq(mfaEnrollmentsTable.sms_challenge_id, challengeId), lte(mfaEnrollmentsTable.sms_failed_attempts, maxAttempts - 1))).returning({
32290
+ attempts: mfaEnrollmentsTable.sms_failed_attempts
32291
+ });
32292
+ return rows[0]?.attempts;
32293
+ },
32294
+ removeEnrollment: async (userId) => {
32295
+ await db.delete(mfaEnrollmentsTable).where(eq(mfaEnrollmentsTable.user_id, userId));
32296
+ },
32297
+ rollbackSmsChallenge: async ({ challengeId, previous, userId }) => {
32298
+ if (previous) {
32299
+ await db.update(mfaEnrollmentsTable).set(toValues(previous)).where(and(eq(mfaEnrollmentsTable.user_id, userId), eq(mfaEnrollmentsTable.sms_challenge_id, challengeId)));
32300
+ return;
32301
+ }
32302
+ await db.delete(mfaEnrollmentsTable).where(and(eq(mfaEnrollmentsTable.user_id, userId), eq(mfaEnrollmentsTable.sms_challenge_id, challengeId)));
32303
+ },
32304
+ saveEnrollment: async (enrollment) => {
32305
+ const values = toValues(enrollment);
32306
+ await db.insert(mfaEnrollmentsTable).values(values).onConflictDoUpdate({
32307
+ set: values,
32308
+ target: mfaEnrollmentsTable.user_id
32309
+ });
32310
+ }
32311
+ };
32312
+ };
32089
32313
  // src/audit/types.ts
32090
32314
  var AUDIT_EVENT_TYPES = [
32091
32315
  "account_deleted",
@@ -36430,6 +36654,10 @@ var mfaSmsDeliveryPolicyMigration = {
36430
36654
  ].join(`
36431
36655
  `)
36432
36656
  };
36657
+ var mfaSmsAtomicChallengeMigration = {
36658
+ id: "0005_sms_atomic_challenge",
36659
+ sql: 'ALTER TABLE "auth_mfa_enrollments" ADD COLUMN IF NOT EXISTS "sms_challenge_id" text;'
36660
+ };
36433
36661
  var oidcResourceAudienceMigration = {
36434
36662
  id: "0002_resource_audience",
36435
36663
  sql: [
@@ -36485,7 +36713,8 @@ var blockMigrations = {
36485
36713
  ...initMigration("mfa", [mfaEnrollmentsTable]).migrations,
36486
36714
  mfaSmsColumnsMigration,
36487
36715
  mfaTotpLockoutMigration,
36488
- mfaSmsDeliveryPolicyMigration
36716
+ mfaSmsDeliveryPolicyMigration,
36717
+ mfaSmsAtomicChallengeMigration
36489
36718
  ]
36490
36719
  },
36491
36720
  oidc: {
@@ -37795,6 +38024,7 @@ export {
37795
38024
  DEFAULT_ORGANIZATIONS_ROUTE,
37796
38025
  DEFAULT_OIDC_ROUTE,
37797
38026
  DEFAULT_MFA_SESSION_TTL_MS,
38027
+ DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS,
37798
38028
  DEFAULT_MFA_ISSUER,
37799
38029
  DEFAULT_MAGIC_LINK_TTL_MS,
37800
38030
  DEFAULT_INVITATION_TTL_MS,
@@ -37810,5 +38040,5 @@ export {
37810
38040
  AGENT_CLAIM_GRANT_TYPE
37811
38041
  };
37812
38042
 
37813
- //# debugId=588FAC011547AB7964756E2164756E21
38043
+ //# debugId=FC3F27A521162E4064756E2164756E21
37814
38044
  //# sourceMappingURL=index.js.map