@vsaas/loopback 12.1.1 → 12.2.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.
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 +258 -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,199 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_runtime = require("../../_virtual/_rolldown/runtime.cjs");
3
+ const require_errors = require("../rate-limit/errors.cjs");
4
+ const require_config = require("../rate-limit/config.cjs");
5
+ //#region src/lib/two-factor/config.ts
6
+ function integer(value, fallback, name, minimum = 1, maximum = 31536e3) {
7
+ const result = value == null ? fallback : value;
8
+ if (!Number.isSafeInteger(result) || result < minimum || result > maximum) throw require_errors.rateLimitConfigurationError(`${name} is invalid`);
9
+ return result;
10
+ }
11
+ function parseKeyring(input, name, exactBytes) {
12
+ if (!input || !KEY_ID.test(input.currentKeyId || "") || !input.keys) throw require_errors.rateLimitConfigurationError(`${name} is invalid`);
13
+ const keys = {};
14
+ for (const [keyId, encoded] of Object.entries(input.keys)) {
15
+ if (!KEY_ID.test(keyId) || typeof encoded !== "string") throw require_errors.rateLimitConfigurationError(`${name} is invalid`);
16
+ const key = Buffer.from(encoded, "base64");
17
+ if (exactBytes != null && key.length !== exactBytes || exactBytes == null && key.length < 32) throw require_errors.rateLimitConfigurationError(`${name} contains an invalid key`);
18
+ keys[keyId] = key;
19
+ }
20
+ if (!keys[input.currentKeyId]) throw require_errors.rateLimitConfigurationError(`${name}.currentKeyId was not found`);
21
+ return Object.freeze({
22
+ currentKeyId: input.currentKeyId,
23
+ keys: Object.freeze(keys)
24
+ });
25
+ }
26
+ function sessionPolicy(value, fallback, name) {
27
+ const result = value || fallback;
28
+ if (result !== "revoke-all" && result !== "revoke-others") throw require_errors.rateLimitConfigurationError(`${name} is invalid`);
29
+ return result;
30
+ }
31
+ function normalizeTwoFactorConfig(input) {
32
+ if (input == null) return null;
33
+ const mode = input.mode || "disabled";
34
+ if (![
35
+ "disabled",
36
+ "verify-only",
37
+ "enroll-and-verify"
38
+ ].includes(mode)) throw require_errors.rateLimitConfigurationError("twoFactor.mode is invalid");
39
+ if (mode === "disabled") return Object.freeze({ mode });
40
+ const algorithm = String(input.totp?.algorithm || "SHA1").toUpperCase();
41
+ if (algorithm !== "SHA1" && algorithm !== "SHA256") throw require_errors.rateLimitConfigurationError("twoFactor.totp.algorithm is invalid");
42
+ const digits = integer(input.totp?.digits, 6, "twoFactor.totp.digits", 6, 8);
43
+ if (digits !== 6 && digits !== 8) throw require_errors.rateLimitConfigurationError("twoFactor.totp.digits is invalid");
44
+ const challengeTtl = integer(input.challengeTtl, 300, "twoFactor.challengeTtl", 60, 600);
45
+ const issuer = String(input.issuer || "").trim();
46
+ if (!issuer) throw require_errors.rateLimitConfigurationError("twoFactor.issuer is required");
47
+ const leaseTtl = integer(input.verificationLeaseTtl, 30, "twoFactor.verificationLeaseTtl", 1, challengeTtl - 1);
48
+ const encryption = parseKeyring(input.encryption, "twoFactor.encryption", 32);
49
+ const recoveryCodeHmac = parseKeyring(input.recoveryCodeHmac, "twoFactor.recoveryCodeHmac");
50
+ const principalBindingHmac = parseKeyring(input.principalBindingHmac, "twoFactor.principalBindingHmac");
51
+ for (const recoveryKey of Object.values(recoveryCodeHmac.keys)) for (const bindingKey of Object.values(principalBindingHmac.keys)) if (crypto.timingSafeEqual(crypto.createHash("sha256").update(recoveryKey).digest(), crypto.createHash("sha256").update(bindingKey).digest())) throw require_errors.rateLimitConfigurationError("Two-factor HMAC keyrings must use distinct keys");
52
+ return Object.freeze({
53
+ mode,
54
+ issuer,
55
+ challengeTtlMs: challengeTtl * 1e3,
56
+ verificationLeaseTtlMs: leaseTtl * 1e3,
57
+ enrollmentTtlMs: integer(input.enrollmentTtl, 600, "twoFactor.enrollmentTtl", 60, 3600) * 1e3,
58
+ maxAttempts: integer(input.maxAttempts, 5, "twoFactor.maxAttempts", 1, 10),
59
+ maxClockSkewSteps: integer(input.maxClockSkewSteps, 1, "twoFactor.maxClockSkewSteps", 0, 2),
60
+ totp: Object.freeze({
61
+ algorithm,
62
+ digits,
63
+ period: integer(input.totp?.period, 30, "twoFactor.totp.period", 15, 120),
64
+ secretBytes: integer(input.totp?.secretBytes, 20, "twoFactor.totp.secretBytes", 20, 64)
65
+ }),
66
+ recoveryCodeCount: integer(input.recoveryCodeCount, 10, "twoFactor.recoveryCodeCount", 1, 32),
67
+ recoveryCodeBytes: integer(input.recoveryCodeBytes, 16, "twoFactor.recoveryCodeBytes", 10, 32),
68
+ loginContextMaxBytes: integer(input.loginContextMaxBytes, 8192, "twoFactor.loginContextMaxBytes", 256, 65536),
69
+ sessions: Object.freeze({
70
+ onEnable: sessionPolicy(input.sessions?.onEnable, "revoke-others", "sessions.onEnable"),
71
+ onDisable: sessionPolicy(input.sessions?.onDisable, "revoke-all", "sessions.onDisable"),
72
+ onRecoveryCodesRegenerated: sessionPolicy(input.sessions?.onRecoveryCodesRegenerated, "revoke-others", "sessions.onRecoveryCodesRegenerated")
73
+ }),
74
+ encryption,
75
+ recoveryCodeHmac,
76
+ principalBindingHmac
77
+ });
78
+ }
79
+ function failurePolicy(input, defaults, name, stateTtlMs) {
80
+ const merged = {
81
+ ...defaults,
82
+ ...input || {}
83
+ };
84
+ const strategy = merged.strategy || "exponential";
85
+ if (strategy !== "fixed" && strategy !== "exponential") throw require_errors.rateLimitConfigurationError(`${name}.strategy is invalid`);
86
+ return Object.freeze(require_config.normalizeFailureLockoutPolicy({
87
+ policyId: merged.policyId,
88
+ policyVersion: String(merged.policyVersion || "1"),
89
+ maxFailedAttempts: integer(merged.maxFailedAttempts, defaults.maxFailedAttempts, `${name}.maxFailedAttempts`, 1, 100),
90
+ observationWindowMs: integer(merged.observationWindow, defaults.observationWindow, `${name}.observationWindow`) * 1e3,
91
+ blockDurationMs: integer(merged.blockDuration, defaults.blockDuration, `${name}.blockDuration`) * 1e3,
92
+ strategy,
93
+ multiplier: integer(merged.multiplier, defaults.multiplier || 2, `${name}.multiplier`, 1, 10),
94
+ maxBlockDurationMs: integer(merged.maxBlockDuration, defaults.maxBlockDuration, `${name}.maxBlockDuration`) * 1e3,
95
+ backoffResetAfterMs: integer(merged.backoffResetAfter, defaults.backoffResetAfter, `${name}.backoffResetAfter`) * 1e3,
96
+ stateTtlMs: merged.stateTtl == null ? stateTtlMs : integer(merged.stateTtl, 0, `${name}.stateTtl`) * 1e3
97
+ }));
98
+ }
99
+ function volumePolicy(input, defaults, name, stateTtlMs) {
100
+ const merged = {
101
+ ...defaults,
102
+ ...input || {}
103
+ };
104
+ return Object.freeze(require_config.normalizeFixedWindowPolicy({
105
+ policyId: merged.policyId,
106
+ policyVersion: String(merged.policyVersion || "1"),
107
+ maxAttempts: integer(merged.maxAttempts, defaults.maxAttempts, `${name}.maxAttempts`, 1, 1e5),
108
+ observationWindowMs: integer(merged.observationWindow, defaults.observationWindow, `${name}.observationWindow`) * 1e3,
109
+ blockDurationMs: integer(merged.blockDuration, defaults.blockDuration, `${name}.blockDuration`) * 1e3,
110
+ stateTtlMs: merged.stateTtl == null ? stateTtlMs : integer(merged.stateTtl, 0, `${name}.stateTtl`) * 1e3
111
+ }));
112
+ }
113
+ function normalizeAuthenticationRateLimitConfig(input, modelName, applicationRateLimit = {}) {
114
+ if (input == null || input.enabled !== true) return null;
115
+ const usesApplicationStore = typeof input.store === "string";
116
+ const store = usesApplicationStore ? applicationRateLimit.stores?.[input.store] : input.store;
117
+ if (!store || typeof store.namespace !== "string" || !store.namespace.trim()) throw require_errors.rateLimitConfigurationError(`${modelName}.authenticationRateLimit.store is invalid`);
118
+ const stateTtlMs = integer(input.stateTtl, 604800, "authenticationRateLimit.stateTtl") * 1e3;
119
+ const storeName = usesApplicationStore ? input.store : `authentication-${modelName}`;
120
+ const keyringName = `authentication-${modelName}`;
121
+ const policies = input.policies || input;
122
+ return Object.freeze({
123
+ enabled: true,
124
+ namespace: store.namespace,
125
+ storeName,
126
+ keyringName,
127
+ store: Object.freeze({ ...store }),
128
+ usesApplicationStore,
129
+ subjectHash: parseKeyring(input.subjectHash, "authenticationRateLimit.subjectHash"),
130
+ retryAfterGranularity: integer(input.retryAfterGranularity, 30, "authenticationRateLimit.retryAfterGranularity", 1, 3600),
131
+ stateTtlMs,
132
+ password: failurePolicy(policies.password, {
133
+ policyId: "authentication-password",
134
+ maxFailedAttempts: 5,
135
+ observationWindow: 900,
136
+ blockDuration: 60,
137
+ multiplier: 2,
138
+ maxBlockDuration: 3600,
139
+ backoffResetAfter: 86400
140
+ }, "authenticationRateLimit.password", stateTtlMs),
141
+ secondFactor: failurePolicy(policies.secondFactor, {
142
+ policyId: "authentication-second-factor",
143
+ maxFailedAttempts: 5,
144
+ observationWindow: 300,
145
+ blockDuration: 30,
146
+ multiplier: 2,
147
+ maxBlockDuration: 900,
148
+ backoffResetAfter: 86400
149
+ }, "authenticationRateLimit.secondFactor", stateTtlMs),
150
+ challengeIssuance: volumePolicy(policies.challengeIssuance, {
151
+ policyId: "authentication-challenge-issuance",
152
+ maxAttempts: 10,
153
+ observationWindow: 300,
154
+ blockDuration: 300
155
+ }, "authenticationRateLimit.challengeIssuance", stateTtlMs),
156
+ management: failurePolicy(policies.management, {
157
+ policyId: "authentication-management",
158
+ maxFailedAttempts: 3,
159
+ observationWindow: 600,
160
+ blockDuration: 300,
161
+ strategy: "fixed",
162
+ multiplier: 1,
163
+ maxBlockDuration: 300,
164
+ backoffResetAfter: 86400
165
+ }, "authenticationRateLimit.management", stateTtlMs),
166
+ source: Object.freeze({
167
+ enabled: input.source?.enabled !== false,
168
+ login: volumePolicy(input.source?.login, {
169
+ policyId: "authentication-source-login",
170
+ maxAttempts: 100,
171
+ observationWindow: 60,
172
+ blockDuration: 60
173
+ }, "authenticationRateLimit.source.login", stateTtlMs),
174
+ secondFactor: volumePolicy(input.source?.secondFactor, {
175
+ policyId: "authentication-source-second-factor",
176
+ maxAttempts: 60,
177
+ observationWindow: 60,
178
+ blockDuration: 60
179
+ }, "authenticationRateLimit.source.secondFactor", stateTtlMs)
180
+ })
181
+ });
182
+ }
183
+ var crypto, KEY_ID;
184
+ var init_config = require_runtime.__esmMin((() => {
185
+ require_config.init_config();
186
+ require_errors.init_errors();
187
+ crypto = require("crypto");
188
+ KEY_ID = /^[A-Za-z0-9._-]{1,64}$/;
189
+ }));
190
+ //#endregion
191
+ init_config();
192
+ Object.defineProperty(exports, "init_config", {
193
+ enumerable: true,
194
+ get: function() {
195
+ return init_config;
196
+ }
197
+ });
198
+ exports.normalizeAuthenticationRateLimitConfig = normalizeAuthenticationRateLimitConfig;
199
+ exports.normalizeTwoFactorConfig = normalizeTwoFactorConfig;
@@ -0,0 +1,55 @@
1
+ //#region src/lib/two-factor/config.d.ts
2
+ interface BinaryKeyring {
3
+ currentKeyId: string;
4
+ keys: Record<string, Buffer>;
5
+ }
6
+ interface NormalizedTwoFactorConfig {
7
+ mode: 'disabled' | 'verify-only' | 'enroll-and-verify';
8
+ issuer: string;
9
+ challengeTtlMs: number;
10
+ verificationLeaseTtlMs: number;
11
+ enrollmentTtlMs: number;
12
+ maxAttempts: number;
13
+ maxClockSkewSteps: number;
14
+ totp: {
15
+ algorithm: 'SHA1' | 'SHA256';
16
+ digits: 6 | 8;
17
+ period: number;
18
+ secretBytes: number;
19
+ };
20
+ recoveryCodeCount: number;
21
+ recoveryCodeBytes: number;
22
+ loginContextMaxBytes: number;
23
+ sessions: {
24
+ onEnable: 'revoke-all' | 'revoke-others';
25
+ onDisable: 'revoke-all' | 'revoke-others';
26
+ onRecoveryCodesRegenerated: 'revoke-all' | 'revoke-others';
27
+ };
28
+ encryption: BinaryKeyring;
29
+ recoveryCodeHmac: BinaryKeyring;
30
+ principalBindingHmac: BinaryKeyring;
31
+ }
32
+ interface NormalizedAuthenticationRateLimitConfig {
33
+ enabled: boolean;
34
+ namespace: string;
35
+ storeName: string;
36
+ keyringName: string;
37
+ store: Record<string, any>;
38
+ usesApplicationStore: boolean;
39
+ subjectHash: BinaryKeyring;
40
+ retryAfterGranularity: number;
41
+ stateTtlMs: number;
42
+ password: Record<string, any>;
43
+ secondFactor: Record<string, any>;
44
+ challengeIssuance: Record<string, any>;
45
+ management: Record<string, any>;
46
+ source: {
47
+ enabled: boolean;
48
+ login: Record<string, any>;
49
+ secondFactor: Record<string, any>;
50
+ };
51
+ }
52
+ declare function normalizeTwoFactorConfig(input: any): NormalizedTwoFactorConfig | null;
53
+ declare function normalizeAuthenticationRateLimitConfig(input: any, modelName: string, applicationRateLimit?: any): NormalizedAuthenticationRateLimitConfig | null;
54
+ //#endregion
55
+ export { BinaryKeyring, NormalizedAuthenticationRateLimitConfig, NormalizedTwoFactorConfig, normalizeAuthenticationRateLimitConfig, normalizeTwoFactorConfig };
@@ -0,0 +1,68 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_runtime = require("../../_virtual/_rolldown/runtime.cjs");
3
+ const require_subject = require("../rate-limit/subject.cjs");
4
+ //#region src/lib/two-factor/crypto.ts
5
+ function sha256(value) {
6
+ return crypto.createHash("sha256").update(value).digest("base64url");
7
+ }
8
+ function randomToken(bytes = 32) {
9
+ return crypto.randomBytes(bytes).toString("base64url");
10
+ }
11
+ function encrypt(plaintext, keyring, aadParts) {
12
+ const keyId = keyring.currentKeyId;
13
+ const nonce = crypto.randomBytes(12);
14
+ const cipher = crypto.createCipheriv("aes-256-gcm", keyring.keys[keyId], nonce);
15
+ cipher.setAAD(require_subject.encodeLengthPrefixed(aadParts));
16
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
17
+ const envelope = {
18
+ v: 1,
19
+ k: keyId,
20
+ n: nonce.toString("base64url"),
21
+ c: ciphertext.toString("base64url"),
22
+ t: cipher.getAuthTag().toString("base64url")
23
+ };
24
+ return {
25
+ envelope: Buffer.from(JSON.stringify(envelope)).toString("base64url"),
26
+ keyId
27
+ };
28
+ }
29
+ function decrypt(encoded, keyring, aadParts) {
30
+ const envelope = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
31
+ if (envelope.v !== 1 || !keyring.keys[envelope.k]) throw new Error("Invalid encrypted envelope");
32
+ const decipher = crypto.createDecipheriv("aes-256-gcm", keyring.keys[envelope.k], Buffer.from(envelope.n, "base64url"));
33
+ decipher.setAAD(require_subject.encodeLengthPrefixed(aadParts));
34
+ decipher.setAuthTag(Buffer.from(envelope.t, "base64url"));
35
+ return Buffer.concat([decipher.update(Buffer.from(envelope.c, "base64url")), decipher.final()]);
36
+ }
37
+ function keyedDigest(domain, parts, keyring, keyId = keyring.currentKeyId) {
38
+ const key = keyring.keys[keyId];
39
+ if (!key) throw new Error("Unknown HMAC key");
40
+ return {
41
+ digest: crypto.createHmac("sha256", key).update(require_subject.encodeLengthPrefixed([domain, ...parts])).digest("base64url"),
42
+ keyId
43
+ };
44
+ }
45
+ function timingSafeEqual(left, right) {
46
+ const a = Buffer.from(left);
47
+ const b = Buffer.from(right);
48
+ return a.length === b.length && crypto.timingSafeEqual(a, b);
49
+ }
50
+ var crypto;
51
+ var init_crypto = require_runtime.__esmMin((() => {
52
+ require_subject.init_subject();
53
+ crypto = require("crypto");
54
+ }));
55
+ //#endregion
56
+ init_crypto();
57
+ exports.decrypt = decrypt;
58
+ exports.encrypt = encrypt;
59
+ Object.defineProperty(exports, "init_crypto", {
60
+ enumerable: true,
61
+ get: function() {
62
+ return init_crypto;
63
+ }
64
+ });
65
+ exports.keyedDigest = keyedDigest;
66
+ exports.randomToken = randomToken;
67
+ exports.sha256 = sha256;
68
+ exports.timingSafeEqual = timingSafeEqual;
@@ -0,0 +1,16 @@
1
+ import { BinaryKeyring } from "./config.cjs";
2
+ //#region src/lib/two-factor/crypto.d.ts
3
+ declare function sha256(value: string | Buffer): string;
4
+ declare function randomToken(bytes?: number): string;
5
+ declare function encrypt(plaintext: string | Buffer, keyring: BinaryKeyring, aadParts: ReadonlyArray<string | Buffer>): {
6
+ envelope: string;
7
+ keyId: string;
8
+ };
9
+ declare function decrypt(encoded: string, keyring: BinaryKeyring, aadParts: ReadonlyArray<string | Buffer>): Buffer;
10
+ declare function keyedDigest(domain: string, parts: ReadonlyArray<string | Buffer>, keyring: BinaryKeyring, keyId?: string): {
11
+ digest: string;
12
+ keyId: string;
13
+ };
14
+ declare function timingSafeEqual(left: string, right: string): boolean;
15
+ //#endregion
16
+ export { decrypt, encrypt, keyedDigest, randomToken, sha256, timingSafeEqual };
@@ -0,0 +1,58 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_runtime = require("../../_virtual/_rolldown/runtime.cjs");
3
+ //#region src/lib/two-factor/errors.ts
4
+ function authError(code, statusCode, message = "Authentication failed") {
5
+ return new AuthenticationSecurityError(code, message, statusCode);
6
+ }
7
+ function invalidChallenge() {
8
+ return authError("INVALID_TWO_FACTOR_CHALLENGE", 401, "Invalid two-factor challenge");
9
+ }
10
+ function invalidFactor() {
11
+ return authError("INVALID_TWO_FACTOR_CODE", 401, "Invalid two-factor code");
12
+ }
13
+ function twoFactorRequired(challenge, expiresIn, methods = ["totp"]) {
14
+ return new AuthenticationSecurityError("TWO_FACTOR_REQUIRED", "Two-factor authentication is required", 403, { details: {
15
+ challengeId: challenge,
16
+ methods,
17
+ expiresIn
18
+ } });
19
+ }
20
+ function authRateLimited(blockedUntilMs, granularitySeconds = 1) {
21
+ const raw = blockedUntilMs == null ? 1 : Math.max(1, Math.ceil((blockedUntilMs - Date.now()) / 1e3));
22
+ const retryAfter = Math.ceil(raw / granularitySeconds) * granularitySeconds;
23
+ return new AuthenticationSecurityError("AUTHENTICATION_RATE_LIMITED", "Too many authentication attempts", 429, {
24
+ retryAfter,
25
+ details: { retryAfter }
26
+ });
27
+ }
28
+ var AuthenticationSecurityError;
29
+ var init_errors = require_runtime.__esmMin((() => {
30
+ AuthenticationSecurityError = class extends Error {
31
+ code;
32
+ statusCode;
33
+ details;
34
+ retryAfter;
35
+ constructor(code, message, statusCode, options = {}) {
36
+ super(message);
37
+ this.name = "AuthenticationSecurityError";
38
+ this.code = code;
39
+ this.statusCode = statusCode;
40
+ this.details = options.details;
41
+ this.retryAfter = options.retryAfter;
42
+ }
43
+ };
44
+ }));
45
+ //#endregion
46
+ init_errors();
47
+ exports.AuthenticationSecurityError = AuthenticationSecurityError;
48
+ exports.authError = authError;
49
+ exports.authRateLimited = authRateLimited;
50
+ Object.defineProperty(exports, "init_errors", {
51
+ enumerable: true,
52
+ get: function() {
53
+ return init_errors;
54
+ }
55
+ });
56
+ exports.invalidChallenge = invalidChallenge;
57
+ exports.invalidFactor = invalidFactor;
58
+ exports.twoFactorRequired = twoFactorRequired;
@@ -0,0 +1,18 @@
1
+ //#region src/lib/two-factor/errors.d.ts
2
+ declare class AuthenticationSecurityError extends Error {
3
+ code: string;
4
+ statusCode: number;
5
+ details?: Record<string, unknown>;
6
+ retryAfter?: number;
7
+ constructor(code: string, message: string, statusCode: number, options?: {
8
+ details?: Record<string, unknown>;
9
+ retryAfter?: number;
10
+ });
11
+ }
12
+ declare function authError(code: string, statusCode: number, message?: string): AuthenticationSecurityError;
13
+ declare function invalidChallenge(): AuthenticationSecurityError;
14
+ declare function invalidFactor(): AuthenticationSecurityError;
15
+ declare function twoFactorRequired(challenge: string, expiresIn: number, methods?: string[]): AuthenticationSecurityError;
16
+ declare function authRateLimited(blockedUntilMs: number | null, granularitySeconds?: number): AuthenticationSecurityError;
17
+ //#endregion
18
+ export { AuthenticationSecurityError, authError, authRateLimited, invalidChallenge, invalidFactor, twoFactorRequired };
@@ -0,0 +1,153 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_runtime = require("../../_virtual/_rolldown/runtime.cjs");
3
+ const require_config = require("../rate-limit/config.cjs");
4
+ const require_lib_two_factor_errors = require("./errors.cjs");
5
+ //#region src/lib/two-factor/rate-limit.ts
6
+ function resolvedPolicy(config, bucket) {
7
+ const value = config[bucket];
8
+ if (bucket === "challengeIssuance") return require_config.normalizeFixedWindowPolicy({
9
+ policyId: value.policyId || `authentication-${bucket}`,
10
+ policyVersion: value.policyVersion,
11
+ maxAttempts: value.maxAttempts,
12
+ observationWindowMs: value.observationWindowMs,
13
+ blockDurationMs: value.blockDurationMs,
14
+ stateTtlMs: value.stateTtlMs
15
+ });
16
+ return require_config.normalizeFailureLockoutPolicy({
17
+ policyId: value.policyId || `authentication-${bucket}`,
18
+ policyVersion: value.policyVersion,
19
+ maxFailedAttempts: value.maxFailedAttempts,
20
+ observationWindowMs: value.observationWindowMs,
21
+ blockDurationMs: value.blockDurationMs,
22
+ strategy: value.strategy,
23
+ multiplier: value.multiplier,
24
+ maxBlockDurationMs: value.maxBlockDurationMs,
25
+ backoffResetAfterMs: value.backoffResetAfterMs,
26
+ stateTtlMs: value.stateTtlMs
27
+ });
28
+ }
29
+ function sourcePolicy(config, bucket) {
30
+ const value = config.source[bucket];
31
+ return require_config.normalizeFixedWindowPolicy({
32
+ policyId: value.policyId || `authentication-source-${bucket}`,
33
+ policyVersion: value.policyVersion,
34
+ maxAttempts: value.maxAttempts,
35
+ observationWindowMs: value.observationWindowMs,
36
+ blockDurationMs: value.blockDurationMs,
37
+ stateTtlMs: value.stateTtlMs
38
+ });
39
+ }
40
+ var AuthenticationRateLimiter;
41
+ var init_rate_limit = require_runtime.__esmMin((() => {
42
+ require_config.init_config();
43
+ require_lib_two_factor_errors.init_errors();
44
+ AuthenticationRateLimiter = class {
45
+ modelName;
46
+ config;
47
+ limiter;
48
+ constructor(modelName, config, limiter) {
49
+ this.modelName = modelName;
50
+ this.config = config;
51
+ this.limiter = limiter;
52
+ }
53
+ get sourceEnabled() {
54
+ return this.config.source.enabled;
55
+ }
56
+ rule(bucket, subjectType, canonicalValue) {
57
+ return {
58
+ ruleId: `authentication-${bucket}-${subjectType}`,
59
+ store: this.config.storeName,
60
+ subjectKeyring: this.config.keyringName,
61
+ subject: {
62
+ namespace: this.config.namespace,
63
+ scope: this.modelName,
64
+ subjectType,
65
+ canonicalValue
66
+ },
67
+ bucket,
68
+ policy: resolvedPolicy(this.config, bucket),
69
+ cost: 1,
70
+ onStoreError: "deny",
71
+ phase: subjectType === "identifier" ? "early" : "resolved",
72
+ order: 0,
73
+ shortCircuitOnDeny: false
74
+ };
75
+ }
76
+ sourceRule(bucket, source) {
77
+ return {
78
+ ruleId: `authentication-source-${bucket}`,
79
+ store: this.config.storeName,
80
+ subjectKeyring: this.config.keyringName,
81
+ subject: {
82
+ namespace: this.config.namespace,
83
+ scope: this.modelName,
84
+ subjectType: "source",
85
+ canonicalValue: source
86
+ },
87
+ bucket: `source-${bucket}`,
88
+ policy: sourcePolicy(this.config, bucket),
89
+ cost: 1,
90
+ onStoreError: "deny",
91
+ phase: "early",
92
+ order: -100,
93
+ shortCircuitOnDeny: true
94
+ };
95
+ }
96
+ async inspect(rule) {
97
+ let result;
98
+ try {
99
+ result = await this.limiter.inspect(rule);
100
+ } catch (error) {
101
+ throw this.translateStoreError(error);
102
+ }
103
+ if (!result.decision.allowed) throw require_lib_two_factor_errors.authRateLimited(result.decision.blockedUntilMs, this.config.retryAfterGranularity);
104
+ return result;
105
+ }
106
+ async consume(rule) {
107
+ let result;
108
+ try {
109
+ result = await this.limiter.consume(rule);
110
+ } catch (error) {
111
+ throw this.translateStoreError(error);
112
+ }
113
+ if (!result.decision.allowed) throw require_lib_two_factor_errors.authRateLimited(result.decision.blockedUntilMs, this.config.retryAfterGranularity);
114
+ return result;
115
+ }
116
+ async reset(observed) {
117
+ for (const item of observed) {
118
+ let result;
119
+ try {
120
+ result = await this.limiter.reset(item.rule, item.revision);
121
+ } catch (error) {
122
+ throw this.translateStoreError(error);
123
+ }
124
+ if (!result.allowed) throw require_lib_two_factor_errors.authRateLimited(result.blockedUntilMs, this.config.retryAfterGranularity);
125
+ }
126
+ }
127
+ async deletePrincipal(principalId) {
128
+ try {
129
+ await this.limiter.delete([
130
+ this.rule("password", "principal", principalId),
131
+ this.rule("secondFactor", "principal", principalId),
132
+ this.rule("challengeIssuance", "principal", principalId),
133
+ this.rule("management", "principal", principalId)
134
+ ]);
135
+ } catch (error) {
136
+ throw this.translateStoreError(error);
137
+ }
138
+ }
139
+ translateStoreError(error) {
140
+ if (error?.code === "RATE_LIMIT_UNAVAILABLE" || error?.code === "RATE_LIMIT_CONFIGURATION_INVALID") return require_lib_two_factor_errors.authError("AUTHENTICATION_RATE_LIMIT_UNAVAILABLE", 503, "Authentication rate limit service unavailable");
141
+ return error;
142
+ }
143
+ };
144
+ }));
145
+ //#endregion
146
+ init_rate_limit();
147
+ exports.AuthenticationRateLimiter = AuthenticationRateLimiter;
148
+ Object.defineProperty(exports, "init_rate_limit", {
149
+ enumerable: true,
150
+ get: function() {
151
+ return init_rate_limit;
152
+ }
153
+ });
@@ -0,0 +1,22 @@
1
+ import { ResolvedRateLimitRule } from "../rate-limit/types.cjs";
2
+ import { NormalizedAuthenticationRateLimitConfig } from "./config.cjs";
3
+ //#region src/lib/two-factor/rate-limit.d.ts
4
+ declare class AuthenticationRateLimiter {
5
+ private readonly modelName;
6
+ private readonly config;
7
+ private readonly limiter;
8
+ constructor(modelName: string, config: NormalizedAuthenticationRateLimitConfig, limiter: any);
9
+ get sourceEnabled(): boolean;
10
+ rule(bucket: 'password' | 'secondFactor' | 'challengeIssuance' | 'management', subjectType: 'identifier' | 'principal', canonicalValue: string): ResolvedRateLimitRule;
11
+ sourceRule(bucket: 'login' | 'secondFactor', source: string): ResolvedRateLimitRule;
12
+ inspect(rule: ResolvedRateLimitRule): Promise<any>;
13
+ consume(rule: ResolvedRateLimitRule): Promise<any>;
14
+ reset(observed: Array<{
15
+ rule: ResolvedRateLimitRule;
16
+ revision: number;
17
+ }>): Promise<void>;
18
+ deletePrincipal(principalId: string): Promise<void>;
19
+ private translateStoreError;
20
+ }
21
+ //#endregion
22
+ export { AuthenticationRateLimiter };
@@ -0,0 +1,49 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_runtime = require("../../_virtual/_rolldown/runtime.cjs");
3
+ const require_lib_two_factor_crypto = require("./crypto.cjs");
4
+ //#region src/lib/two-factor/recovery-codes.ts
5
+ function normalizeRecoveryCode(value) {
6
+ if (typeof value !== "string") return null;
7
+ const normalized = value.replace(/[\s-]/g, "").toUpperCase();
8
+ return /^[A-Z2-7]+$/.test(normalized) ? normalized : null;
9
+ }
10
+ function formatRecoveryCode(value) {
11
+ return value.match(/.{1,5}/g).join("-");
12
+ }
13
+ function generateRecoveryCodes(count, bytes) {
14
+ const unique = /* @__PURE__ */ new Set();
15
+ for (let attempts = 0; unique.size < count && attempts < count * 10; attempts++) unique.add(otplib.generateSecret({ length: bytes }));
16
+ if (unique.size !== count) throw new Error("Unable to generate unique recovery codes");
17
+ const normalized = Array.from(unique);
18
+ return {
19
+ setId: require_lib_two_factor_crypto.randomToken(24),
20
+ codes: normalized.map(formatRecoveryCode),
21
+ normalized
22
+ };
23
+ }
24
+ function recoveryCodeId(credentialId, setId, normalizedCode, keyring, keyId = keyring.currentKeyId) {
25
+ return require_lib_two_factor_crypto.keyedDigest("two-factor-recovery-code:v1", [
26
+ credentialId,
27
+ setId,
28
+ normalizedCode
29
+ ], keyring, keyId);
30
+ }
31
+ var otplib, RECOVERY_CODE_FORMAT_VERSION;
32
+ var init_recovery_codes = require_runtime.__esmMin((() => {
33
+ require_lib_two_factor_crypto.init_crypto();
34
+ otplib = require("otplib");
35
+ RECOVERY_CODE_FORMAT_VERSION = 1;
36
+ }));
37
+ //#endregion
38
+ init_recovery_codes();
39
+ exports.RECOVERY_CODE_FORMAT_VERSION = RECOVERY_CODE_FORMAT_VERSION;
40
+ exports.formatRecoveryCode = formatRecoveryCode;
41
+ exports.generateRecoveryCodes = generateRecoveryCodes;
42
+ Object.defineProperty(exports, "init_recovery_codes", {
43
+ enumerable: true,
44
+ get: function() {
45
+ return init_recovery_codes;
46
+ }
47
+ });
48
+ exports.normalizeRecoveryCode = normalizeRecoveryCode;
49
+ exports.recoveryCodeId = recoveryCodeId;
@@ -0,0 +1,16 @@
1
+ import { BinaryKeyring } from "./config.cjs";
2
+ //#region src/lib/two-factor/recovery-codes.d.ts
3
+ declare const RECOVERY_CODE_FORMAT_VERSION = 1;
4
+ declare function normalizeRecoveryCode(value: unknown): string | null;
5
+ declare function formatRecoveryCode(value: string): string;
6
+ declare function generateRecoveryCodes(count: number, bytes: number): {
7
+ setId: string;
8
+ codes: string[];
9
+ normalized: string[];
10
+ };
11
+ declare function recoveryCodeId(credentialId: string, setId: string, normalizedCode: string, keyring: BinaryKeyring, keyId?: string): {
12
+ digest: string;
13
+ keyId: string;
14
+ };
15
+ //#endregion
16
+ export { RECOVERY_CODE_FORMAT_VERSION, formatRecoveryCode, generateRecoveryCodes, normalizeRecoveryCode, recoveryCodeId };