@vsaas/loopback 12.3.1 → 12.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.
@@ -52,6 +52,12 @@
52
52
  "permission": "ALLOW",
53
53
  "property": "verifyTwoFactor"
54
54
  },
55
+ {
56
+ "principalType": "ROLE",
57
+ "principalId": "$authenticated",
58
+ "permission": "ALLOW",
59
+ "property": "getTwoFactorStatus"
60
+ },
55
61
  {
56
62
  "principalType": "ROLE",
57
63
  "principalId": "$authenticated",
@@ -3,6 +3,7 @@ const require_runtime$1 = require("../../_virtual/_rolldown/runtime.cjs");
3
3
  const require_lib_globalize = require("../../lib/globalize.cjs");
4
4
  const require_lib_runtime = require("../../lib/runtime.cjs");
5
5
  const require_lib_utils = require("../../lib/utils.cjs");
6
+ const require_lib_two_factor_service = require("../../lib/two-factor/service.cjs");
6
7
  //#region src/common/models/user.ts
7
8
  var require_user = /* @__PURE__ */ require_runtime$1.__commonJSMin(((exports, module) => {
8
9
  const path = require("path");
@@ -12,6 +13,7 @@ var require_user = /* @__PURE__ */ require_runtime$1.__commonJSMin(((exports, mo
12
13
  require("fs");
13
14
  const isEmail = require("isemail");
14
15
  const utils = require_lib_utils;
16
+ const { TWO_FACTOR_ENROLLMENT_SCOPE } = (require_lib_two_factor_service.init_service(), require_runtime$1.__toCommonJS(require_lib_two_factor_service.service_exports));
15
17
  const qs = require("querystring");
16
18
  const crypto = require("crypto");
17
19
  const assert = require("assert");
@@ -209,6 +211,17 @@ var require_user = /* @__PURE__ */ require_runtime$1.__commonJSMin(((exports, mo
209
211
  process.nextTick(fn, null, true);
210
212
  return fn.promise;
211
213
  };
214
+ /**
215
+ * Whether the application requires `user` to use a second factor. Override
216
+ * it to enforce a policy: a user that has not enrolled yet logs in with a
217
+ * session restricted to the enrollment, and cannot disable the factor.
218
+ */
219
+ User.isTwoFactorRequired = function(user, options, fn) {
220
+ if (typeof options === "function") fn = options;
221
+ fn = fn || utils.createPromiseCallback();
222
+ process.nextTick(fn, null, false);
223
+ return fn.promise;
224
+ };
212
225
  User.buildTwoFactorLoginContext = function(credentials, options, fn) {
213
226
  if (typeof options === "function") fn = options;
214
227
  fn = fn || utils.createPromiseCallback();
@@ -235,6 +248,61 @@ var require_user = /* @__PURE__ */ require_runtime$1.__commonJSMin(((exports, mo
235
248
  }
236
249
  return this._authenticationSecurity.verifyTwoFactor(verification, options || {}, fn);
237
250
  };
251
+ function twoFactorUnavailable(Model, fn) {
252
+ fn = fn || utils.createPromiseCallback();
253
+ const error = /* @__PURE__ */ new Error("Two-factor authentication is not configured");
254
+ error.statusCode = Model.settings.twoFactor == null ? 404 : 503;
255
+ error.code = Model.settings.twoFactor == null ? "TWO_FACTOR_NOT_CONFIGURED" : "AUTHENTICATION_SECURITY_UNAVAILABLE";
256
+ process.nextTick(fn, error);
257
+ return fn.promise;
258
+ }
259
+ User.getTwoFactorStatus = function(userId, options, fn) {
260
+ if (typeof options === "function") {
261
+ fn = options;
262
+ options = void 0;
263
+ }
264
+ if (!this._authenticationSecurityReady || !this._authenticationSecurity) return twoFactorUnavailable(this, fn);
265
+ return this._authenticationSecurity.status(userId, options || {}, fn);
266
+ };
267
+ /**
268
+ * DANGER: removes the second factor of user `userId` and closes their
269
+ * sessions, so their account is protected by the password alone until they
270
+ * enroll again. Whoever can call this can strip 2FA from that user.
271
+ *
272
+ * It is deliberately not a remote method: an application that exposes it
273
+ * must declare its own endpoint and grant it to the narrowest roles. As a
274
+ * floor, this method requires an authenticated actor other than the user,
275
+ * who must prove themselves with `request.currentPassword` and, when they
276
+ * use 2FA, `request.totpCode`. It emits `twoFactorReset` with the actor.
277
+ */
278
+ User.resetTwoFactor = function(userId, request, options, fn) {
279
+ if (typeof options === "function") {
280
+ fn = options;
281
+ options = void 0;
282
+ }
283
+ fn = fn || utils.createPromiseCallback();
284
+ const Model = this;
285
+ const token = options && options.accessToken;
286
+ if (!Model._authenticationSecurityReady || !Model._authenticationSecurity) return twoFactorUnavailable(Model, fn);
287
+ const ActorModel = token && token.principalType && token.principalType !== Model.modelName ? Model.app && Model.app.models[token.principalType] : Model;
288
+ if (!token || token.userId == null || !ActorModel) {
289
+ const error = /* @__PURE__ */ new Error("Authorization required");
290
+ error.statusCode = 401;
291
+ error.code = "AUTHORIZATION_REQUIRED";
292
+ process.nextTick(fn, error);
293
+ return fn.promise;
294
+ }
295
+ if (ActorModel === Model && String(token.userId) === String(userId)) {
296
+ const error = /* @__PURE__ */ new Error("Use disable to change your own factor");
297
+ error.statusCode = 403;
298
+ error.code = "TWO_FACTOR_RESET_SELF";
299
+ process.nextTick(fn, error);
300
+ return fn.promise;
301
+ }
302
+ if (!ActorModel._authenticationSecurityReady || !ActorModel._authenticationSecurity) return twoFactorUnavailable(ActorModel, fn);
303
+ ActorModel._authenticationSecurity.proveActor(token.userId, request || {}, options).then(() => Model._authenticationSecurity.reset(userId, options)).then((result) => fn(null, result), fn);
304
+ return fn.promise;
305
+ };
238
306
  for (const method of [
239
307
  "beginTwoFactorEnrollment",
240
308
  "confirmTwoFactorEnrollment",
@@ -245,14 +313,7 @@ var require_user = /* @__PURE__ */ require_runtime$1.__commonJSMin(((exports, mo
245
313
  fn = options;
246
314
  options = void 0;
247
315
  }
248
- if (!this._authenticationSecurityReady || !this._authenticationSecurity) {
249
- fn = fn || utils.createPromiseCallback();
250
- const error = /* @__PURE__ */ new Error("Two-factor authentication is not configured");
251
- error.statusCode = this.settings.twoFactor == null ? 404 : 503;
252
- error.code = this.settings.twoFactor == null ? "TWO_FACTOR_NOT_CONFIGURED" : "AUTHENTICATION_SECURITY_UNAVAILABLE";
253
- process.nextTick(fn, error);
254
- return fn.promise;
255
- }
316
+ if (!this._authenticationSecurityReady || !this._authenticationSecurity) return twoFactorUnavailable(this, fn);
256
317
  const serviceMethod = {
257
318
  beginTwoFactorEnrollment: "beginEnrollment",
258
319
  confirmTwoFactorEnrollment: "confirmEnrollment",
@@ -690,9 +751,33 @@ var require_user = /* @__PURE__ */ require_runtime$1.__commonJSMin(((exports, mo
690
751
  path: "/two-factor/verify"
691
752
  }
692
753
  });
693
- function registerTwoFactorManagementMethod(name, path, description) {
754
+ const enrollmentScopes = ["DEFAULT", TWO_FACTOR_ENROLLMENT_SCOPE];
755
+ UserModel.remoteMethod("getTwoFactorStatus", {
756
+ description: "Two-factor state of the current user.",
757
+ accepts: [{
758
+ arg: "userId",
759
+ type: "any",
760
+ http: getUserIdFromRequestContext
761
+ }, {
762
+ arg: "options",
763
+ type: "object",
764
+ http: buildAuthenticationOptions
765
+ }],
766
+ returns: {
767
+ arg: "status",
768
+ type: "object",
769
+ root: true
770
+ },
771
+ accessScopes: enrollmentScopes,
772
+ http: {
773
+ verb: "get",
774
+ path: "/two-factor/status"
775
+ }
776
+ });
777
+ function registerTwoFactorManagementMethod(name, path, description, accessScopes) {
694
778
  UserModel.remoteMethod(name, {
695
779
  description,
780
+ ...accessScopes ? { accessScopes } : {},
696
781
  accepts: [
697
782
  {
698
783
  arg: "userId",
@@ -722,13 +807,14 @@ var require_user = /* @__PURE__ */ require_runtime$1.__commonJSMin(((exports, mo
722
807
  }
723
808
  });
724
809
  }
725
- registerTwoFactorManagementMethod("beginTwoFactorEnrollment", "/two-factor/setup", "Begin two-factor enrollment.");
726
- registerTwoFactorManagementMethod("confirmTwoFactorEnrollment", "/two-factor/confirm", "Confirm two-factor enrollment.");
810
+ registerTwoFactorManagementMethod("beginTwoFactorEnrollment", "/two-factor/setup", "Begin two-factor enrollment.", enrollmentScopes);
811
+ registerTwoFactorManagementMethod("confirmTwoFactorEnrollment", "/two-factor/confirm", "Confirm two-factor enrollment.", enrollmentScopes);
727
812
  registerTwoFactorManagementMethod("regenerateTwoFactorRecoveryCodes", "/two-factor/recovery-codes", "Regenerate two-factor recovery codes.");
728
813
  registerTwoFactorManagementMethod("disableTwoFactor", "/two-factor/disable", "Disable two-factor authentication.");
729
814
  for (const method of [
730
815
  "login",
731
816
  "verifyTwoFactor",
817
+ "getTwoFactorStatus",
732
818
  "beginTwoFactorEnrollment",
733
819
  "confirmTwoFactorEnrollment",
734
820
  "regenerateTwoFactorRecoveryCodes",
@@ -741,6 +827,7 @@ var require_user = /* @__PURE__ */ require_runtime$1.__commonJSMin(((exports, mo
741
827
  for (const method of [
742
828
  "login",
743
829
  "verifyTwoFactor",
830
+ "getTwoFactorStatus",
744
831
  "beginTwoFactorEnrollment",
745
832
  "confirmTwoFactorEnrollment",
746
833
  "regenerateTwoFactorRecoveryCodes",
@@ -765,6 +852,7 @@ var require_user = /* @__PURE__ */ require_runtime$1.__commonJSMin(((exports, mo
765
852
  },
766
853
  description: "Do not supply this argument, it is automatically extracted from request headers."
767
854
  }],
855
+ accessScopes: enrollmentScopes,
768
856
  http: { verb: "all" }
769
857
  });
770
858
  UserModel.remoteMethod("prototype.verify", {
@@ -48,6 +48,12 @@ var require_user = /* @__PURE__ */ require("../../_virtual/_rolldown/runtime.cjs
48
48
  "permission": "ALLOW",
49
49
  "property": "verifyTwoFactor"
50
50
  },
51
+ {
52
+ "principalType": "ROLE",
53
+ "principalId": "$authenticated",
54
+ "permission": "ALLOW",
55
+ "property": "getTwoFactorStatus"
56
+ },
51
57
  {
52
58
  "principalType": "ROLE",
53
59
  "principalId": "$authenticated",
@@ -311,6 +311,7 @@ var require_loopback = /* @__PURE__ */ require_runtime$1.__commonJSMin(((exports
311
311
  */
312
312
  require_lib_builtin_models(loopback);
313
313
  loopback.DataSource = juggler.DataSource;
314
+ loopback.registerNormalizer = juggler.registerNormalizer;
314
315
  function getPackageRoot() {
315
316
  return path.join(__dirname, "..", "..");
316
317
  }
@@ -150,9 +150,11 @@ function installRemoteRateLimiting(app) {
150
150
  });
151
151
  }
152
152
  }
153
- app.on("modelRemoted", function() {
154
- app.models().forEach(installForModel);
155
- });
153
+ function installForSharedClass(sharedClass) {
154
+ installForModel(sharedClass?.ctor);
155
+ }
156
+ app.on("modelRemoted", installForSharedClass);
157
+ app.on("remoteMethodAdded", installForSharedClass);
156
158
  app.installRemoteRateLimits = function() {
157
159
  app.models().forEach(installForModel);
158
160
  return app;
@@ -5,6 +5,10 @@ const require_lib_two_factor_crypto = require("./crypto.cjs");
5
5
  const require_lib_two_factor_totp = require("./totp.cjs");
6
6
  const require_lib_two_factor_recovery_codes = require("./recovery-codes.cjs");
7
7
  //#region src/lib/two-factor/service.ts
8
+ var service_exports = /* @__PURE__ */ require_runtime.__exportAll({
9
+ TWO_FACTOR_ENROLLMENT_SCOPE: () => TWO_FACTOR_ENROLLMENT_SCOPE,
10
+ TwoFactorService: () => TwoFactorService
11
+ });
8
12
  function count(result) {
9
13
  return typeof result === "number" ? result : Number(result?.count || 0);
10
14
  }
@@ -30,6 +34,9 @@ function invoke(target, name, args) {
30
34
  if (result && typeof result.then === "function") result.then((value) => done(null, value), done);
31
35
  });
32
36
  }
37
+ function hasEnrollmentScope(token) {
38
+ return Array.isArray(token?.scopes) && token.scopes.includes("two-factor-enrollment");
39
+ }
33
40
  function includeUser(include) {
34
41
  if (Array.isArray(include)) return include.map(String).map((v) => v.toLowerCase()).includes("user");
35
42
  return String(include || "").toLowerCase() === "user";
@@ -40,12 +47,13 @@ function loginError() {
40
47
  error.code = "LOGIN_FAILED";
41
48
  return error;
42
49
  }
43
- var TwoFactorService;
50
+ var TWO_FACTOR_ENROLLMENT_SCOPE, TwoFactorService;
44
51
  var init_service = require_runtime.__esmMin((() => {
45
52
  require_lib_two_factor_errors.init_errors();
46
53
  require_lib_two_factor_crypto.init_crypto();
47
54
  require_lib_two_factor_totp.init_totp();
48
55
  require_lib_two_factor_recovery_codes.init_recovery_codes();
56
+ TWO_FACTOR_ENROLLMENT_SCOPE = "two-factor-enrollment";
49
57
  TwoFactorService = class {
50
58
  User;
51
59
  config;
@@ -84,12 +92,31 @@ var init_service = require_runtime.__esmMin((() => {
84
92
  confirmEnrollment(userId, request, options, callback) {
85
93
  return optionalCallback(this.confirmEnrollmentInternal(userId, request || {}, options || {}), callback);
86
94
  }
95
+ status(userId, options, callback) {
96
+ return optionalCallback(this.statusInternal(userId, options || {}), callback);
97
+ }
87
98
  regenerateRecoveryCodes(userId, request, options, callback) {
88
99
  return optionalCallback(this.regenerateRecoveryCodesInternal(userId, request || {}, options || {}), callback);
89
100
  }
90
101
  disable(userId, request, options, callback) {
91
102
  return optionalCallback(this.disableInternal(userId, request || {}, options || {}), callback);
92
103
  }
104
+ /**
105
+ * Removes the second factor of another user, e.g. after the device and the
106
+ * recovery codes were lost. Called through `User.resetTwoFactor` once the
107
+ * actor proved itself (`proveActor`); see its warning.
108
+ */
109
+ reset(principalId, options, callback) {
110
+ return optionalCallback(this.resetInternal(String(principalId), options || {}), callback);
111
+ }
112
+ /**
113
+ * Step-up for an operation on another user: the actor of `options` signs
114
+ * it with their password, and with their authenticator code when they use
115
+ * a second factor. Recovery codes are not accepted here.
116
+ */
117
+ proveActor(actorId, request, options, callback) {
118
+ return optionalCallback(this.proveActorInternal(String(actorId), request || {}, options || {}), callback);
119
+ }
93
120
  deletePrincipals(principalIds, callback) {
94
121
  return optionalCallback(this.deletePrincipalsInternal(principalIds.map(String)), callback);
95
122
  }
@@ -147,6 +174,7 @@ var init_service = require_runtime.__esmMin((() => {
147
174
  } }) : null;
148
175
  if (!credential) {
149
176
  if (this.rateLimiter) await this.rateLimiter.reset(observed);
177
+ if (await this.isRequired(user, options)) return this.issueEnrollmentToken(user, normalizedInclude);
150
178
  return this.issueAccessToken(user, credentials, normalizedInclude, false);
151
179
  }
152
180
  const secondFactorObserved = [];
@@ -268,6 +296,32 @@ var init_service = require_runtime.__esmMin((() => {
268
296
  if (includeUser(include)) token.__data.user = user;
269
297
  return token;
270
298
  }
299
+ /**
300
+ * Issues the session of a user that must enroll before using the API. The
301
+ * token has to come back restricted: an application `createAccessToken`
302
+ * override may swap the scope (e.g. a forced password change), but an
303
+ * unrestricted token would bypass the requirement, so it is revoked.
304
+ */
305
+ async issueEnrollmentToken(user, include) {
306
+ const config = this.requireConfig(true);
307
+ const token = await invoke(user, "createAccessToken", [{
308
+ ttl: Math.ceil(config.enrollmentTtlMs / 1e3),
309
+ scopes: [TWO_FACTOR_ENROLLMENT_SCOPE]
310
+ }, {}]);
311
+ if (!token?.id) throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
312
+ const scopes = token.scopes;
313
+ if (!Array.isArray(scopes) || !scopes.length || scopes.includes("DEFAULT")) {
314
+ await this.revokeToken(token);
315
+ throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
316
+ }
317
+ if (includeUser(include)) token.__data.user = user;
318
+ return token;
319
+ }
320
+ /** Enrollment can only be demanded while users are able to enroll. */
321
+ async isRequired(user, options) {
322
+ if (this.config?.mode !== "enroll-and-verify") return false;
323
+ return await invoke(this.User, "isTwoFactorRequired", [user, options]) === true;
324
+ }
271
325
  async buildTwoFactorTokenOptions(credentials, options) {
272
326
  return invoke(this.User, "buildTwoFactorLoginContext", [credentials, options]);
273
327
  }
@@ -607,6 +661,21 @@ var init_service = require_runtime.__esmMin((() => {
607
661
  throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503, "Authentication security reconciliation required");
608
662
  }
609
663
  }
664
+ async statusInternal(userId, options) {
665
+ const config = this.requireConfig();
666
+ const user = await this.authenticatedUser(userId, options);
667
+ const enabled = config.mode !== "disabled" && await this.Credential.count({
668
+ principalType: this.User.modelName,
669
+ principalId: String(userId),
670
+ provider: "totp",
671
+ status: "enabled"
672
+ }) > 0;
673
+ return {
674
+ mode: config.mode,
675
+ enabled,
676
+ required: await this.isRequired(user, options)
677
+ };
678
+ }
610
679
  async beginEnrollmentInternal(userId, request, options) {
611
680
  const config = this.requireConfig(true);
612
681
  const user = await this.authenticatedUser(userId, options);
@@ -707,7 +776,7 @@ var init_service = require_runtime.__esmMin((() => {
707
776
  const generated = require_lib_two_factor_recovery_codes.generateRecoveryCodes(config.recoveryCodeCount, config.recoveryCodeBytes);
708
777
  await this.persistRecoverySet(credential, generated);
709
778
  try {
710
- await this.revokeSessions(principalId, options, config.sessions.onEnable);
779
+ await this.revokeSessions(principalId, options, hasEnrollmentScope(options.accessToken) ? "revoke-all" : config.sessions.onEnable);
711
780
  if (count(await this.Credential.updateAll({
712
781
  id: credential.id,
713
782
  status: "pending",
@@ -941,8 +1010,44 @@ var init_service = require_runtime.__esmMin((() => {
941
1010
  status: "unused"
942
1011
  }) !== rows.length) throw new Error("Incomplete recovery code set");
943
1012
  }
1013
+ async proveActorInternal(actorId, request, options) {
1014
+ const config = this.requireConfig();
1015
+ const actor = await this.authenticatedUser(actorId, options);
1016
+ const observed = await this.reauthenticateManagement(actor, request.currentPassword);
1017
+ const credential = config.mode === "disabled" ? null : await this.Credential.findOne({ where: {
1018
+ principalType: this.User.modelName,
1019
+ principalId: actorId,
1020
+ provider: "totp",
1021
+ status: "enabled"
1022
+ } });
1023
+ if (credential) {
1024
+ if (!(await this.verifyCredentialFactor(credential, "totp", request.totpCode, `management:${require_lib_two_factor_crypto.randomToken(16)}`)).valid) {
1025
+ await this.consumeManagementFailure(actorId);
1026
+ throw require_lib_two_factor_errors.invalidFactor();
1027
+ }
1028
+ }
1029
+ if (this.rateLimiter && observed) await this.rateLimiter.reset([observed]);
1030
+ }
1031
+ async resetInternal(principalId, options) {
1032
+ this.requireConfig();
1033
+ const token = options.accessToken;
1034
+ if (!token || token.userId == null) throw require_lib_two_factor_errors.authError("AUTHORIZATION_REQUIRED", 401, "Authorization required");
1035
+ if (String(token.userId) === principalId && (!("principalType" in token) || token.principalType === this.User.modelName)) throw require_lib_two_factor_errors.authError("TWO_FACTOR_RESET_SELF", 403, "Use disable to change your own factor");
1036
+ if (!await this.User.findById(principalId)) throw require_lib_two_factor_errors.authError("NOT_FOUND", 404, "User not found");
1037
+ await this.revokeSessions(principalId, {}, "revoke-all");
1038
+ await this.destroyFactors([principalId]);
1039
+ this.audit("twoFactorReset", {
1040
+ principalId,
1041
+ actorPrincipalType: token.principalType ?? this.User.modelName,
1042
+ actorPrincipalId: String(token.userId)
1043
+ });
1044
+ return { enabled: false };
1045
+ }
944
1046
  async deletePrincipalsInternal(principalIds) {
945
1047
  if (!principalIds.length) return;
1048
+ await Promise.all([this.destroyFactors(principalIds), this.rateLimiter ? Promise.all(principalIds.map((principalId) => this.rateLimiter.deletePrincipal(principalId))) : Promise.resolve()]);
1049
+ }
1050
+ async destroyFactors(principalIds) {
946
1051
  const credentialIds = (await this.Credential.find({
947
1052
  where: {
948
1053
  principalType: this.User.modelName,
@@ -959,8 +1064,7 @@ var init_service = require_runtime.__esmMin((() => {
959
1064
  this.Credential.destroyAll({
960
1065
  principalType: this.User.modelName,
961
1066
  principalId: { inq: principalIds }
962
- }),
963
- this.rateLimiter ? Promise.all(principalIds.map((principalId) => this.rateLimiter.deletePrincipal(principalId))) : Promise.resolve()
1067
+ })
964
1068
  ]);
965
1069
  }
966
1070
  async rotateEncryptionInternal(options) {
@@ -1099,6 +1203,7 @@ var init_service = require_runtime.__esmMin((() => {
1099
1203
  }));
1100
1204
  //#endregion
1101
1205
  init_service();
1206
+ exports.TWO_FACTOR_ENROLLMENT_SCOPE = TWO_FACTOR_ENROLLMENT_SCOPE;
1102
1207
  exports.TwoFactorService = TwoFactorService;
1103
1208
  Object.defineProperty(exports, "init_service", {
1104
1209
  enumerable: true,
@@ -1106,3 +1211,9 @@ Object.defineProperty(exports, "init_service", {
1106
1211
  return init_service;
1107
1212
  }
1108
1213
  });
1214
+ Object.defineProperty(exports, "service_exports", {
1215
+ enumerable: true,
1216
+ get: function() {
1217
+ return service_exports;
1218
+ }
1219
+ });
@@ -1,6 +1,12 @@
1
1
  import { NormalizedTwoFactorConfig } from "./config.cjs";
2
2
  import { AuthenticationRateLimiter } from "./rate-limit.cjs";
3
3
  //#region src/lib/two-factor/service.d.ts
4
+ /**
5
+ * Scope of the access token issued at login when the application requires a
6
+ * second factor (`User.isTwoFactorRequired`) and the user has not enrolled yet.
7
+ * It only reaches the methods that complete the enrollment.
8
+ */
9
+ declare const TWO_FACTOR_ENROLLMENT_SCOPE = "two-factor-enrollment";
4
10
  declare class TwoFactorService {
5
11
  private readonly User;
6
12
  private readonly config;
@@ -23,12 +29,31 @@ declare class TwoFactorService {
23
29
  enabled: boolean;
24
30
  recoveryCodes: string[];
25
31
  }>;
32
+ status(userId: any, options: any, callback?: Function): Promise<{
33
+ mode: "disabled" | "verify-only" | "enroll-and-verify";
34
+ enabled: boolean;
35
+ required: boolean;
36
+ }>;
26
37
  regenerateRecoveryCodes(userId: any, request: any, options: any, callback?: Function): Promise<{
27
38
  recoveryCodes: any;
28
39
  }>;
29
40
  disable(userId: any, request: any, options: any, callback?: Function): Promise<{
30
41
  enabled: boolean;
31
42
  }>;
43
+ /**
44
+ * Removes the second factor of another user, e.g. after the device and the
45
+ * recovery codes were lost. Called through `User.resetTwoFactor` once the
46
+ * actor proved itself (`proveActor`); see its warning.
47
+ */
48
+ reset(principalId: any, options: any, callback?: Function): Promise<{
49
+ enabled: boolean;
50
+ }>;
51
+ /**
52
+ * Step-up for an operation on another user: the actor of `options` signs
53
+ * it with their password, and with their authenticator code when they use
54
+ * a second factor. Recovery codes are not accepted here.
55
+ */
56
+ proveActor(actorId: any, request: any, options: any, callback?: Function): Promise<void>;
32
57
  deletePrincipals(principalIds: ReadonlyArray<any>, callback?: Function): Promise<void>;
33
58
  rotateEncryption(options?: any, callback?: Function): Promise<{
34
59
  credentialsReencrypted: number;
@@ -45,6 +70,15 @@ declare class TwoFactorService {
45
70
  private resolveSource;
46
71
  private hasPassword;
47
72
  private issueAccessToken;
73
+ /**
74
+ * Issues the session of a user that must enroll before using the API. The
75
+ * token has to come back restricted: an application `createAccessToken`
76
+ * override may swap the scope (e.g. a forced password change), but an
77
+ * unrestricted token would bypass the requirement, so it is revoked.
78
+ */
79
+ private issueEnrollmentToken;
80
+ /** Enrollment can only be demanded while users are able to enroll. */
81
+ private isRequired;
48
82
  private buildTwoFactorTokenOptions;
49
83
  private secretAad;
50
84
  private challengeAad;
@@ -58,6 +92,7 @@ declare class TwoFactorService {
58
92
  private confirmRecoveryReservation;
59
93
  private releaseRecoveryReservation;
60
94
  private compensateChallengeFailure;
95
+ private statusInternal;
61
96
  private beginEnrollmentInternal;
62
97
  private confirmEnrollmentInternal;
63
98
  private regenerateRecoveryCodesInternal;
@@ -67,7 +102,10 @@ declare class TwoFactorService {
67
102
  private consumeManagementFailure;
68
103
  private enabledCredential;
69
104
  private persistRecoverySet;
105
+ private proveActorInternal;
106
+ private resetInternal;
70
107
  private deletePrincipalsInternal;
108
+ private destroyFactors;
71
109
  private rotateEncryptionInternal;
72
110
  private revokeSessions;
73
111
  private revokeToken;
@@ -75,4 +113,4 @@ declare class TwoFactorService {
75
113
  private audit;
76
114
  }
77
115
  //#endregion
78
- export { TwoFactorService };
116
+ export { TWO_FACTOR_ENROLLMENT_SCOPE, TwoFactorService };
package/dist/package.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
  var require_package = /* @__PURE__ */ require("./_virtual/_rolldown/runtime.cjs").__commonJSMin(((exports, module) => {
3
3
  module.exports = {
4
4
  "name": "@vsaas/loopback",
5
- "version": "12.3.1",
5
+ "version": "12.5.0",
6
6
  "description": "Fork of LoopBack 3 framework for building APIs and microservices in Node.js",
7
7
  "keywords": [
8
8
  "StrongLoop",
@@ -1031,6 +1031,132 @@ that the validation entrypoint itself remains callback-oriented. In
1031
1031
  promise-first codebases, persisting with `await` is usually the cleanest way to
1032
1032
  run `validatePromise` rules.
1033
1033
 
1034
+ ### Property normalization
1035
+
1036
+ String properties can declare `normalize`, an ordered list of steps applied
1037
+ before validation and persistence. With `trim`, a `required` property rejects
1038
+ `" "` instead of storing it.
1039
+
1040
+ ```json
1041
+ "properties": {
1042
+ "email": { "type": "string", "required": true, "normalize": ["trim", "lowercase"] },
1043
+ "name": { "type": "string", "normalize": ["trim", "collapseSpaces", "nfc"] },
1044
+ "nickname": { "type": "string", "normalize": ["trim", "emptyToNull"] },
1045
+ "bio": { "type": "string", "normalize": ["stripHtml", "trim"] },
1046
+ "tags": { "type": ["string"], "normalize": ["trim", "lowercase"] }
1047
+ }
1048
+ ```
1049
+
1050
+ Built-in steps:
1051
+
1052
+ | Name | Effect |
1053
+ | ---------------- | --------------------------------------------------------------------- |
1054
+ | `trim` | Remove leading and trailing whitespace |
1055
+ | `lowercase` | Lowercase |
1056
+ | `uppercase` | Uppercase |
1057
+ | `nfc` | Unicode NFC normalization |
1058
+ | `unaccent` | Remove diacritics (`Ñandú` → `Nandu`) |
1059
+ | `collapseSpaces` | Replace whitespace runs, newlines included, with one space |
1060
+ | `emptyToNull` | Store `""` as `null`; place it after `trim` to also catch blanks |
1061
+ | `stripInvisible` | Remove control, zero-width, bidi-override and BOM characters |
1062
+ | `numeric` | Keep digits only |
1063
+ | `alphanumeric` | Keep letters and digits only |
1064
+ | `stripHtml` | Drop tags, comments and `script`/`style` content; keep the plain text |
1065
+
1066
+ `stripHtml` neither decodes nor escapes entities: `Tom &amp; Jerry` stays as is
1067
+ and `a < b` is kept. It is data hygiene, not XSS protection; still escape on
1068
+ output. It keeps no markup, so it is not meant for rich-text fields.
1069
+
1070
+ Register project-specific steps before models are defined, for example at the
1071
+ top of `server/server.js`, and reference them by name. Inline functions also
1072
+ work in programmatic models. A step returning `null` or `undefined` ends the
1073
+ pipeline with that value.
1074
+
1075
+ ```js
1076
+ loopback.registerNormalizer('rut', (value) => value.replace(/[.\s]/g, '').toUpperCase());
1077
+ ```
1078
+
1079
+ When it runs:
1080
+
1081
+ - On every assignment through the model: `create`, `createAll`, `save`,
1082
+ `patchAttributes`/`updateAttributes`, `updateAttribute`, `replaceById`,
1083
+ `replaceOrCreate`, `patchOrCreate`/`upsert`, `upsertWithWhere`,
1084
+ `findOrCreate`, relation and embedded-model writes, and values set by
1085
+ `before save` hooks. `persist` hooks already see normalized data.
1086
+ - On partial updates, only the properties present in the payload are
1087
+ normalized; other stored values are untouched. `updateAll` normalizes its
1088
+ data, and validates the same values it persists.
1089
+ - On `where` filters, values compared for equality (`eq`, `neq`, `inq`, `nin`
1090
+ and plain values) are normalized like stored values, so
1091
+ `{ email: ' Ana@X.com ' }` matches `ana@x.com`. Pattern and range operators
1092
+ (`like`, `ilike`, `regexp`, `gt`, `between`, …) are left as written.
1093
+ - Not on reads: rows written before a property gained `normalize` are returned
1094
+ as stored. They are normalized the next time they are written through
1095
+ `save()`, so migrate existing data when you add a step that changes it,
1096
+ especially `lowercase`, since equality filters will no longer match the old
1097
+ values.
1098
+ - Not inside extended operators (`$set`, `$push`, …) or custom property
1099
+ setters; both receive the value as written.
1100
+
1101
+ Unknown step names, or `normalize` on a non-string property, throw when the
1102
+ model is defined.
1103
+
1104
+ ### Property constraints
1105
+
1106
+ Properties can declare the JSON Schema keywords that the API explorer already
1107
+ publishes, and they are enforced as model validations. They are checked after
1108
+ normalization, so `"normalize": "trim"` with `"maxLength": 128` validates the
1109
+ trimmed value.
1110
+
1111
+ ```json
1112
+ "properties": {
1113
+ "name": { "type": "string", "required": true, "maxLength": 128, "normalize": "trim" },
1114
+ "code": { "type": "string", "pattern": "^[A-Z]{3}-[0-9]{4}$" },
1115
+ "contact": { "type": "string", "format": "email" },
1116
+ "operability": { "type": "number", "min": 0, "max": 100 },
1117
+ "mode": { "type": "string", "enum": ["NONE", "CRON"] },
1118
+ "tags": { "type": ["string"], "maxItems": 20, "maxLength": 32 }
1119
+ }
1120
+ ```
1121
+
1122
+ | Keyword | Applies to | Error code |
1123
+ | -------------------------------- | ----------------------- | ------------------------------------------- |
1124
+ | `minLength`, `maxLength` | strings | `length.min`, `length.max` |
1125
+ | `pattern` | strings | `format` |
1126
+ | `format` | strings | `format.email`, `format.uri`, `format.uuid` |
1127
+ | `minimum`/`min`, `maximum`/`max` | numbers | `range.min`, `range.max` |
1128
+ | `enum` | string, number, boolean | `inclusion` |
1129
+ | `minItems`, `maxItems` | arrays | `items.min`, `items.max` |
1130
+
1131
+ - Lengths count Unicode characters, so an emoji counts as one.
1132
+ - Bounds are inclusive. `min`/`max` are aliases of `minimum`/`maximum`.
1133
+ - On array properties, `minItems`/`maxItems` apply to the array and the other
1134
+ keywords to each item.
1135
+ - `format` validates `email`, `uri` (absolute) and `uuid`. Other formats, such
1136
+ as `date-time` or `int32`, are documentation only.
1137
+ - Constraints only check present values: `null` and `undefined` pass, and
1138
+ `required` decides whether a value must be present. An empty string is a
1139
+ value; use `"normalize": ["trim", "emptyToNull"]` to treat blanks as absent.
1140
+ - A keyword on a property of the wrong type, or an invalid limit, throws when
1141
+ the model is defined.
1142
+
1143
+ Failures are regular validation errors: the write is rejected with
1144
+ `ValidationError` (HTTP 422) and `error.details.codes` lists the codes above
1145
+ per property.
1146
+
1147
+ Constraints run wherever model validation runs: `create`, `save`,
1148
+ `patchAttributes`, `replaceById`, `replaceOrCreate`, `upsertWithWhere`, and
1149
+ `patchOrCreate` with `validateUpsert: true`. Two writes skip full validation by
1150
+ default because their partial data would fail `required`: `updateAll` (unless
1151
+ `validateUpdate: true`) and `patchOrCreate` (unless `validateUpsert: true`).
1152
+ They still check the constraints of the properties they write. Passing
1153
+ `{ validate: false }`, or setting `validateUpdate`/`validateUpsert` or
1154
+ `automaticValidation` to `false`, skips both.
1155
+
1156
+ Constraints declared on existing models start being enforced when you upgrade.
1157
+ Writes of values outside them, including a `patchAttributes` that leaves an
1158
+ invalid stored value untouched, will be rejected, so fix stored data first.
1159
+
1034
1160
  ## Optional packages
1035
1161
 
1036
1162
  ### `@vsaas/loopback-component-storage`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vsaas/loopback",
3
- "version": "12.3.1",
3
+ "version": "12.5.0",
4
4
  "description": "Fork of LoopBack 3 framework for building APIs and microservices in Node.js",
5
5
  "keywords": [
6
6
  "StrongLoop",
@@ -104,9 +104,9 @@
104
104
  "nodemailer": "^9.1.1",
105
105
  "otplib": "13.4.1",
106
106
  "serve-favicon": "^2.5.1",
107
- "@vsaas/remoting": "^13.0.2",
108
- "@vsaas/loopback-datasource-juggler": "^13.0.3",
109
- "@vsaas/loopback-connector-remote": "^12.0.2"
107
+ "@vsaas/loopback-connector-remote": "^12.0.2",
108
+ "@vsaas/loopback-datasource-juggler": "^13.1.0",
109
+ "@vsaas/remoting": "^13.0.2"
110
110
  },
111
111
  "devDependencies": {
112
112
  "@types/node": "25.5.0",