@klnap/next-proxy-chain 1.0.2 → 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 +129 -145
- package/package.json +6 -2
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,117 +1,70 @@
|
|
|
1
1
|
# @klnap/next-proxy-chain
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Composable request pipeline for Next.js middleware and proxy.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
---
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
- **Header Mutation Conflicts**: Independent middleware steps often overwrite request headers (`x-middleware-request-*`) or drop previously injected values.
|
|
13
|
-
- **Cookie Directive Overwrites**: Multiple intermediate steps setting `Set-Cookie` directives risk clobbering each other or emitting duplicate headers that CDNs and browsers handle unpredictably.
|
|
14
|
-
- **Stateful Concurrency Hazards**: Shared state or stateful regular expressions (such as `RegExp` instances with global `g` or sticky `y` flags) cause non-deterministic route matching under high concurrency in Edge worker runtimes.
|
|
15
|
-
- **Header Injection Vulnerabilities**: Untrusted incoming requests can attempt to manipulate protected transport headers such as `Host`, `Authorization`, or Next.js internal headers (`x-middleware-*`).
|
|
11
|
+
## Features
|
|
16
12
|
|
|
17
|
-
|
|
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.
|
|
18
22
|
|
|
19
23
|
---
|
|
20
24
|
|
|
21
25
|
## Installation
|
|
22
26
|
|
|
23
|
-
Install the package via your preferred package manager:
|
|
24
|
-
|
|
25
27
|
```bash
|
|
26
|
-
# npm
|
|
27
28
|
npm install @klnap/next-proxy-chain
|
|
29
|
+
```
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
bun add @klnap/next-proxy-chain
|
|
31
|
+
Or with your preferred package manager:
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
```bash
|
|
33
34
|
pnpm add @klnap/next-proxy-chain
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
# or
|
|
36
|
+
bun add @klnap/next-proxy-chain
|
|
37
|
+
# or
|
|
36
38
|
yarn add @klnap/next-proxy-chain
|
|
37
39
|
```
|
|
38
40
|
|
|
39
41
|
---
|
|
40
42
|
|
|
41
|
-
##
|
|
42
|
-
|
|
43
|
-
`next-proxy-chain` orchestrates incoming requests through a linear pipeline of isolated rules:
|
|
44
|
-
|
|
45
|
-
```
|
|
46
|
-
[Incoming Request]
|
|
47
|
-
│
|
|
48
|
-
▼
|
|
49
|
-
┌──────────────┐
|
|
50
|
-
│ createContext│ ───> Map<string, unknown> (Isolated per request)
|
|
51
|
-
└──────┬───────┘
|
|
52
|
-
│
|
|
53
|
-
▼
|
|
54
|
-
┌──────────────┐ (Matched by path/host filter)
|
|
55
|
-
│ Rule 1 │ ───> return next({ request, headers })
|
|
56
|
-
└──────┬───────┘
|
|
57
|
-
│ Materializes mutated NextRequest only when next rule executes
|
|
58
|
-
▼
|
|
59
|
-
┌──────────────┐ (Matched by path/host filter)
|
|
60
|
-
│ Rule 2 │ ───> return next() | NextResponse.redirect()
|
|
61
|
-
└──────┬───────┘
|
|
62
|
-
│
|
|
63
|
-
▼
|
|
64
|
-
┌──────────────┐
|
|
65
|
-
│ Final Merge │ ───> Merges all request overrides, response headers, & cookies
|
|
66
|
-
└──────┬───────┘
|
|
67
|
-
│
|
|
68
|
-
▼
|
|
69
|
-
[Outgoing NextResponse / Response]
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### Key Architectural Properties
|
|
73
|
-
|
|
74
|
-
1. **Pure Core**: The underlying rule engine operates strictly on Web Standards (`Request`, `Response`, `Headers`, `URL`, `Map`).
|
|
75
|
-
2. **Deterministic Conflict Resolution**: Outgoing response headers and cookies are merged according to configurable strategies (`last-write-wins`, `first-write-wins`, or custom resolvers).
|
|
76
|
-
3. **Lazy Request Materialization**: Cloned `NextRequest` objects are only instantiated when downstream rules read mutated headers, minimizing memory allocations.
|
|
77
|
-
4. **Edge Runtime Hardening**: Strips `g` and `y` flags from regular expressions to prevent race conditions during concurrent request processing.
|
|
78
|
-
5. **Fail-Safe Recovery**: Unhandled rule exceptions are caught and routed through optional `onError` handlers, falling back to a safe HTTP 500 response.
|
|
79
|
-
|
|
80
|
-
---
|
|
81
|
-
|
|
82
|
-
## Integration Guide
|
|
83
|
-
|
|
84
|
-
### Next.js 15 Integration (`middleware.ts`)
|
|
85
|
-
|
|
86
|
-
In Next.js 15, export the proxy chain directly as the middleware handler:
|
|
43
|
+
## Quick Start
|
|
87
44
|
|
|
88
45
|
```typescript
|
|
89
|
-
// middleware.ts
|
|
46
|
+
// proxy.ts or middleware.ts
|
|
90
47
|
import { defineProxy, next, path, proxyChain } from "@klnap/next-proxy-chain"
|
|
91
48
|
import { NextResponse } from "next/server"
|
|
92
49
|
|
|
93
|
-
const
|
|
94
|
-
const id = req.headers.get("x-request-id") ?? crypto.randomUUID()
|
|
95
|
-
return next({
|
|
96
|
-
request: { "x-request-id": id },
|
|
97
|
-
headers: { "x-request-id": id },
|
|
98
|
-
})
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
const authProxy = defineProxy("auth", async (req, ctx) => {
|
|
50
|
+
const auth = defineProxy("auth", (req, ctx) => {
|
|
102
51
|
const token = req.cookies.get("session")?.value
|
|
103
|
-
|
|
104
52
|
if (!token) {
|
|
105
53
|
return NextResponse.redirect(new URL("/login", req.url))
|
|
106
54
|
}
|
|
107
|
-
|
|
108
|
-
ctx.set("userId", "usr_100")
|
|
55
|
+
ctx.set("userId", "usr_123")
|
|
109
56
|
return next()
|
|
110
57
|
})
|
|
111
58
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
59
|
+
const security = defineProxy("security", () => {
|
|
60
|
+
return next({
|
|
61
|
+
headers: { "x-frame-options": "DENY" },
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
export default proxyChain({ debug: process.env.NODE_ENV !== "production" }, [
|
|
66
|
+
auth({ include: path("/dashboard/*") }),
|
|
67
|
+
security,
|
|
115
68
|
])
|
|
116
69
|
|
|
117
70
|
export const config = {
|
|
@@ -119,48 +72,80 @@ export const config = {
|
|
|
119
72
|
}
|
|
120
73
|
```
|
|
121
74
|
|
|
122
|
-
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Routing & Scoping
|
|
123
78
|
|
|
124
|
-
|
|
79
|
+
Scope rules to specific paths, hosts, or combinations using `defineProxy`:
|
|
125
80
|
|
|
126
81
|
```typescript
|
|
127
|
-
|
|
128
|
-
import { defineProxy, host, next, path, proxyChain } from "@klnap/next-proxy-chain"
|
|
129
|
-
import { NextResponse } from "next/server"
|
|
82
|
+
import { defineProxy, host, next, path } from "@klnap/next-proxy-chain"
|
|
130
83
|
|
|
131
|
-
const
|
|
132
|
-
|
|
84
|
+
const apiAuth = defineProxy("apiAuth", async (req, ctx) => {
|
|
85
|
+
// Executed only for /api/* requests except /api/public/*
|
|
86
|
+
return next()
|
|
87
|
+
})
|
|
133
88
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
89
|
+
// Scoped rule configuration
|
|
90
|
+
const scopedRule = apiAuth({
|
|
91
|
+
include: path("/api/*"),
|
|
92
|
+
exclude: path("/api/public/*"),
|
|
93
|
+
})
|
|
140
94
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
95
|
+
// Combined path and host filter
|
|
96
|
+
const adminRule = apiAuth({
|
|
97
|
+
include: {
|
|
98
|
+
host: host("admin.example.com"),
|
|
99
|
+
path: path("/settings/**"),
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
```
|
|
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]+\/.*/)`
|
|
147
110
|
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## State & Header Mutation
|
|
114
|
+
|
|
115
|
+
### Request Context
|
|
116
|
+
|
|
117
|
+
Rules share state via an isolated `Map<string, unknown>` created per request:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const enrichUser = defineProxy("enrichUser", async (req, ctx) => {
|
|
121
|
+
ctx.set("user", { id: "usr_1", role: "admin" })
|
|
148
122
|
return next()
|
|
149
123
|
})
|
|
150
124
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
})
|
|
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()
|
|
158
131
|
})
|
|
132
|
+
```
|
|
159
133
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
134
|
+
### Request and Response Headers
|
|
135
|
+
|
|
136
|
+
Use `next()` to inject request headers (visible to downstream rules and route handlers) and response headers:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
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
|
+
})
|
|
164
149
|
```
|
|
165
150
|
|
|
166
151
|
---
|
|
@@ -169,53 +154,52 @@ export default proxyChain({ debug: process.env.NODE_ENV !== "production" }, [
|
|
|
169
154
|
|
|
170
155
|
### Core Functions
|
|
171
156
|
|
|
172
|
-
| Function |
|
|
173
|
-
| :--- | :--- | :--- |
|
|
174
|
-
| `proxyChain(rules)` | `Entry
|
|
175
|
-
| `proxyChain(options, rules)` | `
|
|
176
|
-
| `defineProxy(name?, fn)` | `string
|
|
177
|
-
| `next(options?)` | `NextOptions
|
|
178
|
-
| `path(pattern, options?)` | `string \| RegExp, PathOptions
|
|
179
|
-
| `host(matcher)` | `HostMatcher
|
|
180
|
-
| `createProxyContext()` | `
|
|
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. |
|
|
181
166
|
|
|
182
167
|
### Configuration Options (`ProxyChainOptions`)
|
|
183
168
|
|
|
184
169
|
| Option | Type | Default | Description |
|
|
185
170
|
| :--- | :--- | :--- | :--- |
|
|
186
|
-
| `createContext` | `(req
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
190
|
-
| `
|
|
191
|
-
| `requestHeaderPolicy` | `
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
|
|
195
|
-
### Rule Scoping Options (`ProxyPathFilter`)
|
|
196
|
-
|
|
197
|
-
| Property | Type | Description |
|
|
198
|
-
| :--- | :--- | :--- |
|
|
199
|
-
| `include` | `RouteScopeList` | Routes or hosts where the rule must execute. |
|
|
200
|
-
| `exclude` | `RouteScopeList` | Routes or hosts where the rule must be skipped (takes precedence over `include`). |
|
|
201
|
-
| `name` | `string` | Human-readable identifier used for logs and diagnostic metrics. |
|
|
202
|
-
| `pathOptions` | `PathOptions` | Route compilation options, such as locale prefixes (`locales`). |
|
|
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`). |
|
|
203
179
|
|
|
204
180
|
### Return Value Protocol
|
|
205
181
|
|
|
206
|
-
A `ProxyFn`
|
|
182
|
+
A rule (`ProxyFn`) can return:
|
|
207
183
|
|
|
208
|
-
| Return Value |
|
|
184
|
+
| Return Value | Behavior |
|
|
209
185
|
| :--- | :--- |
|
|
210
|
-
| `next({ request?, headers? })` | Continues
|
|
211
|
-
| `undefined`
|
|
186
|
+
| `next({ request?, headers? })` | Continues to next rule, queuing header mutations. |
|
|
187
|
+
| `undefined` / `void` | Continues to next rule without mutating headers. |
|
|
212
188
|
| `NextResponse.next()` | Continues execution while extracting any internal request header overrides. |
|
|
213
|
-
| `NextResponse.rewrite(url)` |
|
|
214
|
-
| `NextResponse.redirect(url)` |
|
|
215
|
-
| `NextResponse.json(
|
|
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)
|
|
216
199
|
|
|
217
200
|
---
|
|
218
201
|
|
|
219
202
|
## License
|
|
220
203
|
|
|
221
|
-
MIT
|
|
204
|
+
[MIT](./LICENSE)
|
|
205
|
+
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@klnap/next-proxy-chain",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Composable request
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "Composable request pipeline for Next.js middleware and proxy.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "klnap",
|
|
7
7
|
"type": "module",
|
|
@@ -10,8 +10,12 @@
|
|
|
10
10
|
"nextjs",
|
|
11
11
|
"nextjs15",
|
|
12
12
|
"nextjs16",
|
|
13
|
+
"next-proxy",
|
|
13
14
|
"proxy",
|
|
15
|
+
"next-middleware",
|
|
14
16
|
"middleware",
|
|
17
|
+
"middleware-chain",
|
|
18
|
+
"request-pipeline",
|
|
15
19
|
"chain",
|
|
16
20
|
"edge"
|
|
17
21
|
],
|