@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,353 @@
1
+ const require_runtime = require("../../../_virtual/_rolldown/runtime.cjs");
2
+ const require_errors = require("../errors.cjs");
3
+ const require_subject = require("../subject.cjs");
4
+ const require_config = require("../config.cjs");
5
+ const require_common = require("../algorithms/common.cjs");
6
+ const require_index = require("../algorithms/index.cjs");
7
+ //#region src/lib/rate-limit/stores/persisted-model.ts
8
+ var persisted_model_exports = /* @__PURE__ */ require_runtime.__exportAll({ PersistedModelRateLimitStore: () => PersistedModelRateLimitStore });
9
+ function resultCount(result) {
10
+ return typeof result === "number" ? result : Number(result?.count || 0);
11
+ }
12
+ function dateMs(value) {
13
+ if (value == null) return null;
14
+ const result = value instanceof Date ? value.getTime() : new Date(value).getTime();
15
+ if (!Number.isFinite(result)) throw require_errors.rateLimitUnavailableError(/* @__PURE__ */ new Error("Invalid persisted rate limit timestamp"));
16
+ return result;
17
+ }
18
+ function nonNegativeSafeInteger(value, name, fallback = 0) {
19
+ const result = value == null ? fallback : Number(value);
20
+ if (!Number.isSafeInteger(result) || result < 0) throw require_errors.rateLimitUnavailableError(/* @__PURE__ */ new Error(`Invalid persisted rate limit ${name}`));
21
+ return result;
22
+ }
23
+ function isExpired(record, nowMs) {
24
+ const expiresAtMs = dateMs(record.expiresAt);
25
+ if (expiresAtMs == null) throw require_errors.rateLimitUnavailableError(/* @__PURE__ */ new Error("Persisted rate limit expiry is missing"));
26
+ return expiresAtMs <= nowMs;
27
+ }
28
+ function stateFromRecord(record, nowMs) {
29
+ if (isExpired(record, nowMs)) return require_common.emptyRateLimitState();
30
+ return {
31
+ failureCount: nonNegativeSafeInteger(record.failureCount, "failureCount"),
32
+ attemptCount: nonNegativeSafeInteger(record.attemptCount, "attemptCount"),
33
+ lockoutLevel: nonNegativeSafeInteger(record.lockoutLevel, "lockoutLevel"),
34
+ windowStartedAtMs: dateMs(record.windowStartedAt),
35
+ lastAttemptAtMs: dateMs(record.lastAttemptAt),
36
+ lastFailureAtMs: dateMs(record.lastFailureAt),
37
+ blockedUntilMs: dateMs(record.blockedUntil)
38
+ };
39
+ }
40
+ function stateToRecord(state) {
41
+ return {
42
+ failureCount: state.failureCount,
43
+ attemptCount: state.attemptCount,
44
+ lockoutLevel: state.lockoutLevel,
45
+ windowStartedAt: state.windowStartedAtMs == null ? null : new Date(state.windowStartedAtMs),
46
+ lastAttemptAt: state.lastAttemptAtMs == null ? null : new Date(state.lastAttemptAtMs),
47
+ lastFailureAt: state.lastFailureAtMs == null ? null : new Date(state.lastFailureAtMs),
48
+ blockedUntil: state.blockedUntilMs == null ? null : new Date(state.blockedUntilMs)
49
+ };
50
+ }
51
+ function isDuplicateKey(error) {
52
+ return error && (error.code === 11e3 || error.code === 11001 || error.code === "E11000" || error.statusCode === 409 || error.status === 409);
53
+ }
54
+ function validateDecision(value, fingerprint) {
55
+ if (!value || value.resultVersion !== 1 || value.policyFingerprint !== fingerprint || typeof value.allowed !== "boolean" || !Number.isSafeInteger(value.revision) || value.blockedUntilMs !== null && !Number.isSafeInteger(value.blockedUntilMs)) throw require_errors.rateLimitUnavailableError(/* @__PURE__ */ new Error("Invalid persisted operation result"));
56
+ return value;
57
+ }
58
+ var crypto, PersistedModelRateLimitStore;
59
+ var init_persisted_model = require_runtime.__esmMin((() => {
60
+ require_index.init_algorithms();
61
+ require_common.init_common();
62
+ require_subject.init_subject();
63
+ require_errors.init_errors();
64
+ require_config.init_config();
65
+ crypto = require("crypto");
66
+ PersistedModelRateLimitStore = class {
67
+ model;
68
+ clock;
69
+ maxRetries;
70
+ allowMemory;
71
+ constructor(options) {
72
+ this.model = options.model;
73
+ this.clock = options.clock || Date.now;
74
+ this.maxRetries = options.maxRetries || 20;
75
+ this.allowMemory = options.allowMemory === true;
76
+ }
77
+ async initialize() {
78
+ if (!this.model || !this.model.dataSource) throw require_errors.rateLimitConfigurationError("Persisted rate limit model is not connected");
79
+ const connectorName = this.model.dataSource.connector && (this.model.dataSource.connector.name || this.model.dataSource.connector.constructor?.name);
80
+ if (!this.allowMemory && String(connectorName).toLowerCase().includes("memory")) throw require_errors.rateLimitConfigurationError("Memory connector is not a supported deployed rate limit store");
81
+ await this.healthCheck();
82
+ await this.verifyCompareAndSetCapability();
83
+ }
84
+ async healthCheck() {
85
+ try {
86
+ await this.model.count({ id: { neq: "__rate_limit_health__" } });
87
+ } catch (error) {
88
+ throw require_errors.rateLimitUnavailableError(error);
89
+ }
90
+ }
91
+ async verifyCompareAndSetCapability() {
92
+ const id = `__rate_limit_capability_probe__${crypto.randomBytes(12).toString("hex")}`;
93
+ const now = new Date(this.clock());
94
+ const probe = {
95
+ id,
96
+ namespace: "__probe__",
97
+ scope: "__probe__",
98
+ subjectType: "__probe__",
99
+ bucket: "__probe__",
100
+ subjectKeyId: "__probe__",
101
+ formatVersion: 1,
102
+ policyFingerprint: "0".repeat(64),
103
+ failureCount: 0,
104
+ attemptCount: 0,
105
+ lockoutLevel: 0,
106
+ revision: 0,
107
+ createdAt: now,
108
+ updatedAt: now,
109
+ expiresAt: new Date(now.getTime() + 6e4)
110
+ };
111
+ let created = false;
112
+ try {
113
+ created = true;
114
+ await this.model.create(probe);
115
+ const roundTripped = await this.model.findById(id);
116
+ if (!roundTripped || String(roundTripped.id) !== id) throw new Error("Connector does not preserve deterministic string IDs");
117
+ try {
118
+ await this.model.create(probe);
119
+ throw new Error("Connector does not enforce unique rate-limit state IDs");
120
+ } catch (error) {
121
+ if (!isDuplicateKey(error)) throw new Error(`Connector duplicate-key errors are not recognized: ${error?.message || error}`);
122
+ }
123
+ const [first, second] = await Promise.all([this.model.updateAll({
124
+ id,
125
+ revision: 0
126
+ }, { revision: 1 }), this.model.updateAll({
127
+ id,
128
+ revision: 0
129
+ }, { revision: 2 })]);
130
+ const updated = resultCount(first) + resultCount(second);
131
+ const stale = await this.model.updateAll({
132
+ id,
133
+ revision: 0
134
+ }, { revision: 2 });
135
+ const winner = await this.model.findById(id);
136
+ if (updated !== 1 || resultCount(stale) !== 0 || !winner || ![1, 2].includes(Number(winner.revision))) throw new Error("Connector does not provide concurrent conditional update semantics");
137
+ } catch (error) {
138
+ throw require_errors.rateLimitConfigurationError(`Persisted rate limit connector failed its compare-and-set capability check: ${error?.message || error}`);
139
+ } finally {
140
+ if (created) try {
141
+ await this.model.destroyAll({ id });
142
+ } catch (error) {
143
+ throw require_errors.rateLimitConfigurationError(`Persisted rate limit capability probe cleanup failed: ${error?.message || error}`);
144
+ }
145
+ }
146
+ }
147
+ async inspect(input) {
148
+ const nowMs = this.clock();
149
+ const record = await this.findRecord(input.subject);
150
+ if (!record) return this.decision(input.policy.policyFingerprint, true, 0, null);
151
+ this.validateRecord(record, input.policy.policyFingerprint);
152
+ if (isExpired(record, nowMs)) return this.decision(input.policy.policyFingerprint, true, 0, null);
153
+ const transition = require_index.inspectState(stateFromRecord(record, nowMs), input.policy, nowMs);
154
+ return this.decision(input.policy.policyFingerprint, transition.allowed, Number(record.revision), transition.blockedUntilMs);
155
+ }
156
+ async consume(input) {
157
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
158
+ const nowMs = this.clock();
159
+ const record = await this.findRecord(input.subject);
160
+ if (!record) {
161
+ const transition = require_index.consumeState(require_common.emptyRateLimitState(), input.policy, input.cost, nowMs);
162
+ const decision = this.decision(input.policy.policyFingerprint, transition.allowed, 1, transition.blockedUntilMs);
163
+ try {
164
+ await this.model.create({
165
+ id: require_subject.rateLimitStateId(input.subject),
166
+ namespace: input.subject.namespace,
167
+ scope: input.subject.scope,
168
+ subjectType: input.subject.subjectType,
169
+ bucket: input.subject.bucket,
170
+ subjectKeyId: input.subject.subjectKeyId,
171
+ formatVersion: 1,
172
+ policyFingerprint: input.policy.policyFingerprint,
173
+ ...stateToRecord(transition.state),
174
+ revision: 1,
175
+ lastOperationId: input.operationId,
176
+ lastOperationResultJson: require_config.canonicalJson(decision),
177
+ createdAt: new Date(nowMs),
178
+ updatedAt: new Date(nowMs),
179
+ expiresAt: new Date(nowMs + input.policy.stateTtlMs)
180
+ });
181
+ return decision;
182
+ } catch (error) {
183
+ if (isDuplicateKey(error)) continue;
184
+ return this.recoverAmbiguous(input.subject, input.operationId, input.policy.policyFingerprint, error);
185
+ }
186
+ }
187
+ this.validateRecord(record, input.policy.policyFingerprint);
188
+ const deduplicated = this.readDeduplicated(record, input.operationId, input.policy.policyFingerprint);
189
+ if (deduplicated) return deduplicated;
190
+ const transition = require_index.consumeState(stateFromRecord(record, nowMs), input.policy, input.cost, nowMs);
191
+ const revision = Number(record.revision) + 1;
192
+ const decision = this.decision(input.policy.policyFingerprint, transition.allowed, revision, transition.blockedUntilMs);
193
+ try {
194
+ const result = await this.model.updateAll({
195
+ id: record.id,
196
+ formatVersion: 1,
197
+ policyFingerprint: input.policy.policyFingerprint,
198
+ revision: Number(record.revision)
199
+ }, {
200
+ ...stateToRecord(transition.state),
201
+ revision,
202
+ lastOperationId: input.operationId,
203
+ lastOperationResultJson: require_config.canonicalJson(decision),
204
+ updatedAt: new Date(nowMs),
205
+ expiresAt: new Date(nowMs + input.policy.stateTtlMs)
206
+ });
207
+ if (result && result.count === 1) return decision;
208
+ } catch (error) {
209
+ return this.recoverAmbiguous(input.subject, input.operationId, input.policy.policyFingerprint, error);
210
+ }
211
+ }
212
+ throw require_errors.rateLimitUnavailableError(/* @__PURE__ */ new Error("Rate limit CAS retries exhausted"));
213
+ }
214
+ async reset(input) {
215
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
216
+ const nowMs = this.clock();
217
+ const record = await this.findRecord(input.subject);
218
+ if (!record) {
219
+ const result = {
220
+ ...this.decision(input.policy.policyFingerprint, true, 1, null),
221
+ outcome: "reset"
222
+ };
223
+ try {
224
+ await this.model.create({
225
+ id: require_subject.rateLimitStateId(input.subject),
226
+ namespace: input.subject.namespace,
227
+ scope: input.subject.scope,
228
+ subjectType: input.subject.subjectType,
229
+ bucket: input.subject.bucket,
230
+ subjectKeyId: input.subject.subjectKeyId,
231
+ formatVersion: 1,
232
+ policyFingerprint: input.policy.policyFingerprint,
233
+ ...stateToRecord(require_common.emptyRateLimitState()),
234
+ revision: 1,
235
+ lastOperationId: input.operationId,
236
+ lastOperationResultJson: require_config.canonicalJson(result),
237
+ createdAt: new Date(nowMs),
238
+ updatedAt: new Date(nowMs),
239
+ expiresAt: new Date(nowMs + input.policy.stateTtlMs)
240
+ });
241
+ return result;
242
+ } catch (error) {
243
+ if (isDuplicateKey(error)) continue;
244
+ return await this.recoverAmbiguous(input.subject, input.operationId, input.policy.policyFingerprint, error);
245
+ }
246
+ }
247
+ this.validateRecord(record, input.policy.policyFingerprint);
248
+ const deduplicated = this.readDeduplicated(record, input.operationId, input.policy.policyFingerprint);
249
+ if (deduplicated) return deduplicated;
250
+ if (Number(record.revision) !== input.expectedRevision) {
251
+ const current = require_index.inspectState(stateFromRecord(record, nowMs), input.policy, nowMs);
252
+ return {
253
+ ...this.decision(input.policy.policyFingerprint, current.allowed, Number(record.revision), current.blockedUntilMs),
254
+ outcome: "preserved-newer-state"
255
+ };
256
+ }
257
+ const revision = Number(record.revision) + 1;
258
+ const result = {
259
+ ...this.decision(input.policy.policyFingerprint, true, revision, null),
260
+ outcome: "reset"
261
+ };
262
+ try {
263
+ const updated = await this.model.updateAll({
264
+ id: record.id,
265
+ formatVersion: 1,
266
+ policyFingerprint: input.policy.policyFingerprint,
267
+ revision: Number(record.revision)
268
+ }, {
269
+ ...stateToRecord(require_common.emptyRateLimitState()),
270
+ revision,
271
+ lastOperationId: input.operationId,
272
+ lastOperationResultJson: require_config.canonicalJson(result),
273
+ updatedAt: new Date(nowMs),
274
+ expiresAt: new Date(nowMs + input.policy.stateTtlMs)
275
+ });
276
+ if (updated && updated.count === 1) return result;
277
+ } catch (error) {
278
+ return await this.recoverAmbiguous(input.subject, input.operationId, input.policy.policyFingerprint, error);
279
+ }
280
+ }
281
+ throw require_errors.rateLimitUnavailableError(/* @__PURE__ */ new Error("Rate limit reset CAS retries exhausted"));
282
+ }
283
+ async delete(input) {
284
+ const ids = input.subjects.map(require_subject.rateLimitStateId);
285
+ if (!ids.length) return;
286
+ try {
287
+ await this.model.destroyAll({ id: { inq: ids } });
288
+ } catch (error) {
289
+ throw require_errors.rateLimitUnavailableError(error);
290
+ }
291
+ }
292
+ async findRecord(subject) {
293
+ try {
294
+ return await this.model.findById(require_subject.rateLimitStateId(subject));
295
+ } catch (error) {
296
+ throw require_errors.rateLimitUnavailableError(error);
297
+ }
298
+ }
299
+ validateRecord(record, fingerprint) {
300
+ const revision = Number(record.revision);
301
+ if (Number(record.formatVersion) !== 1 || record.policyFingerprint !== fingerprint || !Number.isSafeInteger(revision) || revision < 1) throw require_errors.rateLimitUnavailableError(/* @__PURE__ */ new Error("Rate limit state format or policy mismatch"));
302
+ nonNegativeSafeInteger(record.failureCount, "failureCount");
303
+ nonNegativeSafeInteger(record.attemptCount, "attemptCount");
304
+ nonNegativeSafeInteger(record.lockoutLevel, "lockoutLevel");
305
+ dateMs(record.windowStartedAt);
306
+ dateMs(record.lastAttemptAt);
307
+ dateMs(record.lastFailureAt);
308
+ dateMs(record.blockedUntil);
309
+ isExpired(record, this.clock());
310
+ }
311
+ readDeduplicated(record, operationId, fingerprint) {
312
+ if (record.lastOperationId !== operationId) return null;
313
+ try {
314
+ return validateDecision(JSON.parse(record.lastOperationResultJson), fingerprint);
315
+ } catch (error) {
316
+ if (error instanceof require_errors.RateLimitError) throw error;
317
+ throw require_errors.rateLimitUnavailableError(error);
318
+ }
319
+ }
320
+ async recoverAmbiguous(subject, operationId, fingerprint, cause) {
321
+ const current = await this.findRecord(subject);
322
+ if (current) {
323
+ const result = this.readDeduplicated(current, operationId, fingerprint);
324
+ if (result) return result;
325
+ }
326
+ throw new require_errors.RateLimitError("RATE_LIMIT_OPERATION_AMBIGUOUS", "Rate limit operation outcome is ambiguous", 503, { cause });
327
+ }
328
+ decision(fingerprint, allowed, revision, blockedUntilMs) {
329
+ return {
330
+ resultVersion: 1,
331
+ policyFingerprint: fingerprint,
332
+ allowed,
333
+ revision,
334
+ blockedUntilMs
335
+ };
336
+ }
337
+ };
338
+ }));
339
+ //#endregion
340
+ init_persisted_model();
341
+ exports.PersistedModelRateLimitStore = PersistedModelRateLimitStore;
342
+ Object.defineProperty(exports, "init_persisted_model", {
343
+ enumerable: true,
344
+ get: function() {
345
+ return init_persisted_model;
346
+ }
347
+ });
348
+ Object.defineProperty(exports, "persisted_model_exports", {
349
+ enumerable: true,
350
+ get: function() {
351
+ return persisted_model_exports;
352
+ }
353
+ });
@@ -0,0 +1,127 @@
1
+ const require_runtime = require("../../_virtual/_rolldown/runtime.cjs");
2
+ const require_errors = require("./errors.cjs");
3
+ //#region src/lib/rate-limit/subject.ts
4
+ var subject_exports = /* @__PURE__ */ require_runtime.__exportAll({
5
+ encodeLengthPrefixed: () => encodeLengthPrefixed,
6
+ hashRateLimitSubject: () => hashRateLimitSubject,
7
+ normalizeKeyring: () => normalizeKeyring,
8
+ rateLimitStateId: () => rateLimitStateId,
9
+ timingSafeEqualText: () => timingSafeEqualText,
10
+ validateSafeIdentifier: () => validateSafeIdentifier,
11
+ withBucket: () => withBucket
12
+ });
13
+ function encodeLengthPrefixed(values) {
14
+ const encoded = values.map((value) => Buffer.isBuffer(value) ? Buffer.from(value) : Buffer.from(value, "utf8"));
15
+ const total = encoded.reduce((size, value) => size + 4 + value.length, 0);
16
+ const output = Buffer.allocUnsafe(total);
17
+ let offset = 0;
18
+ for (const value of encoded) {
19
+ output.writeUInt32BE(value.length, offset);
20
+ offset += 4;
21
+ value.copy(output, offset);
22
+ offset += value.length;
23
+ }
24
+ return output;
25
+ }
26
+ function validateSafeIdentifier(value, name) {
27
+ if (typeof value !== "string" || !SAFE_IDENTIFIER.test(value)) throw require_errors.rateLimitConfigurationError(`${name} has an invalid format`);
28
+ }
29
+ function normalizeKeyring(input, name) {
30
+ if (!input || typeof input !== "object") throw require_errors.rateLimitConfigurationError(`${name} is required`);
31
+ validateSafeIdentifier(input.currentKeyId, `${name}.currentKeyId`);
32
+ const keys = {};
33
+ for (const keyId of Object.keys(input.keys || {})) {
34
+ validateSafeIdentifier(keyId, `${name}.keys key`);
35
+ const raw = input.keys[keyId];
36
+ const key = Buffer.isBuffer(raw) ? Buffer.from(raw) : Buffer.from(String(raw), "base64");
37
+ if (key.length < 32) throw require_errors.rateLimitConfigurationError(`${name}.keys.${keyId} must contain at least 32 bytes`);
38
+ keys[keyId] = key;
39
+ }
40
+ if (!keys[input.currentKeyId]) throw require_errors.rateLimitConfigurationError(`${name}.currentKeyId does not exist in keys`);
41
+ return Object.freeze({
42
+ currentKeyId: input.currentKeyId,
43
+ keys: Object.freeze(keys)
44
+ });
45
+ }
46
+ function hashRateLimitSubject(subject, keyring, keyId = keyring.currentKeyId) {
47
+ validateSafeIdentifier(subject.namespace, "subject.namespace");
48
+ validateSafeIdentifier(subject.scope, "subject.scope");
49
+ validateSafeIdentifier(subject.subjectType, "subject.subjectType");
50
+ if (typeof subject.canonicalValue !== "string" && !Buffer.isBuffer(subject.canonicalValue)) throw require_errors.rateLimitConfigurationError("subject.canonicalValue must be a string or Buffer");
51
+ const canonicalLength = Buffer.isBuffer(subject.canonicalValue) ? subject.canonicalValue.length : Buffer.byteLength(String(subject.canonicalValue), "utf8");
52
+ if (canonicalLength < 1 || canonicalLength > 4096) throw require_errors.rateLimitConfigurationError("subject.canonicalValue has an invalid length");
53
+ const key = keyring.keys[keyId];
54
+ if (!key) throw require_errors.rateLimitConfigurationError(`Unknown subject key ID ${keyId}`);
55
+ const material = encodeLengthPrefixed([
56
+ "rate-limit-subject:v1",
57
+ subject.namespace,
58
+ subject.scope,
59
+ subject.subjectType,
60
+ subject.canonicalValue
61
+ ]);
62
+ const subjectHash = crypto.createHmac("sha256", key).update(material).digest("base64url");
63
+ material.fill(0);
64
+ return {
65
+ namespace: subject.namespace,
66
+ scope: subject.scope,
67
+ subjectType: subject.subjectType,
68
+ subjectHash,
69
+ subjectKeyId: keyId
70
+ };
71
+ }
72
+ function withBucket(subject, bucket, policyId, policyVersion) {
73
+ validateSafeIdentifier(bucket, "bucket");
74
+ validateSafeIdentifier(policyId, "policyId");
75
+ validateSafeIdentifier(policyVersion, "policyVersion");
76
+ return {
77
+ ...subject,
78
+ bucket,
79
+ policyId,
80
+ policyVersion
81
+ };
82
+ }
83
+ function rateLimitStateId(subject) {
84
+ return crypto.createHash("sha256").update(encodeLengthPrefixed([
85
+ "rate-limit-state:v1",
86
+ subject.namespace,
87
+ subject.scope,
88
+ subject.subjectType,
89
+ subject.subjectKeyId,
90
+ subject.subjectHash,
91
+ subject.bucket,
92
+ subject.policyId,
93
+ subject.policyVersion
94
+ ])).digest("base64url");
95
+ }
96
+ function timingSafeEqualText(left, right) {
97
+ const a = Buffer.from(left);
98
+ const b = Buffer.from(right);
99
+ return a.length === b.length && crypto.timingSafeEqual(a, b);
100
+ }
101
+ var crypto, SAFE_IDENTIFIER;
102
+ var init_subject = require_runtime.__esmMin((() => {
103
+ require_errors.init_errors();
104
+ crypto = require("crypto");
105
+ SAFE_IDENTIFIER = /^[A-Za-z0-9._-]{1,128}$/;
106
+ }));
107
+ //#endregion
108
+ init_subject();
109
+ exports.encodeLengthPrefixed = encodeLengthPrefixed;
110
+ exports.hashRateLimitSubject = hashRateLimitSubject;
111
+ Object.defineProperty(exports, "init_subject", {
112
+ enumerable: true,
113
+ get: function() {
114
+ return init_subject;
115
+ }
116
+ });
117
+ exports.normalizeKeyring = normalizeKeyring;
118
+ exports.rateLimitStateId = rateLimitStateId;
119
+ Object.defineProperty(exports, "subject_exports", {
120
+ enumerable: true,
121
+ get: function() {
122
+ return subject_exports;
123
+ }
124
+ });
125
+ exports.timingSafeEqualText = timingSafeEqualText;
126
+ exports.validateSafeIdentifier = validateSafeIdentifier;
127
+ exports.withBucket = withBucket;
@@ -0,0 +1 @@
1
+ require("../../_virtual/_rolldown/runtime.cjs");
@@ -0,0 +1,167 @@
1
+ //#region src/lib/rate-limit/types.d.ts
2
+ type MaybePromise<T> = T | Promise<T>;
3
+ type RateLimitAlgorithmName = 'fixed-window' | 'failure-lockout';
4
+ type RateLimitStoreErrorMode = 'deny' | 'allow';
5
+ type RateLimitPhase = 'early' | 'resolved' | 'domain';
6
+ interface RateLimitSubjectMaterial {
7
+ namespace: string;
8
+ scope: string;
9
+ subjectType: string;
10
+ canonicalValue: string | Buffer;
11
+ }
12
+ interface HashedRateLimitSubject {
13
+ namespace: string;
14
+ scope: string;
15
+ subjectType: string;
16
+ subjectHash: string;
17
+ subjectKeyId: string;
18
+ }
19
+ interface RateLimitBucketSubject extends HashedRateLimitSubject {
20
+ policyId: string;
21
+ policyVersion: string;
22
+ bucket: string;
23
+ }
24
+ interface RateLimitSubjectKeyring {
25
+ currentKeyId: string;
26
+ keys: Record<string, Buffer>;
27
+ }
28
+ interface BaseRateLimitPolicy {
29
+ policyId: string;
30
+ policyVersion: string;
31
+ algorithm: RateLimitAlgorithmName;
32
+ stateTtlMs: number;
33
+ policyFingerprint: string;
34
+ }
35
+ interface FixedWindowRateLimitPolicy extends BaseRateLimitPolicy {
36
+ algorithm: 'fixed-window';
37
+ maxAttempts: number;
38
+ observationWindowMs: number;
39
+ blockDurationMs: number;
40
+ }
41
+ interface FailureLockoutRateLimitPolicy extends BaseRateLimitPolicy {
42
+ algorithm: 'failure-lockout';
43
+ maxFailedAttempts: number;
44
+ observationWindowMs: number;
45
+ blockDurationMs: number;
46
+ strategy: 'fixed' | 'exponential';
47
+ multiplier: number;
48
+ maxBlockDurationMs: number;
49
+ backoffResetAfterMs: number;
50
+ }
51
+ type NormalizedRateLimitPolicy = FixedWindowRateLimitPolicy | FailureLockoutRateLimitPolicy;
52
+ interface ResolvedRateLimitRule {
53
+ ruleId: string;
54
+ store: string;
55
+ subjectKeyring: string;
56
+ subject: RateLimitSubjectMaterial;
57
+ bucket: string;
58
+ policy: NormalizedRateLimitPolicy;
59
+ cost: number;
60
+ onStoreError: RateLimitStoreErrorMode;
61
+ phase: RateLimitPhase;
62
+ order: number;
63
+ shortCircuitOnDeny: boolean;
64
+ }
65
+ interface RateLimitDecision {
66
+ resultVersion: 1;
67
+ policyFingerprint: string;
68
+ allowed: boolean;
69
+ revision: number;
70
+ blockedUntilMs: number | null;
71
+ }
72
+ interface RateLimitResetResult extends RateLimitDecision {
73
+ outcome: 'reset' | 'not-found' | 'preserved-newer-state';
74
+ }
75
+ interface RateLimitInspection {
76
+ subject: RateLimitBucketSubject;
77
+ policy: NormalizedRateLimitPolicy;
78
+ }
79
+ interface RateLimitConsumption extends RateLimitInspection {
80
+ cost: number;
81
+ operationId: string;
82
+ }
83
+ interface RateLimitReset extends RateLimitInspection {
84
+ expectedRevision: number;
85
+ operationId: string;
86
+ }
87
+ interface RateLimitDelete {
88
+ subjects: ReadonlyArray<RateLimitBucketSubject>;
89
+ }
90
+ interface RateLimitStore {
91
+ initialize(): Promise<void>;
92
+ healthCheck(): Promise<void>;
93
+ inspect(input: RateLimitInspection): Promise<RateLimitDecision>;
94
+ consume(input: RateLimitConsumption): Promise<RateLimitDecision>;
95
+ reset(input: RateLimitReset): Promise<RateLimitResetResult>;
96
+ delete(input: RateLimitDelete): Promise<void>;
97
+ }
98
+ interface RateLimitRuleDecision {
99
+ ruleId: string;
100
+ decision: RateLimitDecision;
101
+ }
102
+ interface AggregatedRateLimitDecision {
103
+ allowed: boolean;
104
+ blockedUntilMs: number | null;
105
+ decisions: ReadonlyArray<RateLimitRuleDecision>;
106
+ }
107
+ interface RateLimitStateData {
108
+ failureCount: number;
109
+ attemptCount: number;
110
+ lockoutLevel: number;
111
+ windowStartedAtMs: number | null;
112
+ lastAttemptAtMs: number | null;
113
+ lastFailureAtMs: number | null;
114
+ blockedUntilMs: number | null;
115
+ }
116
+ interface RateLimitTransition {
117
+ state: RateLimitStateData;
118
+ allowed: boolean;
119
+ blockedUntilMs: number | null;
120
+ }
121
+ interface DynamicFixedWindowLimit {
122
+ limit: number;
123
+ windowMs: number;
124
+ blockDurationMs?: number;
125
+ stateTtlMs?: number;
126
+ revision: string;
127
+ }
128
+ interface RegisteredFixedWindowRule<TContext = unknown> {
129
+ id: string;
130
+ algorithm: 'fixed-window';
131
+ store: string;
132
+ subjectKeyring: string;
133
+ bucket: string;
134
+ phase?: RateLimitPhase;
135
+ order?: number;
136
+ shortCircuitOnDeny?: boolean;
137
+ onStoreError: RateLimitStoreErrorMode;
138
+ subject(context: Readonly<TContext>): MaybePromise<RateLimitSubjectMaterial>;
139
+ limit: DynamicFixedWindowLimit | ((context: Readonly<TContext>) => MaybePromise<DynamicFixedWindowLimit | null>);
140
+ cost?: number | ((context: Readonly<TContext>) => MaybePromise<number>);
141
+ }
142
+ interface RegisteredRateLimitDecision {
143
+ ruleId: string;
144
+ applied: boolean;
145
+ allowed: boolean;
146
+ blockedUntilMs: number | null;
147
+ revision: number;
148
+ }
149
+ interface RateLimitBatchEntry<TContext = unknown> {
150
+ entryId: string;
151
+ context: TContext;
152
+ }
153
+ interface RateLimitBatchDecision extends RegisteredRateLimitDecision {
154
+ entryId: string;
155
+ }
156
+ interface RateLimitRemoteContext {
157
+ app: any;
158
+ model: any;
159
+ methodName: string;
160
+ args: Readonly<Record<string, unknown>>;
161
+ request?: any;
162
+ accessToken?: any;
163
+ source?: string | null;
164
+ }
165
+ type RemoteRateLimitResolver = (context: RateLimitRemoteContext) => MaybePromise<ResolvedRateLimitRule | ReadonlyArray<ResolvedRateLimitRule> | null | undefined>;
166
+ //#endregion
167
+ export { AggregatedRateLimitDecision, BaseRateLimitPolicy, DynamicFixedWindowLimit, FailureLockoutRateLimitPolicy, FixedWindowRateLimitPolicy, HashedRateLimitSubject, MaybePromise, NormalizedRateLimitPolicy, RateLimitAlgorithmName, RateLimitBatchDecision, RateLimitBatchEntry, RateLimitBucketSubject, RateLimitConsumption, RateLimitDecision, RateLimitDelete, RateLimitInspection, RateLimitPhase, RateLimitRemoteContext, RateLimitReset, RateLimitResetResult, RateLimitRuleDecision, RateLimitStateData, RateLimitStore, RateLimitStoreErrorMode, RateLimitSubjectKeyring, RateLimitSubjectMaterial, RateLimitTransition, RegisteredFixedWindowRule, RegisteredRateLimitDecision, RemoteRateLimitResolver, ResolvedRateLimitRule };
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,10 @@
1
+ //#region src/lib/runtime.d.ts
2
+ /**
3
+ * True if running in a server environment; false otherwise.
4
+ * @header loopback.isServer
5
+ */
6
+ declare const runtime: {
7
+ isBrowser: boolean;
8
+ isServer: boolean;
9
+ };
10
+ export = runtime;
@@ -0,0 +1,3 @@
1
+ //#region src/lib/server-app.d.ts
2
+ declare function loopbackExpress(): any;
3
+ export = loopbackExpress;