halt-rate 0.2.0 → 0.4.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 (38) hide show
  1. package/README.md +34 -921
  2. package/dist/adapters/express.d.mts +1 -1
  3. package/dist/adapters/express.d.ts +1 -1
  4. package/dist/adapters/fastify.d.mts +29 -0
  5. package/dist/adapters/fastify.d.ts +29 -0
  6. package/dist/adapters/fastify.js +38 -0
  7. package/dist/adapters/fastify.js.map +1 -0
  8. package/dist/adapters/fastify.mjs +35 -0
  9. package/dist/adapters/fastify.mjs.map +1 -0
  10. package/dist/adapters/graphql.d.mts +22 -0
  11. package/dist/adapters/graphql.d.ts +22 -0
  12. package/dist/adapters/graphql.js +51 -0
  13. package/dist/adapters/graphql.js.map +1 -0
  14. package/dist/adapters/graphql.mjs +49 -0
  15. package/dist/adapters/graphql.mjs.map +1 -0
  16. package/dist/adapters/hono.d.mts +30 -0
  17. package/dist/adapters/hono.d.ts +30 -0
  18. package/dist/adapters/hono.js +56 -0
  19. package/dist/adapters/hono.js.map +1 -0
  20. package/dist/adapters/hono.mjs +54 -0
  21. package/dist/adapters/hono.mjs.map +1 -0
  22. package/dist/adapters/next.d.mts +1 -1
  23. package/dist/adapters/next.d.ts +1 -1
  24. package/dist/adapters/next.js +26 -3
  25. package/dist/adapters/next.js.map +1 -1
  26. package/dist/adapters/next.mjs +26 -3
  27. package/dist/adapters/next.mjs.map +1 -1
  28. package/dist/index.d.mts +174 -3
  29. package/dist/index.d.ts +174 -3
  30. package/dist/index.js +631 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/index.mjs +614 -4
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/limiter-DqVVE0Kl.d.mts +310 -0
  35. package/dist/limiter-DqVVE0Kl.d.ts +310 -0
  36. package/package.json +80 -13
  37. package/dist/limiter-HCt8ADZ2.d.mts +0 -177
  38. package/dist/limiter-HCt8ADZ2.d.ts +0 -177
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { A as AtomicStore, R as RedisClientLike, E as EvaluateInput, D as Decision, P as Policy } from './limiter-HCt8ADZ2.js';
2
- export { a as Algorithm, I as InMemoryStore, K as KeyStrategy, b as RateLimiter, c as RateLimiterOptions, S as Store, i as isAtomicStore, n as normalizePolicy, t as toHeaders } from './limiter-HCt8ADZ2.js';
1
+ import { A as AtomicStore, R as RedisClientLike, E as EvaluateInput, D as Decision, P as Policy, T as TelemetryHooks, Q as Quota, a as Penalty } from './limiter-DqVVE0Kl.js';
2
+ export { b as Algorithm, C as CompositeTelemetry, I as InMemoryStore, K as KeyStrategy, L as LoggingTelemetry, M as MetricsTelemetry, c as PENALTY_LENIENT, d as PENALTY_MODERATE, e as PENALTY_STRICT, f as PenaltyConfig, g as PenaltyManager, h as QUOTA_ENTERPRISE_MONTHLY, i as QUOTA_FREE_DAILY, j as QUOTA_FREE_MONTHLY, k as QUOTA_PRO_DAILY, l as QUOTA_PRO_MONTHLY, m as QuotaManager, n as QuotaPeriod, o as RateLimiter, p as RateLimiterOptions, S as Store, q as isAtomicStore, r as normalizePolicy, t as toHeaders } from './limiter-DqVVE0Kl.js';
3
3
 
4
4
  /**
5
5
  * Redis storage backend for Halt — production-grade, atomic rate limiting.
@@ -130,4 +130,175 @@ declare namespace extractors {
130
130
  export { extractors_HEALTH_CHECK_PATHS as HEALTH_CHECK_PATHS, extractors_extractApiKey as extractApiKey, extractors_extractIp as extractIp, extractors_extractPath as extractPath, extractors_extractUserId as extractUserId, extractors_isHealthCheck as isHealthCheck, extractors_isPrivateIp as isPrivateIp };
131
131
  }
132
132
 
133
- export { AtomicStore, Decision, EvaluateInput, type FailMode, Policy, RedisClientLike, RedisStore, type RedisStoreOptions, extractors, index as presets };
133
+ /**
134
+ * Dynamic limits — change policies at runtime without restarting.
135
+ *
136
+ * `PolicyRegistry` holds named policies you can mutate live. Use it as the
137
+ * limiter's `policy` via `registry.resolver(selector)`:
138
+ *
139
+ * const registry = new PolicyRegistry([presets.PUBLIC_API]);
140
+ * const limiter = new RateLimiter({
141
+ * store,
142
+ * policy: registry.resolver((req) => 'public_api'),
143
+ * });
144
+ * // later, no restart:
145
+ * registry.update('public_api', { limit: 500 });
146
+ *
147
+ * For limits stored in Redis/DB/config (and shared across a fleet), use
148
+ * `cachedPolicyResolver` with a loader that reads that shared state.
149
+ */
150
+
151
+ declare class PolicyRegistry {
152
+ private policies;
153
+ constructor(initial?: Policy[]);
154
+ /** Add or replace a policy (keyed by `policy.name`). */
155
+ register(policy: Policy): this;
156
+ get(name: string): Policy | undefined;
157
+ has(name: string): boolean;
158
+ /** Mutate fields of an existing policy at runtime (e.g. `{ limit: 500 }`). */
159
+ update(name: string, patch: Partial<Omit<Policy, 'name'>>): Policy;
160
+ remove(name: string): boolean;
161
+ list(): Policy[];
162
+ /**
163
+ * Build a resolver for the limiter's `policy` option.
164
+ * @param selector maps a request to a registered policy name.
165
+ */
166
+ resolver(selector: (request: any) => string): (request: any) => Policy;
167
+ }
168
+ interface CachedPolicyResolverOptions {
169
+ /** Cache entry lifetime in ms (default 5000). */
170
+ ttlMs?: number;
171
+ /** Cache key for a request (default: the loaded policy is shared under one key). */
172
+ key?: (request: any) => string;
173
+ }
174
+ /**
175
+ * Wrap a (possibly async) loader with a per-key TTL cache. The loader reads your
176
+ * source of truth (Redis/DB/config), so limits propagate across a fleet and
177
+ * refresh live — without restarting. Use the returned function as `policy`.
178
+ */
179
+ declare function cachedPolicyResolver(loader: (request: any) => Policy | Promise<Policy>, options?: CachedPolicyResolverOptions): (request: any) => Promise<Policy>;
180
+
181
+ /**
182
+ * In-process statistics collector for rate-limit observability.
183
+ *
184
+ * Implements `TelemetryHooks`, so you plug it into a RateLimiter (and optionally
185
+ * QuotaManager / PenaltyManager) and it aggregates everything in memory. Call
186
+ * `snapshot()` to expose a `/halt/stats` endpoint or log periodically.
187
+ *
188
+ * It is per-process by design — for a fleet, run it on each instance and roll up
189
+ * across instances with the OpenTelemetry adapter (see observability/otel). Memory
190
+ * is bounded: the tracked-key table is capped and the smallest counters are evicted.
191
+ *
192
+ * const stats = new StatsCollector();
193
+ * const limiter = new RateLimiter({ store, policy, telemetry: stats });
194
+ * app.get('/halt/stats', (_req, res) => res.json(stats.snapshot()));
195
+ */
196
+
197
+ interface StatsCollectorOptions {
198
+ /** How many top limited keys `snapshot()` returns (default 20). */
199
+ topN?: number;
200
+ /** Max distinct keys tracked for the top-N table before eviction (default 10000). */
201
+ maxTrackedKeys?: number;
202
+ }
203
+ interface Tally {
204
+ allowed: number;
205
+ blocked: number;
206
+ }
207
+ interface EndpointTally extends Tally {
208
+ cost: number;
209
+ }
210
+ interface StatsSnapshot {
211
+ allowedTotal: number;
212
+ blockedTotal: number;
213
+ /** Per-policy allowed/blocked counts. */
214
+ byPolicy: Record<string, Tally>;
215
+ /** Per-endpoint allowed/blocked counts and consumed cost. */
216
+ byEndpoint: Record<string, EndpointTally>;
217
+ /** Highest-blocked keys (users / API keys / IPs), descending. */
218
+ topLimitedKeys: Array<{
219
+ key: string;
220
+ blocked: number;
221
+ }>;
222
+ /** Consumed cost per plan (allowed requests only). */
223
+ costByPlan: Record<string, number>;
224
+ /** Count of quota-exceeded events (from QuotaManager telemetry). */
225
+ quotaExceeded: number;
226
+ /** Count of penalties applied (from PenaltyManager telemetry). */
227
+ penaltiesApplied: number;
228
+ /** Count of recorded violations (from PenaltyManager telemetry). */
229
+ violations: number;
230
+ /** Number of distinct keys currently tracked for top-N. */
231
+ trackedKeys: number;
232
+ }
233
+ declare class StatsCollector implements TelemetryHooks {
234
+ private topN;
235
+ private maxTrackedKeys;
236
+ private allowedTotal;
237
+ private blockedTotal;
238
+ private byPolicy;
239
+ private byEndpoint;
240
+ private limitedKeys;
241
+ private costByPlan;
242
+ private quotaExceededCount;
243
+ private penaltiesAppliedCount;
244
+ private violationsCount;
245
+ constructor(options?: StatsCollectorOptions);
246
+ onAllowed(_key: string, _decision: Decision, metadata?: Record<string, any>): void;
247
+ onBlocked(key: string, _decision: Decision, metadata?: Record<string, any>): void;
248
+ onQuotaExceeded(_identifier: string, _quota: Quota): void;
249
+ onPenaltyApplied(_identifier: string, _penalty: Penalty): void;
250
+ onViolation(_identifier: string, _penalty: Penalty, _severity: number): void;
251
+ /** Point-in-time view of the aggregated stats. */
252
+ snapshot(): StatsSnapshot;
253
+ /** Clear all counters (e.g. after exporting). */
254
+ reset(): void;
255
+ private policyTally;
256
+ private endpointTally;
257
+ private trackLimitedKey;
258
+ /** Evict the lowest-count tracked key to keep memory bounded. */
259
+ private evictSmallest;
260
+ }
261
+
262
+ /**
263
+ * OpenTelemetry metrics adapter for Halt.
264
+ *
265
+ * Implements `TelemetryHooks` over the OTel metrics API. You inject a Meter, so
266
+ * Halt keeps no hard dependency on `@opentelemetry/api`:
267
+ *
268
+ * import { metrics } from '@opentelemetry/api';
269
+ * import { OpenTelemetryMetrics } from 'halt-rate';
270
+ *
271
+ * const telemetry = new OpenTelemetryMetrics(metrics.getMeter('halt'));
272
+ * const limiter = new RateLimiter({ store, policy, telemetry });
273
+ *
274
+ * Request spans are already covered by the limiter's `otelTracer` option; this
275
+ * adapter adds counters for dashboards/alerts. Roll up across instances with your
276
+ * OTel collector.
277
+ */
278
+
279
+ /** Minimal structural types satisfied by `@opentelemetry/api` (no hard dep). */
280
+ interface OTelCounterLike {
281
+ add(value: number, attributes?: Record<string, string | number | boolean>): void;
282
+ }
283
+ interface OTelMeterLike {
284
+ createCounter(name: string, options?: {
285
+ description?: string;
286
+ unit?: string;
287
+ }): OTelCounterLike;
288
+ }
289
+ declare class OpenTelemetryMetrics implements TelemetryHooks {
290
+ private requests;
291
+ private blocked;
292
+ private cost;
293
+ private quotaExceededCounter;
294
+ private penaltyAppliedCounter;
295
+ private violationsCounter;
296
+ constructor(meter: OTelMeterLike);
297
+ onAllowed(_key: string, _decision: Decision, metadata?: Record<string, any>): void;
298
+ onBlocked(_key: string, _decision: Decision, metadata?: Record<string, any>): void;
299
+ onQuotaExceeded(_identifier: string, quota: Quota): void;
300
+ onPenaltyApplied(_identifier: string, _penalty: Penalty): void;
301
+ onViolation(_identifier: string, _penalty: Penalty, severity: number): void;
302
+ }
303
+
304
+ export { AtomicStore, type CachedPolicyResolverOptions, Decision, EvaluateInput, type FailMode, type OTelCounterLike, type OTelMeterLike, OpenTelemetryMetrics, Penalty, Policy, PolicyRegistry, Quota, RedisClientLike, RedisStore, type RedisStoreOptions, StatsCollector, type StatsCollectorOptions, type StatsSnapshot, TelemetryHooks, cachedPolicyResolver, extractors, index as presets };