nextlimiter 1.0.3 → 1.0.4

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/package.json CHANGED
@@ -1,9 +1,15 @@
1
1
  {
2
2
  "name": "nextlimiter",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Production-ready rate limiting for Node.js — sliding window, token bucket, SaaS plans, smart limiting, and built-in analytics.",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./fastify": "./src/adapters/fastify.js",
10
+ "./next": "./src/adapters/next.js",
11
+ "./hono": "./src/adapters/hono.js"
12
+ },
7
13
  "files": [
8
14
  "src/",
9
15
  "types/",
@@ -36,7 +42,10 @@
36
42
  },
37
43
  "peerDependencies": {
38
44
  "express": ">=4.0.0",
39
- "ioredis": ">=4.0.0"
45
+ "ioredis": ">=4.0.0",
46
+ "fastify": ">=4.0.0",
47
+ "fastify-plugin": ">=4.0.0",
48
+ "hono": ">=3.0.0"
40
49
  },
41
50
  "peerDependenciesMeta": {
42
51
  "express": {
@@ -44,6 +53,15 @@
44
53
  },
45
54
  "ioredis": {
46
55
  "optional": true
56
+ },
57
+ "fastify": {
58
+ "optional": true
59
+ },
60
+ "fastify-plugin": {
61
+ "optional": true
62
+ },
63
+ "hono": {
64
+ "optional": true
47
65
  }
48
66
  },
49
67
  "devDependencies": {
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * NextLimiter — Fastify adapter
5
+ *
6
+ * Registers a Fastify plugin that applies rate limiting via an onRequest hook.
7
+ * Uses the same createLimiter() core — no rate limit logic lives here.
8
+ *
9
+ * Peer dependency: fastify-plugin (npm install fastify-plugin)
10
+ *
11
+ * @example
12
+ * const fastify = require('fastify')();
13
+ * const fastifyRateLimit = require('nextlimiter/fastify');
14
+ *
15
+ * fastify.register(fastifyRateLimit, { max: 100, windowMs: 60_000 });
16
+ */
17
+
18
+ const { createLimiter } = require('../index');
19
+
20
+ /**
21
+ * Extract the best available IP from a Fastify request.
22
+ * @param {object} request - Fastify request object
23
+ * @returns {string}
24
+ */
25
+ function extractIp(request) {
26
+ return (
27
+ request.headers['cf-connecting-ip'] ||
28
+ request.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
29
+ request.ip ||
30
+ 'unknown'
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Fastify plugin factory.
36
+ * @param {import('fastify').FastifyInstance} fastify
37
+ * @param {object} options - Same options as createLimiter()
38
+ * @param {function} done
39
+ */
40
+ async function plugin(fastify, options) {
41
+ const limiter = createLimiter(options);
42
+
43
+ fastify.addHook('onRequest', async (request, reply) => {
44
+ try {
45
+ const ip = extractIp(request);
46
+ const result = await limiter.check(ip);
47
+
48
+ if (!result.allowed) {
49
+ return reply.code(429).send({
50
+ error: 'Too Many Requests',
51
+ retryAfter: result.retryAfter,
52
+ limit: result.limit,
53
+ resetAt: new Date(result.resetAt).toISOString(),
54
+ });
55
+ }
56
+
57
+ // Set rate limit headers on allowed requests
58
+ reply.header('X-RateLimit-Limit', String(result.limit));
59
+ reply.header('X-RateLimit-Remaining', String(result.remaining));
60
+ reply.header('X-RateLimit-Reset', String(Math.ceil(result.resetAt / 1000)));
61
+ reply.header('X-RateLimit-Strategy', result.strategy);
62
+ } catch (err) {
63
+ // Fail open — never let the rate limiter take down the app
64
+ request.log.warn(`[NextLimiter] Rate limiter error: ${err.message}. Failing open.`);
65
+ }
66
+ });
67
+ }
68
+
69
+ let fastifyPlugin;
70
+ try {
71
+ fastifyPlugin = require('fastify-plugin');
72
+ } catch {
73
+ // If fastify-plugin is not installed, wrap minimally so the plugin still works
74
+ fastifyPlugin = (fn, meta) => fn;
75
+ }
76
+
77
+ module.exports = fastifyPlugin(plugin, {
78
+ fastify: '>=4.0.0',
79
+ name: 'nextlimiter',
80
+ });
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * NextLimiter — Hono adapter
5
+ *
6
+ * Provides a Hono-compatible middleware factory for Cloudflare Workers,
7
+ * Bun, Deno, and any edge runtime supported by Hono.
8
+ *
9
+ * Uses Web APIs only (fetch/Request/Response/Headers) — no Node.js built-ins.
10
+ * Safe to deploy on Cloudflare Workers, Vercel Edge, and Deno Deploy.
11
+ *
12
+ * No peer dependencies required beyond Hono itself.
13
+ *
14
+ * @example
15
+ * const { Hono } = require('hono');
16
+ * const { rateLimitMiddleware } = require('nextlimiter/hono');
17
+ *
18
+ * const app = new Hono();
19
+ * app.use('*', rateLimitMiddleware({ max: 100, windowMs: 60_000 }));
20
+ *
21
+ * app.get('/', (c) => c.text('Hello World!'));
22
+ * export default app;
23
+ */
24
+
25
+ const { createLimiter } = require('../index');
26
+
27
+ /**
28
+ * Extract the real client IP from a Hono context.
29
+ * Prioritises Cloudflare's CF-Connecting-IP header, then X-Forwarded-For.
30
+ *
31
+ * @param {import('hono').Context} c
32
+ * @returns {string}
33
+ */
34
+ function extractIp(c) {
35
+ return (
36
+ c.req.header('cf-connecting-ip') ||
37
+ c.req.header('x-forwarded-for')?.split(',')[0]?.trim() ||
38
+ c.req.header('x-real-ip') ||
39
+ 'unknown'
40
+ );
41
+ }
42
+
43
+ /**
44
+ * Hono middleware factory.
45
+ *
46
+ * @param {object} options - Same options as createLimiter()
47
+ * @returns {import('hono').MiddlewareHandler}
48
+ *
49
+ * @example
50
+ * app.use('/api/*', rateLimitMiddleware({ max: 100, windowMs: 60_000, strategy: 'sliding-window' }));
51
+ */
52
+ function rateLimitMiddleware(options = {}) {
53
+ const limiter = createLimiter(options);
54
+
55
+ return async function honoRateLimitMiddleware(c, next) {
56
+ try {
57
+ const ip = extractIp(c);
58
+ const result = await limiter.check(ip);
59
+
60
+ if (!result.allowed) {
61
+ return c.json(
62
+ {
63
+ error: 'Too Many Requests',
64
+ retryAfter: result.retryAfter,
65
+ limit: result.limit,
66
+ resetAt: new Date(result.resetAt).toISOString(),
67
+ },
68
+ 429
69
+ );
70
+ }
71
+
72
+ // Set rate limit headers before passing to next handler
73
+ c.header('X-RateLimit-Limit', String(result.limit));
74
+ c.header('X-RateLimit-Remaining', String(result.remaining));
75
+ c.header('X-RateLimit-Reset', String(Math.ceil(result.resetAt / 1000)));
76
+ c.header('X-RateLimit-Strategy', result.strategy);
77
+
78
+ await next();
79
+ } catch (err) {
80
+ // Fail open — never let the rate limiter crash the Hono app
81
+ console.warn(`[NextLimiter] Rate limiter error: ${err.message}. Failing open.`);
82
+ await next();
83
+ }
84
+ };
85
+ }
86
+
87
+ module.exports = { rateLimitMiddleware };
@@ -0,0 +1,157 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * NextLimiter — Next.js adapter
5
+ *
6
+ * Provides two higher-order wrappers:
7
+ * withRateLimit() — for Next.js Pages Router API routes (Node.js runtime)
8
+ * withRateLimitEdge() — for App Router / middleware.js (Edge runtime, Web APIs only)
9
+ *
10
+ * No peer dependencies required.
11
+ *
12
+ * @example
13
+ * // Pages Router (pages/api/hello.js)
14
+ * const { withRateLimit } = require('nextlimiter/next');
15
+ * export default withRateLimit(handler, { max: 100, windowMs: 60_000 });
16
+ *
17
+ * // App Router / Edge (middleware.js)
18
+ * const { withRateLimitEdge } = require('nextlimiter/next');
19
+ * export default withRateLimitEdge(handler, { max: 50 });
20
+ */
21
+
22
+ const { createLimiter } = require('../index');
23
+
24
+ // ── Shared helpers ────────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Memoize limiter instances per options reference so the limiter is only
28
+ * created once per HOC call, not on every request.
29
+ */
30
+ const limiterCache = new WeakMap();
31
+
32
+ function getLimiter(options) {
33
+ if (limiterCache.has(options)) return limiterCache.get(options);
34
+ const limiter = createLimiter(options);
35
+ limiterCache.set(options, limiter);
36
+ return limiter;
37
+ }
38
+
39
+ // ── Pages Router (Node.js runtime) ───────────────────────────────────────────
40
+
41
+ /**
42
+ * Higher-order function for Next.js Pages Router API routes.
43
+ * Wraps a handler with rate limiting using the Node.js HTTP req/res API.
44
+ *
45
+ * @param {function} handler - Next.js API route handler (req, res) => void
46
+ * @param {object} options - createLimiter() options
47
+ * @returns {function} wrapped async handler
48
+ *
49
+ * @example
50
+ * export default withRateLimit(async (req, res) => {
51
+ * res.json({ hello: 'world' });
52
+ * }, { max: 100, windowMs: 60_000 });
53
+ */
54
+ function withRateLimit(handler, options = {}) {
55
+ const limiter = getLimiter(options);
56
+
57
+ return async function rateLimitedHandler(req, res) {
58
+ try {
59
+ const ip =
60
+ (req.headers['x-forwarded-for']?.split(',')[0]?.trim()) ||
61
+ req.socket?.remoteAddress ||
62
+ 'unknown';
63
+
64
+ const result = await limiter.check(ip);
65
+
66
+ if (!result.allowed) {
67
+ return res.status(429).json({
68
+ error: 'Too Many Requests',
69
+ retryAfter: result.retryAfter,
70
+ limit: result.limit,
71
+ resetAt: new Date(result.resetAt).toISOString(),
72
+ });
73
+ }
74
+
75
+ // Set rate limit headers
76
+ res.setHeader('X-RateLimit-Limit', String(result.limit));
77
+ res.setHeader('X-RateLimit-Remaining', String(result.remaining));
78
+ res.setHeader('X-RateLimit-Reset', String(Math.ceil(result.resetAt / 1000)));
79
+ res.setHeader('X-RateLimit-Strategy', result.strategy);
80
+
81
+ return handler(req, res);
82
+ } catch (err) {
83
+ // Fail open — never let the rate limiter crash the API route
84
+ console.warn(`[NextLimiter] Rate limiter error: ${err.message}. Failing open.`);
85
+ return handler(req, res);
86
+ }
87
+ };
88
+ }
89
+
90
+ // ── App Router / Edge runtime (Web APIs only) ─────────────────────────────────
91
+
92
+ /**
93
+ * Higher-order function for Next.js App Router or middleware.js.
94
+ * Uses Web Request/Response API only — no Node.js built-ins.
95
+ * Safe to run on Vercel Edge, Cloudflare Workers, and Deno.
96
+ *
97
+ * @param {function} handler - async (request: Request) => Response
98
+ * @param {object} options - createLimiter() options
99
+ * @returns {function} wrapped async handler
100
+ *
101
+ * @example
102
+ * // middleware.js
103
+ * export default withRateLimitEdge(async (req) => {
104
+ * return new Response('OK');
105
+ * }, { max: 50, windowMs: 60_000 });
106
+ */
107
+ function withRateLimitEdge(handler, options = {}) {
108
+ const limiter = getLimiter(options);
109
+
110
+ return async function rateLimitedEdgeHandler(request) {
111
+ try {
112
+ const ip =
113
+ request.headers.get('cf-connecting-ip') ||
114
+ request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
115
+ 'unknown';
116
+
117
+ const result = await limiter.check(ip);
118
+
119
+ if (!result.allowed) {
120
+ return new Response(
121
+ JSON.stringify({
122
+ error: 'Too Many Requests',
123
+ retryAfter: result.retryAfter,
124
+ limit: result.limit,
125
+ resetAt: new Date(result.resetAt).toISOString(),
126
+ }),
127
+ {
128
+ status: 429,
129
+ headers: { 'Content-Type': 'application/json' },
130
+ }
131
+ );
132
+ }
133
+
134
+ // Call the handler and inject rate limit headers into the response
135
+ const response = await handler(request);
136
+
137
+ // Clone with additional headers (Response is immutable)
138
+ const headers = new Headers(response.headers);
139
+ headers.set('X-RateLimit-Limit', String(result.limit));
140
+ headers.set('X-RateLimit-Remaining', String(result.remaining));
141
+ headers.set('X-RateLimit-Reset', String(Math.ceil(result.resetAt / 1000)));
142
+ headers.set('X-RateLimit-Strategy', result.strategy);
143
+
144
+ return new Response(response.body, {
145
+ status: response.status,
146
+ statusText: response.statusText,
147
+ headers,
148
+ });
149
+ } catch (err) {
150
+ // Fail open
151
+ console.warn(`[NextLimiter] Rate limiter error: ${err.message}. Failing open.`);
152
+ return handler(request);
153
+ }
154
+ };
155
+ }
156
+
157
+ module.exports = { withRateLimit, withRateLimitEdge };
package/types/index.d.ts CHANGED
@@ -275,3 +275,52 @@ export declare class RedisStore implements Store {
275
275
 
276
276
  export declare const PRESETS: Record<BuiltInPreset, LimiterOptions>;
277
277
  export declare const DEFAULT_PLANS: Record<BuiltInPlan, PlanDefinition>;
278
+
279
+ // ── Adapters ─────────────────────────────────────────────────────────────────
280
+ //
281
+ // These are available as subpath imports:
282
+ // import fastifyRateLimit from 'nextlimiter/fastify'
283
+ // import { withRateLimit, withRateLimitEdge } from 'nextlimiter/next'
284
+ // import { rateLimitMiddleware } from 'nextlimiter/hono'
285
+
286
+ // fastify adapter
287
+ declare module 'nextlimiter/fastify' {
288
+ import { FastifyPluginAsync } from 'fastify';
289
+ const fastifyRateLimit: FastifyPluginAsync<LimiterOptions>;
290
+ export default fastifyRateLimit;
291
+ }
292
+
293
+ // next.js adapter
294
+ declare module 'nextlimiter/next' {
295
+ import type { NextApiRequest, NextApiResponse } from 'next';
296
+
297
+ /**
298
+ * Wrap a Next.js Pages Router API handler with rate limiting (Node.js runtime).
299
+ */
300
+ export function withRateLimit<T extends (req: NextApiRequest, res: NextApiResponse) => any>(
301
+ handler: T,
302
+ options?: LimiterOptions
303
+ ): (req: NextApiRequest, res: NextApiResponse) => Promise<void>;
304
+
305
+ /**
306
+ * Wrap a Next.js App Router / middleware handler with rate limiting (Edge runtime).
307
+ * Uses Web Request / Response API only — no Node.js built-ins.
308
+ */
309
+ export function withRateLimitEdge<T extends (req: Request) => Promise<Response>>(
310
+ handler: T,
311
+ options?: LimiterOptions
312
+ ): (req: Request) => Promise<Response>;
313
+ }
314
+
315
+ // hono adapter
316
+ declare module 'nextlimiter/hono' {
317
+ /**
318
+ * Returns a Hono middleware that applies rate limiting.
319
+ * Safe for Cloudflare Workers, Bun, Deno — uses Web APIs only.
320
+ *
321
+ * @example
322
+ * app.use('*', rateLimitMiddleware({ max: 100, windowMs: 60_000 }));
323
+ */
324
+ export function rateLimitMiddleware(options?: LimiterOptions): (c: any, next: () => Promise<void>) => Promise<void>;
325
+ }
326
+