@ttt-productions/rate-limit-core 0.6.0 → 0.8.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.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { createRedisClientFactory } from "./redis-client.js";
2
2
  export type { RedisCredentialsProvider, RedisClientFactoryOptions, } from "./redis-client.js";
3
3
  export { createRateLimiterFactory } from "./rate-limiter.js";
4
- export type { RateLimitConfig, RateLimiterFactoryOptions, } from "./rate-limiter.js";
4
+ export type { RateLimitConfig, RateLimiterFactoryOptions, RateLimiterDegradation, } from "./rate-limiter.js";
5
5
  export { getRateLimitErrorMessage } from "./error-message.js";
6
6
  export type { RateLimitErrorMessageOptions } from "./error-message.js";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,YAAY,EACV,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,YAAY,EACV,eAAe,EACf,yBAAyB,GAC1B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,YAAY,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,YAAY,EACV,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,YAAY,EACV,eAAe,EACf,yBAAyB,EACzB,sBAAsB,GACvB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,YAAY,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAM7D,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAM7D,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAM7D,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAO7D,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC"}
@@ -8,11 +8,39 @@ export interface RateLimitConfig {
8
8
  /** Window duration (e.g. "1 h", "10 m", "1 d"). */
9
9
  window: string;
10
10
  }
11
+ /**
12
+ * Structured report of a rate-limiter degradation. Passed to the injected
13
+ * `onDegraded` hook so the consuming app can decide the alerting posture
14
+ * (e.g. Sentry `captureMessage` in prod, a warn line in the emulator) WITHOUT
15
+ * this generic package importing a monitoring provider directly.
16
+ *
17
+ * - `reason: 'unavailable'` — a limiter could not be built (Redis is null:
18
+ * missing credentials or the disable hook). Fired at most once per factory
19
+ * instance, on the FIRST `checkRateLimit` call, so an unconfigured prod deploy
20
+ * pages within minutes of traffic instead of silently allowing everything.
21
+ * - `reason: 'limit-error'` — `limiter.limit()` rejected at runtime (e.g. an
22
+ * Upstash outage). The call fails OPEN (`allowed: true`) after reporting.
23
+ */
24
+ export interface RateLimiterDegradation {
25
+ reason: 'unavailable' | 'limit-error';
26
+ /** The limiter prefix in play, when known. */
27
+ prefix?: string;
28
+ /** The underlying error for `reason: 'limit-error'`. */
29
+ error?: unknown;
30
+ }
11
31
  export interface RateLimiterFactoryOptions {
12
32
  /** Returns the Redis client or null when disabled. */
13
33
  getRedis: () => Redis | null;
14
34
  /** When true, includes Upstash analytics. Default true. */
15
35
  analytics?: boolean;
36
+ /**
37
+ * Optional degradation hook. The consuming app injects its own alerting
38
+ * (Sentry capture in prod, warn in local/emulator) — this package stays
39
+ * provider-agnostic. `unavailable` fires at most once per factory instance;
40
+ * `limit-error` fires per failed `limit()` call. The hook must never throw
41
+ * (it is invoked inside the fail-open path).
42
+ */
43
+ onDegraded?: (info: RateLimiterDegradation) => void;
16
44
  }
17
45
  /**
18
46
  * Create a memoized Ratelimit factory. Limiters are cached by their `prefix`
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../src/rate-limiter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAE5C,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,yBAAyB;IACxC,sDAAsD;IACtD,QAAQ,EAAE,MAAM,KAAK,GAAG,IAAI,CAAC;IAC7B,2DAA2D;IAC3D,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB;6BAIzC,eAAe,KAAG,SAAS,GAAG,IAAI;0BAsB3D,MAAM,UACH,eAAe,KACtB,OAAO,CAAC;QAAE,OAAO,EAAE,IAAI,CAAA;KAAE,GAAG;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;EASlE"}
1
+ {"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../src/rate-limiter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAE5C,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,aAAa,GAAG,aAAa,CAAC;IACtC,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,yBAAyB;IACxC,sDAAsD;IACtD,QAAQ,EAAE,MAAM,KAAK,GAAG,IAAI,CAAC;IAC7B,2DAA2D;IAC3D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,sBAAsB,KAAK,IAAI,CAAC;CACrD;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB;6BAczC,eAAe,KAAG,SAAS,GAAG,IAAI;0BA6B3D,MAAM,UACH,eAAe,KACtB,OAAO,CAAC;QAAE,OAAO,EAAE,IAAI,CAAA;KAAE,GAAG;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;EAqBlE"}
@@ -6,6 +6,16 @@ import { Ratelimit } from "@upstash/ratelimit";
6
6
  export function createRateLimiterFactory(options) {
7
7
  const limiters = new Map();
8
8
  const analytics = options.analytics ?? true;
9
+ // Fire the `unavailable` degradation at most once per factory instance.
10
+ let reportedUnavailable = false;
11
+ function report(info) {
12
+ try {
13
+ options.onDegraded?.(info);
14
+ }
15
+ catch {
16
+ // The alerting hook must never break the fail-open path.
17
+ }
18
+ }
9
19
  function getRateLimiter(config) {
10
20
  const redis = options.getRedis();
11
21
  if (!redis)
@@ -24,16 +34,35 @@ export function createRateLimiterFactory(options) {
24
34
  }
25
35
  /**
26
36
  * Check rate limit for a key (e.g. userId). Returns `{ allowed: true }`
27
- * when Redis is disabled OR the limit isn't exceeded.
37
+ * when Redis is unavailable OR the limit isn't exceeded.
38
+ *
39
+ * FAIL-OPEN POSTURE (decided 2026-07-03): both failure modes degrade to
40
+ * "allow, but observed" rather than blocking every write. When the limiter is
41
+ * unavailable (Redis null — missing credentials / disabled), the first call
42
+ * reports `unavailable` so an unconfigured prod deploy is loud. When a built
43
+ * limiter's `limit()` rejects (Upstash outage), the error is reported and the
44
+ * call still allows — an outage must not take down every rate-limited callable.
28
45
  */
29
46
  async function checkRateLimit(key, config) {
30
47
  const limiter = getRateLimiter(config);
31
- if (!limiter)
48
+ if (!limiter) {
49
+ if (!reportedUnavailable) {
50
+ reportedUnavailable = true;
51
+ report({ reason: 'unavailable', prefix: config.prefix });
52
+ }
53
+ return { allowed: true };
54
+ }
55
+ try {
56
+ const { success, reset } = await limiter.limit(key);
57
+ if (!success)
58
+ return { allowed: false, reset };
59
+ return { allowed: true };
60
+ }
61
+ catch (error) {
62
+ // Runtime Redis error (e.g. Upstash outage). Fail OPEN, but report.
63
+ report({ reason: 'limit-error', prefix: config.prefix, error });
32
64
  return { allowed: true };
33
- const { success, reset } = await limiter.limit(key);
34
- if (!success)
35
- return { allowed: false, reset };
36
- return { allowed: true };
65
+ }
37
66
  }
38
67
  return { getRateLimiter, checkRateLimit };
39
68
  }
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../src/rate-limiter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAmB/C;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAkC;IACzE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAqB,CAAC;IAC9C,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC;IAE5C,SAAS,cAAc,CAAC,MAAuB;QAC7C,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;QACjC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QAExB,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC7C,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAE9B,MAAM,OAAO,GAAG,IAAI,SAAS,CAAC;YAC5B,KAAK;YACL,OAAO,EAAE,SAAS,CAAC,aAAa,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,MAAuD,CAAC;YACpH,SAAS;YACT,MAAM,EAAE,MAAM,CAAC,MAAM;SACtB,CAAC,CAAC;QACH,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACrC,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;;OAGG;IACH,KAAK,UAAU,cAAc,CAC3B,GAAW,EACX,MAAuB;QAEvB,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACvC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACpD,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QAC/C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC;IAED,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,CAAC;AAC5C,CAAC"}
1
+ {"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../src/rate-limiter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAgD/C;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAkC;IACzE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAqB,CAAC;IAC9C,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC;IAC5C,wEAAwE;IACxE,IAAI,mBAAmB,GAAG,KAAK,CAAC;IAEhC,SAAS,MAAM,CAAC,IAA4B;QAC1C,IAAI,CAAC;YACH,OAAO,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,CAAC;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,yDAAyD;QAC3D,CAAC;IACH,CAAC;IAED,SAAS,cAAc,CAAC,MAAuB;QAC7C,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;QACjC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QAExB,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC7C,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAE9B,MAAM,OAAO,GAAG,IAAI,SAAS,CAAC;YAC5B,KAAK;YACL,OAAO,EAAE,SAAS,CAAC,aAAa,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,MAAuD,CAAC;YACpH,SAAS;YACT,MAAM,EAAE,MAAM,CAAC,MAAM;SACtB,CAAC,CAAC;QACH,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACrC,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;;;;;;;;;OAUG;IACH,KAAK,UAAU,cAAc,CAC3B,GAAW,EACX,MAAuB;QAEvB,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,IAAI,CAAC,mBAAmB,EAAE,CAAC;gBACzB,mBAAmB,GAAG,IAAI,CAAC;gBAC3B,MAAM,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;YAC3D,CAAC;YACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC3B,CAAC;QACD,IAAI,CAAC;YACH,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACpD,IAAI,CAAC,OAAO;gBAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;YAC/C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC3B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,oEAAoE;YACpE,MAAM,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;YAChE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC3B,CAAC;IACH,CAAC;IAED,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,CAAC;AAC5C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/rate-limit-core",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ttt-productions/ttt-packages.git",