@klnap/next-proxy-chain 1.0.1 → 1.0.3
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/LICENSE +21 -0
- package/README.md +124 -169
- package/dist/index.cjs +196 -174
- package/dist/index.d.cts +255 -35
- package/dist/index.d.ts +255 -35
- package/dist/index.js +443 -81
- package/package.json +20 -12
- package/dist/chunk-PTAOIFNB.js +0 -412
- package/dist/core.cjs +0 -467
- package/dist/core.d.cts +0 -210
- package/dist/core.d.ts +0 -210
- package/dist/core.d.ts.map +0 -1
- package/dist/core.js +0 -68
- package/dist/core.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 klnap
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,41 +1,40 @@
|
|
|
1
1
|
# @klnap/next-proxy-chain
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Composable request pipeline for Next.js middleware and proxy.
|
|
4
4
|
|
|
5
|
-
[](https://bun.sh)
|
|
5
|
+
[](https://www.npmjs.com/package/@klnap/next-proxy-chain)
|
|
6
|
+
[](https://www.npmjs.com/package/@klnap/next-proxy-chain)
|
|
7
|
+
[](https://github.com/klnap/next-proxy-chain/blob/main/LICENSE)
|
|
9
8
|
|
|
10
9
|
---
|
|
11
10
|
|
|
12
|
-
##
|
|
11
|
+
## Features
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
- **Linear Composition**: Chain independent rules sequentially into a single Next.js middleware or proxy handler.
|
|
14
|
+
- **Next.js 15 & 16**: Supports both `middleware.ts` and `proxy.ts`.
|
|
15
|
+
- **Request-Scoped Context**: Isolated per-request context for sharing state between rules without global state.
|
|
16
|
+
- **Deterministic Header & Cookie Merging**: Propagate request header overrides downstream, deduplicate `Set-Cookie` headers, and merge response headers with configurable conflict strategies.
|
|
17
|
+
- **Route & Host Scoping**: Declarative path matching (globs, parameters, regex) and host filtering with `include` and `exclude` rules.
|
|
18
|
+
- **Safe Pattern Matching**: Normalizes stateful regular expressions to avoid `lastIndex`-related matching issues.
|
|
19
|
+
- **Explicit Flow Control**: Return `next({ request, headers })` to pass data downstream, or return standard `Response` / `NextResponse` (`redirect`, `rewrite`, `json`) to exit early.
|
|
20
|
+
- **Timeouts & Error Recovery**: Optional per-rule timeouts (`ruleTimeoutMs`) and error recovery handlers (`onError`).
|
|
21
|
+
- **Zero Runtime Dependencies**: Fully typed in TypeScript with no runtime dependencies.
|
|
23
22
|
|
|
24
23
|
---
|
|
25
24
|
|
|
26
25
|
## Installation
|
|
27
26
|
|
|
28
27
|
```bash
|
|
29
|
-
# Using Bun
|
|
30
|
-
bun add @klnap/next-proxy-chain
|
|
31
|
-
|
|
32
|
-
# Using npm
|
|
33
28
|
npm install @klnap/next-proxy-chain
|
|
29
|
+
```
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
pnpm add @klnap/next-proxy-chain
|
|
31
|
+
Or with your preferred package manager:
|
|
37
32
|
|
|
38
|
-
|
|
33
|
+
```bash
|
|
34
|
+
pnpm add @klnap/next-proxy-chain
|
|
35
|
+
# or
|
|
36
|
+
bun add @klnap/next-proxy-chain
|
|
37
|
+
# or
|
|
39
38
|
yarn add @klnap/next-proxy-chain
|
|
40
39
|
```
|
|
41
40
|
|
|
@@ -43,208 +42,164 @@ yarn add @klnap/next-proxy-chain
|
|
|
43
42
|
|
|
44
43
|
## Quick Start
|
|
45
44
|
|
|
46
|
-
### 1. Define Modular Proxy Rules
|
|
47
|
-
|
|
48
|
-
Create self-contained proxy rules in your application:
|
|
49
|
-
|
|
50
|
-
```typescript
|
|
51
|
-
// src/proxies/request-id.ts
|
|
52
|
-
import { defineProxy, next } from "@klnap/next-proxy-chain"
|
|
53
|
-
|
|
54
|
-
export const requestIdProxy = defineProxy("requestId", (req) => {
|
|
55
|
-
const requestId = req.headers.get("x-request-id") ?? crypto.randomUUID()
|
|
56
|
-
|
|
57
|
-
return next({
|
|
58
|
-
request: { "x-request-id": requestId },
|
|
59
|
-
headers: { "x-request-id": requestId },
|
|
60
|
-
})
|
|
61
|
-
})
|
|
62
|
-
```
|
|
63
|
-
|
|
64
45
|
```typescript
|
|
65
|
-
//
|
|
66
|
-
import { defineProxy, next } from "@klnap/next-proxy-chain"
|
|
46
|
+
// proxy.ts or middleware.ts
|
|
47
|
+
import { defineProxy, next, path, proxyChain } from "@klnap/next-proxy-chain"
|
|
67
48
|
import { NextResponse } from "next/server"
|
|
68
49
|
|
|
69
|
-
|
|
70
|
-
const token = req.cookies.get("
|
|
71
|
-
|
|
50
|
+
const auth = defineProxy("auth", (req, ctx) => {
|
|
51
|
+
const token = req.cookies.get("session")?.value
|
|
72
52
|
if (!token) {
|
|
73
|
-
|
|
74
|
-
loginUrl.searchParams.set("from", req.nextUrl.pathname)
|
|
75
|
-
return NextResponse.redirect(loginUrl)
|
|
53
|
+
return NextResponse.redirect(new URL("/login", req.url))
|
|
76
54
|
}
|
|
77
|
-
|
|
78
|
-
// Store user info in isolated per-request context
|
|
79
|
-
ctx.set("userId", "user_abc123")
|
|
55
|
+
ctx.set("userId", "usr_123")
|
|
80
56
|
return next()
|
|
81
57
|
})
|
|
82
|
-
```
|
|
83
58
|
|
|
84
|
-
|
|
59
|
+
const security = defineProxy("security", () => {
|
|
60
|
+
return next({
|
|
61
|
+
headers: { "x-frame-options": "DENY" },
|
|
62
|
+
})
|
|
63
|
+
})
|
|
85
64
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
import { requestIdProxy } from "./proxies/request-id"
|
|
90
|
-
import { authProxy } from "./proxies/auth"
|
|
91
|
-
import { i18nProxy } from "./proxies/i18n"
|
|
92
|
-
|
|
93
|
-
const isDev = process.env.NODE_ENV !== "production"
|
|
94
|
-
|
|
95
|
-
export default proxyChain({ debug: isDev }, [
|
|
96
|
-
// 1. Injects request ID on all incoming traffic
|
|
97
|
-
requestIdProxy,
|
|
98
|
-
|
|
99
|
-
// 2. Guards dashboard routes, excluding public preview assets
|
|
100
|
-
authProxy({
|
|
101
|
-
include: path("/dashboard/*"),
|
|
102
|
-
exclude: path("/dashboard/public/*"),
|
|
103
|
-
}),
|
|
104
|
-
|
|
105
|
-
// 3. Handles internationalization routing
|
|
106
|
-
i18nProxy,
|
|
65
|
+
export default proxyChain({ debug: process.env.NODE_ENV !== "production" }, [
|
|
66
|
+
auth({ include: path("/dashboard/*") }),
|
|
67
|
+
security,
|
|
107
68
|
])
|
|
108
69
|
|
|
109
70
|
export const config = {
|
|
110
|
-
matcher: "/((?!
|
|
71
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
111
72
|
}
|
|
112
73
|
```
|
|
113
74
|
|
|
114
75
|
---
|
|
115
76
|
|
|
116
|
-
##
|
|
77
|
+
## Routing & Scoping
|
|
117
78
|
|
|
118
|
-
|
|
79
|
+
Scope rules to specific paths, hosts, or combinations using `defineProxy`:
|
|
119
80
|
|
|
120
81
|
```typescript
|
|
121
|
-
import { defineProxy,
|
|
122
|
-
|
|
123
|
-
// Scoped to specific path patterns on ANY host
|
|
124
|
-
const adminRule = defineProxy("admin", handler, {
|
|
125
|
-
include: path("/admin/*"),
|
|
126
|
-
})
|
|
82
|
+
import { defineProxy, host, next, path } from "@klnap/next-proxy-chain"
|
|
127
83
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
84
|
+
const apiAuth = defineProxy("apiAuth", async (req, ctx) => {
|
|
85
|
+
// Executed only for /api/* requests except /api/public/*
|
|
86
|
+
return next()
|
|
131
87
|
})
|
|
132
88
|
|
|
133
|
-
// Scoped
|
|
134
|
-
const
|
|
135
|
-
include:
|
|
89
|
+
// Scoped rule configuration
|
|
90
|
+
const scopedRule = apiAuth({
|
|
91
|
+
include: path("/api/*"),
|
|
92
|
+
exclude: path("/api/public/*"),
|
|
136
93
|
})
|
|
137
94
|
|
|
138
|
-
//
|
|
139
|
-
const
|
|
140
|
-
include:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
exclude: [
|
|
145
|
-
path("/api/v1/health"),
|
|
146
|
-
{ host: "internal.example.com" },
|
|
147
|
-
],
|
|
95
|
+
// Combined path and host filter
|
|
96
|
+
const adminRule = apiAuth({
|
|
97
|
+
include: {
|
|
98
|
+
host: host("admin.example.com"),
|
|
99
|
+
path: path("/settings/**"),
|
|
100
|
+
},
|
|
148
101
|
})
|
|
149
102
|
```
|
|
150
103
|
|
|
104
|
+
### Path Patterns
|
|
105
|
+
|
|
106
|
+
- Wildcards: `path("/api/*")` (single segment) or `path("/docs/**")` (multi-segment)
|
|
107
|
+
- Parameterized: `path("/users/:id")`
|
|
108
|
+
- Locales: `path("/dashboard", { locales: ["en", "pl", "de"] })`
|
|
109
|
+
- Regular expressions: `path(/^\/v[0-9]+\/.*/)`
|
|
110
|
+
|
|
151
111
|
---
|
|
152
112
|
|
|
153
|
-
##
|
|
113
|
+
## State & Header Mutation
|
|
154
114
|
|
|
155
|
-
|
|
115
|
+
### Request Context
|
|
156
116
|
|
|
157
|
-
|
|
158
|
-
- **Response Headers (`next({ headers: { ... } })`)**: Accumulated using last-write-wins strategy, written to the terminal response.
|
|
159
|
-
- **Cookies (`Set-Cookie`)**: Multiple `Set-Cookie` directives across different rules are deduplicated by name and path attributes without dropping earlier cookies.
|
|
117
|
+
Rules share state via an isolated `Map<string, unknown>` created per request:
|
|
160
118
|
|
|
161
119
|
```typescript
|
|
162
|
-
|
|
120
|
+
const enrichUser = defineProxy("enrichUser", async (req, ctx) => {
|
|
121
|
+
ctx.set("user", { id: "usr_1", role: "admin" })
|
|
122
|
+
return next()
|
|
123
|
+
})
|
|
163
124
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
"x-content-type-options": "nosniff",
|
|
171
|
-
"x-frame-options": "DENY",
|
|
172
|
-
},
|
|
173
|
-
})
|
|
125
|
+
const checkPermission = defineProxy("checkPermission", (req, ctx) => {
|
|
126
|
+
const user = ctx.get("user") as { id: string; role: string } | undefined
|
|
127
|
+
if (user?.role !== "admin") {
|
|
128
|
+
return new Response("Forbidden", { status: 403 })
|
|
129
|
+
}
|
|
130
|
+
return next()
|
|
174
131
|
})
|
|
175
132
|
```
|
|
176
133
|
|
|
177
|
-
|
|
134
|
+
### Request and Response Headers
|
|
178
135
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
For non-Next.js environments (Cloudflare Workers, Fastly Compute, Deno Deploy, Bun HTTP servers), use the pure core subpath:
|
|
136
|
+
Use `next()` to inject request headers (visible to downstream rules and route handlers) and response headers:
|
|
182
137
|
|
|
183
138
|
```typescript
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const context = createProxyContext()
|
|
139
|
+
return next({
|
|
140
|
+
// Downstream request headers (NextRequest & server components)
|
|
141
|
+
request: {
|
|
142
|
+
"x-user-id": "usr_123",
|
|
143
|
+
},
|
|
144
|
+
// Final outgoing response headers
|
|
145
|
+
headers: {
|
|
146
|
+
"cache-control": "no-store",
|
|
147
|
+
},
|
|
148
|
+
})
|
|
195
149
|
```
|
|
196
150
|
|
|
197
151
|
---
|
|
198
152
|
|
|
199
|
-
##
|
|
153
|
+
## API Reference
|
|
200
154
|
|
|
201
|
-
|
|
155
|
+
### Core Functions
|
|
202
156
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
157
|
+
| Function | Signature | Description |
|
|
158
|
+
| :--- | :--- | :--- |
|
|
159
|
+
| `proxyChain(rules)` | `(rules: Entry[]) => Handler` | Creates a middleware/proxy handler from a list of rules. |
|
|
160
|
+
| `proxyChain(options, rules)` | `(options: ProxyChainOptions, rules: Entry[]) => Handler` | Creates a handler with custom pipeline options. |
|
|
161
|
+
| `defineProxy(name?, fn)` | `(name?: string, fn: ProxyFn) => DefinedProxy` | Defines a reusable proxy rule with scoping capabilities. |
|
|
162
|
+
| `next(options?)` | `(options?: NextOptions) => NextResult` | Returns a continuation token with optional header mutations. |
|
|
163
|
+
| `path(pattern, options?)` | `(pattern: string \| RegExp, options?: PathOptions) => RegExp` | Compiles a path pattern into a stateless `RegExp`. |
|
|
164
|
+
| `host(matcher)` | `(matcher: HostMatcher) => RegExp` | Compiles hostname(s) into an anchored `RegExp`. |
|
|
165
|
+
| `createProxyContext()` | `() => ProxyContext` | Creates a new `Map<string, unknown>` context instance. |
|
|
206
166
|
|
|
207
|
-
|
|
208
|
-
{
|
|
209
|
-
timeoutMs: 3000, // Global timeout budget per rule in milliseconds
|
|
210
|
-
onError: (error, ruleIndex, req, ctx) => {
|
|
211
|
-
console.error(`Rule at index ${ruleIndex} failed:`, error)
|
|
212
|
-
return new NextResponse("Service Temporarily Unavailable", { status: 503 })
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
[
|
|
216
|
-
// rules...
|
|
217
|
-
]
|
|
218
|
-
)
|
|
219
|
-
```
|
|
167
|
+
### Configuration Options (`ProxyChainOptions`)
|
|
220
168
|
|
|
221
|
-
|
|
169
|
+
| Option | Type | Default | Description |
|
|
170
|
+
| :--- | :--- | :--- | :--- |
|
|
171
|
+
| `createContext` | `(req, event) => TContext` | `() => new Map()` | Factory for custom per-request context objects. |
|
|
172
|
+
| `headerMergeStrategy` | `"last-write-wins" \| "first-write-wins" \| Function` | `"last-write-wins"` | Strategy for resolving conflicting response headers. |
|
|
173
|
+
| `cookieMergeStrategy` | `"last-write-wins" \| "first-write-wins" \| Function` | `"last-write-wins"` | Strategy for resolving duplicate `Set-Cookie` directives. |
|
|
174
|
+
| `ruleTimeoutMs` | `number` | `0` | Wall-clock execution timeout per rule in milliseconds (`0` disables timeout). |
|
|
175
|
+
| `onError` | `(err, index, req, ctx) => Response \| void` | `undefined` | Custom error recovery hook when a rule throws. |
|
|
176
|
+
| `requestHeaderPolicy` | `{ blocked?: string[], allowed?: string[] }` | `{}` | Custom blocklist/allowlist for request header mutations. |
|
|
177
|
+
| `debug` | `boolean` | `false` | Enables structured single-line execution logs per rule. |
|
|
178
|
+
| `logger` | `ChainLogger` | `console` | Custom logger instance (`debug`, `info`, `warn`, `error`). |
|
|
222
179
|
|
|
223
|
-
|
|
180
|
+
### Return Value Protocol
|
|
224
181
|
|
|
225
|
-
|
|
182
|
+
A rule (`ProxyFn`) can return:
|
|
226
183
|
|
|
227
|
-
|
|
|
184
|
+
| Return Value | Behavior |
|
|
228
185
|
| :--- | :--- |
|
|
229
|
-
| `
|
|
230
|
-
| `
|
|
231
|
-
| `
|
|
232
|
-
| `
|
|
233
|
-
| `
|
|
234
|
-
| `
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
-
|
|
241
|
-
-
|
|
242
|
-
- `ProxyContext`: Isolated `Map<string, unknown>` instance passed to each rule.
|
|
243
|
-
- `NextResult`: Continuation action containing mutation payloads.
|
|
244
|
-
- `ProxyPathFilter`: Declarative configuration containing `include` and `exclude` clauses.
|
|
186
|
+
| `next({ request?, headers? })` | Continues to next rule, queuing header mutations. |
|
|
187
|
+
| `undefined` / `void` | Continues to next rule without mutating headers. |
|
|
188
|
+
| `NextResponse.next()` | Continues execution while extracting any internal request header overrides. |
|
|
189
|
+
| `NextResponse.rewrite(url)` | Stages a URL rewrite for downstream rules while continuing pipeline execution. |
|
|
190
|
+
| `NextResponse.redirect(url)` | Stops pipeline immediately and returns redirect response. |
|
|
191
|
+
| `Response` / `NextResponse.json()` | Stops pipeline immediately and returns terminal response. |
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Links
|
|
196
|
+
|
|
197
|
+
- [GitHub](https://github.com/klnap/next-proxy-chain)
|
|
198
|
+
- [npm](https://www.npmjs.com/package/@klnap/next-proxy-chain?activeTab=code)
|
|
245
199
|
|
|
246
200
|
---
|
|
247
201
|
|
|
248
202
|
## License
|
|
249
203
|
|
|
250
|
-
MIT
|
|
204
|
+
[MIT](./LICENSE)
|
|
205
|
+
|