@vsaas/loopback 12.1.0 → 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 +55 -19
@@ -0,0 +1,368 @@
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
+ //#region src/lib/rate-limit/limiter.ts
6
+ var limiter_exports = /* @__PURE__ */ require_runtime.__exportAll({
7
+ RateLimiter: () => RateLimiter,
8
+ RegisteredRateLimiter: () => RegisteredRateLimiter
9
+ });
10
+ function operationId() {
11
+ return crypto.randomBytes(16).toString("hex");
12
+ }
13
+ function validateResolvedRule(rule) {
14
+ require_subject.validateSafeIdentifier(rule.ruleId, "rule.ruleId");
15
+ require_subject.validateSafeIdentifier(rule.store, "rule.store");
16
+ require_subject.validateSafeIdentifier(rule.subjectKeyring, "rule.subjectKeyring");
17
+ require_subject.validateSafeIdentifier(rule.bucket, "rule.bucket");
18
+ require_subject.validateSafeIdentifier(rule.subject.namespace, "rule.subject.namespace");
19
+ require_subject.validateSafeIdentifier(rule.subject.scope, "rule.subject.scope");
20
+ require_subject.validateSafeIdentifier(rule.subject.subjectType, "rule.subject.subjectType");
21
+ require_config.validateRateLimitCost(rule.cost, Number.MAX_SAFE_INTEGER);
22
+ if (!/^[a-f0-9]{64}$/.test(rule.policy?.policyFingerprint || "")) throw require_errors.rateLimitConfigurationError("Rate limit policy fingerprint is invalid");
23
+ if (rule.onStoreError !== "allow" && rule.onStoreError !== "deny") throw require_errors.rateLimitConfigurationError("Rate limit store error mode is invalid");
24
+ }
25
+ function stableRules(rules) {
26
+ const phaseRank = {
27
+ early: 0,
28
+ resolved: 1,
29
+ domain: 2
30
+ };
31
+ return rules.map((rule, index) => ({
32
+ rule,
33
+ index
34
+ })).sort((left, right) => phaseRank[left.rule.phase] - phaseRank[right.rule.phase] || left.rule.order - right.rule.order || left.index - right.index).map(({ rule }) => rule);
35
+ }
36
+ var crypto, RateLimiter, RegisteredRateLimiter;
37
+ var init_limiter = require_runtime.__esmMin((() => {
38
+ require_subject.init_subject();
39
+ require_config.init_config();
40
+ require_errors.init_errors();
41
+ crypto = require("crypto");
42
+ RateLimiter = class {
43
+ registry;
44
+ constructor(registry) {
45
+ this.registry = registry;
46
+ }
47
+ async inspect(rule) {
48
+ return this.execute(rule, "inspect");
49
+ }
50
+ async consume(rule) {
51
+ return this.execute(rule, "consume");
52
+ }
53
+ async inspectAll(rules) {
54
+ return this.executeAll(rules, "inspect");
55
+ }
56
+ async consumeAll(rules) {
57
+ return this.executeAll(rules, "consume");
58
+ }
59
+ async reset(rule, expectedRevision) {
60
+ try {
61
+ const store = await this.registry.resolveStore(rule.store);
62
+ const subject = await this.selectSubject(rule, store);
63
+ return await store.reset({
64
+ subject,
65
+ policy: rule.policy,
66
+ expectedRevision,
67
+ operationId: operationId()
68
+ });
69
+ } catch (error) {
70
+ if (rule.onStoreError === "allow") return {
71
+ resultVersion: 1,
72
+ policyFingerprint: rule.policy.policyFingerprint,
73
+ outcome: "not-found",
74
+ allowed: true,
75
+ revision: expectedRevision,
76
+ blockedUntilMs: null
77
+ };
78
+ throw require_errors.rateLimitUnavailableError(error);
79
+ }
80
+ }
81
+ async delete(rules) {
82
+ const byStore = /* @__PURE__ */ new Map();
83
+ for (const rule of rules) {
84
+ const keyring = this.registry.getKeyring(rule.subjectKeyring);
85
+ for (const keyId of Object.keys(keyring.keys)) {
86
+ const subject = require_subject.withBucket(require_subject.hashRateLimitSubject(rule.subject, keyring, keyId), rule.bucket, rule.policy.policyId, rule.policy.policyVersion);
87
+ const current = byStore.get(rule.store) || [];
88
+ current.push(subject);
89
+ byStore.set(rule.store, current);
90
+ }
91
+ }
92
+ await Promise.all(Array.from(byStore, async ([storeName, subjects]) => {
93
+ await (await this.registry.resolveStore(storeName)).delete({ subjects });
94
+ }));
95
+ }
96
+ async subjectIdentity(rule) {
97
+ const keyring = this.registry.getKeyring(rule.subjectKeyring);
98
+ const subject = require_subject.hashRateLimitSubject(rule.subject, keyring);
99
+ return crypto.createHash("sha256").update(JSON.stringify([
100
+ subject.namespace,
101
+ subject.scope,
102
+ subject.subjectType,
103
+ subject.subjectKeyId,
104
+ subject.subjectHash,
105
+ rule.bucket
106
+ ])).digest("base64url");
107
+ }
108
+ async executeAll(rules, action) {
109
+ const decisions = [];
110
+ for (const rule of stableRules(rules)) {
111
+ const result = await this.execute(rule, action);
112
+ decisions.push(result);
113
+ if (!result.decision.allowed && rule.shortCircuitOnDeny) break;
114
+ }
115
+ const denied = decisions.filter(({ decision }) => !decision.allowed);
116
+ return {
117
+ allowed: denied.length === 0,
118
+ blockedUntilMs: denied.reduce((maximum, { decision }) => decision.blockedUntilMs == null ? maximum : Math.max(maximum || 0, decision.blockedUntilMs), null),
119
+ decisions
120
+ };
121
+ }
122
+ async execute(rule, action) {
123
+ validateResolvedRule(rule);
124
+ try {
125
+ const store = await this.registry.resolveStore(rule.store);
126
+ const subject = await this.selectSubject(rule, store);
127
+ const decision = action === "inspect" ? await store.inspect({
128
+ subject,
129
+ policy: rule.policy
130
+ }) : await store.consume({
131
+ subject,
132
+ policy: rule.policy,
133
+ cost: rule.cost,
134
+ operationId: operationId()
135
+ });
136
+ if (decision.policyFingerprint !== rule.policy.policyFingerprint) throw require_errors.rateLimitUnavailableError(/* @__PURE__ */ new Error("Rate limit decision policy mismatch"));
137
+ return {
138
+ ruleId: rule.ruleId,
139
+ decision
140
+ };
141
+ } catch (error) {
142
+ if (rule.onStoreError === "allow") return {
143
+ ruleId: rule.ruleId,
144
+ decision: {
145
+ resultVersion: 1,
146
+ policyFingerprint: rule.policy.policyFingerprint,
147
+ allowed: true,
148
+ revision: 0,
149
+ blockedUntilMs: null
150
+ }
151
+ };
152
+ throw require_errors.rateLimitUnavailableError(error);
153
+ }
154
+ }
155
+ async selectSubject(rule, store) {
156
+ const keyring = this.registry.getKeyring(rule.subjectKeyring);
157
+ const candidates = [];
158
+ for (const keyId of Object.keys(keyring.keys)) {
159
+ const subject = require_subject.withBucket(require_subject.hashRateLimitSubject(rule.subject, keyring, keyId), rule.bucket, rule.policy.policyId, rule.policy.policyVersion);
160
+ const decision = await store.inspect({
161
+ subject,
162
+ policy: rule.policy
163
+ });
164
+ if (decision.revision > 0) candidates.push({
165
+ subject,
166
+ decision
167
+ });
168
+ }
169
+ if (candidates.length > 1) throw require_errors.rateLimitUnavailableError(/* @__PURE__ */ new Error("Multiple subject-key candidates exist"));
170
+ if (candidates.length === 1) return candidates[0].subject;
171
+ return require_subject.withBucket(require_subject.hashRateLimitSubject(rule.subject, keyring), rule.bucket, rule.policy.policyId, rule.policy.policyVersion);
172
+ }
173
+ };
174
+ RegisteredRateLimiter = class {
175
+ limiter;
176
+ registry;
177
+ definitions = /* @__PURE__ */ new Map();
178
+ policyRevisions = /* @__PURE__ */ new Map();
179
+ maxBatchEntries = 1e3;
180
+ maxCostPerConsume = 1e5;
181
+ constructor(limiter, registry, options = {}) {
182
+ this.limiter = limiter;
183
+ this.registry = registry;
184
+ this.configureLimits(options);
185
+ }
186
+ configureLimits(options = {}) {
187
+ for (const [name, value] of Object.entries(options)) if (!Number.isSafeInteger(value) || Number(value) < 1) throw require_errors.rateLimitConfigurationError(`Rate limit ${name} must be a positive integer`);
188
+ if (options.maxBatchEntries != null) this.maxBatchEntries = options.maxBatchEntries;
189
+ if (options.maxCostPerConsume != null) this.maxCostPerConsume = options.maxCostPerConsume;
190
+ }
191
+ defineRule(definition) {
192
+ require_subject.validateSafeIdentifier(definition.id, "rule.id");
193
+ require_subject.validateSafeIdentifier(definition.bucket, "rule.bucket");
194
+ if (this.definitions.has(definition.id)) throw require_errors.rateLimitConfigurationError(`Rate limit rule ${definition.id} is already defined`);
195
+ if (definition.algorithm !== "fixed-window") throw require_errors.rateLimitConfigurationError("Registered rules currently support fixed-window");
196
+ if (typeof definition.subject !== "function") throw require_errors.rateLimitConfigurationError("Registered rule subject resolver is required");
197
+ const snapshot = {
198
+ ...definition,
199
+ limit: typeof definition.limit === "function" ? definition.limit : Object.freeze({ ...definition.limit })
200
+ };
201
+ this.definitions.set(definition.id, Object.freeze(snapshot));
202
+ }
203
+ async inspect(ruleId, context) {
204
+ return this.executeOne(ruleId, context, "inspect");
205
+ }
206
+ async consume(ruleId, context) {
207
+ return this.executeOne(ruleId, context, "consume");
208
+ }
209
+ async enforce(ruleId, context) {
210
+ const result = await this.consume(ruleId, context);
211
+ if (!result.allowed) throw require_errors.rateLimitedError(result.blockedUntilMs);
212
+ return result;
213
+ }
214
+ async consumeBatch(ruleId, entries) {
215
+ if (!Array.isArray(entries)) throw require_errors.rateLimitConfigurationError("Rate limit batch entries must be an array");
216
+ if (!entries.length) return [];
217
+ if (entries.length > this.maxBatchEntries) throw require_errors.rateLimitConfigurationError("Rate limit batch exceeds maxBatchEntries");
218
+ const entryIds = /* @__PURE__ */ new Set();
219
+ for (const entry of entries) {
220
+ if (!entry || typeof entry.entryId !== "string" || entry.entryId.length < 1 || entry.entryId.length > 256 || entryIds.has(entry.entryId)) throw require_errors.rateLimitConfigurationError("Rate limit batch entryId is invalid or duplicated");
221
+ entryIds.add(entry.entryId);
222
+ }
223
+ const definition = this.getDefinition(ruleId);
224
+ const limitResults = await Promise.all(entries.map((entry) => this.resolveLimit(definition, entry.context)));
225
+ const resolutions = await Promise.all(entries.map(async (entry, index) => {
226
+ const limit = limitResults[index];
227
+ if (!limit) return { applied: false };
228
+ return this.resolveApplicable(definition, entry.context, limit);
229
+ }));
230
+ const groups = /* @__PURE__ */ new Map();
231
+ const subjectPolicies = /* @__PURE__ */ new Map();
232
+ for (let index = 0; index < resolutions.length; index++) {
233
+ const resolution = resolutions[index];
234
+ if (!resolution.applied) continue;
235
+ const rule = resolution.rule;
236
+ const identity = await this.limiter.subjectIdentity(rule);
237
+ const subjectIdentity = [
238
+ rule.store,
239
+ rule.subjectKeyring,
240
+ rule.subject.namespace,
241
+ rule.subject.scope,
242
+ rule.subject.subjectType,
243
+ rule.bucket,
244
+ identity
245
+ ].join("|");
246
+ const previousFingerprint = subjectPolicies.get(subjectIdentity);
247
+ if (previousFingerprint && previousFingerprint !== rule.policy.policyFingerprint) throw require_errors.rateLimitConfigurationError("Same rate limit subject resolved incompatible policies in one batch");
248
+ subjectPolicies.set(subjectIdentity, rule.policy.policyFingerprint);
249
+ const groupKey = `${subjectIdentity}|${rule.policy.policyFingerprint}`;
250
+ const group = groups.get(groupKey);
251
+ if (group) {
252
+ group.cost = require_config.validateRateLimitCost(group.cost + rule.cost, this.maxCostPerConsume);
253
+ group.indexes.push(index);
254
+ } else groups.set(groupKey, {
255
+ rule: { ...rule },
256
+ indexes: [index],
257
+ cost: rule.cost
258
+ });
259
+ }
260
+ const decisions = /* @__PURE__ */ new Map();
261
+ await Promise.all(Array.from(groups.values(), async (group) => {
262
+ group.rule.cost = group.cost;
263
+ const result = await this.limiter.consume(group.rule);
264
+ for (const index of group.indexes) decisions.set(index, this.publicDecision(ruleId, result.decision));
265
+ }));
266
+ return entries.map((entry, index) => {
267
+ return {
268
+ ...decisions.get(index) || {
269
+ ruleId,
270
+ applied: false,
271
+ allowed: true,
272
+ blockedUntilMs: null,
273
+ revision: 0
274
+ },
275
+ entryId: entry.entryId
276
+ };
277
+ });
278
+ }
279
+ async executeOne(ruleId, context, action) {
280
+ const resolution = await this.resolve(ruleId, context);
281
+ if (!resolution.applied) return {
282
+ ruleId,
283
+ applied: false,
284
+ allowed: true,
285
+ blockedUntilMs: null,
286
+ revision: 0
287
+ };
288
+ const result = action === "inspect" ? await this.limiter.inspect(resolution.rule) : await this.limiter.consume(resolution.rule);
289
+ return this.publicDecision(ruleId, result.decision);
290
+ }
291
+ async resolve(ruleId, context) {
292
+ const definition = this.getDefinition(ruleId);
293
+ const limit = await this.resolveLimit(definition, context);
294
+ if (!limit) return { applied: false };
295
+ return this.resolveApplicable(definition, context, limit);
296
+ }
297
+ async resolveApplicable(definition, context, limit) {
298
+ const subject = await definition.subject(context);
299
+ const rawCost = typeof definition.cost === "function" ? await definition.cost(context) : definition.cost || 1;
300
+ const cost = require_config.validateRateLimitCost(rawCost, this.maxCostPerConsume);
301
+ const stateTtlMs = limit.stateTtlMs || Math.min(31536e6, Math.max(limit.windowMs, limit.blockDurationMs || limit.windowMs) * 2);
302
+ const policy = require_config.normalizeFixedWindowPolicy({
303
+ policyId: definition.id,
304
+ policyVersion: String(limit.revision),
305
+ maxAttempts: limit.limit,
306
+ observationWindowMs: limit.windowMs,
307
+ blockDurationMs: limit.blockDurationMs || limit.windowMs,
308
+ stateTtlMs
309
+ });
310
+ const revisionKey = `${definition.id}\u0000${limit.revision}`;
311
+ const previousFingerprint = this.policyRevisions.get(revisionKey);
312
+ if (previousFingerprint && previousFingerprint !== policy.policyFingerprint) throw require_errors.rateLimitConfigurationError(`Rate limit rule ${definition.id} changed policy without changing revision`);
313
+ this.policyRevisions.set(revisionKey, policy.policyFingerprint);
314
+ return {
315
+ applied: true,
316
+ rule: {
317
+ ruleId: definition.id,
318
+ store: definition.store,
319
+ subjectKeyring: definition.subjectKeyring,
320
+ subject,
321
+ bucket: definition.bucket,
322
+ policy,
323
+ cost,
324
+ onStoreError: definition.onStoreError,
325
+ phase: definition.phase || "domain",
326
+ order: definition.order || 0,
327
+ shortCircuitOnDeny: definition.shortCircuitOnDeny === true
328
+ }
329
+ };
330
+ }
331
+ async resolveLimit(definition, context) {
332
+ const result = typeof definition.limit === "function" ? await definition.limit(context) : definition.limit;
333
+ if (result == null) return null;
334
+ if (typeof result !== "object" || typeof result.revision !== "string" || !result.revision) throw require_errors.rateLimitConfigurationError("Rate limit resolver returned an invalid limit");
335
+ return result;
336
+ }
337
+ getDefinition(ruleId) {
338
+ const definition = this.definitions.get(ruleId);
339
+ if (!definition) throw require_errors.rateLimitConfigurationError(`Rate limit rule ${ruleId} was not found`);
340
+ return definition;
341
+ }
342
+ publicDecision(ruleId, decision) {
343
+ return {
344
+ ruleId,
345
+ applied: true,
346
+ allowed: decision.allowed,
347
+ blockedUntilMs: decision.blockedUntilMs,
348
+ revision: decision.revision
349
+ };
350
+ }
351
+ };
352
+ }));
353
+ //#endregion
354
+ init_limiter();
355
+ exports.RateLimiter = RateLimiter;
356
+ exports.RegisteredRateLimiter = RegisteredRateLimiter;
357
+ Object.defineProperty(exports, "init_limiter", {
358
+ enumerable: true,
359
+ get: function() {
360
+ return init_limiter;
361
+ }
362
+ });
363
+ Object.defineProperty(exports, "limiter_exports", {
364
+ enumerable: true,
365
+ get: function() {
366
+ return limiter_exports;
367
+ }
368
+ });
@@ -0,0 +1,92 @@
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_persisted_model = require("./stores/persisted-model.cjs");
5
+ //#region src/lib/rate-limit/registry.ts
6
+ var registry_exports = /* @__PURE__ */ require_runtime.__exportAll({ RateLimitRegistry: () => RateLimitRegistry });
7
+ var RateLimitRegistry;
8
+ var init_registry = require_runtime.__esmMin((() => {
9
+ require_subject.init_subject();
10
+ require_errors.init_errors();
11
+ require_persisted_model.init_persisted_model();
12
+ RateLimitRegistry = class {
13
+ app;
14
+ factories = /* @__PURE__ */ new Map();
15
+ stores = /* @__PURE__ */ new Map();
16
+ keyrings = /* @__PURE__ */ new Map();
17
+ frozen = false;
18
+ constructor(app) {
19
+ this.app = app;
20
+ this.registerProvider("persisted-model", (config, currentApp) => {
21
+ const model = currentApp.models && currentApp.models[config.model];
22
+ if (!model) throw require_errors.rateLimitConfigurationError(`Rate limit state model ${config.model || "(missing)"} was not found`);
23
+ return new require_persisted_model.PersistedModelRateLimitStore({
24
+ model,
25
+ allowMemory: config.allowMemory === true,
26
+ maxRetries: config.maxRetries
27
+ });
28
+ });
29
+ }
30
+ registerProvider(name, factory) {
31
+ if (this.frozen) throw require_errors.rateLimitConfigurationError("Rate limit registry is frozen");
32
+ if (!name || typeof factory !== "function") throw require_errors.rateLimitConfigurationError("Invalid rate limit provider registration");
33
+ if (this.factories.has(name)) throw require_errors.rateLimitConfigurationError(`Rate limit provider ${name} is already registered`);
34
+ this.factories.set(name, factory);
35
+ }
36
+ registerStore(name, store) {
37
+ if (this.frozen) throw require_errors.rateLimitConfigurationError("Rate limit registry is frozen");
38
+ if (this.stores.has(name)) throw require_errors.rateLimitConfigurationError(`Rate limit store ${name} is already registered`);
39
+ this.stores.set(name, Promise.resolve(store).then(async (value) => {
40
+ await value.initialize();
41
+ return value;
42
+ }));
43
+ }
44
+ registerKeyring(name, keyring) {
45
+ if (this.frozen) throw require_errors.rateLimitConfigurationError("Rate limit registry is frozen");
46
+ if (this.keyrings.has(name)) throw require_errors.rateLimitConfigurationError(`Rate limit subject keyring ${name} is already registered`);
47
+ this.keyrings.set(name, require_subject.normalizeKeyring(keyring, `subjectKeyrings.${name}`));
48
+ }
49
+ configure(config = {}) {
50
+ if (this.frozen) throw require_errors.rateLimitConfigurationError("Rate limit registry is frozen");
51
+ for (const [name, keyring] of Object.entries(config.subjectKeyrings || {})) this.registerKeyring(name, keyring);
52
+ }
53
+ getKeyring(name) {
54
+ const keyring = this.keyrings.get(name);
55
+ if (!keyring) throw require_errors.rateLimitConfigurationError(`Rate limit subject keyring ${name} was not found`);
56
+ return keyring;
57
+ }
58
+ async resolveStore(name) {
59
+ const existing = this.stores.get(name);
60
+ if (existing) return existing;
61
+ const config = (this.app.get("rateLimit") || {}).stores?.[name];
62
+ if (!config) throw require_errors.rateLimitConfigurationError(`Rate limit store ${name} was not found`);
63
+ const factory = this.factories.get(config.provider);
64
+ if (!factory) throw require_errors.rateLimitConfigurationError(`Rate limit provider ${config.provider || "(missing)"} is not registered`);
65
+ const promise = Promise.resolve().then(async () => {
66
+ const store = factory(config, this.app);
67
+ await store.initialize();
68
+ return store;
69
+ });
70
+ this.stores.set(name, promise);
71
+ return promise;
72
+ }
73
+ freeze() {
74
+ this.frozen = true;
75
+ }
76
+ };
77
+ }));
78
+ //#endregion
79
+ init_registry();
80
+ exports.RateLimitRegistry = RateLimitRegistry;
81
+ Object.defineProperty(exports, "init_registry", {
82
+ enumerable: true,
83
+ get: function() {
84
+ return init_registry;
85
+ }
86
+ });
87
+ Object.defineProperty(exports, "registry_exports", {
88
+ enumerable: true,
89
+ get: function() {
90
+ return registry_exports;
91
+ }
92
+ });
@@ -0,0 +1,180 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_runtime = require("../../_virtual/_rolldown/runtime.cjs");
3
+ const require_errors = require("./errors.cjs");
4
+ const require_subject = require("./subject.cjs");
5
+ const require_config = require("./config.cjs");
6
+ //#region src/lib/rate-limit/remote.ts
7
+ var remote_exports = /* @__PURE__ */ require_runtime.__exportAll({
8
+ installRemoteRateLimiting: () => installRemoteRateLimiting,
9
+ normalizeNamedPolicy: () => normalizeNamedPolicy
10
+ });
11
+ function normalizeNamedPolicy(name, input) {
12
+ if (!input || typeof input !== "object") throw require_errors.rateLimitConfigurationError(`Rate limit policy ${name} was not found`);
13
+ const common = {
14
+ policyId: input.policyId || name,
15
+ policyVersion: String(input.policyVersion || ""),
16
+ stateTtlMs: Number(input.stateTtl) * 1e3
17
+ };
18
+ if (input.algorithm === "fixed-window") return require_config.normalizeFixedWindowPolicy({
19
+ ...common,
20
+ maxAttempts: input.maxAttempts,
21
+ observationWindowMs: Number(input.observationWindow) * 1e3,
22
+ blockDurationMs: Number(input.blockDuration) * 1e3
23
+ });
24
+ if (input.algorithm === "failure-lockout") return require_config.normalizeFailureLockoutPolicy({
25
+ ...common,
26
+ maxFailedAttempts: input.maxFailedAttempts,
27
+ observationWindowMs: Number(input.observationWindow) * 1e3,
28
+ blockDurationMs: Number(input.blockDuration) * 1e3,
29
+ strategy: input.strategy,
30
+ multiplier: input.multiplier,
31
+ maxBlockDurationMs: Number(input.maxBlockDuration) * 1e3,
32
+ backoffResetAfterMs: Number(input.backoffResetAfter) * 1e3
33
+ });
34
+ throw require_errors.rateLimitConfigurationError(`Rate limit policy ${name} has an invalid algorithm`);
35
+ }
36
+ function installRemoteRateLimiting(app) {
37
+ const resolvers = /* @__PURE__ */ new Map();
38
+ const installed = /* @__PURE__ */ new WeakSet();
39
+ const staticRules = /* @__PURE__ */ new WeakMap();
40
+ app.registerRateLimitResolver = function(name, resolver) {
41
+ if (!name || typeof resolver !== "function" || resolvers.has(name)) throw require_errors.rateLimitConfigurationError(`Rate limit resolver ${name || "(missing)"} is invalid`);
42
+ resolvers.set(name, resolver);
43
+ return app;
44
+ };
45
+ function staticRuleFor(Model, method) {
46
+ const cached = staticRules.get(method);
47
+ if (cached) return cached;
48
+ const metadata = method.rateLimit;
49
+ const policies = app.get("rateLimitPolicies") || {};
50
+ if (!((app.get("rateLimit") || {}).stores?.[metadata.store])?.namespace) throw require_errors.rateLimitConfigurationError(`Rate limit store ${metadata.store || "(missing)"} has no namespace`);
51
+ const onStoreError = metadata.onStoreError || "deny";
52
+ if (onStoreError !== "allow" && onStoreError !== "deny") throw require_errors.rateLimitConfigurationError("Rate limit store error mode is invalid");
53
+ const phase = metadata.phase || "resolved";
54
+ if (![
55
+ "early",
56
+ "resolved",
57
+ "domain"
58
+ ].includes(phase)) throw require_errors.rateLimitConfigurationError("Rate limit phase is invalid");
59
+ const order = metadata.order == null ? 0 : Number(metadata.order);
60
+ if (!Number.isSafeInteger(order)) throw require_errors.rateLimitConfigurationError("Rate limit order is invalid");
61
+ const bucket = metadata.bucket || `${Model.modelName}.${method.name}`;
62
+ const subjectKeyring = metadata.subjectKeyring || "default";
63
+ require_subject.validateSafeIdentifier(metadata.store, "rateLimit.store");
64
+ require_subject.validateSafeIdentifier(subjectKeyring, "rateLimit.subjectKeyring");
65
+ require_subject.validateSafeIdentifier(bucket, "rateLimit.bucket");
66
+ const rule = Object.freeze({
67
+ ruleId: `${Model.modelName}.${method.name}`,
68
+ store: metadata.store,
69
+ subjectKeyring,
70
+ bucket,
71
+ policy: Object.freeze(normalizeNamedPolicy(metadata.policy, policies[metadata.policy])),
72
+ cost: require_config.validateRateLimitCost(Number(metadata.cost || 1), (app.get("rateLimit") || {}).limits?.maxCostPerConsume || 1e5),
73
+ onStoreError,
74
+ phase,
75
+ order,
76
+ shortCircuitOnDeny: metadata.shortCircuitOnDeny === true
77
+ });
78
+ staticRules.set(method, rule);
79
+ return rule;
80
+ }
81
+ app.validateRemoteRateLimits = async function() {
82
+ for (const Model of app.models()) {
83
+ if (!Model?.sharedClass?.methods) continue;
84
+ for (const method of Model.sharedClass.methods()) {
85
+ const metadata = method.rateLimit;
86
+ if (!metadata) continue;
87
+ if (metadata.resolver) {
88
+ if (!resolvers.has(metadata.resolver)) throw require_errors.rateLimitConfigurationError(`Rate limit resolver ${metadata.resolver} was not found`);
89
+ continue;
90
+ }
91
+ const rule = staticRuleFor(Model, method);
92
+ await app.rateLimit.registry.resolveStore(rule.store);
93
+ app.rateLimit.registry.getKeyring(rule.subjectKeyring);
94
+ }
95
+ }
96
+ return app;
97
+ };
98
+ function installForModel(Model) {
99
+ if (!Model?.sharedClass?.methods) return;
100
+ for (const method of Model.sharedClass.methods()) {
101
+ const metadata = method.rateLimit;
102
+ if (!metadata || installed.has(method)) continue;
103
+ installed.add(method);
104
+ const methodName = method.isStatic ? method.name : `prototype.${method.name}`;
105
+ Model.beforeRemote(methodName, function(ctx, unused, next) {
106
+ Promise.resolve().then(async () => {
107
+ if (!app.rateLimitReadiness.ready) throw require_errors.rateLimitConfigurationError("Rate limiting is not initialized");
108
+ const request = ctx.req;
109
+ const source = request && typeof Model.getRateLimitSource === "function" ? await Model.getRateLimitSource(request) : request?.ip || null;
110
+ const remoteContext = Object.freeze({
111
+ app,
112
+ model: Model,
113
+ methodName: `${Model.modelName}.${method.name}`,
114
+ args: Object.freeze({ ...ctx.args }),
115
+ request,
116
+ accessToken: request?.accessToken,
117
+ source
118
+ });
119
+ let rules;
120
+ if (metadata.resolver) {
121
+ const resolver = resolvers.get(metadata.resolver);
122
+ if (!resolver) throw require_errors.rateLimitConfigurationError(`Rate limit resolver ${metadata.resolver} was not found`);
123
+ const resolved = await resolver(remoteContext);
124
+ rules = resolved == null ? [] : Array.isArray(resolved) ? resolved : [resolved];
125
+ } else {
126
+ const storeConfig = (app.get("rateLimit") || {}).stores?.[metadata.store];
127
+ rules = [{
128
+ ...staticRuleFor(Model, method),
129
+ subject: {
130
+ namespace: storeConfig.namespace,
131
+ scope: "remote-method",
132
+ subjectType: "endpoint",
133
+ canonicalValue: `${Model.modelName}.${method.name}`
134
+ }
135
+ }];
136
+ }
137
+ if (!rules.length) return;
138
+ const result = await app.rateLimit.limiter.consumeAll(rules);
139
+ if (!result.allowed) throw require_errors.rateLimitedError(result.blockedUntilMs);
140
+ }).then(() => next(), (error) => next(error));
141
+ });
142
+ Model.afterRemoteError(methodName, function(ctx, next) {
143
+ const error = ctx.error;
144
+ if (error?.code === "RATE_LIMITED" || error?.code === "RATE_LIMIT_UNAVAILABLE") {
145
+ ctx.res.setHeader("Cache-Control", "no-store");
146
+ if (error.retryAfter) ctx.res.setHeader("Retry-After", String(error.retryAfter));
147
+ }
148
+ next();
149
+ });
150
+ }
151
+ }
152
+ app.on("modelRemoted", function() {
153
+ app.models().forEach(installForModel);
154
+ });
155
+ app.installRemoteRateLimits = function() {
156
+ app.models().forEach(installForModel);
157
+ return app;
158
+ };
159
+ }
160
+ var init_remote = require_runtime.__esmMin((() => {
161
+ require_config.init_config();
162
+ require_errors.init_errors();
163
+ require_subject.init_subject();
164
+ }));
165
+ //#endregion
166
+ init_remote();
167
+ Object.defineProperty(exports, "init_remote", {
168
+ enumerable: true,
169
+ get: function() {
170
+ return init_remote;
171
+ }
172
+ });
173
+ exports.installRemoteRateLimiting = installRemoteRateLimiting;
174
+ exports.normalizeNamedPolicy = normalizeNamedPolicy;
175
+ Object.defineProperty(exports, "remote_exports", {
176
+ enumerable: true,
177
+ get: function() {
178
+ return remote_exports;
179
+ }
180
+ });
@@ -0,0 +1,6 @@
1
+ import { FailureLockoutRateLimitPolicy, FixedWindowRateLimitPolicy } from "./types.cjs";
2
+ //#region src/lib/rate-limit/remote.d.ts
3
+ declare function normalizeNamedPolicy(name: string, input: any): FixedWindowRateLimitPolicy | FailureLockoutRateLimitPolicy;
4
+ declare function installRemoteRateLimiting(app: any): void;
5
+ //#endregion
6
+ export { installRemoteRateLimiting, normalizeNamedPolicy };