@superfunctions/middleware 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,11 +27,14 @@ export interface CheckLimitInput {
27
27
  }
28
28
  export interface CheckManyLimitsInput {
29
29
  keys: string[];
30
+ /** Override the shared limit for each unprefixed key. */
31
+ limitsByKey?: Record<string, number>;
30
32
  windowSeconds?: number;
31
33
  limit?: number;
32
34
  }
33
35
  export interface RateLimitManyResult {
34
36
  allowed: boolean;
37
+ blockedKeys?: string[];
35
38
  remainingByKey: Map<string, number>;
36
39
  resetAtByKey: Map<string, string>;
37
40
  resetAt: string;
@@ -289,6 +289,7 @@ export function createRateLimiter(config) {
289
289
  const uniqueKeys = [...new Set(input.keys)];
290
290
  const effectiveWindowMs = (input.windowSeconds !== undefined ? input.windowSeconds * 1000 : undefined) ?? windowMs;
291
291
  const effectiveLimit = input.limit ?? maxRequests;
292
+ const limits = uniqueKeys.map((key) => input.limitsByKey && Object.prototype.hasOwnProperty.call(input.limitsByKey, key) ? input.limitsByKey[key] ?? effectiveLimit : effectiveLimit);
292
293
  const namespacedKeys = uniqueKeys.map((key) => `${keyPrefix}${key}`);
293
294
  if (uniqueKeys.length === 0) {
294
295
  return {
@@ -301,11 +302,12 @@ export function createRateLimiter(config) {
301
302
  }
302
303
  return withKeyLocks(namespacedKeys, async () => {
303
304
  const currentTime = now();
304
- const preflight = await Promise.all(namespacedKeys.map((key) => evaluateConfiguredKey(key, currentTime, effectiveWindowMs, effectiveLimit, false)));
305
+ const preflight = await Promise.all(namespacedKeys.map((key, index) => evaluateConfiguredKey(key, currentTime, effectiveWindowMs, limits[index], false)));
305
306
  const blocked = preflight.filter((result) => !result.allowed);
306
307
  if (blocked.length > 0) {
307
308
  return {
308
309
  allowed: false,
310
+ blockedKeys: uniqueKeys.filter((_key, index) => !preflight[index].allowed),
309
311
  remainingByKey: new Map(uniqueKeys.map((key, index) => [key, preflight[index].remaining])),
310
312
  resetAtByKey: new Map(uniqueKeys.map((key, index) => [key, preflight[index].resetAt])),
311
313
  resetAt: new Date(Math.max(...blocked.map((result) => Date.parse(result.resetAt)))).toISOString(),
@@ -318,13 +320,14 @@ export function createRateLimiter(config) {
318
320
  // Commit each key through the configured store. If a later commit
319
321
  // becomes blocked or fails, retain earlier quota charges (fail
320
322
  // closed) instead of restoring stale snapshots over concurrent work.
321
- const result = await evaluateConfiguredKey(key, currentTime, effectiveWindowMs, effectiveLimit, true);
323
+ const result = await evaluateConfiguredKey(key, currentTime, effectiveWindowMs, limits[index], true);
322
324
  committed.push(result);
323
325
  if (!result.allowed) {
324
326
  const actual = preflight.map((entry, entryIndex) => committed[entryIndex] ?? entry);
325
327
  const blockedActual = actual.filter((entry) => !entry.allowed);
326
328
  return {
327
329
  allowed: false,
330
+ blockedKeys: uniqueKeys.filter((_key, index) => !actual[index].allowed),
328
331
  remainingByKey: new Map(uniqueKeys.map((entry, entryIndex) => [entry, actual[entryIndex].remaining])),
329
332
  resetAtByKey: new Map(uniqueKeys.map((entry, entryIndex) => [entry, actual[entryIndex].resetAt])),
330
333
  resetAt: new Date(Math.max(...blockedActual.map((entry) => Date.parse(entry.resetAt)))).toISOString(),
@@ -348,3 +348,15 @@ describe('rate-limit', () => {
348
348
  });
349
349
  });
350
350
  });
351
+ it('uses only own limit overrides, including inherited property names', async () => {
352
+ const limiter = createRateLimiter({ windowMs: 60000, maxRequests: 2, algorithm: 'fixed-window' });
353
+ const keys = ['constructor', 'toString', '__proto__'];
354
+ const limitsByKey = Object.create({ constructor: 0 });
355
+ limitsByKey.toString = 1;
356
+ const first = await limiter.checkMany({ keys, limitsByKey });
357
+ expect(first.allowed).toBe(true);
358
+ expect(first.remainingByKey.get('constructor')).toBe(1);
359
+ expect(first.remainingByKey.get('toString')).toBe(0);
360
+ expect(first.remainingByKey.get('__proto__')).toBe(1);
361
+ expect((await limiter.checkMany({ keys, limitsByKey })).blockedKeys).toEqual(['toString']);
362
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superfunctions/middleware",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Shared middleware utilities for Superfunctions",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,37 +15,50 @@
15
15
  "default": "./dist/rate-limit.js"
16
16
  }
17
17
  },
18
- "files": ["dist"],
18
+ "files": [
19
+ "dist"
20
+ ],
19
21
  "scripts": {
20
22
  "build": "tsc",
21
- "test": "vitest",
23
+ "test": "vitest run",
22
24
  "test:watch": "vitest --watch",
23
25
  "typecheck": "tsc --noEmit",
24
26
  "clean": "rm -rf dist"
25
27
  },
26
- "keywords": ["middleware", "rate-limit", "superfunctions"],
28
+ "keywords": [
29
+ "middleware",
30
+ "rate-limit",
31
+ "superfunctions"
32
+ ],
27
33
  "author": "21n",
28
34
  "license": "MIT",
29
- "bugs": { "url": "https://github.com/21nCo/super-functions/issues" },
35
+ "bugs": {
36
+ "url": "https://github.com/21nCo/super-functions/issues"
37
+ },
30
38
  "repository": {
31
39
  "type": "git",
32
- "url": "git+https://github.com/21nCo/super-functions.git",
40
+ "url": "git+https://github.com/21nCo/superfunctions.git",
33
41
  "directory": "packages/middleware"
34
42
  },
35
- "publishConfig": { "access": "public" },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
36
46
  "peerDependencies": {
37
- "@superfunctions/db": "0.2.0"
47
+ "@superfunctions/db": "^0.2.0 || ^0.3.0"
38
48
  },
39
49
  "peerDependenciesMeta": {
40
- "@superfunctions/db": { "optional": true }
50
+ "@superfunctions/db": {
51
+ "optional": true
52
+ }
41
53
  },
42
54
  "dependencies": {
43
- "@superfunctions/observability": "0.0.1"
55
+ "@superfunctions/observability": "0.0.3"
44
56
  },
45
57
  "devDependencies": {
46
- "@superfunctions/db": "^0.2.0",
58
+ "@superfunctions/db": "0.3.2",
47
59
  "@types/node": "^22.0.0",
48
60
  "typescript": "^5.6.0",
49
61
  "vitest": "^3.2.4"
50
- }
62
+ },
63
+ "gitHead": "699d9d09932199d660333c57fe7bc646492ea3bf"
51
64
  }