@vsaas/loopback 12.1.1 → 12.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +6 -0
  2. package/common/models/rate-limit-state.json +121 -0
  3. package/common/models/two-factor-challenge.json +112 -0
  4. package/common/models/two-factor-credential.json +117 -0
  5. package/common/models/two-factor-recovery-code.json +63 -0
  6. package/common/models/user.json +30 -0
  7. package/dist/_virtual/_rolldown/runtime.cjs +9 -2
  8. package/dist/common/models/access-token.d.cts +3 -0
  9. package/dist/common/models/acl.cjs +1 -3
  10. package/dist/common/models/acl.d.cts +1 -0
  11. package/dist/common/models/application.d.cts +3 -0
  12. package/dist/common/models/change.cjs +21 -18
  13. package/dist/common/models/change.d.cts +1 -0
  14. package/dist/common/models/checkpoint.d.cts +3 -0
  15. package/dist/common/models/email.d.cts +3 -0
  16. package/dist/common/models/key-value-model.d.cts +3 -0
  17. package/dist/common/models/rate-limit-state.cjs +7 -0
  18. package/dist/common/models/rate-limit-state.d.cts +3 -0
  19. package/dist/common/models/rate-limit-state2.cjs +109 -0
  20. package/dist/common/models/role-mapping.d.cts +3 -0
  21. package/dist/common/models/role.cjs +12 -8
  22. package/dist/common/models/role.d.cts +1 -0
  23. package/dist/common/models/scope.d.cts +3 -0
  24. package/dist/common/models/two-factor-challenge.cjs +7 -0
  25. package/dist/common/models/two-factor-challenge.d.cts +3 -0
  26. package/dist/common/models/two-factor-challenge2.cjs +108 -0
  27. package/dist/common/models/two-factor-credential.cjs +7 -0
  28. package/dist/common/models/two-factor-credential.d.cts +3 -0
  29. package/dist/common/models/two-factor-credential2.cjs +103 -0
  30. package/dist/common/models/two-factor-recovery-code.cjs +7 -0
  31. package/dist/common/models/two-factor-recovery-code.d.cts +3 -0
  32. package/dist/common/models/two-factor-recovery-code2.cjs +57 -0
  33. package/dist/common/models/user.cjs +261 -21
  34. package/dist/common/models/user.d.cts +3 -0
  35. package/dist/common/models/user2.cjs +30 -0
  36. package/dist/index.cjs +2 -0
  37. package/dist/index.d.cts +3 -0
  38. package/dist/lib/access-context.d.cts +33 -0
  39. package/dist/lib/application.d.cts +1 -0
  40. package/dist/lib/authentication-security-initializer.cjs +351 -0
  41. package/dist/lib/authentication-security-initializer.d.cts +4 -0
  42. package/dist/lib/builtin-models.cjs +12 -0
  43. package/dist/lib/builtin-models.d.cts +3 -0
  44. package/dist/lib/configure-shared-methods.cjs +7 -4
  45. package/dist/lib/configure-shared-methods.d.cts +3 -0
  46. package/dist/lib/connectors/base-connector.d.cts +6 -0
  47. package/dist/lib/connectors/mail-direct-transport.cjs +26 -23
  48. package/dist/lib/connectors/mail-direct-transport.d.cts +22 -0
  49. package/dist/lib/connectors/mail-stub-transport.d.cts +14 -0
  50. package/dist/lib/connectors/mail.d.cts +7 -0
  51. package/dist/lib/connectors/memory.d.cts +6 -0
  52. package/dist/lib/current-context.d.cts +3 -0
  53. package/dist/lib/globalize.d.cts +10 -0
  54. package/dist/lib/loopback.cjs +6 -0
  55. package/dist/lib/loopback.d.cts +1 -0
  56. package/dist/lib/model.d.cts +1 -0
  57. package/dist/lib/persisted-model.cjs +7 -12
  58. package/dist/lib/persisted-model.d.cts +1 -0
  59. package/dist/lib/rate-limit/algorithms/common.cjs +37 -0
  60. package/dist/lib/rate-limit/algorithms/failure-lockout.cjs +70 -0
  61. package/dist/lib/rate-limit/algorithms/fixed-window.cjs +61 -0
  62. package/dist/lib/rate-limit/algorithms/index.cjs +55 -0
  63. package/dist/lib/rate-limit/config.cjs +107 -0
  64. package/dist/lib/rate-limit/errors.cjs +58 -0
  65. package/dist/lib/rate-limit/index.cjs +15 -0
  66. package/dist/lib/rate-limit/index.d.cts +1 -0
  67. package/dist/lib/rate-limit/initializer.cjs +92 -0
  68. package/dist/lib/rate-limit/initializer.d.cts +4 -0
  69. package/dist/lib/rate-limit/limiter.cjs +368 -0
  70. package/dist/lib/rate-limit/registry.cjs +92 -0
  71. package/dist/lib/rate-limit/remote.cjs +180 -0
  72. package/dist/lib/rate-limit/remote.d.cts +6 -0
  73. package/dist/lib/rate-limit/stores/persisted-model.cjs +353 -0
  74. package/dist/lib/rate-limit/subject.cjs +127 -0
  75. package/dist/lib/rate-limit/types.cjs +1 -0
  76. package/dist/lib/rate-limit/types.d.cts +167 -0
  77. package/dist/lib/registry.d.cts +1 -0
  78. package/dist/lib/runtime.d.cts +10 -0
  79. package/dist/lib/server-app.d.cts +3 -0
  80. package/dist/lib/two-factor/config.cjs +199 -0
  81. package/dist/lib/two-factor/config.d.cts +55 -0
  82. package/dist/lib/two-factor/crypto.cjs +68 -0
  83. package/dist/lib/two-factor/crypto.d.cts +16 -0
  84. package/dist/lib/two-factor/errors.cjs +58 -0
  85. package/dist/lib/two-factor/errors.d.cts +18 -0
  86. package/dist/lib/two-factor/rate-limit.cjs +153 -0
  87. package/dist/lib/two-factor/rate-limit.d.cts +22 -0
  88. package/dist/lib/two-factor/recovery-codes.cjs +49 -0
  89. package/dist/lib/two-factor/recovery-codes.d.cts +16 -0
  90. package/dist/lib/two-factor/service.cjs +1108 -0
  91. package/dist/lib/two-factor/service.d.cts +78 -0
  92. package/dist/lib/two-factor/totp.cjs +50 -0
  93. package/dist/lib/two-factor/totp.d.cts +14 -0
  94. package/dist/lib/utils.d.cts +36 -0
  95. package/dist/package.cjs +46 -10
  96. package/dist/server/middleware/context.d.cts +3 -0
  97. package/dist/server/middleware/error-handler.d.cts +3 -0
  98. package/dist/server/middleware/favicon.d.cts +7 -0
  99. package/dist/server/middleware/rest.d.cts +6 -0
  100. package/dist/server/middleware/static.d.cts +3 -0
  101. package/dist/server/middleware/status.d.cts +19 -0
  102. package/dist/server/middleware/token.d.cts +3 -0
  103. package/dist/server/middleware/url-not-found.d.cts +13 -0
  104. package/docs/api-explorer-details.md +110 -0
  105. package/docs/application-guide.md +1110 -0
  106. package/docs/assets/explorer-api.png +0 -0
  107. package/docs/assets/explorer-endpoint.png +0 -0
  108. package/docs/assets/explorer-listing.png +0 -0
  109. package/docs/assets/explorer-req-res.png +0 -0
  110. package/docs/assets/lb-modules.png +0 -0
  111. package/docs/assets/loopback-architecture.png +0 -0
  112. package/docs/assets/loopback-concepts.png +0 -0
  113. package/docs/assets/loopback_ov.png +0 -0
  114. package/docs/rate-limiting.md +879 -0
  115. package/docs/two-factor-authentication.md +611 -0
  116. package/docs/two-factor-authenticator-loss-runbook.md +117 -0
  117. package/package.json +56 -20
@@ -0,0 +1,1108 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_runtime = require("../../_virtual/_rolldown/runtime.cjs");
3
+ const require_lib_two_factor_errors = require("./errors.cjs");
4
+ const require_lib_two_factor_crypto = require("./crypto.cjs");
5
+ const require_lib_two_factor_totp = require("./totp.cjs");
6
+ const require_lib_two_factor_recovery_codes = require("./recovery-codes.cjs");
7
+ //#region src/lib/two-factor/service.ts
8
+ function count(result) {
9
+ return typeof result === "number" ? result : Number(result?.count || 0);
10
+ }
11
+ function optionalCallback(promise, callback) {
12
+ if (typeof callback !== "function") return promise;
13
+ promise.then((result) => callback(null, result), callback);
14
+ }
15
+ function invoke(target, name, args) {
16
+ return new Promise((resolve, reject) => {
17
+ let completed = false;
18
+ const done = (error, value) => {
19
+ if (completed) return;
20
+ completed = true;
21
+ error ? reject(error) : resolve(value);
22
+ };
23
+ let result;
24
+ try {
25
+ result = target[name](...args, done);
26
+ } catch (error) {
27
+ done(error);
28
+ return;
29
+ }
30
+ if (result && typeof result.then === "function") result.then((value) => done(null, value), done);
31
+ });
32
+ }
33
+ function includeUser(include) {
34
+ if (Array.isArray(include)) return include.map(String).map((v) => v.toLowerCase()).includes("user");
35
+ return String(include || "").toLowerCase() === "user";
36
+ }
37
+ function loginError() {
38
+ const error = /* @__PURE__ */ new Error("login failed");
39
+ error.statusCode = 401;
40
+ error.code = "LOGIN_FAILED";
41
+ return error;
42
+ }
43
+ var TwoFactorService;
44
+ var init_service = require_runtime.__esmMin((() => {
45
+ require_lib_two_factor_errors.init_errors();
46
+ require_lib_two_factor_crypto.init_crypto();
47
+ require_lib_two_factor_totp.init_totp();
48
+ require_lib_two_factor_recovery_codes.init_recovery_codes();
49
+ TwoFactorService = class {
50
+ User;
51
+ config;
52
+ rateLimiter;
53
+ app;
54
+ Credential;
55
+ Challenge;
56
+ RecoveryCode;
57
+ dummyPasswordUser;
58
+ constructor(User, config, rateLimiter, app) {
59
+ this.User = User;
60
+ this.config = config;
61
+ this.rateLimiter = rateLimiter;
62
+ this.app = app;
63
+ this.Credential = app.models.TwoFactorCredential;
64
+ this.Challenge = app.models.TwoFactorChallenge;
65
+ this.RecoveryCode = app.models.TwoFactorRecoveryCode;
66
+ this.dummyPasswordUser = {
67
+ password: this.User._hashPasswordWithoutValidation(require_lib_two_factor_crypto.randomToken(24)),
68
+ hasPassword: this.User.prototype.hasPassword
69
+ };
70
+ }
71
+ login(credentials, include, options, callback) {
72
+ return optionalCallback(this.loginInternal(credentials || {}, include, options || {}), callback);
73
+ }
74
+ verifyTwoFactor(verification, options, callback) {
75
+ if (typeof options === "function") {
76
+ callback = options;
77
+ options = {};
78
+ }
79
+ return optionalCallback(this.verifyChallengeInternal(verification || {}, options || {}), callback);
80
+ }
81
+ beginEnrollment(userId, request, options, callback) {
82
+ return optionalCallback(this.beginEnrollmentInternal(userId, request || {}, options || {}), callback);
83
+ }
84
+ confirmEnrollment(userId, request, options, callback) {
85
+ return optionalCallback(this.confirmEnrollmentInternal(userId, request || {}, options || {}), callback);
86
+ }
87
+ regenerateRecoveryCodes(userId, request, options, callback) {
88
+ return optionalCallback(this.regenerateRecoveryCodesInternal(userId, request || {}, options || {}), callback);
89
+ }
90
+ disable(userId, request, options, callback) {
91
+ return optionalCallback(this.disableInternal(userId, request || {}, options || {}), callback);
92
+ }
93
+ deletePrincipals(principalIds, callback) {
94
+ return optionalCallback(this.deletePrincipalsInternal(principalIds.map(String)), callback);
95
+ }
96
+ rotateEncryption(options = {}, callback) {
97
+ return optionalCallback(this.rotateEncryptionInternal(options || {}), callback);
98
+ }
99
+ requireConfig(enrollment = false) {
100
+ if (!this.config) throw require_lib_two_factor_errors.authError("TWO_FACTOR_NOT_CONFIGURED", 404);
101
+ if (enrollment && (this.config.mode === "disabled" || this.config.mode === "verify-only")) throw require_lib_two_factor_errors.authError("TWO_FACTOR_ENROLLMENT_DISABLED", 403);
102
+ return this.config;
103
+ }
104
+ async loginInternal(credentials, include, options) {
105
+ const normalizedInclude = Array.isArray(include) ? include.map((value) => String(value).toLowerCase()) : String(include || "").toLowerCase();
106
+ const realmRequired = !!(this.User.settings.realmRequired || this.User.settings.realmDelimiter);
107
+ const query = this.User.normalizeCredentials(credentials, realmRequired, this.User.settings.realmDelimiter);
108
+ this.validateLoginQuery(query, realmRequired);
109
+ const identifier = this.canonicalIdentifier(query);
110
+ const observed = [];
111
+ const source = await this.resolveSource(options);
112
+ if (this.rateLimiter) {
113
+ if (source && this.rateLimiter.sourceEnabled) await this.rateLimiter.consume(this.rateLimiter.sourceRule("login", source));
114
+ }
115
+ let user;
116
+ try {
117
+ user = await this.User.findOne({ where: query });
118
+ } catch {
119
+ throw loginError();
120
+ }
121
+ const resolvedPrincipalId = user ? String(user[this.User.definition.idName() || "id"]) : null;
122
+ if (this.rateLimiter) {
123
+ const rule = resolvedPrincipalId ? this.rateLimiter.rule("password", "principal", resolvedPrincipalId) : this.rateLimiter.rule("password", "identifier", identifier);
124
+ const result = await this.rateLimiter.inspect(rule);
125
+ observed.push({
126
+ rule,
127
+ revision: result.decision.revision
128
+ });
129
+ }
130
+ if (!await this.hasPassword(user || this.dummyPasswordUser, credentials.password)) {
131
+ if (this.rateLimiter) await this.consumePasswordFailures(identifier, user);
132
+ throw loginError();
133
+ }
134
+ if (this.User.settings.emailVerificationRequired && !user.emailVerified) {
135
+ const error = /* @__PURE__ */ new Error("login failed as the email has not been verified");
136
+ error.statusCode = 401;
137
+ error.code = "LOGIN_FAILED_EMAIL_NOT_VERIFIED";
138
+ error.details = { userId: user.id };
139
+ throw error;
140
+ }
141
+ const principalId = resolvedPrincipalId;
142
+ const credential = this.config && this.config.mode !== "disabled" ? await this.Credential.findOne({ where: {
143
+ principalType: this.User.modelName,
144
+ principalId,
145
+ provider: "totp",
146
+ status: "enabled"
147
+ } }) : null;
148
+ if (!credential) {
149
+ if (this.rateLimiter) await this.rateLimiter.reset(observed);
150
+ return this.issueAccessToken(user, credentials, normalizedInclude, false);
151
+ }
152
+ const secondFactorObserved = [];
153
+ if (this.rateLimiter) {
154
+ const rule = this.rateLimiter.rule("secondFactor", "principal", principalId);
155
+ const result = await this.rateLimiter.inspect(rule);
156
+ secondFactorObserved.push({
157
+ rule,
158
+ revision: result.decision.revision
159
+ });
160
+ if (source && this.rateLimiter.sourceEnabled && credentials.twoFactorCode) await this.rateLimiter.consume(this.rateLimiter.sourceRule("secondFactor", source));
161
+ }
162
+ if (credentials.twoFactorCode) {
163
+ const method = credentials.twoFactorMethod || "totp";
164
+ const binding = await this.principalBinding(user);
165
+ const inlineChallenge = method === "recovery" ? await this.createInlineRecoveryOperation(user, credential, binding) : null;
166
+ const factor = await this.verifyCredentialFactor(credential, method, credentials.twoFactorCode, inlineChallenge ? `challenge:${inlineChallenge.id}` : `inline:${require_lib_two_factor_crypto.randomToken(12)}`);
167
+ if (!factor.valid) {
168
+ if (inlineChallenge) await this.Challenge.updateAll({
169
+ id: inlineChallenge.id,
170
+ status: "verifying"
171
+ }, { status: "locked" });
172
+ if (this.rateLimiter) await this.rateLimiter.consume(this.rateLimiter.rule("secondFactor", "principal", principalId));
173
+ throw require_lib_two_factor_errors.invalidFactor();
174
+ }
175
+ let token;
176
+ try {
177
+ if (inlineChallenge) {
178
+ if (count(await this.Challenge.updateAll({
179
+ id: inlineChallenge.id,
180
+ status: "verifying"
181
+ }, { status: "consuming" })) !== 1) throw require_lib_two_factor_errors.invalidChallenge();
182
+ }
183
+ const reloadedUser = await this.User.findById(principalId);
184
+ if (!reloadedUser) throw require_lib_two_factor_errors.invalidChallenge();
185
+ await this.assertPrincipalBinding(reloadedUser, binding, options);
186
+ if (this.rateLimiter) await this.rateLimiter.reset([...observed, ...secondFactorObserved]);
187
+ token = await this.issueAccessToken(reloadedUser, credentials, normalizedInclude, true, options);
188
+ if (inlineChallenge) {
189
+ if (count(await this.Challenge.updateAll({
190
+ id: inlineChallenge.id,
191
+ status: "consuming"
192
+ }, {
193
+ status: "consumed",
194
+ consumedAt: /* @__PURE__ */ new Date()
195
+ })) !== 1) throw new Error("Inline challenge completion conflict");
196
+ }
197
+ if (factor.reservation) await this.confirmRecoveryReservation(factor.reservation);
198
+ return token;
199
+ } catch (error) {
200
+ if (inlineChallenge) await this.compensateChallengeFailure(inlineChallenge.id, token || error?.issuedToken, factor.reservation);
201
+ throw error;
202
+ }
203
+ }
204
+ if (this.rateLimiter) await this.rateLimiter.consume(this.rateLimiter.rule("challengeIssuance", "principal", principalId));
205
+ const protectedTokenOptions = await this.buildTwoFactorTokenOptions(credentials, options);
206
+ const challenge = await this.createChallenge(user, credential, {
207
+ tokenOptions: protectedTokenOptions,
208
+ rateLimitObserved: observed
209
+ }, includeUser(normalizedInclude), options);
210
+ throw require_lib_two_factor_errors.twoFactorRequired(challenge.raw, Math.ceil(this.config.challengeTtlMs / 1e3), credential.activeRecoveryCodeSetId ? ["totp", "recovery"] : ["totp"]);
211
+ }
212
+ validateLoginQuery(query, realmRequired) {
213
+ const invalid = (code, message) => {
214
+ const error = new Error(message);
215
+ error.statusCode = 400;
216
+ error.code = code;
217
+ throw error;
218
+ };
219
+ if (realmRequired && !query.realm) invalid("REALM_REQUIRED", "realm is required");
220
+ if (query.realm != null && typeof query.realm !== "string") invalid("INVALID_REALM", "Invalid realm");
221
+ if (!query.email && !query.username) invalid("USERNAME_EMAIL_REQUIRED", "username or email is required");
222
+ if (query.username != null && typeof query.username !== "string") invalid("INVALID_USERNAME", "Invalid username");
223
+ if (query.email != null && typeof query.email !== "string") invalid("INVALID_EMAIL", "Invalid email");
224
+ }
225
+ canonicalIdentifier(query) {
226
+ const kind = query.email ? "email" : "username";
227
+ let value = String(query[kind]).trim();
228
+ if (kind === "email" && !this.User.settings.caseSensitiveEmail) value = value.toLowerCase();
229
+ return JSON.stringify([
230
+ String(query.realm || ""),
231
+ kind,
232
+ value
233
+ ]);
234
+ }
235
+ async consumePasswordFailures(identifier, user) {
236
+ if (user) {
237
+ await this.rateLimiter.consume(this.rateLimiter.rule("password", "principal", String(user[this.User.definition.idName() || "id"])));
238
+ return;
239
+ }
240
+ await this.rateLimiter.consume(this.rateLimiter.rule("password", "identifier", identifier));
241
+ }
242
+ async resolveSource(options) {
243
+ if (!this.rateLimiter) return null;
244
+ if (typeof this.User.resolveAuthenticationRateLimitSource === "function") {
245
+ const value = await invoke(this.User, "resolveAuthenticationRateLimitSource", [options]);
246
+ return value == null || value === "" ? null : String(value);
247
+ }
248
+ return options.authenticationSource ? String(options.authenticationSource) : null;
249
+ }
250
+ async hasPassword(user, password) {
251
+ try {
252
+ return await invoke(user, "hasPassword", [password]);
253
+ } catch {
254
+ return false;
255
+ }
256
+ }
257
+ async issueAccessToken(user, tokenInput, include, protectedPath, securityOptions = {}) {
258
+ let tokenOptions = tokenInput;
259
+ if (protectedPath) tokenOptions = await this.buildTwoFactorTokenOptions(tokenInput, securityOptions);
260
+ let token;
261
+ if (user.createAccessToken.length === 2) token = await invoke(user, "createAccessToken", [tokenOptions.ttl]);
262
+ else token = await invoke(user, "createAccessToken", [tokenOptions.ttl, tokenOptions]);
263
+ if (!token?.id) {
264
+ const error = require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
265
+ error.issuedToken = token;
266
+ throw error;
267
+ }
268
+ if (includeUser(include)) token.__data.user = user;
269
+ return token;
270
+ }
271
+ async buildTwoFactorTokenOptions(credentials, options) {
272
+ return invoke(this.User, "buildTwoFactorLoginContext", [credentials, options]);
273
+ }
274
+ secretAad(credential) {
275
+ return [
276
+ "two-factor-secret:v1",
277
+ this.User.modelName,
278
+ String(credential.principalId),
279
+ String(credential.id),
280
+ String(credential.version)
281
+ ];
282
+ }
283
+ challengeAad(challenge) {
284
+ return [
285
+ "two-factor-login-context:v1",
286
+ this.User.modelName,
287
+ String(challenge.principalId),
288
+ String(challenge.id),
289
+ String(challenge.credentialVersion)
290
+ ];
291
+ }
292
+ async principalBinding(user, keyId) {
293
+ const config = this.requireConfig();
294
+ const material = await invoke(this.User, "getTwoFactorPrincipalBindingMaterial", [user, {}]);
295
+ if (typeof material !== "string" && !Buffer.isBuffer(material) || Buffer.byteLength(material) > config.loginContextMaxBytes) throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
296
+ const principalId = String(user[this.User.definition.idName() || "id"]);
297
+ return require_lib_two_factor_crypto.keyedDigest("two-factor-principal-binding:v1", [
298
+ this.User.modelName,
299
+ principalId,
300
+ material
301
+ ], config.principalBindingHmac, keyId);
302
+ }
303
+ async assertPrincipalBinding(user, expected, options) {
304
+ const current = await this.principalBinding(user, expected.keyId);
305
+ if (!require_lib_two_factor_crypto.timingSafeEqual(current.digest, expected.digest)) throw require_lib_two_factor_errors.invalidChallenge();
306
+ if (typeof this.User.validateTwoFactorLoginPrincipal === "function") {
307
+ if (await invoke(this.User, "validateTwoFactorLoginPrincipal", [user, options]) === false) throw require_lib_two_factor_errors.invalidChallenge();
308
+ }
309
+ }
310
+ async createChallenge(user, credential, tokenContext, shouldIncludeUser, options) {
311
+ const config = this.requireConfig();
312
+ const raw = require_lib_two_factor_crypto.randomToken(32);
313
+ const id = require_lib_two_factor_crypto.sha256(raw);
314
+ const principalId = String(user[this.User.definition.idName() || "id"]);
315
+ const binding = await this.principalBinding(user);
316
+ const base = {
317
+ id,
318
+ principalType: this.User.modelName,
319
+ principalId,
320
+ credentialId: String(credential.id),
321
+ credentialVersion: Number(credential.version),
322
+ flowType: "external",
323
+ principalBindingKeyId: binding.keyId,
324
+ principalBindingDigest: binding.digest,
325
+ status: "pending",
326
+ attempts: 0,
327
+ maxAttempts: config.maxAttempts,
328
+ includeUser: shouldIncludeUser,
329
+ createdAt: /* @__PURE__ */ new Date(),
330
+ expiresAt: new Date(Date.now() + config.challengeTtlMs)
331
+ };
332
+ const serialized = Buffer.from(JSON.stringify(tokenContext));
333
+ if (serialized.length > config.loginContextMaxBytes) throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
334
+ const encrypted = require_lib_two_factor_crypto.encrypt(serialized, config.encryption, this.challengeAad(base));
335
+ await this.Challenge.create({
336
+ ...base,
337
+ encryptedLoginContext: encrypted.envelope,
338
+ loginContextKeyId: encrypted.keyId
339
+ });
340
+ this.audit("twoFactorChallengeCreated", {
341
+ principalId,
342
+ credentialId: String(credential.id),
343
+ challengeDigestPrefix: id.slice(0, 12)
344
+ });
345
+ return {
346
+ raw,
347
+ id
348
+ };
349
+ }
350
+ async createInlineRecoveryOperation(user, credential, binding, flowType = "inline-recovery") {
351
+ const config = this.requireConfig();
352
+ const id = require_lib_two_factor_crypto.sha256(require_lib_two_factor_crypto.randomToken(32));
353
+ const principalId = String(user[this.User.definition.idName() || "id"]);
354
+ const base = {
355
+ id,
356
+ principalType: this.User.modelName,
357
+ principalId,
358
+ credentialId: String(credential.id),
359
+ credentialVersion: Number(credential.version),
360
+ flowType,
361
+ principalBindingKeyId: binding.keyId,
362
+ principalBindingDigest: binding.digest,
363
+ status: "verifying",
364
+ attempts: 1,
365
+ maxAttempts: 1,
366
+ includeUser: false,
367
+ createdAt: /* @__PURE__ */ new Date(),
368
+ expiresAt: new Date(Date.now() + config.challengeTtlMs),
369
+ claimedAt: /* @__PURE__ */ new Date()
370
+ };
371
+ const encrypted = require_lib_two_factor_crypto.encrypt("{}", config.encryption, this.challengeAad(base));
372
+ await this.Challenge.create({
373
+ ...base,
374
+ encryptedLoginContext: encrypted.envelope,
375
+ loginContextKeyId: encrypted.keyId
376
+ });
377
+ return { id };
378
+ }
379
+ async verifyChallengeInternal(verification, options) {
380
+ const config = this.requireConfig();
381
+ if (config.mode === "disabled") throw require_lib_two_factor_errors.invalidChallenge();
382
+ if (typeof verification.challengeId !== "string") throw require_lib_two_factor_errors.invalidChallenge();
383
+ const source = await this.resolveSource(options);
384
+ if (this.rateLimiter?.sourceEnabled && source) await this.rateLimiter.consume(this.rateLimiter.sourceRule("secondFactor", source));
385
+ const id = require_lib_two_factor_crypto.sha256(verification.challengeId);
386
+ let challenge = await this.Challenge.findById(id);
387
+ if (challenge?.status === "verifying" && challenge.verificationLeaseExpiresAt && new Date(challenge.verificationLeaseExpiresAt).getTime() <= Date.now()) {
388
+ await this.Challenge.updateAll({
389
+ id,
390
+ status: "verifying",
391
+ verificationLeaseId: challenge.verificationLeaseId
392
+ }, {
393
+ status: "pending",
394
+ verificationLeaseId: null,
395
+ verificationLeaseExpiresAt: null
396
+ });
397
+ challenge = await this.Challenge.findById(id);
398
+ }
399
+ if (!challenge || challenge.principalType !== this.User.modelName || challenge.status !== "pending" || new Date(challenge.expiresAt).getTime() <= Date.now()) {
400
+ if (challenge?.status === "pending" && new Date(challenge.expiresAt).getTime() <= Date.now()) await this.Challenge.updateAll({
401
+ id,
402
+ status: "pending"
403
+ }, { status: "expired" });
404
+ throw require_lib_two_factor_errors.invalidChallenge();
405
+ }
406
+ const principalId = String(challenge.principalId);
407
+ const observed = [];
408
+ if (this.rateLimiter) {
409
+ const rule = this.rateLimiter.rule("secondFactor", "principal", principalId);
410
+ const result = await this.rateLimiter.inspect(rule);
411
+ observed.push({
412
+ rule,
413
+ revision: result.decision.revision
414
+ });
415
+ }
416
+ const leaseId = require_lib_two_factor_crypto.randomToken(16);
417
+ const attempts = Number(challenge.attempts);
418
+ if (count(await this.Challenge.updateAll({
419
+ id,
420
+ status: "pending",
421
+ attempts
422
+ }, {
423
+ status: "verifying",
424
+ attempts: attempts + 1,
425
+ verificationLeaseId: leaseId,
426
+ verificationLeaseExpiresAt: new Date(Date.now() + config.verificationLeaseTtlMs),
427
+ claimedAt: /* @__PURE__ */ new Date()
428
+ })) !== 1) throw require_lib_two_factor_errors.invalidChallenge();
429
+ challenge = await this.Challenge.findById(id);
430
+ try {
431
+ const credential = await this.Credential.findOne({ where: {
432
+ id: challenge.credentialId,
433
+ principalType: this.User.modelName,
434
+ principalId,
435
+ status: "enabled",
436
+ version: challenge.credentialVersion
437
+ } });
438
+ const user = await this.User.findById(principalId);
439
+ if (!credential || !user) throw require_lib_two_factor_errors.invalidChallenge();
440
+ await this.assertPrincipalBinding(user, {
441
+ keyId: challenge.principalBindingKeyId,
442
+ digest: challenge.principalBindingDigest
443
+ }, options);
444
+ const factor = await this.verifyCredentialFactor(credential, verification.method, verification.code, `challenge:${id}`);
445
+ if (!factor.valid) {
446
+ const locked = attempts + 1 >= Number(challenge.maxAttempts);
447
+ await this.Challenge.updateAll({
448
+ id,
449
+ status: "verifying",
450
+ verificationLeaseId: leaseId
451
+ }, {
452
+ status: locked ? "locked" : "pending",
453
+ verificationLeaseId: null,
454
+ verificationLeaseExpiresAt: null
455
+ });
456
+ if (this.rateLimiter) await this.rateLimiter.consume(this.rateLimiter.rule("secondFactor", "principal", principalId));
457
+ throw require_lib_two_factor_errors.invalidFactor();
458
+ }
459
+ let token;
460
+ try {
461
+ if (count(await this.Challenge.updateAll({
462
+ id,
463
+ status: "verifying",
464
+ verificationLeaseId: leaseId
465
+ }, { status: "consuming" })) !== 1) throw require_lib_two_factor_errors.invalidChallenge();
466
+ const tokenContext = JSON.parse(require_lib_two_factor_crypto.decrypt(challenge.encryptedLoginContext, config.encryption, this.challengeAad(challenge)).toString("utf8"));
467
+ if (this.rateLimiter) await this.rateLimiter.reset([...tokenContext.rateLimitObserved || [], ...observed]);
468
+ token = await this.issueAccessToken(user, tokenContext.tokenOptions || {}, challenge.includeUser ? "user" : "", false, options);
469
+ if (count(await this.Challenge.updateAll({
470
+ id,
471
+ status: "consuming"
472
+ }, {
473
+ status: "consumed",
474
+ consumedAt: /* @__PURE__ */ new Date()
475
+ })) !== 1) throw new Error("Challenge completion conflict");
476
+ if (factor.reservation) await this.confirmRecoveryReservation(factor.reservation);
477
+ return token;
478
+ } catch (error) {
479
+ await this.compensateChallengeFailure(id, token || error?.issuedToken, factor.reservation);
480
+ throw error;
481
+ }
482
+ } catch (error) {
483
+ if (error instanceof require_lib_two_factor_errors.AuthenticationSecurityError) throw error;
484
+ throw require_lib_two_factor_errors.invalidChallenge();
485
+ }
486
+ }
487
+ async verifyCredentialFactor(credential, method, code, ownerId) {
488
+ const config = this.requireConfig();
489
+ if (method === "recovery") {
490
+ const reservation = await this.reserveRecoveryCode(credential, code, ownerId);
491
+ return {
492
+ valid: !!reservation,
493
+ reservation: reservation || void 0
494
+ };
495
+ }
496
+ if (method !== "totp") return { valid: false };
497
+ let secret;
498
+ try {
499
+ secret = require_lib_two_factor_crypto.decrypt(credential.encryptedSecret, config.encryption, this.secretAad(credential)).toString("utf8");
500
+ } catch {
501
+ throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
502
+ }
503
+ const verification = require_lib_two_factor_totp.verifyTotp(secret, code, {
504
+ algorithm: credential.totpAlgorithm,
505
+ digits: credential.totpDigits,
506
+ period: credential.totpPeriod
507
+ }, config.maxClockSkewSteps);
508
+ if (!verification.valid || verification.counter <= Number(credential.lastUsedCounter)) return { valid: false };
509
+ return { valid: count(await this.Credential.updateAll({
510
+ id: credential.id,
511
+ status: "enabled",
512
+ version: credential.version,
513
+ lastUsedCounter: credential.lastUsedCounter
514
+ }, {
515
+ lastUsedCounter: verification.counter,
516
+ updatedAt: /* @__PURE__ */ new Date()
517
+ })) === 1 };
518
+ }
519
+ async reserveRecoveryCode(credential, code, ownerId) {
520
+ const config = this.requireConfig();
521
+ const normalized = require_lib_two_factor_recovery_codes.normalizeRecoveryCode(code);
522
+ if (!normalized || !credential.activeRecoveryCodeSetId || credential.activeRecoveryCodeFormatVersion !== 1 || normalized.length !== Number(credential.activeRecoveryCodeLength)) return null;
523
+ for (const keyId of Object.keys(config.recoveryCodeHmac.keys)) {
524
+ const candidate = require_lib_two_factor_recovery_codes.recoveryCodeId(String(credential.id), credential.activeRecoveryCodeSetId, normalized, config.recoveryCodeHmac, keyId);
525
+ const reservationId = require_lib_two_factor_crypto.randomToken(16);
526
+ if (count(await this.RecoveryCode.updateAll({
527
+ id: candidate.digest,
528
+ credentialId: String(credential.id),
529
+ setId: credential.activeRecoveryCodeSetId,
530
+ keyId,
531
+ status: "unused"
532
+ }, {
533
+ status: "reserved",
534
+ reservationId,
535
+ reservationExpiresAt: new Date(Date.now() + config.challengeTtlMs),
536
+ reservationOwnerType: "two-factor-operation",
537
+ reservationOwnerId: ownerId
538
+ })) === 1) return {
539
+ id: candidate.digest,
540
+ reservationId,
541
+ ownerId
542
+ };
543
+ }
544
+ return null;
545
+ }
546
+ async confirmRecoveryReservation(reservation) {
547
+ if (count(await this.RecoveryCode.updateAll({
548
+ id: reservation.id,
549
+ status: "reserved",
550
+ reservationId: reservation.reservationId,
551
+ reservationOwnerId: reservation.ownerId
552
+ }, {
553
+ status: "used",
554
+ usedAt: /* @__PURE__ */ new Date(),
555
+ reservationId: null,
556
+ reservationExpiresAt: null,
557
+ reservationOwnerType: null,
558
+ reservationOwnerId: null
559
+ })) !== 1) throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
560
+ }
561
+ async releaseRecoveryReservation(reservation) {
562
+ if (count(await this.RecoveryCode.updateAll({
563
+ id: reservation.id,
564
+ status: "reserved",
565
+ reservationId: reservation.reservationId,
566
+ reservationOwnerId: reservation.ownerId
567
+ }, {
568
+ status: "unused",
569
+ reservationId: null,
570
+ reservationExpiresAt: null,
571
+ reservationOwnerType: null,
572
+ reservationOwnerId: null
573
+ })) !== 1) throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
574
+ }
575
+ async compensateChallengeFailure(challengeId, token, reservation) {
576
+ let revocationConfirmed = token == null;
577
+ let revocationError = token && !token.id ? /* @__PURE__ */ new Error("Issued access token has no durable identifier") : null;
578
+ if (token?.id) try {
579
+ await this.revokeToken(token);
580
+ revocationConfirmed = true;
581
+ } catch (error) {
582
+ revocationError = error;
583
+ }
584
+ await this.Challenge.updateAll({
585
+ id: challengeId,
586
+ status: { inq: [
587
+ "pending",
588
+ "verifying",
589
+ "reconciling_no_token",
590
+ "consuming",
591
+ "consumed",
592
+ "token_revocation_pending"
593
+ ] }
594
+ }, {
595
+ status: revocationConfirmed ? "token_failed" : "token_revocation_pending",
596
+ issuedTokenId: token?.id == null ? null : String(token.id),
597
+ verificationLeaseId: null,
598
+ verificationLeaseExpiresAt: null
599
+ }).catch(() => void 0);
600
+ if (reservation && revocationConfirmed) await this.releaseRecoveryReservation(reservation);
601
+ if (!revocationConfirmed) {
602
+ this.audit("twoFactorTokenRevocationPending", {
603
+ challengeId,
604
+ tokenId: String(token.id),
605
+ cause: revocationError?.message
606
+ });
607
+ throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503, "Authentication security reconciliation required");
608
+ }
609
+ }
610
+ async beginEnrollmentInternal(userId, request, options) {
611
+ const config = this.requireConfig(true);
612
+ const user = await this.authenticatedUser(userId, options);
613
+ const managementObserved = await this.reauthenticateManagement(user, request.currentPassword);
614
+ const principalId = String(userId);
615
+ const existing = await this.Credential.findOne({ where: {
616
+ principalType: this.User.modelName,
617
+ principalId,
618
+ provider: "totp"
619
+ } });
620
+ if (existing?.status === "enabled") throw require_lib_two_factor_errors.authError("TWO_FACTOR_ALREADY_ENABLED", 409);
621
+ const id = existing ? String(existing.id) : require_lib_two_factor_crypto.randomToken(24);
622
+ const version = existing ? Number(existing.version) + 1 : 1;
623
+ const secret = require_lib_two_factor_totp.generateTotpSecret(config.totp.secretBytes);
624
+ const enrollmentId = require_lib_two_factor_crypto.randomToken(32);
625
+ const base = {
626
+ id,
627
+ principalType: this.User.modelName,
628
+ principalId,
629
+ provider: "totp",
630
+ totpAlgorithm: config.totp.algorithm,
631
+ totpDigits: config.totp.digits,
632
+ totpPeriod: config.totp.period,
633
+ status: "pending",
634
+ version,
635
+ enrollmentTokenDigest: require_lib_two_factor_crypto.sha256(enrollmentId),
636
+ activeRecoveryCodeSetId: null,
637
+ activeRecoveryCodeFormatVersion: null,
638
+ activeRecoveryCodeLength: null,
639
+ lastUsedCounter: -1,
640
+ pendingExpiresAt: new Date(Date.now() + config.enrollmentTtlMs),
641
+ confirmedAt: null,
642
+ disabledAt: null,
643
+ updatedAt: /* @__PURE__ */ new Date()
644
+ };
645
+ const encrypted = require_lib_two_factor_crypto.encrypt(secret, config.encryption, this.secretAad(base));
646
+ const data = {
647
+ ...base,
648
+ encryptedSecret: encrypted.envelope,
649
+ secretKeyId: encrypted.keyId
650
+ };
651
+ if (existing) {
652
+ if (count(await this.Credential.updateAll({
653
+ id,
654
+ version: existing.version,
655
+ status: existing.status
656
+ }, data)) !== 1) throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
657
+ } else await this.Credential.create({
658
+ ...data,
659
+ createdAt: /* @__PURE__ */ new Date()
660
+ });
661
+ this.audit("twoFactorEnrollmentStarted", {
662
+ principalId,
663
+ credentialId: id
664
+ });
665
+ if (this.rateLimiter && managementObserved) await this.rateLimiter.reset([managementObserved]);
666
+ return {
667
+ enrollmentId,
668
+ secret,
669
+ keyUri: require_lib_two_factor_totp.generateTotpUri(secret, config.issuer, String(user.email || user.username || principalId), config.totp),
670
+ expiresIn: Math.ceil(config.enrollmentTtlMs / 1e3)
671
+ };
672
+ }
673
+ async confirmEnrollmentInternal(userId, request, options) {
674
+ const config = this.requireConfig(true);
675
+ await this.authenticatedUser(userId, options);
676
+ const principalId = String(userId);
677
+ let managementObserved = null;
678
+ if (this.rateLimiter) {
679
+ const rule = this.rateLimiter.rule("management", "principal", principalId);
680
+ managementObserved = {
681
+ rule,
682
+ revision: (await this.rateLimiter.inspect(rule)).decision.revision
683
+ };
684
+ }
685
+ const credential = await this.Credential.findOne({ where: {
686
+ principalType: this.User.modelName,
687
+ principalId,
688
+ provider: "totp",
689
+ status: "pending"
690
+ } });
691
+ if (!credential) throw require_lib_two_factor_errors.authError("TWO_FACTOR_ENROLLMENT_EXPIRED", 410);
692
+ if (new Date(credential.pendingExpiresAt).getTime() <= Date.now()) throw require_lib_two_factor_errors.authError("TWO_FACTOR_ENROLLMENT_EXPIRED", 410);
693
+ if (typeof request.enrollmentId !== "string" || !require_lib_two_factor_crypto.timingSafeEqual(require_lib_two_factor_crypto.sha256(request.enrollmentId), credential.enrollmentTokenDigest)) {
694
+ await this.consumeManagementFailure(principalId);
695
+ throw require_lib_two_factor_errors.authError("TWO_FACTOR_ENROLLMENT_EXPIRED", 410);
696
+ }
697
+ const secret = require_lib_two_factor_crypto.decrypt(credential.encryptedSecret, config.encryption, this.secretAad(credential)).toString("utf8");
698
+ const verification = require_lib_two_factor_totp.verifyTotp(secret, request.code, {
699
+ algorithm: credential.totpAlgorithm,
700
+ digits: credential.totpDigits,
701
+ period: credential.totpPeriod
702
+ }, config.maxClockSkewSteps);
703
+ if (!verification.valid) {
704
+ await this.consumeManagementFailure(principalId);
705
+ throw require_lib_two_factor_errors.invalidFactor();
706
+ }
707
+ const generated = require_lib_two_factor_recovery_codes.generateRecoveryCodes(config.recoveryCodeCount, config.recoveryCodeBytes);
708
+ await this.persistRecoverySet(credential, generated);
709
+ try {
710
+ await this.revokeSessions(principalId, options, config.sessions.onEnable);
711
+ if (count(await this.Credential.updateAll({
712
+ id: credential.id,
713
+ status: "pending",
714
+ version: credential.version
715
+ }, {
716
+ status: "enabled",
717
+ enrollmentTokenDigest: null,
718
+ pendingExpiresAt: null,
719
+ lastUsedCounter: verification.counter,
720
+ activeRecoveryCodeSetId: generated.setId,
721
+ activeRecoveryCodeFormatVersion: 1,
722
+ activeRecoveryCodeLength: generated.normalized[0].length,
723
+ confirmedAt: /* @__PURE__ */ new Date(),
724
+ updatedAt: /* @__PURE__ */ new Date()
725
+ })) !== 1) throw new Error("Credential activation conflict");
726
+ } catch (error) {
727
+ await this.RecoveryCode.destroyAll({
728
+ credentialId: String(credential.id),
729
+ setId: generated.setId
730
+ }).catch(() => void 0);
731
+ throw error;
732
+ }
733
+ this.audit("twoFactorEnabled", {
734
+ principalId,
735
+ credentialId: String(credential.id)
736
+ });
737
+ if (this.rateLimiter && managementObserved) await this.rateLimiter.reset([managementObserved]);
738
+ return {
739
+ enabled: true,
740
+ recoveryCodes: generated.codes
741
+ };
742
+ }
743
+ async regenerateRecoveryCodesInternal(userId, request, options) {
744
+ const config = this.requireConfig();
745
+ if (config.mode === "disabled") throw require_lib_two_factor_errors.authError("TWO_FACTOR_NOT_ENABLED", 409);
746
+ const user = await this.authenticatedUser(userId, options);
747
+ const managementObserved = await this.reauthenticateManagement(user, request.currentPassword);
748
+ const credential = await this.enabledCredential(String(userId));
749
+ const recoveryOperation = request.method === "recovery" ? await this.createInlineRecoveryOperation(user, credential, await this.principalBinding(user), "management-recovery-regeneration") : null;
750
+ const factor = await this.verifyCredentialFactor(credential, request.method, request.code, recoveryOperation ? `challenge:${recoveryOperation.id}` : `management:${require_lib_two_factor_crypto.randomToken(16)}`);
751
+ if (!factor.valid) {
752
+ if (recoveryOperation) await this.Challenge.updateAll({
753
+ id: recoveryOperation.id,
754
+ status: "verifying"
755
+ }, { status: "locked" });
756
+ await this.consumeManagementFailure(String(userId));
757
+ throw require_lib_two_factor_errors.invalidFactor();
758
+ }
759
+ let generated = null;
760
+ let reservationConfirmed = false;
761
+ let switched = false;
762
+ const previousSetId = credential.activeRecoveryCodeSetId;
763
+ try {
764
+ if (recoveryOperation) {
765
+ if (count(await this.Challenge.updateAll({
766
+ id: recoveryOperation.id,
767
+ status: "verifying"
768
+ }, { status: "consuming" })) !== 1) throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
769
+ }
770
+ if (this.rateLimiter && managementObserved) await this.rateLimiter.reset([managementObserved]);
771
+ generated = require_lib_two_factor_recovery_codes.generateRecoveryCodes(config.recoveryCodeCount, config.recoveryCodeBytes);
772
+ await this.persistRecoverySet(credential, generated);
773
+ await this.revokeSessions(String(userId), options, config.sessions.onRecoveryCodesRegenerated);
774
+ if (factor.reservation) {
775
+ await this.confirmRecoveryReservation(factor.reservation);
776
+ reservationConfirmed = true;
777
+ }
778
+ if (count(await this.Credential.updateAll({
779
+ id: credential.id,
780
+ status: "enabled",
781
+ activeRecoveryCodeSetId: previousSetId
782
+ }, {
783
+ activeRecoveryCodeSetId: generated.setId,
784
+ activeRecoveryCodeFormatVersion: 1,
785
+ activeRecoveryCodeLength: generated.normalized[0].length,
786
+ updatedAt: /* @__PURE__ */ new Date()
787
+ })) !== 1) throw new Error("Recovery set switch conflict");
788
+ switched = true;
789
+ if (recoveryOperation) await this.Challenge.updateAll({
790
+ id: recoveryOperation.id,
791
+ status: "consuming"
792
+ }, {
793
+ status: "consumed",
794
+ consumedAt: /* @__PURE__ */ new Date()
795
+ }).catch((error) => this.reportCleanupError(error));
796
+ } catch (error) {
797
+ if (generated && !switched) await this.RecoveryCode.destroyAll({
798
+ credentialId: String(credential.id),
799
+ setId: generated.setId
800
+ }).catch((cleanupError) => this.reportCleanupError(cleanupError));
801
+ if (factor.reservation && !reservationConfirmed) await this.releaseRecoveryReservation(factor.reservation);
802
+ if (recoveryOperation) await this.Challenge.updateAll({
803
+ id: recoveryOperation.id,
804
+ status: { inq: ["verifying", "consuming"] }
805
+ }, { status: "token_failed" }).catch((cleanupError) => this.reportCleanupError(cleanupError));
806
+ throw error;
807
+ }
808
+ if (previousSetId) await this.RecoveryCode.destroyAll({
809
+ credentialId: String(credential.id),
810
+ setId: previousSetId
811
+ }).catch((error) => this.reportCleanupError(error));
812
+ this.audit("twoFactorRecoveryCodesRegenerated", {
813
+ principalId: String(userId),
814
+ credentialId: String(credential.id)
815
+ });
816
+ return { recoveryCodes: generated.codes };
817
+ }
818
+ async disableInternal(userId, request, options) {
819
+ const config = this.requireConfig();
820
+ if (config.mode === "disabled") throw require_lib_two_factor_errors.authError("TWO_FACTOR_NOT_ENABLED", 409);
821
+ const user = await this.authenticatedUser(userId, options);
822
+ const managementObserved = await this.reauthenticateManagement(user, request.currentPassword);
823
+ const credential = await this.enabledCredential(String(userId));
824
+ const recoveryOperation = request.method === "recovery" ? await this.createInlineRecoveryOperation(user, credential, await this.principalBinding(user), "management-recovery-disable") : null;
825
+ const factor = await this.verifyCredentialFactor(credential, request.method, request.code, recoveryOperation ? `challenge:${recoveryOperation.id}` : `management:${require_lib_two_factor_crypto.randomToken(16)}`);
826
+ if (!factor.valid) {
827
+ if (recoveryOperation) await this.Challenge.updateAll({
828
+ id: recoveryOperation.id,
829
+ status: "verifying"
830
+ }, { status: "locked" });
831
+ await this.consumeManagementFailure(String(userId));
832
+ throw require_lib_two_factor_errors.invalidFactor();
833
+ }
834
+ let reservationConfirmed = false;
835
+ try {
836
+ if (recoveryOperation) {
837
+ if (count(await this.Challenge.updateAll({
838
+ id: recoveryOperation.id,
839
+ status: "verifying"
840
+ }, { status: "consuming" })) !== 1) throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
841
+ }
842
+ if (this.rateLimiter && managementObserved) await this.rateLimiter.reset([managementObserved]);
843
+ await this.revokeSessions(String(userId), options, config.sessions.onDisable);
844
+ if (factor.reservation) {
845
+ await this.confirmRecoveryReservation(factor.reservation);
846
+ reservationConfirmed = true;
847
+ }
848
+ if (count(await this.Credential.updateAll({
849
+ id: credential.id,
850
+ status: "enabled",
851
+ version: credential.version
852
+ }, {
853
+ status: "disabled",
854
+ version: Number(credential.version) + 1,
855
+ encryptedSecret: null,
856
+ secretKeyId: null,
857
+ activeRecoveryCodeSetId: null,
858
+ activeRecoveryCodeFormatVersion: null,
859
+ activeRecoveryCodeLength: null,
860
+ disabledAt: /* @__PURE__ */ new Date(),
861
+ updatedAt: /* @__PURE__ */ new Date()
862
+ })) !== 1) throw new Error("Credential disable conflict");
863
+ if (recoveryOperation) await this.Challenge.updateAll({
864
+ id: recoveryOperation.id,
865
+ status: "consuming"
866
+ }, {
867
+ status: "consumed",
868
+ consumedAt: /* @__PURE__ */ new Date()
869
+ }).catch((error) => this.reportCleanupError(error));
870
+ } catch (error) {
871
+ if (factor.reservation && !reservationConfirmed) await this.releaseRecoveryReservation(factor.reservation);
872
+ if (recoveryOperation) await this.Challenge.updateAll({
873
+ id: recoveryOperation.id,
874
+ status: { inq: ["verifying", "consuming"] }
875
+ }, { status: "token_failed" }).catch((cleanupError) => this.reportCleanupError(cleanupError));
876
+ throw error;
877
+ }
878
+ await Promise.all([this.RecoveryCode.destroyAll({ credentialId: String(credential.id) }), this.Challenge.updateAll({
879
+ credentialId: String(credential.id),
880
+ status: { inq: ["pending", "verifying"] }
881
+ }, { status: "expired" })]).catch((error) => this.reportCleanupError(error));
882
+ this.audit("twoFactorDisabled", {
883
+ principalId: String(userId),
884
+ credentialId: String(credential.id)
885
+ });
886
+ return { enabled: false };
887
+ }
888
+ async authenticatedUser(userId, options) {
889
+ const token = options?.accessToken;
890
+ if (!token || String(token.userId) !== String(userId) || "principalType" in token && token.principalType !== this.User.modelName) throw require_lib_two_factor_errors.authError("AUTHORIZATION_REQUIRED", 401, "Authorization required");
891
+ const user = await this.User.findById(userId);
892
+ if (!user) throw require_lib_two_factor_errors.authError("AUTHORIZATION_REQUIRED", 401, "Authorization required");
893
+ return user;
894
+ }
895
+ async reauthenticateManagement(user, currentPassword) {
896
+ const principalId = String(user[this.User.definition.idName() || "id"]);
897
+ let observed = null;
898
+ if (this.rateLimiter) {
899
+ const rule = this.rateLimiter.rule("management", "principal", principalId);
900
+ observed = {
901
+ rule,
902
+ revision: (await this.rateLimiter.inspect(rule)).decision.revision
903
+ };
904
+ }
905
+ if (!await this.hasPassword(user, currentPassword)) {
906
+ await this.consumeManagementFailure(principalId);
907
+ throw loginError();
908
+ }
909
+ return observed;
910
+ }
911
+ async consumeManagementFailure(principalId) {
912
+ if (this.rateLimiter) await this.rateLimiter.consume(this.rateLimiter.rule("management", "principal", principalId));
913
+ }
914
+ async enabledCredential(principalId) {
915
+ const credential = await this.Credential.findOne({ where: {
916
+ principalType: this.User.modelName,
917
+ principalId,
918
+ provider: "totp",
919
+ status: "enabled"
920
+ } });
921
+ if (!credential) throw require_lib_two_factor_errors.authError("TWO_FACTOR_NOT_ENABLED", 409);
922
+ return credential;
923
+ }
924
+ async persistRecoverySet(credential, generated) {
925
+ const config = this.requireConfig();
926
+ const rows = generated.normalized.map((code) => {
927
+ const digest = require_lib_two_factor_recovery_codes.recoveryCodeId(String(credential.id), generated.setId, code, config.recoveryCodeHmac);
928
+ return {
929
+ id: digest.digest,
930
+ credentialId: String(credential.id),
931
+ setId: generated.setId,
932
+ keyId: digest.keyId,
933
+ status: "unused",
934
+ createdAt: /* @__PURE__ */ new Date()
935
+ };
936
+ });
937
+ await this.RecoveryCode.create(rows);
938
+ if (await this.RecoveryCode.count({
939
+ credentialId: String(credential.id),
940
+ setId: generated.setId,
941
+ status: "unused"
942
+ }) !== rows.length) throw new Error("Incomplete recovery code set");
943
+ }
944
+ async deletePrincipalsInternal(principalIds) {
945
+ if (!principalIds.length) return;
946
+ const credentialIds = (await this.Credential.find({
947
+ where: {
948
+ principalType: this.User.modelName,
949
+ principalId: { inq: principalIds }
950
+ },
951
+ fields: { id: true }
952
+ })).map((credential) => String(credential.id));
953
+ await Promise.all([
954
+ credentialIds.length ? this.RecoveryCode.destroyAll({ credentialId: { inq: credentialIds } }) : Promise.resolve(),
955
+ this.Challenge.destroyAll({
956
+ principalType: this.User.modelName,
957
+ principalId: { inq: principalIds }
958
+ }),
959
+ this.Credential.destroyAll({
960
+ principalType: this.User.modelName,
961
+ principalId: { inq: principalIds }
962
+ }),
963
+ this.rateLimiter ? Promise.all(principalIds.map((principalId) => this.rateLimiter.deletePrincipal(principalId))) : Promise.resolve()
964
+ ]);
965
+ }
966
+ async rotateEncryptionInternal(options) {
967
+ const config = this.requireConfig();
968
+ const batchSize = options.batchSize == null ? 100 : Number(options.batchSize);
969
+ const maxBatches = options.maxBatches == null ? 1e4 : Number(options.maxBatches);
970
+ if (!Number.isSafeInteger(batchSize) || batchSize < 1 || batchSize > 1e3) throw new Error("batchSize must be an integer between 1 and 1000");
971
+ if (!Number.isSafeInteger(maxBatches) || maxBatches < 1 || maxBatches > 1e5) throw new Error("maxBatches must be an integer between 1 and 100000");
972
+ const currentKeyId = config.encryption.currentKeyId;
973
+ const result = {
974
+ credentialsReencrypted: 0,
975
+ challengesReencrypted: 0,
976
+ conflicts: 0,
977
+ remainingCredentials: 0,
978
+ remainingChallenges: 0
979
+ };
980
+ for (let batch = 0; batch < maxBatches; batch++) {
981
+ const credentials = await this.Credential.find({
982
+ where: {
983
+ principalType: this.User.modelName,
984
+ encryptedSecret: { neq: null },
985
+ secretKeyId: { neq: currentKeyId }
986
+ },
987
+ limit: batchSize
988
+ });
989
+ if (!credentials.length) break;
990
+ let progress = 0;
991
+ for (const credential of credentials) {
992
+ let plaintext;
993
+ try {
994
+ plaintext = require_lib_two_factor_crypto.decrypt(credential.encryptedSecret, config.encryption, this.secretAad(credential));
995
+ } catch {
996
+ throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503, "Unable to decrypt a two-factor credential during key rotation");
997
+ }
998
+ const encrypted = require_lib_two_factor_crypto.encrypt(plaintext, config.encryption, this.secretAad(credential));
999
+ if (count(await this.Credential.updateAll({
1000
+ id: credential.id,
1001
+ principalType: this.User.modelName,
1002
+ version: credential.version,
1003
+ encryptedSecret: credential.encryptedSecret,
1004
+ secretKeyId: credential.secretKeyId
1005
+ }, {
1006
+ encryptedSecret: encrypted.envelope,
1007
+ secretKeyId: encrypted.keyId,
1008
+ updatedAt: /* @__PURE__ */ new Date()
1009
+ })) === 1) {
1010
+ result.credentialsReencrypted++;
1011
+ progress++;
1012
+ } else result.conflicts++;
1013
+ }
1014
+ if (progress === 0) break;
1015
+ }
1016
+ const liveChallengeWhere = {
1017
+ principalType: this.User.modelName,
1018
+ loginContextKeyId: { neq: currentKeyId },
1019
+ expiresAt: { gt: /* @__PURE__ */ new Date() },
1020
+ status: { inq: [
1021
+ "pending",
1022
+ "verifying",
1023
+ "reconciling_no_token",
1024
+ "consuming",
1025
+ "token_revocation_pending"
1026
+ ] }
1027
+ };
1028
+ for (let batch = 0; batch < maxBatches; batch++) {
1029
+ const challenges = await this.Challenge.find({
1030
+ where: liveChallengeWhere,
1031
+ limit: batchSize
1032
+ });
1033
+ if (!challenges.length) break;
1034
+ let progress = 0;
1035
+ for (const challenge of challenges) {
1036
+ let plaintext;
1037
+ try {
1038
+ plaintext = require_lib_two_factor_crypto.decrypt(challenge.encryptedLoginContext, config.encryption, this.challengeAad(challenge));
1039
+ } catch {
1040
+ throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503, "Unable to decrypt a two-factor challenge during key rotation");
1041
+ }
1042
+ const encrypted = require_lib_two_factor_crypto.encrypt(plaintext, config.encryption, this.challengeAad(challenge));
1043
+ if (count(await this.Challenge.updateAll({
1044
+ id: challenge.id,
1045
+ principalType: this.User.modelName,
1046
+ status: challenge.status,
1047
+ encryptedLoginContext: challenge.encryptedLoginContext,
1048
+ loginContextKeyId: challenge.loginContextKeyId
1049
+ }, {
1050
+ encryptedLoginContext: encrypted.envelope,
1051
+ loginContextKeyId: encrypted.keyId
1052
+ })) === 1) {
1053
+ result.challengesReencrypted++;
1054
+ progress++;
1055
+ } else result.conflicts++;
1056
+ }
1057
+ if (progress === 0) break;
1058
+ }
1059
+ result.remainingCredentials = await this.Credential.count({
1060
+ principalType: this.User.modelName,
1061
+ encryptedSecret: { neq: null },
1062
+ secretKeyId: { neq: currentKeyId }
1063
+ });
1064
+ result.remainingChallenges = await this.Challenge.count(liveChallengeWhere);
1065
+ this.audit("twoFactorEncryptionRotated", {
1066
+ keyId: currentKeyId,
1067
+ ...result
1068
+ });
1069
+ return result;
1070
+ }
1071
+ async revokeSessions(principalId, options, policy) {
1072
+ const AccessToken = this.User.relations.accessTokens?.modelTo;
1073
+ if (!AccessToken) return;
1074
+ const where = { userId: principalId };
1075
+ if (policy === "revoke-others" && options?.accessToken?.id) where.id = { neq: options.accessToken.id };
1076
+ if (AccessToken.relations?.user?.polymorphic) where.principalType = this.User.modelName;
1077
+ await AccessToken.destroyAll(where, options || {});
1078
+ }
1079
+ async revokeToken(token) {
1080
+ const AccessToken = this.User.relations.accessTokens?.modelTo;
1081
+ if (!AccessToken) throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
1082
+ await AccessToken.destroyById(token.id);
1083
+ if (await AccessToken.findById(token.id)) throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
1084
+ }
1085
+ reportCleanupError(error) {
1086
+ try {
1087
+ this.app.emit("authenticationSecurityCleanupError", error);
1088
+ } catch {}
1089
+ }
1090
+ audit(event, values) {
1091
+ this.User.emit(event, {
1092
+ principalType: this.User.modelName,
1093
+ modelName: this.User.modelName,
1094
+ timestamp: /* @__PURE__ */ new Date(),
1095
+ ...values
1096
+ });
1097
+ }
1098
+ };
1099
+ }));
1100
+ //#endregion
1101
+ init_service();
1102
+ exports.TwoFactorService = TwoFactorService;
1103
+ Object.defineProperty(exports, "init_service", {
1104
+ enumerable: true,
1105
+ get: function() {
1106
+ return init_service;
1107
+ }
1108
+ });