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
@@ -1,5 +1,5 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
- import { b as RateLimiter } from '../limiter-HCt8ADZ2.mjs';
2
+ import { o as RateLimiter } from '../limiter-DqVVE0Kl.mjs';
3
3
 
4
4
  /**
5
5
  * Express middleware adapter for Halt rate limiting.
@@ -1,5 +1,5 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
- import { b as RateLimiter } from '../limiter-HCt8ADZ2.js';
2
+ import { o as RateLimiter } from '../limiter-DqVVE0Kl.js';
3
3
 
4
4
  /**
5
5
  * Express middleware adapter for Halt rate limiting.
@@ -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"]}
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
- import { c as RateLimiterOptions, P as Policy } from '../limiter-HCt8ADZ2.mjs';
2
+ import { p as RateLimiterOptions, P as Policy } from '../limiter-DqVVE0Kl.mjs';
3
3
 
4
4
  /**
5
5
  * Next.js adapter for Halt rate limiting.
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
- import { c as RateLimiterOptions, P as Policy } from '../limiter-HCt8ADZ2.js';
2
+ import { p as RateLimiterOptions, P as Policy } from '../limiter-DqVVE0Kl.js';
3
3
 
4
4
  /**
5
5
  * Next.js adapter for Halt rate limiting.
@@ -16,7 +16,8 @@ function normalizePolicy(policy) {
16
16
  cost: policy.cost ?? 1,
17
17
  blockDuration: policy.blockDuration ?? void 0,
18
18
  keyExtractor: policy.keyExtractor ?? void 0,
19
- exemptions: policy.exemptions ?? []
19
+ exemptions: policy.exemptions ?? [],
20
+ plan: policy.plan ?? void 0
20
21
  };
21
22
  if (normalized.limit <= 0) {
22
23
  throw new Error("limit must be positive");
@@ -377,6 +378,25 @@ var RateLimiter = class {
377
378
  this.algorithmCache = /* @__PURE__ */ new Map();
378
379
  this.otelTracer = options.otelTracer;
379
380
  this.metricsRecorder = options.metricsRecorder;
381
+ this.telemetry = options.telemetry;
382
+ }
383
+ /** Fan a finished decision out to the telemetry hooks with rich metadata. */
384
+ emitTelemetry(request, key, decision, policy, cost) {
385
+ if (!this.telemetry) return;
386
+ const metadata = {
387
+ policy: policy.name,
388
+ algorithm: policy.algorithm,
389
+ keyStrategy: policy.keyStrategy,
390
+ endpoint: extractPath(request) ?? void 0,
391
+ cost,
392
+ plan: policy.plan
393
+ };
394
+ this.telemetry.onCheck?.(key, decision, metadata);
395
+ if (decision.allowed) {
396
+ this.telemetry.onAllowed?.(key, decision, metadata);
397
+ } else {
398
+ this.telemetry.onBlocked?.(key, decision, metadata);
399
+ }
380
400
  }
381
401
  /**
382
402
  * Check if request is allowed under rate limit.
@@ -435,10 +455,12 @@ var RateLimiter = class {
435
455
  1
436
456
  );
437
457
  span2?.end?.();
458
+ this.emitTelemetry(request, key, decision2, policy, requestCost);
438
459
  return decision2;
439
460
  }
440
461
  const store = this.store;
441
- 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);
442
464
  if (!algorithm) {
443
465
  if (policy.algorithm === "token_bucket" /* TOKEN_BUCKET */) {
444
466
  algorithm = new TokenBucket(policy.burst, policy.limit, policy.window);
@@ -452,7 +474,7 @@ var RateLimiter = class {
452
474
  } else {
453
475
  throw new Error(`Algorithm ${policy.algorithm} not implemented`);
454
476
  }
455
- this.algorithmCache.set(policy.name, algorithm);
477
+ this.algorithmCache.set(cacheKey, algorithm);
456
478
  }
457
479
  const span = this.otelTracer?.startSpan?.("halt.check", { attributes: { policy: policy.name, key } });
458
480
  const state = store.get(storageKey);
@@ -518,6 +540,7 @@ var RateLimiter = class {
518
540
  this.metricsRecorder?.("halt.request.blocked", { policy: policy.name }, 1);
519
541
  }
520
542
  span?.end?.();
543
+ this.emitTelemetry(request, key, decision, policy, requestCost);
521
544
  return decision;
522
545
  }
523
546
  /**