@klnap/next-proxy-chain 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -53
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,52 +1,91 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @klnap/next-proxy-chain
|
|
2
2
|
|
|
3
|
-
Composable Next.js Edge proxy
|
|
3
|
+
> Composable Next.js 16+ Edge proxy and middleware rule chain with deterministic header/cookie accumulation, isolated per-request context, and a pure Web API core.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@klnap/next-proxy-chain)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://bun.sh)
|
|
4
9
|
|
|
5
10
|
---
|
|
6
11
|
|
|
7
|
-
##
|
|
12
|
+
## Why `@klnap/next-proxy-chain`?
|
|
13
|
+
|
|
14
|
+
Next.js 16 introduces the `proxy.ts` file convention to replace monolithic middleware files. In non-trivial production applications, middleware pipelines easily devolve into deeply nested wrappers, conflicting `Set-Cookie` headers, race conditions from stateful regular expressions, and leaked transport headers.
|
|
8
15
|
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
`@klnap/next-proxy-chain` solves this by providing:
|
|
17
|
+
|
|
18
|
+
1. **Linear Pipeline Execution**: Run multiple independent proxy rules sequentially without nested wrapper callbacks.
|
|
19
|
+
2. **Deterministic Header & Cookie Merging**: Combines request mutations (`x-middleware-request-*`), outgoing response headers, and multiple `Set-Cookie` directives without silent overwrites.
|
|
20
|
+
3. **Pure Web API Core**: Zero Next.js dependencies in `@klnap/next-proxy-chain/core` — fully testable on Cloudflare Workers, Fastly, Bun, Deno, and standard Web runtimes.
|
|
21
|
+
4. **Edge Runtime Hardening**: Strips stateful RegExp flags (`g`, `y`) to eliminate concurrent request cross-talk in multi-tenant edge workers, enforces per-rule timeouts, and protects against prototype pollution via `Map` context.
|
|
22
|
+
5. **Header Poisoning Defense**: Protects client responses by stripping sensitive transport headers (`x-middleware-*`) and blocks unauthorized override of protected headers (`Host`, `Authorization`, `Connection`).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Using Bun
|
|
30
|
+
bun add @klnap/next-proxy-chain
|
|
31
|
+
|
|
32
|
+
# Using npm
|
|
33
|
+
npm install @klnap/next-proxy-chain
|
|
34
|
+
|
|
35
|
+
# Using pnpm
|
|
36
|
+
pnpm add @klnap/next-proxy-chain
|
|
37
|
+
|
|
38
|
+
# Using Yarn
|
|
39
|
+
yarn add @klnap/next-proxy-chain
|
|
40
|
+
```
|
|
16
41
|
|
|
17
42
|
---
|
|
18
43
|
|
|
19
44
|
## Quick Start
|
|
20
45
|
|
|
21
|
-
### 1. Define
|
|
46
|
+
### 1. Define Modular Proxy Rules
|
|
47
|
+
|
|
48
|
+
Create self-contained proxy rules in your application:
|
|
22
49
|
|
|
23
|
-
```
|
|
24
|
-
|
|
50
|
+
```typescript
|
|
51
|
+
// src/proxies/request-id.ts
|
|
52
|
+
import { defineProxy, next } from "@klnap/next-proxy-chain"
|
|
25
53
|
|
|
26
|
-
// Request ID injector
|
|
27
54
|
export const requestIdProxy = defineProxy("requestId", (req) => {
|
|
28
|
-
const
|
|
55
|
+
const requestId = req.headers.get("x-request-id") ?? crypto.randomUUID()
|
|
56
|
+
|
|
29
57
|
return next({
|
|
30
|
-
request: { "x-request-id":
|
|
31
|
-
headers: { "x-request-id":
|
|
58
|
+
request: { "x-request-id": requestId },
|
|
59
|
+
headers: { "x-request-id": requestId },
|
|
32
60
|
})
|
|
33
61
|
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// src/proxies/auth.ts
|
|
66
|
+
import { defineProxy, next } from "@klnap/next-proxy-chain"
|
|
67
|
+
import { NextResponse } from "next/server"
|
|
34
68
|
|
|
35
|
-
// Authentication guard
|
|
36
69
|
export const authProxy = defineProxy("auth", async (req, ctx) => {
|
|
37
|
-
const token = req.cookies.get("
|
|
70
|
+
const token = req.cookies.get("session_token")?.value
|
|
71
|
+
|
|
38
72
|
if (!token) {
|
|
39
|
-
|
|
73
|
+
const loginUrl = new URL("/login", req.url)
|
|
74
|
+
loginUrl.searchParams.set("from", req.nextUrl.pathname)
|
|
75
|
+
return NextResponse.redirect(loginUrl)
|
|
40
76
|
}
|
|
41
|
-
|
|
77
|
+
|
|
78
|
+
// Store user info in isolated per-request context
|
|
79
|
+
ctx.set("userId", "user_abc123")
|
|
42
80
|
return next()
|
|
43
81
|
})
|
|
44
82
|
```
|
|
45
83
|
|
|
46
|
-
### 2. Assemble
|
|
84
|
+
### 2. Assemble Pipeline in `src/proxy.ts` (Next.js 16)
|
|
47
85
|
|
|
48
|
-
```
|
|
49
|
-
|
|
86
|
+
```typescript
|
|
87
|
+
// src/proxy.ts
|
|
88
|
+
import { proxyChain, path } from "@klnap/next-proxy-chain"
|
|
50
89
|
import { requestIdProxy } from "./proxies/request-id"
|
|
51
90
|
import { authProxy } from "./proxies/auth"
|
|
52
91
|
import { i18nProxy } from "./proxies/i18n"
|
|
@@ -54,11 +93,16 @@ import { i18nProxy } from "./proxies/i18n"
|
|
|
54
93
|
const isDev = process.env.NODE_ENV !== "production"
|
|
55
94
|
|
|
56
95
|
export default proxyChain({ debug: isDev }, [
|
|
96
|
+
// 1. Injects request ID on all incoming traffic
|
|
57
97
|
requestIdProxy,
|
|
98
|
+
|
|
99
|
+
// 2. Guards dashboard routes, excluding public preview assets
|
|
58
100
|
authProxy({
|
|
59
101
|
include: path("/dashboard/*"),
|
|
60
|
-
exclude: path("/dashboard/public"),
|
|
102
|
+
exclude: path("/dashboard/public/*"),
|
|
61
103
|
}),
|
|
104
|
+
|
|
105
|
+
// 3. Handles internationalization routing
|
|
62
106
|
i18nProxy,
|
|
63
107
|
])
|
|
64
108
|
|
|
@@ -69,41 +113,103 @@ export const config = {
|
|
|
69
113
|
|
|
70
114
|
---
|
|
71
115
|
|
|
72
|
-
##
|
|
116
|
+
## Route & Host Scoping
|
|
117
|
+
|
|
118
|
+
Rules can be declaratively scoped by path, host, or combinations. Scoping evaluates exclusions first — if an exclusion matches, the rule is skipped immediately.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { defineProxy, path, host } from "@klnap/next-proxy-chain"
|
|
122
|
+
|
|
123
|
+
// Scoped to specific path patterns on ANY host
|
|
124
|
+
const adminRule = defineProxy("admin", handler, {
|
|
125
|
+
include: path("/admin/*"),
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Scoped to a specific domain or tenant
|
|
129
|
+
const tenantRule = defineProxy("tenant", handler, {
|
|
130
|
+
include: { host: "app.example.com" },
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// Scoped to specific path on a specific host
|
|
134
|
+
const checkoutRule = defineProxy("checkout", handler, {
|
|
135
|
+
include: { host: "store.example.com", path: "/checkout/*" },
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// Array of clauses (OR evaluation) with strict exclusions
|
|
139
|
+
const apiRule = defineProxy("apiGuard", handler, {
|
|
140
|
+
include: [
|
|
141
|
+
path("/api/v1/*"),
|
|
142
|
+
{ host: "api.example.com" },
|
|
143
|
+
],
|
|
144
|
+
exclude: [
|
|
145
|
+
path("/api/v1/health"),
|
|
146
|
+
{ host: "internal.example.com" },
|
|
147
|
+
],
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Deterministic Header & Cookie Merging
|
|
73
154
|
|
|
74
|
-
|
|
155
|
+
When multiple rules in the chain mutate headers or cookies, `@klnap/next-proxy-chain` merges them deterministically:
|
|
75
156
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
157
|
+
- **Request Overrides (`next({ request: { ... } })`)**: Downstream rules and final page route handlers receive the accumulated mutated headers via Next.js request override encoding.
|
|
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.
|
|
79
160
|
|
|
80
|
-
|
|
81
|
-
|
|
161
|
+
```typescript
|
|
162
|
+
import { defineProxy, next } from "@klnap/next-proxy-chain"
|
|
82
163
|
|
|
83
|
-
|
|
84
|
-
|
|
164
|
+
export const headerEnricher = defineProxy("enricher", () => {
|
|
165
|
+
return next({
|
|
166
|
+
request: {
|
|
167
|
+
"x-geo-region": "eu-central",
|
|
168
|
+
},
|
|
169
|
+
headers: {
|
|
170
|
+
"x-content-type-options": "nosniff",
|
|
171
|
+
"x-frame-options": "DENY",
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
85
178
|
|
|
86
|
-
|
|
87
|
-
include: [
|
|
88
|
-
path("/health"),
|
|
89
|
-
{ host: "shop.localhost", path: "/checkout/*" },
|
|
90
|
-
]
|
|
179
|
+
## Pure Web API Core (`@klnap/next-proxy-chain/core`)
|
|
91
180
|
|
|
92
|
-
|
|
93
|
-
|
|
181
|
+
For non-Next.js environments (Cloudflare Workers, Fastly Compute, Deno Deploy, Bun HTTP servers), use the pure core subpath:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import {
|
|
185
|
+
createProxyContext,
|
|
186
|
+
mergeWrites,
|
|
187
|
+
withPaths,
|
|
188
|
+
next,
|
|
189
|
+
isNextResult,
|
|
190
|
+
toStatelessRegExp
|
|
191
|
+
} from "@klnap/next-proxy-chain/core"
|
|
192
|
+
|
|
193
|
+
// Zero dependencies on next/server — standard Fetch API only
|
|
194
|
+
const context = createProxyContext()
|
|
94
195
|
```
|
|
95
196
|
|
|
96
197
|
---
|
|
97
198
|
|
|
98
|
-
## Error Handling
|
|
199
|
+
## Error Handling & Timeout Budgets
|
|
200
|
+
|
|
201
|
+
Prevent unresponsive downstream authentication services or external APIs from blocking Edge execution:
|
|
99
202
|
|
|
100
|
-
|
|
203
|
+
```typescript
|
|
204
|
+
import { proxyChain } from "@klnap/next-proxy-chain"
|
|
205
|
+
import { NextResponse } from "next/server"
|
|
101
206
|
|
|
102
|
-
```ts
|
|
103
207
|
export default proxyChain(
|
|
104
208
|
{
|
|
105
|
-
|
|
106
|
-
|
|
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 })
|
|
107
213
|
},
|
|
108
214
|
},
|
|
109
215
|
[
|
|
@@ -116,11 +222,29 @@ export default proxyChain(
|
|
|
116
222
|
|
|
117
223
|
## API Reference
|
|
118
224
|
|
|
119
|
-
###
|
|
225
|
+
### Exported Functions (`@klnap/next-proxy-chain`)
|
|
226
|
+
|
|
227
|
+
| Function | Description |
|
|
228
|
+
| :--- | :--- |
|
|
229
|
+
| `proxyChain(options?, rules)` | Assembles rules into a Next.js proxy/middleware handler. |
|
|
230
|
+
| `defineProxy(name, fn, filter?)` | Factory creating a reusable, scope-aware proxy rule function. |
|
|
231
|
+
| `withPaths(fn, filter)` | Wraps a raw proxy function with declarative path/host matching rules. |
|
|
232
|
+
| `next(options?)` | Returns a continuation action with optional `{ request?, headers? }` mutations. |
|
|
233
|
+
| `path(pattern)` | Helper compiling glob or RegExp string into a path matcher. |
|
|
234
|
+
| `host(matcher)` | Helper compiling domain or RegExp matcher into a host matcher. |
|
|
235
|
+
| `createProxyContext()` | Creates a clean, isolated `Map<string, unknown>` context. |
|
|
236
|
+
| `decodeOverriddenRequestHeaders(headers)` | Extracts internal Next.js middleware request header overrides. |
|
|
237
|
+
|
|
238
|
+
### Exported Types
|
|
239
|
+
|
|
240
|
+
- `ProxyFn<TContext>`: Contract for individual proxy execution functions.
|
|
241
|
+
- `ProxyRule<TContext>`: Composed rule containing execution handler and compiled scope filters.
|
|
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.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## License
|
|
120
249
|
|
|
121
|
-
|
|
122
|
-
- `defineProxy(name?, fn)`: Creates a proxy function that can be called with `{ include, exclude }` to scope.
|
|
123
|
-
- `withPaths(rule, filter)`: Scopes any rule function with path/host filters.
|
|
124
|
-
- `next(options?)`: Signals pipeline continuation with optional `{ request?, headers? }` mutations.
|
|
125
|
-
- `decodeOverriddenRequestHeaders(headers)`: Extracts Next.js middleware request header overrides.
|
|
126
|
-
- `createProxyContext()`: Instantiates a fresh `Map` for per-request context.
|
|
250
|
+
MIT © [klnap](https://github.com/klnap)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@klnap/next-proxy-chain",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Composable Next.js 16+ proxy rule chain with Edge-safe logging and a pure core (zero Next imports).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "klnap",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"clean": "rm -rf dist .turbo coverage node_modules"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"next": ">=
|
|
49
|
+
"next": ">=16.0.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^20",
|