halt-rate 0.3.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.
package/README.md CHANGED
@@ -39,9 +39,17 @@ observability are covered in the docs → **[halt.afroawi.com/docs](https://halt
39
39
  - Algorithms: token bucket, fixed/sliding window, leaky bucket
40
40
  - Keys: IP, user, API key, composite, or custom
41
41
  - Atomic **Redis** store (Lua, cluster-safe, fail-open) + in-memory dev store
42
+ - **Dynamic limits**: change limits at runtime with `PolicyRegistry` / cached loaders — no restart
42
43
  - SaaS: per-plan limits, quotas, weighted endpoints, abuse penalties
43
44
  - Observability: `StatsCollector` + OpenTelemetry metrics
44
- - Adapters: Express, Next.js
45
+ - Adapters: Express, Next.js, **Hono**, **Fastify**, GraphQL (Apollo)
46
+
47
+ ## Runtime support
48
+
49
+ Ships dual **ESM + CJS** with types. The core (limiter, algorithms, in-memory store) is
50
+ **edge-safe** — runs on Cloudflare Workers, Deno, Bun, and Vercel Edge. `RedisStore` is Node-only
51
+ (TCP); for edge distributed limits, inject any `RedisClientLike` (e.g. a fetch/REST client).
52
+ Full guidance: **[halt.afroawi.com/docs](https://halt.afroawi.com/docs)**.
45
53
 
46
54
  ## Links
47
55
 
@@ -0,0 +1,29 @@
1
+ import { FastifyRequest, FastifyReply } from 'fastify';
2
+ import { o as RateLimiter } from '../limiter-DqVVE0Kl.mjs';
3
+
4
+ /**
5
+ * Fastify adapter for Halt rate limiting.
6
+ *
7
+ * import Fastify from 'fastify';
8
+ * import { RateLimiter, InMemoryStore, presets } from 'halt-rate';
9
+ * import { haltHook } from 'halt-rate/fastify';
10
+ *
11
+ * const limiter = new RateLimiter({ store: new InMemoryStore(), policy: presets.PUBLIC_API });
12
+ * const app = Fastify();
13
+ * app.addHook('preHandler', haltHook({ limiter }));
14
+ */
15
+
16
+ interface FastifyHaltOptions {
17
+ limiter: RateLimiter;
18
+ /** Custom blocked response. */
19
+ onBlocked?: (request: FastifyRequest, reply: FastifyReply) => unknown;
20
+ }
21
+ /**
22
+ * Create a Fastify `preHandler` hook for rate limiting. Fastify's request already
23
+ * exposes `ip`, `headers`, and `url`, which Halt's extractors understand directly.
24
+ */
25
+ declare function haltHook(options: FastifyHaltOptions): (request: FastifyRequest, reply: FastifyReply) => Promise<unknown>;
26
+ /** Alias for symmetry with the other adapters. */
27
+ declare const haltMiddleware: typeof haltHook;
28
+
29
+ export { type FastifyHaltOptions, haltHook, haltMiddleware };
@@ -0,0 +1,29 @@
1
+ import { FastifyRequest, FastifyReply } from 'fastify';
2
+ import { o as RateLimiter } from '../limiter-DqVVE0Kl.js';
3
+
4
+ /**
5
+ * Fastify adapter for Halt rate limiting.
6
+ *
7
+ * import Fastify from 'fastify';
8
+ * import { RateLimiter, InMemoryStore, presets } from 'halt-rate';
9
+ * import { haltHook } from 'halt-rate/fastify';
10
+ *
11
+ * const limiter = new RateLimiter({ store: new InMemoryStore(), policy: presets.PUBLIC_API });
12
+ * const app = Fastify();
13
+ * app.addHook('preHandler', haltHook({ limiter }));
14
+ */
15
+
16
+ interface FastifyHaltOptions {
17
+ limiter: RateLimiter;
18
+ /** Custom blocked response. */
19
+ onBlocked?: (request: FastifyRequest, reply: FastifyReply) => unknown;
20
+ }
21
+ /**
22
+ * Create a Fastify `preHandler` hook for rate limiting. Fastify's request already
23
+ * exposes `ip`, `headers`, and `url`, which Halt's extractors understand directly.
24
+ */
25
+ declare function haltHook(options: FastifyHaltOptions): (request: FastifyRequest, reply: FastifyReply) => Promise<unknown>;
26
+ /** Alias for symmetry with the other adapters. */
27
+ declare const haltMiddleware: typeof haltHook;
28
+
29
+ export { type FastifyHaltOptions, haltHook, haltMiddleware };
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ // src/core/decision.ts
4
+ function toHeaders(decision) {
5
+ const headers = {
6
+ "RateLimit-Limit": String(decision.limit),
7
+ "RateLimit-Remaining": String(Math.max(0, decision.remaining)),
8
+ "RateLimit-Reset": String(decision.resetAt)
9
+ };
10
+ if (!decision.allowed && decision.retryAfter !== void 0) {
11
+ headers["Retry-After"] = String(decision.retryAfter);
12
+ }
13
+ return headers;
14
+ }
15
+
16
+ // src/adapters/fastify.ts
17
+ function haltHook(options) {
18
+ const { limiter, onBlocked } = options;
19
+ return async (request, reply) => {
20
+ const decision = await limiter.check(request);
21
+ for (const [key, value] of Object.entries(toHeaders(decision))) {
22
+ reply.header(key, value);
23
+ }
24
+ if (decision.allowed) return;
25
+ if (onBlocked) return onBlocked(request, reply);
26
+ return reply.code(429).send({
27
+ error: "rate_limit_exceeded",
28
+ message: "Too many requests. Please try again later.",
29
+ retryAfter: decision.retryAfter
30
+ });
31
+ };
32
+ }
33
+ var haltMiddleware = haltHook;
34
+
35
+ exports.haltHook = haltHook;
36
+ exports.haltMiddleware = haltMiddleware;
37
+ //# sourceMappingURL=fastify.js.map
38
+ //# sourceMappingURL=fastify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/decision.ts","../../src/adapters/fastify.ts"],"names":[],"mappings":";;;AAoBO,SAAS,UAAU,QAAA,EAA4C;AAClE,EAAA,MAAM,OAAA,GAAkC;AAAA,IACpC,iBAAA,EAAmB,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA;AAAA,IACxC,uBAAuB,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,EAAG,QAAA,CAAS,SAAS,CAAC,CAAA;AAAA,IAC7D,iBAAA,EAAmB,MAAA,CAAO,QAAA,CAAS,OAAO;AAAA,GAC9C;AAEA,EAAA,IAAI,CAAC,QAAA,CAAS,OAAA,IAAW,QAAA,CAAS,eAAe,MAAA,EAAW;AACxD,IAAA,OAAA,CAAQ,aAAa,CAAA,GAAI,MAAA,CAAO,QAAA,CAAS,UAAU,CAAA;AAAA,EACvD;AAEA,EAAA,OAAO,OAAA;AACX;;;ACNO,SAAS,SAAS,OAAA,EAA6B;AAClD,EAAA,MAAM,EAAE,OAAA,EAAS,SAAA,EAAU,GAAI,OAAA;AAE/B,EAAA,OAAO,OAAO,SAAyB,KAAA,KAAwB;AAC3D,IAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,KAAA,CAAM,OAAO,CAAA;AAE5C,IAAA,KAAA,MAAW,CAAC,KAAK,KAAK,CAAA,IAAK,OAAO,OAAA,CAAQ,SAAA,CAAU,QAAQ,CAAC,CAAA,EAAG;AAC5D,MAAA,KAAA,CAAM,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,IAC3B;AAEA,IAAA,IAAI,SAAS,OAAA,EAAS;AAEtB,IAAA,IAAI,SAAA,EAAW,OAAO,SAAA,CAAU,OAAA,EAAS,KAAK,CAAA;AAC9C,IAAA,OAAO,KAAA,CAAM,IAAA,CAAK,GAAG,CAAA,CAAE,IAAA,CAAK;AAAA,MACxB,KAAA,EAAO,qBAAA;AAAA,MACP,OAAA,EAAS,4CAAA;AAAA,MACT,YAAY,QAAA,CAAS;AAAA,KACxB,CAAA;AAAA,EACL,CAAA;AACJ;AAGO,IAAM,cAAA,GAAiB","file":"fastify.js","sourcesContent":["/**\n * Decision model for rate limiting results.\n */\n\nexport interface Decision {\n /** Whether the request is allowed */\n allowed: boolean;\n /** Maximum number of requests allowed in the window */\n limit: number;\n /** Number of requests remaining in the current window */\n remaining: number;\n /** Unix timestamp when the limit resets */\n resetAt: number;\n /** Seconds to wait before retrying (only set when blocked) */\n retryAfter?: number;\n}\n\n/**\n * Convert decision to standard rate limit headers.\n */\nexport function toHeaders(decision: Decision): Record<string, string> {\n const headers: Record<string, string> = {\n 'RateLimit-Limit': String(decision.limit),\n 'RateLimit-Remaining': String(Math.max(0, decision.remaining)),\n 'RateLimit-Reset': String(decision.resetAt),\n };\n\n if (!decision.allowed && decision.retryAfter !== undefined) {\n headers['Retry-After'] = String(decision.retryAfter);\n }\n\n return headers;\n}\n","/**\n * Fastify adapter for Halt rate limiting.\n *\n * import Fastify from 'fastify';\n * import { RateLimiter, InMemoryStore, presets } from 'halt-rate';\n * import { haltHook } from 'halt-rate/fastify';\n *\n * const limiter = new RateLimiter({ store: new InMemoryStore(), policy: presets.PUBLIC_API });\n * const app = Fastify();\n * app.addHook('preHandler', haltHook({ limiter }));\n */\n\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport { RateLimiter } from '../core/limiter';\nimport { toHeaders } from '../core/decision';\n\nexport interface FastifyHaltOptions {\n limiter: RateLimiter;\n /** Custom blocked response. */\n onBlocked?: (request: FastifyRequest, reply: FastifyReply) => unknown;\n}\n\n/**\n * Create a Fastify `preHandler` hook for rate limiting. Fastify's request already\n * exposes `ip`, `headers`, and `url`, which Halt's extractors understand directly.\n */\nexport function haltHook(options: FastifyHaltOptions) {\n const { limiter, onBlocked } = options;\n\n return async (request: FastifyRequest, reply: FastifyReply) => {\n const decision = await limiter.check(request);\n\n for (const [key, value] of Object.entries(toHeaders(decision))) {\n reply.header(key, value);\n }\n\n if (decision.allowed) return;\n\n if (onBlocked) return onBlocked(request, reply);\n return reply.code(429).send({\n error: 'rate_limit_exceeded',\n message: 'Too many requests. Please try again later.',\n retryAfter: decision.retryAfter,\n });\n };\n}\n\n/** Alias for symmetry with the other adapters. */\nexport const haltMiddleware = haltHook;\n"]}
@@ -0,0 +1,35 @@
1
+ // src/core/decision.ts
2
+ function toHeaders(decision) {
3
+ const headers = {
4
+ "RateLimit-Limit": String(decision.limit),
5
+ "RateLimit-Remaining": String(Math.max(0, decision.remaining)),
6
+ "RateLimit-Reset": String(decision.resetAt)
7
+ };
8
+ if (!decision.allowed && decision.retryAfter !== void 0) {
9
+ headers["Retry-After"] = String(decision.retryAfter);
10
+ }
11
+ return headers;
12
+ }
13
+
14
+ // src/adapters/fastify.ts
15
+ function haltHook(options) {
16
+ const { limiter, onBlocked } = options;
17
+ return async (request, reply) => {
18
+ const decision = await limiter.check(request);
19
+ for (const [key, value] of Object.entries(toHeaders(decision))) {
20
+ reply.header(key, value);
21
+ }
22
+ if (decision.allowed) return;
23
+ if (onBlocked) return onBlocked(request, reply);
24
+ return reply.code(429).send({
25
+ error: "rate_limit_exceeded",
26
+ message: "Too many requests. Please try again later.",
27
+ retryAfter: decision.retryAfter
28
+ });
29
+ };
30
+ }
31
+ var haltMiddleware = haltHook;
32
+
33
+ export { haltHook, haltMiddleware };
34
+ //# sourceMappingURL=fastify.mjs.map
35
+ //# sourceMappingURL=fastify.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/decision.ts","../../src/adapters/fastify.ts"],"names":[],"mappings":";AAoBO,SAAS,UAAU,QAAA,EAA4C;AAClE,EAAA,MAAM,OAAA,GAAkC;AAAA,IACpC,iBAAA,EAAmB,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA;AAAA,IACxC,uBAAuB,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,EAAG,QAAA,CAAS,SAAS,CAAC,CAAA;AAAA,IAC7D,iBAAA,EAAmB,MAAA,CAAO,QAAA,CAAS,OAAO;AAAA,GAC9C;AAEA,EAAA,IAAI,CAAC,QAAA,CAAS,OAAA,IAAW,QAAA,CAAS,eAAe,MAAA,EAAW;AACxD,IAAA,OAAA,CAAQ,aAAa,CAAA,GAAI,MAAA,CAAO,QAAA,CAAS,UAAU,CAAA;AAAA,EACvD;AAEA,EAAA,OAAO,OAAA;AACX;;;ACNO,SAAS,SAAS,OAAA,EAA6B;AAClD,EAAA,MAAM,EAAE,OAAA,EAAS,SAAA,EAAU,GAAI,OAAA;AAE/B,EAAA,OAAO,OAAO,SAAyB,KAAA,KAAwB;AAC3D,IAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,KAAA,CAAM,OAAO,CAAA;AAE5C,IAAA,KAAA,MAAW,CAAC,KAAK,KAAK,CAAA,IAAK,OAAO,OAAA,CAAQ,SAAA,CAAU,QAAQ,CAAC,CAAA,EAAG;AAC5D,MAAA,KAAA,CAAM,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,IAC3B;AAEA,IAAA,IAAI,SAAS,OAAA,EAAS;AAEtB,IAAA,IAAI,SAAA,EAAW,OAAO,SAAA,CAAU,OAAA,EAAS,KAAK,CAAA;AAC9C,IAAA,OAAO,KAAA,CAAM,IAAA,CAAK,GAAG,CAAA,CAAE,IAAA,CAAK;AAAA,MACxB,KAAA,EAAO,qBAAA;AAAA,MACP,OAAA,EAAS,4CAAA;AAAA,MACT,YAAY,QAAA,CAAS;AAAA,KACxB,CAAA;AAAA,EACL,CAAA;AACJ;AAGO,IAAM,cAAA,GAAiB","file":"fastify.mjs","sourcesContent":["/**\n * Decision model for rate limiting results.\n */\n\nexport interface Decision {\n /** Whether the request is allowed */\n allowed: boolean;\n /** Maximum number of requests allowed in the window */\n limit: number;\n /** Number of requests remaining in the current window */\n remaining: number;\n /** Unix timestamp when the limit resets */\n resetAt: number;\n /** Seconds to wait before retrying (only set when blocked) */\n retryAfter?: number;\n}\n\n/**\n * Convert decision to standard rate limit headers.\n */\nexport function toHeaders(decision: Decision): Record<string, string> {\n const headers: Record<string, string> = {\n 'RateLimit-Limit': String(decision.limit),\n 'RateLimit-Remaining': String(Math.max(0, decision.remaining)),\n 'RateLimit-Reset': String(decision.resetAt),\n };\n\n if (!decision.allowed && decision.retryAfter !== undefined) {\n headers['Retry-After'] = String(decision.retryAfter);\n }\n\n return headers;\n}\n","/**\n * Fastify adapter for Halt rate limiting.\n *\n * import Fastify from 'fastify';\n * import { RateLimiter, InMemoryStore, presets } from 'halt-rate';\n * import { haltHook } from 'halt-rate/fastify';\n *\n * const limiter = new RateLimiter({ store: new InMemoryStore(), policy: presets.PUBLIC_API });\n * const app = Fastify();\n * app.addHook('preHandler', haltHook({ limiter }));\n */\n\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport { RateLimiter } from '../core/limiter';\nimport { toHeaders } from '../core/decision';\n\nexport interface FastifyHaltOptions {\n limiter: RateLimiter;\n /** Custom blocked response. */\n onBlocked?: (request: FastifyRequest, reply: FastifyReply) => unknown;\n}\n\n/**\n * Create a Fastify `preHandler` hook for rate limiting. Fastify's request already\n * exposes `ip`, `headers`, and `url`, which Halt's extractors understand directly.\n */\nexport function haltHook(options: FastifyHaltOptions) {\n const { limiter, onBlocked } = options;\n\n return async (request: FastifyRequest, reply: FastifyReply) => {\n const decision = await limiter.check(request);\n\n for (const [key, value] of Object.entries(toHeaders(decision))) {\n reply.header(key, value);\n }\n\n if (decision.allowed) return;\n\n if (onBlocked) return onBlocked(request, reply);\n return reply.code(429).send({\n error: 'rate_limit_exceeded',\n message: 'Too many requests. Please try again later.',\n retryAfter: decision.retryAfter,\n });\n };\n}\n\n/** Alias for symmetry with the other adapters. */\nexport const haltMiddleware = haltHook;\n"]}
@@ -0,0 +1,22 @@
1
+ import { o as RateLimiter } from '../limiter-DqVVE0Kl.mjs';
2
+
3
+ /**
4
+ * Apollo Server plugin adapter for Halt rate limiting.
5
+ *
6
+ * Usage:
7
+ * const plugin = haltApolloPlugin({ limiter });
8
+ * const server = new ApolloServer({ typeDefs, resolvers, plugins: [plugin] });
9
+ */
10
+
11
+ interface ApolloHaltOptions {
12
+ limiter: RateLimiter;
13
+ /** Optional function to extract a resource name for per-field limits */
14
+ resourceExtractor?: (request: any) => string | null;
15
+ }
16
+ declare function haltApolloPlugin(options: ApolloHaltOptions): {
17
+ requestDidStart(): Promise<{
18
+ didResolveOperation(requestContext: any): Promise<void>;
19
+ }>;
20
+ };
21
+
22
+ export { type ApolloHaltOptions, haltApolloPlugin };
@@ -0,0 +1,22 @@
1
+ import { o as RateLimiter } from '../limiter-DqVVE0Kl.js';
2
+
3
+ /**
4
+ * Apollo Server plugin adapter for Halt rate limiting.
5
+ *
6
+ * Usage:
7
+ * const plugin = haltApolloPlugin({ limiter });
8
+ * const server = new ApolloServer({ typeDefs, resolvers, plugins: [plugin] });
9
+ */
10
+
11
+ interface ApolloHaltOptions {
12
+ limiter: RateLimiter;
13
+ /** Optional function to extract a resource name for per-field limits */
14
+ resourceExtractor?: (request: any) => string | null;
15
+ }
16
+ declare function haltApolloPlugin(options: ApolloHaltOptions): {
17
+ requestDidStart(): Promise<{
18
+ didResolveOperation(requestContext: any): Promise<void>;
19
+ }>;
20
+ };
21
+
22
+ export { type ApolloHaltOptions, haltApolloPlugin };
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ // src/adapters/graphql.ts
4
+ function haltApolloPlugin(options) {
5
+ const { limiter, resourceExtractor } = options;
6
+ return {
7
+ async requestDidStart() {
8
+ return {
9
+ async didResolveOperation(requestContext) {
10
+ try {
11
+ const req = requestContext.request?.http || {};
12
+ if (resourceExtractor) {
13
+ req.resource = resourceExtractor(requestContext.request);
14
+ }
15
+ const decision = await limiter.check(req);
16
+ if (!decision.allowed) {
17
+ requestContext.response = {
18
+ http: {
19
+ status: 429,
20
+ headers: {
21
+ ...requestContext.response?.http?.headers,
22
+ "Retry-After": String(decision.retryAfter ?? 0)
23
+ }
24
+ },
25
+ errors: [
26
+ new Error("rate_limit_exceeded: Too many requests. Please try again later.")
27
+ ]
28
+ };
29
+ } else {
30
+ requestContext.response = requestContext.response || {};
31
+ requestContext.response.http = requestContext.response.http || {};
32
+ requestContext.response.http.headers = {
33
+ ...requestContext.response.http.headers,
34
+ "RateLimit-Limit": String(decision.limit),
35
+ "RateLimit-Remaining": String(Math.max(0, decision.remaining)),
36
+ "RateLimit-Reset": String(decision.resetAt)
37
+ };
38
+ }
39
+ } catch (err) {
40
+ requestContext.errors = requestContext.errors || [];
41
+ requestContext.errors.push(err);
42
+ }
43
+ }
44
+ };
45
+ }
46
+ };
47
+ }
48
+
49
+ exports.haltApolloPlugin = haltApolloPlugin;
50
+ //# sourceMappingURL=graphql.js.map
51
+ //# sourceMappingURL=graphql.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/graphql.ts"],"names":[],"mappings":";;;AAgBO,SAAS,iBAAiB,OAAA,EAA4B;AACzD,EAAA,MAAM,EAAE,OAAA,EAAS,iBAAA,EAAkB,GAAI,OAAA;AAEvC,EAAA,OAAO;AAAA,IACH,MAAM,eAAA,GAAkB;AACpB,MAAA,OAAO;AAAA,QACH,MAAM,oBAAoB,cAAA,EAAqB;AAC3C,UAAA,IAAI;AAEA,YAAA,MAAM,GAAA,GAAM,cAAA,CAAe,OAAA,EAAS,IAAA,IAAQ,EAAC;AAC7C,YAAA,IAAI,iBAAA,EAAmB;AAEnB,cAAC,GAAA,CAAY,QAAA,GAAW,iBAAA,CAAkB,cAAA,CAAe,OAAO,CAAA;AAAA,YACpE;AAEA,YAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,KAAA,CAAM,GAAG,CAAA;AAExC,YAAA,IAAI,CAAC,SAAS,OAAA,EAAS;AAEnB,cAAA,cAAA,CAAe,QAAA,GAAW;AAAA,gBACtB,IAAA,EAAM;AAAA,kBACF,MAAA,EAAQ,GAAA;AAAA,kBACR,OAAA,EAAS;AAAA,oBACL,GAAG,cAAA,CAAe,QAAA,EAAU,IAAA,EAAM,OAAA;AAAA,oBAClC,aAAA,EAAe,MAAA,CAAO,QAAA,CAAS,UAAA,IAAc,CAAC;AAAA;AAClD,iBACJ;AAAA,gBACA,MAAA,EAAQ;AAAA,kBACJ,IAAI,MAAM,iEAAiE;AAAA;AAC/E,eACJ;AAAA,YACJ,CAAA,MAAO;AAEH,cAAA,cAAA,CAAe,QAAA,GAAW,cAAA,CAAe,QAAA,IAAY,EAAC;AACtD,cAAA,cAAA,CAAe,QAAA,CAAS,IAAA,GAAO,cAAA,CAAe,QAAA,CAAS,QAAQ,EAAC;AAChE,cAAA,cAAA,CAAe,QAAA,CAAS,KAAK,OAAA,GAAU;AAAA,gBACnC,GAAG,cAAA,CAAe,QAAA,CAAS,IAAA,CAAK,OAAA;AAAA,gBAChC,iBAAA,EAAmB,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA;AAAA,gBACxC,uBAAuB,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,EAAG,QAAA,CAAS,SAAS,CAAC,CAAA;AAAA,gBAC7D,iBAAA,EAAmB,MAAA,CAAO,QAAA,CAAS,OAAO;AAAA,eAC9C;AAAA,YACJ;AAAA,UACJ,SAAS,GAAA,EAAK;AAEV,YAAA,cAAA,CAAe,MAAA,GAAS,cAAA,CAAe,MAAA,IAAU,EAAC;AAClD,YAAA,cAAA,CAAe,MAAA,CAAO,KAAK,GAAY,CAAA;AAAA,UAC3C;AAAA,QACJ;AAAA,OACJ;AAAA,IACJ;AAAA,GACJ;AACJ","file":"graphql.js","sourcesContent":["/**\n * Apollo Server plugin adapter for Halt rate limiting.\n *\n * Usage:\n * const plugin = haltApolloPlugin({ limiter });\n * const server = new ApolloServer({ typeDefs, resolvers, plugins: [plugin] });\n */\n\nimport { RateLimiter } from '../core/limiter';\n\nexport interface ApolloHaltOptions {\n limiter: RateLimiter;\n /** Optional function to extract a resource name for per-field limits */\n resourceExtractor?: (request: any) => string | null;\n}\n\nexport function haltApolloPlugin(options: ApolloHaltOptions) {\n const { limiter, resourceExtractor } = options;\n\n return {\n async requestDidStart() {\n return {\n async didResolveOperation(requestContext: any) {\n try {\n // Provide request object compatible with limiter.extractors\n const req = requestContext.request?.http || {};\n if (resourceExtractor) {\n // Allow users to attach custom resource info\n (req as any).resource = resourceExtractor(requestContext.request);\n }\n\n const decision = await limiter.check(req);\n\n if (!decision.allowed) {\n // Short-circuit the request by adding an error\n requestContext.response = {\n http: {\n status: 429,\n headers: {\n ...requestContext.response?.http?.headers,\n 'Retry-After': String(decision.retryAfter ?? 0),\n },\n },\n errors: [\n new Error('rate_limit_exceeded: Too many requests. Please try again later.'),\n ],\n };\n } else {\n // Attach rate limit headers for downstream\n requestContext.response = requestContext.response || {};\n requestContext.response.http = requestContext.response.http || {};\n requestContext.response.http.headers = {\n ...requestContext.response.http.headers,\n 'RateLimit-Limit': String(decision.limit),\n 'RateLimit-Remaining': String(Math.max(0, decision.remaining)),\n 'RateLimit-Reset': String(decision.resetAt),\n };\n }\n } catch (err) {\n // Don't block on errors from limiter; surface as server error\n requestContext.errors = requestContext.errors || [];\n requestContext.errors.push(err as Error);\n }\n },\n };\n },\n };\n}\n"]}
@@ -0,0 +1,49 @@
1
+ // src/adapters/graphql.ts
2
+ function haltApolloPlugin(options) {
3
+ const { limiter, resourceExtractor } = options;
4
+ return {
5
+ async requestDidStart() {
6
+ return {
7
+ async didResolveOperation(requestContext) {
8
+ try {
9
+ const req = requestContext.request?.http || {};
10
+ if (resourceExtractor) {
11
+ req.resource = resourceExtractor(requestContext.request);
12
+ }
13
+ const decision = await limiter.check(req);
14
+ if (!decision.allowed) {
15
+ requestContext.response = {
16
+ http: {
17
+ status: 429,
18
+ headers: {
19
+ ...requestContext.response?.http?.headers,
20
+ "Retry-After": String(decision.retryAfter ?? 0)
21
+ }
22
+ },
23
+ errors: [
24
+ new Error("rate_limit_exceeded: Too many requests. Please try again later.")
25
+ ]
26
+ };
27
+ } else {
28
+ requestContext.response = requestContext.response || {};
29
+ requestContext.response.http = requestContext.response.http || {};
30
+ requestContext.response.http.headers = {
31
+ ...requestContext.response.http.headers,
32
+ "RateLimit-Limit": String(decision.limit),
33
+ "RateLimit-Remaining": String(Math.max(0, decision.remaining)),
34
+ "RateLimit-Reset": String(decision.resetAt)
35
+ };
36
+ }
37
+ } catch (err) {
38
+ requestContext.errors = requestContext.errors || [];
39
+ requestContext.errors.push(err);
40
+ }
41
+ }
42
+ };
43
+ }
44
+ };
45
+ }
46
+
47
+ export { haltApolloPlugin };
48
+ //# sourceMappingURL=graphql.mjs.map
49
+ //# sourceMappingURL=graphql.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/graphql.ts"],"names":[],"mappings":";AAgBO,SAAS,iBAAiB,OAAA,EAA4B;AACzD,EAAA,MAAM,EAAE,OAAA,EAAS,iBAAA,EAAkB,GAAI,OAAA;AAEvC,EAAA,OAAO;AAAA,IACH,MAAM,eAAA,GAAkB;AACpB,MAAA,OAAO;AAAA,QACH,MAAM,oBAAoB,cAAA,EAAqB;AAC3C,UAAA,IAAI;AAEA,YAAA,MAAM,GAAA,GAAM,cAAA,CAAe,OAAA,EAAS,IAAA,IAAQ,EAAC;AAC7C,YAAA,IAAI,iBAAA,EAAmB;AAEnB,cAAC,GAAA,CAAY,QAAA,GAAW,iBAAA,CAAkB,cAAA,CAAe,OAAO,CAAA;AAAA,YACpE;AAEA,YAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,KAAA,CAAM,GAAG,CAAA;AAExC,YAAA,IAAI,CAAC,SAAS,OAAA,EAAS;AAEnB,cAAA,cAAA,CAAe,QAAA,GAAW;AAAA,gBACtB,IAAA,EAAM;AAAA,kBACF,MAAA,EAAQ,GAAA;AAAA,kBACR,OAAA,EAAS;AAAA,oBACL,GAAG,cAAA,CAAe,QAAA,EAAU,IAAA,EAAM,OAAA;AAAA,oBAClC,aAAA,EAAe,MAAA,CAAO,QAAA,CAAS,UAAA,IAAc,CAAC;AAAA;AAClD,iBACJ;AAAA,gBACA,MAAA,EAAQ;AAAA,kBACJ,IAAI,MAAM,iEAAiE;AAAA;AAC/E,eACJ;AAAA,YACJ,CAAA,MAAO;AAEH,cAAA,cAAA,CAAe,QAAA,GAAW,cAAA,CAAe,QAAA,IAAY,EAAC;AACtD,cAAA,cAAA,CAAe,QAAA,CAAS,IAAA,GAAO,cAAA,CAAe,QAAA,CAAS,QAAQ,EAAC;AAChE,cAAA,cAAA,CAAe,QAAA,CAAS,KAAK,OAAA,GAAU;AAAA,gBACnC,GAAG,cAAA,CAAe,QAAA,CAAS,IAAA,CAAK,OAAA;AAAA,gBAChC,iBAAA,EAAmB,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA;AAAA,gBACxC,uBAAuB,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,EAAG,QAAA,CAAS,SAAS,CAAC,CAAA;AAAA,gBAC7D,iBAAA,EAAmB,MAAA,CAAO,QAAA,CAAS,OAAO;AAAA,eAC9C;AAAA,YACJ;AAAA,UACJ,SAAS,GAAA,EAAK;AAEV,YAAA,cAAA,CAAe,MAAA,GAAS,cAAA,CAAe,MAAA,IAAU,EAAC;AAClD,YAAA,cAAA,CAAe,MAAA,CAAO,KAAK,GAAY,CAAA;AAAA,UAC3C;AAAA,QACJ;AAAA,OACJ;AAAA,IACJ;AAAA,GACJ;AACJ","file":"graphql.mjs","sourcesContent":["/**\n * Apollo Server plugin adapter for Halt rate limiting.\n *\n * Usage:\n * const plugin = haltApolloPlugin({ limiter });\n * const server = new ApolloServer({ typeDefs, resolvers, plugins: [plugin] });\n */\n\nimport { RateLimiter } from '../core/limiter';\n\nexport interface ApolloHaltOptions {\n limiter: RateLimiter;\n /** Optional function to extract a resource name for per-field limits */\n resourceExtractor?: (request: any) => string | null;\n}\n\nexport function haltApolloPlugin(options: ApolloHaltOptions) {\n const { limiter, resourceExtractor } = options;\n\n return {\n async requestDidStart() {\n return {\n async didResolveOperation(requestContext: any) {\n try {\n // Provide request object compatible with limiter.extractors\n const req = requestContext.request?.http || {};\n if (resourceExtractor) {\n // Allow users to attach custom resource info\n (req as any).resource = resourceExtractor(requestContext.request);\n }\n\n const decision = await limiter.check(req);\n\n if (!decision.allowed) {\n // Short-circuit the request by adding an error\n requestContext.response = {\n http: {\n status: 429,\n headers: {\n ...requestContext.response?.http?.headers,\n 'Retry-After': String(decision.retryAfter ?? 0),\n },\n },\n errors: [\n new Error('rate_limit_exceeded: Too many requests. Please try again later.'),\n ],\n };\n } else {\n // Attach rate limit headers for downstream\n requestContext.response = requestContext.response || {};\n requestContext.response.http = requestContext.response.http || {};\n requestContext.response.http.headers = {\n ...requestContext.response.http.headers,\n 'RateLimit-Limit': String(decision.limit),\n 'RateLimit-Remaining': String(Math.max(0, decision.remaining)),\n 'RateLimit-Reset': String(decision.resetAt),\n };\n }\n } catch (err) {\n // Don't block on errors from limiter; surface as server error\n requestContext.errors = requestContext.errors || [];\n requestContext.errors.push(err as Error);\n }\n },\n };\n },\n };\n}\n"]}
@@ -0,0 +1,30 @@
1
+ import { Context, Next } from 'hono';
2
+ import { o as RateLimiter } from '../limiter-DqVVE0Kl.mjs';
3
+
4
+ /**
5
+ * Hono adapter for Halt rate limiting.
6
+ *
7
+ * Works on Node and on edge runtimes (Cloudflare Workers, Deno, Bun, Vercel Edge)
8
+ * when paired with an edge-compatible store (the in-memory store, or a Redis client
9
+ * satisfying `RedisClientLike`).
10
+ *
11
+ * import { Hono } from 'hono';
12
+ * import { RateLimiter, InMemoryStore, presets } from 'halt-rate';
13
+ * import { haltMiddleware } from 'halt-rate/hono';
14
+ *
15
+ * const limiter = new RateLimiter({ store: new InMemoryStore(), policy: presets.PUBLIC_API });
16
+ * const app = new Hono();
17
+ * app.use('*', haltMiddleware({ limiter }));
18
+ */
19
+
20
+ interface HonoHaltOptions {
21
+ limiter: RateLimiter;
22
+ /** Custom blocked response; return a Response. */
23
+ onBlocked?: (c: Context) => Response | Promise<Response>;
24
+ /** Resolve the client IP (edge runtimes have no socket). Defaults to common headers. */
25
+ getClientIp?: (c: Context) => string | undefined;
26
+ }
27
+ /** Create a Hono middleware for rate limiting. */
28
+ declare function haltMiddleware(options: HonoHaltOptions): (c: Context, next: Next) => Promise<Response | undefined>;
29
+
30
+ export { type HonoHaltOptions, haltMiddleware };
@@ -0,0 +1,30 @@
1
+ import { Context, Next } from 'hono';
2
+ import { o as RateLimiter } from '../limiter-DqVVE0Kl.js';
3
+
4
+ /**
5
+ * Hono adapter for Halt rate limiting.
6
+ *
7
+ * Works on Node and on edge runtimes (Cloudflare Workers, Deno, Bun, Vercel Edge)
8
+ * when paired with an edge-compatible store (the in-memory store, or a Redis client
9
+ * satisfying `RedisClientLike`).
10
+ *
11
+ * import { Hono } from 'hono';
12
+ * import { RateLimiter, InMemoryStore, presets } from 'halt-rate';
13
+ * import { haltMiddleware } from 'halt-rate/hono';
14
+ *
15
+ * const limiter = new RateLimiter({ store: new InMemoryStore(), policy: presets.PUBLIC_API });
16
+ * const app = new Hono();
17
+ * app.use('*', haltMiddleware({ limiter }));
18
+ */
19
+
20
+ interface HonoHaltOptions {
21
+ limiter: RateLimiter;
22
+ /** Custom blocked response; return a Response. */
23
+ onBlocked?: (c: Context) => Response | Promise<Response>;
24
+ /** Resolve the client IP (edge runtimes have no socket). Defaults to common headers. */
25
+ getClientIp?: (c: Context) => string | undefined;
26
+ }
27
+ /** Create a Hono middleware for rate limiting. */
28
+ declare function haltMiddleware(options: HonoHaltOptions): (c: Context, next: Next) => Promise<Response | undefined>;
29
+
30
+ export { type HonoHaltOptions, haltMiddleware };
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ // src/core/decision.ts
4
+ function toHeaders(decision) {
5
+ const headers = {
6
+ "RateLimit-Limit": String(decision.limit),
7
+ "RateLimit-Remaining": String(Math.max(0, decision.remaining)),
8
+ "RateLimit-Reset": String(decision.resetAt)
9
+ };
10
+ if (!decision.allowed && decision.retryAfter !== void 0) {
11
+ headers["Retry-After"] = String(decision.retryAfter);
12
+ }
13
+ return headers;
14
+ }
15
+
16
+ // src/adapters/hono.ts
17
+ function defaultClientIp(c) {
18
+ const h = (name) => c.req.header(name);
19
+ const xff = h("x-forwarded-for");
20
+ if (xff) return xff.split(",")[0].trim();
21
+ return h("cf-connecting-ip") || h("x-real-ip") || void 0;
22
+ }
23
+ function haltMiddleware(options) {
24
+ const { limiter, onBlocked, getClientIp = defaultClientIp } = options;
25
+ return async (c, next) => {
26
+ const request = {
27
+ headers: c.req.header(),
28
+ // Record<string, string>, lowercased keys
29
+ ip: getClientIp(c),
30
+ path: c.req.path,
31
+ url: c.req.url,
32
+ user: c.get("user")
33
+ };
34
+ const decision = await limiter.check(request);
35
+ for (const [key, value] of Object.entries(toHeaders(decision))) {
36
+ c.header(key, value);
37
+ }
38
+ if (decision.allowed) {
39
+ await next();
40
+ return;
41
+ }
42
+ if (onBlocked) return onBlocked(c);
43
+ return c.json(
44
+ {
45
+ error: "rate_limit_exceeded",
46
+ message: "Too many requests. Please try again later.",
47
+ retryAfter: decision.retryAfter
48
+ },
49
+ 429
50
+ );
51
+ };
52
+ }
53
+
54
+ exports.haltMiddleware = haltMiddleware;
55
+ //# sourceMappingURL=hono.js.map
56
+ //# sourceMappingURL=hono.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/decision.ts","../../src/adapters/hono.ts"],"names":[],"mappings":";;;AAoBO,SAAS,UAAU,QAAA,EAA4C;AAClE,EAAA,MAAM,OAAA,GAAkC;AAAA,IACpC,iBAAA,EAAmB,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA;AAAA,IACxC,uBAAuB,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,EAAG,QAAA,CAAS,SAAS,CAAC,CAAA;AAAA,IAC7D,iBAAA,EAAmB,MAAA,CAAO,QAAA,CAAS,OAAO;AAAA,GAC9C;AAEA,EAAA,IAAI,CAAC,QAAA,CAAS,OAAA,IAAW,QAAA,CAAS,eAAe,MAAA,EAAW;AACxD,IAAA,OAAA,CAAQ,aAAa,CAAA,GAAI,MAAA,CAAO,QAAA,CAAS,UAAU,CAAA;AAAA,EACvD;AAEA,EAAA,OAAO,OAAA;AACX;;;ACJA,SAAS,gBAAgB,CAAA,EAAgC;AACrD,EAAA,MAAM,IAAI,CAAC,IAAA,KAAiB,CAAA,CAAE,GAAA,CAAI,OAAO,IAAI,CAAA;AAC7C,EAAA,MAAM,GAAA,GAAM,EAAE,iBAAiB,CAAA;AAC/B,EAAA,IAAI,GAAA,SAAY,GAAA,CAAI,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,EAAE,IAAA,EAAK;AACvC,EAAA,OAAO,CAAA,CAAE,kBAAkB,CAAA,IAAK,CAAA,CAAE,WAAW,CAAA,IAAK,MAAA;AACtD;AAGO,SAAS,eAAe,OAAA,EAA0B;AACrD,EAAA,MAAM,EAAE,OAAA,EAAS,SAAA,EAAW,WAAA,GAAc,iBAAgB,GAAI,OAAA;AAE9D,EAAA,OAAO,OAAO,GAAY,IAAA,KAAe;AACrC,IAAA,MAAM,OAAA,GAAU;AAAA,MACZ,OAAA,EAAS,CAAA,CAAE,GAAA,CAAI,MAAA,EAAO;AAAA;AAAA,MACtB,EAAA,EAAI,YAAY,CAAC,CAAA;AAAA,MACjB,IAAA,EAAM,EAAE,GAAA,CAAI,IAAA;AAAA,MACZ,GAAA,EAAK,EAAE,GAAA,CAAI,GAAA;AAAA,MACX,IAAA,EAAM,CAAA,CAAE,GAAA,CAAI,MAAM;AAAA,KACtB;AAEA,IAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,KAAA,CAAM,OAAO,CAAA;AAE5C,IAAA,KAAA,MAAW,CAAC,KAAK,KAAK,CAAA,IAAK,OAAO,OAAA,CAAQ,SAAA,CAAU,QAAQ,CAAC,CAAA,EAAG;AAC5D,MAAA,CAAA,CAAE,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,IACvB;AAEA,IAAA,IAAI,SAAS,OAAA,EAAS;AAClB,MAAA,MAAM,IAAA,EAAK;AACX,MAAA;AAAA,IACJ;AAEA,IAAA,IAAI,SAAA,EAAW,OAAO,SAAA,CAAU,CAAC,CAAA;AACjC,IAAA,OAAO,CAAA,CAAE,IAAA;AAAA,MACL;AAAA,QACI,KAAA,EAAO,qBAAA;AAAA,QACP,OAAA,EAAS,4CAAA;AAAA,QACT,YAAY,QAAA,CAAS;AAAA,OACzB;AAAA,MACA;AAAA,KACJ;AAAA,EACJ,CAAA;AACJ","file":"hono.js","sourcesContent":["/**\n * Decision model for rate limiting results.\n */\n\nexport interface Decision {\n /** Whether the request is allowed */\n allowed: boolean;\n /** Maximum number of requests allowed in the window */\n limit: number;\n /** Number of requests remaining in the current window */\n remaining: number;\n /** Unix timestamp when the limit resets */\n resetAt: number;\n /** Seconds to wait before retrying (only set when blocked) */\n retryAfter?: number;\n}\n\n/**\n * Convert decision to standard rate limit headers.\n */\nexport function toHeaders(decision: Decision): Record<string, string> {\n const headers: Record<string, string> = {\n 'RateLimit-Limit': String(decision.limit),\n 'RateLimit-Remaining': String(Math.max(0, decision.remaining)),\n 'RateLimit-Reset': String(decision.resetAt),\n };\n\n if (!decision.allowed && decision.retryAfter !== undefined) {\n headers['Retry-After'] = String(decision.retryAfter);\n }\n\n return headers;\n}\n","/**\n * Hono adapter for Halt rate limiting.\n *\n * Works on Node and on edge runtimes (Cloudflare Workers, Deno, Bun, Vercel Edge)\n * when paired with an edge-compatible store (the in-memory store, or a Redis client\n * satisfying `RedisClientLike`).\n *\n * import { Hono } from 'hono';\n * import { RateLimiter, InMemoryStore, presets } from 'halt-rate';\n * import { haltMiddleware } from 'halt-rate/hono';\n *\n * const limiter = new RateLimiter({ store: new InMemoryStore(), policy: presets.PUBLIC_API });\n * const app = new Hono();\n * app.use('*', haltMiddleware({ limiter }));\n */\n\nimport type { Context, Next } from 'hono';\nimport { RateLimiter } from '../core/limiter';\nimport { toHeaders } from '../core/decision';\n\nexport interface HonoHaltOptions {\n limiter: RateLimiter;\n /** Custom blocked response; return a Response. */\n onBlocked?: (c: Context) => Response | Promise<Response>;\n /** Resolve the client IP (edge runtimes have no socket). Defaults to common headers. */\n getClientIp?: (c: Context) => string | undefined;\n}\n\nfunction defaultClientIp(c: Context): string | undefined {\n const h = (name: string) => c.req.header(name);\n const xff = h('x-forwarded-for');\n if (xff) return xff.split(',')[0].trim();\n return h('cf-connecting-ip') || h('x-real-ip') || undefined;\n}\n\n/** Create a Hono middleware for rate limiting. */\nexport function haltMiddleware(options: HonoHaltOptions) {\n const { limiter, onBlocked, getClientIp = defaultClientIp } = options;\n\n return async (c: Context, next: Next) => {\n const request = {\n headers: c.req.header(), // Record<string, string>, lowercased keys\n ip: getClientIp(c),\n path: c.req.path,\n url: c.req.url,\n user: c.get('user'),\n };\n\n const decision = await limiter.check(request);\n\n for (const [key, value] of Object.entries(toHeaders(decision))) {\n c.header(key, value);\n }\n\n if (decision.allowed) {\n await next();\n return;\n }\n\n if (onBlocked) return onBlocked(c);\n return c.json(\n {\n error: 'rate_limit_exceeded',\n message: 'Too many requests. Please try again later.',\n retryAfter: decision.retryAfter,\n },\n 429\n );\n };\n}\n"]}
@@ -0,0 +1,54 @@
1
+ // src/core/decision.ts
2
+ function toHeaders(decision) {
3
+ const headers = {
4
+ "RateLimit-Limit": String(decision.limit),
5
+ "RateLimit-Remaining": String(Math.max(0, decision.remaining)),
6
+ "RateLimit-Reset": String(decision.resetAt)
7
+ };
8
+ if (!decision.allowed && decision.retryAfter !== void 0) {
9
+ headers["Retry-After"] = String(decision.retryAfter);
10
+ }
11
+ return headers;
12
+ }
13
+
14
+ // src/adapters/hono.ts
15
+ function defaultClientIp(c) {
16
+ const h = (name) => c.req.header(name);
17
+ const xff = h("x-forwarded-for");
18
+ if (xff) return xff.split(",")[0].trim();
19
+ return h("cf-connecting-ip") || h("x-real-ip") || void 0;
20
+ }
21
+ function haltMiddleware(options) {
22
+ const { limiter, onBlocked, getClientIp = defaultClientIp } = options;
23
+ return async (c, next) => {
24
+ const request = {
25
+ headers: c.req.header(),
26
+ // Record<string, string>, lowercased keys
27
+ ip: getClientIp(c),
28
+ path: c.req.path,
29
+ url: c.req.url,
30
+ user: c.get("user")
31
+ };
32
+ const decision = await limiter.check(request);
33
+ for (const [key, value] of Object.entries(toHeaders(decision))) {
34
+ c.header(key, value);
35
+ }
36
+ if (decision.allowed) {
37
+ await next();
38
+ return;
39
+ }
40
+ if (onBlocked) return onBlocked(c);
41
+ return c.json(
42
+ {
43
+ error: "rate_limit_exceeded",
44
+ message: "Too many requests. Please try again later.",
45
+ retryAfter: decision.retryAfter
46
+ },
47
+ 429
48
+ );
49
+ };
50
+ }
51
+
52
+ export { haltMiddleware };
53
+ //# sourceMappingURL=hono.mjs.map
54
+ //# sourceMappingURL=hono.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/decision.ts","../../src/adapters/hono.ts"],"names":[],"mappings":";AAoBO,SAAS,UAAU,QAAA,EAA4C;AAClE,EAAA,MAAM,OAAA,GAAkC;AAAA,IACpC,iBAAA,EAAmB,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA;AAAA,IACxC,uBAAuB,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,EAAG,QAAA,CAAS,SAAS,CAAC,CAAA;AAAA,IAC7D,iBAAA,EAAmB,MAAA,CAAO,QAAA,CAAS,OAAO;AAAA,GAC9C;AAEA,EAAA,IAAI,CAAC,QAAA,CAAS,OAAA,IAAW,QAAA,CAAS,eAAe,MAAA,EAAW;AACxD,IAAA,OAAA,CAAQ,aAAa,CAAA,GAAI,MAAA,CAAO,QAAA,CAAS,UAAU,CAAA;AAAA,EACvD;AAEA,EAAA,OAAO,OAAA;AACX;;;ACJA,SAAS,gBAAgB,CAAA,EAAgC;AACrD,EAAA,MAAM,IAAI,CAAC,IAAA,KAAiB,CAAA,CAAE,GAAA,CAAI,OAAO,IAAI,CAAA;AAC7C,EAAA,MAAM,GAAA,GAAM,EAAE,iBAAiB,CAAA;AAC/B,EAAA,IAAI,GAAA,SAAY,GAAA,CAAI,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,EAAE,IAAA,EAAK;AACvC,EAAA,OAAO,CAAA,CAAE,kBAAkB,CAAA,IAAK,CAAA,CAAE,WAAW,CAAA,IAAK,MAAA;AACtD;AAGO,SAAS,eAAe,OAAA,EAA0B;AACrD,EAAA,MAAM,EAAE,OAAA,EAAS,SAAA,EAAW,WAAA,GAAc,iBAAgB,GAAI,OAAA;AAE9D,EAAA,OAAO,OAAO,GAAY,IAAA,KAAe;AACrC,IAAA,MAAM,OAAA,GAAU;AAAA,MACZ,OAAA,EAAS,CAAA,CAAE,GAAA,CAAI,MAAA,EAAO;AAAA;AAAA,MACtB,EAAA,EAAI,YAAY,CAAC,CAAA;AAAA,MACjB,IAAA,EAAM,EAAE,GAAA,CAAI,IAAA;AAAA,MACZ,GAAA,EAAK,EAAE,GAAA,CAAI,GAAA;AAAA,MACX,IAAA,EAAM,CAAA,CAAE,GAAA,CAAI,MAAM;AAAA,KACtB;AAEA,IAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,KAAA,CAAM,OAAO,CAAA;AAE5C,IAAA,KAAA,MAAW,CAAC,KAAK,KAAK,CAAA,IAAK,OAAO,OAAA,CAAQ,SAAA,CAAU,QAAQ,CAAC,CAAA,EAAG;AAC5D,MAAA,CAAA,CAAE,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,IACvB;AAEA,IAAA,IAAI,SAAS,OAAA,EAAS;AAClB,MAAA,MAAM,IAAA,EAAK;AACX,MAAA;AAAA,IACJ;AAEA,IAAA,IAAI,SAAA,EAAW,OAAO,SAAA,CAAU,CAAC,CAAA;AACjC,IAAA,OAAO,CAAA,CAAE,IAAA;AAAA,MACL;AAAA,QACI,KAAA,EAAO,qBAAA;AAAA,QACP,OAAA,EAAS,4CAAA;AAAA,QACT,YAAY,QAAA,CAAS;AAAA,OACzB;AAAA,MACA;AAAA,KACJ;AAAA,EACJ,CAAA;AACJ","file":"hono.mjs","sourcesContent":["/**\n * Decision model for rate limiting results.\n */\n\nexport interface Decision {\n /** Whether the request is allowed */\n allowed: boolean;\n /** Maximum number of requests allowed in the window */\n limit: number;\n /** Number of requests remaining in the current window */\n remaining: number;\n /** Unix timestamp when the limit resets */\n resetAt: number;\n /** Seconds to wait before retrying (only set when blocked) */\n retryAfter?: number;\n}\n\n/**\n * Convert decision to standard rate limit headers.\n */\nexport function toHeaders(decision: Decision): Record<string, string> {\n const headers: Record<string, string> = {\n 'RateLimit-Limit': String(decision.limit),\n 'RateLimit-Remaining': String(Math.max(0, decision.remaining)),\n 'RateLimit-Reset': String(decision.resetAt),\n };\n\n if (!decision.allowed && decision.retryAfter !== undefined) {\n headers['Retry-After'] = String(decision.retryAfter);\n }\n\n return headers;\n}\n","/**\n * Hono adapter for Halt rate limiting.\n *\n * Works on Node and on edge runtimes (Cloudflare Workers, Deno, Bun, Vercel Edge)\n * when paired with an edge-compatible store (the in-memory store, or a Redis client\n * satisfying `RedisClientLike`).\n *\n * import { Hono } from 'hono';\n * import { RateLimiter, InMemoryStore, presets } from 'halt-rate';\n * import { haltMiddleware } from 'halt-rate/hono';\n *\n * const limiter = new RateLimiter({ store: new InMemoryStore(), policy: presets.PUBLIC_API });\n * const app = new Hono();\n * app.use('*', haltMiddleware({ limiter }));\n */\n\nimport type { Context, Next } from 'hono';\nimport { RateLimiter } from '../core/limiter';\nimport { toHeaders } from '../core/decision';\n\nexport interface HonoHaltOptions {\n limiter: RateLimiter;\n /** Custom blocked response; return a Response. */\n onBlocked?: (c: Context) => Response | Promise<Response>;\n /** Resolve the client IP (edge runtimes have no socket). Defaults to common headers. */\n getClientIp?: (c: Context) => string | undefined;\n}\n\nfunction defaultClientIp(c: Context): string | undefined {\n const h = (name: string) => c.req.header(name);\n const xff = h('x-forwarded-for');\n if (xff) return xff.split(',')[0].trim();\n return h('cf-connecting-ip') || h('x-real-ip') || undefined;\n}\n\n/** Create a Hono middleware for rate limiting. */\nexport function haltMiddleware(options: HonoHaltOptions) {\n const { limiter, onBlocked, getClientIp = defaultClientIp } = options;\n\n return async (c: Context, next: Next) => {\n const request = {\n headers: c.req.header(), // Record<string, string>, lowercased keys\n ip: getClientIp(c),\n path: c.req.path,\n url: c.req.url,\n user: c.get('user'),\n };\n\n const decision = await limiter.check(request);\n\n for (const [key, value] of Object.entries(toHeaders(decision))) {\n c.header(key, value);\n }\n\n if (decision.allowed) {\n await next();\n return;\n }\n\n if (onBlocked) return onBlocked(c);\n return c.json(\n {\n error: 'rate_limit_exceeded',\n message: 'Too many requests. Please try again later.',\n retryAfter: decision.retryAfter,\n },\n 429\n );\n };\n}\n"]}
@@ -459,7 +459,8 @@ var RateLimiter = class {
459
459
  return decision2;
460
460
  }
461
461
  const store = this.store;
462
- let algorithm = this.algorithmCache.get(policy.name);
462
+ const cacheKey = `${policy.name}|${policy.algorithm}|${policy.limit}|${policy.window}|${policy.burst}`;
463
+ let algorithm = this.algorithmCache.get(cacheKey);
463
464
  if (!algorithm) {
464
465
  if (policy.algorithm === "token_bucket" /* TOKEN_BUCKET */) {
465
466
  algorithm = new TokenBucket(policy.burst, policy.limit, policy.window);
@@ -473,7 +474,7 @@ var RateLimiter = class {
473
474
  } else {
474
475
  throw new Error(`Algorithm ${policy.algorithm} not implemented`);
475
476
  }
476
- this.algorithmCache.set(policy.name, algorithm);
477
+ this.algorithmCache.set(cacheKey, algorithm);
477
478
  }
478
479
  const span = this.otelTracer?.startSpan?.("halt.check", { attributes: { policy: policy.name, key } });
479
480
  const state = store.get(storageKey);