@klnap/next-proxy-chain 1.0.0 → 1.0.2
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 +174 -79
- 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 +16 -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/README.md
CHANGED
|
@@ -1,126 +1,221 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @klnap/next-proxy-chain
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A composable request-chaining and proxy engine for Next.js architectures with deterministic header and cookie accumulation, isolated request context, and a pure core design.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Overview & Motivation
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
- **
|
|
12
|
-
- **Header
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
9
|
+
In modern Next.js architectures, edge routing, backend-for-frontend (BFF) layers, multi-tenant resolution, and security policies frequently require multi-hop request processing. Standard monolithic middleware implementations lead to architectural challenges:
|
|
10
|
+
|
|
11
|
+
- **Nested Wrapper Hell**: Composing authentication, localization, rate limiting, and multi-tenant routing often results in deeply nested higher-order functions with fragile control flow.
|
|
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-*`).
|
|
16
|
+
|
|
17
|
+
`next-proxy-chain` provides a linear, deterministic execution pipeline with zero framework-specific adapter boilerplate. It executes natively in both Next.js 15 (`middleware.ts`) and Next.js 16 (`proxy.ts`).
|
|
16
18
|
|
|
17
19
|
---
|
|
18
20
|
|
|
19
|
-
##
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
Install the package via your preferred package manager:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# npm
|
|
27
|
+
npm install @klnap/next-proxy-chain
|
|
28
|
+
|
|
29
|
+
# bun
|
|
30
|
+
bun add @klnap/next-proxy-chain
|
|
31
|
+
|
|
32
|
+
# pnpm
|
|
33
|
+
pnpm add @klnap/next-proxy-chain
|
|
20
34
|
|
|
21
|
-
|
|
35
|
+
# yarn
|
|
36
|
+
yarn add @klnap/next-proxy-chain
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
22
40
|
|
|
23
|
-
|
|
24
|
-
import { defineProxy, next } from "@repo/next-proxy-chain"
|
|
41
|
+
## Core Architecture
|
|
25
42
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// middleware.ts
|
|
90
|
+
import { defineProxy, next, path, proxyChain } from "@klnap/next-proxy-chain"
|
|
91
|
+
import { NextResponse } from "next/server"
|
|
92
|
+
|
|
93
|
+
const requestIdProxy = defineProxy("requestId", (req) => {
|
|
94
|
+
const id = req.headers.get("x-request-id") ?? crypto.randomUUID()
|
|
29
95
|
return next({
|
|
30
96
|
request: { "x-request-id": id },
|
|
31
97
|
headers: { "x-request-id": id },
|
|
32
98
|
})
|
|
33
99
|
})
|
|
34
100
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
101
|
+
const authProxy = defineProxy("auth", async (req, ctx) => {
|
|
102
|
+
const token = req.cookies.get("session")?.value
|
|
103
|
+
|
|
38
104
|
if (!token) {
|
|
39
105
|
return NextResponse.redirect(new URL("/login", req.url))
|
|
40
106
|
}
|
|
41
|
-
|
|
107
|
+
|
|
108
|
+
ctx.set("userId", "usr_100")
|
|
42
109
|
return next()
|
|
43
110
|
})
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
### 2. Assemble the Pipeline in `proxy.ts`
|
|
47
|
-
|
|
48
|
-
```ts
|
|
49
|
-
import { proxyChain, path } from "@repo/next-proxy-chain"
|
|
50
|
-
import { requestIdProxy } from "./proxies/request-id"
|
|
51
|
-
import { authProxy } from "./proxies/auth"
|
|
52
|
-
import { i18nProxy } from "./proxies/i18n"
|
|
53
111
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
export default proxyChain({ debug: isDev }, [
|
|
112
|
+
export const middleware = proxyChain([
|
|
57
113
|
requestIdProxy,
|
|
58
|
-
authProxy({
|
|
59
|
-
include: path("/dashboard/*"),
|
|
60
|
-
exclude: path("/dashboard/public"),
|
|
61
|
-
}),
|
|
62
|
-
i18nProxy,
|
|
114
|
+
authProxy({ include: path("/dashboard/*") }),
|
|
63
115
|
])
|
|
64
116
|
|
|
65
117
|
export const config = {
|
|
66
|
-
matcher: "/((?!
|
|
118
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
67
119
|
}
|
|
68
120
|
```
|
|
69
121
|
|
|
70
|
-
|
|
122
|
+
### Next.js 16 Integration (`proxy.ts`)
|
|
71
123
|
|
|
72
|
-
|
|
124
|
+
In Next.js 16, export the proxy chain as default or named `proxy` export:
|
|
73
125
|
|
|
74
|
-
|
|
126
|
+
```typescript
|
|
127
|
+
// proxy.ts
|
|
128
|
+
import { defineProxy, host, next, path, proxyChain } from "@klnap/next-proxy-chain"
|
|
129
|
+
import { NextResponse } from "next/server"
|
|
75
130
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
include: path("/admin/*")
|
|
131
|
+
const multiTenantProxy = defineProxy("multiTenant", (req, ctx) => {
|
|
132
|
+
const hostname = req.headers.get("host") ?? ""
|
|
79
133
|
|
|
80
|
-
|
|
81
|
-
|
|
134
|
+
if (hostname.startsWith("admin.")) {
|
|
135
|
+
ctx.set("tenant", "admin")
|
|
136
|
+
return next({
|
|
137
|
+
request: { "x-tenant-id": "admin-portal" },
|
|
138
|
+
})
|
|
139
|
+
}
|
|
82
140
|
|
|
83
|
-
|
|
84
|
-
|
|
141
|
+
if (hostname.startsWith("app.")) {
|
|
142
|
+
ctx.set("tenant", "app")
|
|
143
|
+
return next({
|
|
144
|
+
request: { "x-tenant-id": "customer-app" },
|
|
145
|
+
})
|
|
146
|
+
}
|
|
85
147
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
148
|
+
return next()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const securityHeadersProxy = defineProxy("securityHeaders", () => {
|
|
152
|
+
return next({
|
|
153
|
+
headers: {
|
|
154
|
+
"x-content-type-options": "nosniff",
|
|
155
|
+
"x-frame-options": "DENY",
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
})
|
|
91
159
|
|
|
92
|
-
|
|
93
|
-
|
|
160
|
+
export default proxyChain({ debug: process.env.NODE_ENV !== "production" }, [
|
|
161
|
+
multiTenantProxy,
|
|
162
|
+
securityHeadersProxy,
|
|
163
|
+
])
|
|
94
164
|
```
|
|
95
165
|
|
|
96
166
|
---
|
|
97
167
|
|
|
98
|
-
##
|
|
168
|
+
## API Reference
|
|
99
169
|
|
|
100
|
-
|
|
170
|
+
### Core Functions
|
|
101
171
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
)
|
|
113
|
-
|
|
172
|
+
| Function | Parameters | Return Type | Description |
|
|
173
|
+
| :--- | :--- | :--- | :--- |
|
|
174
|
+
| `proxyChain(rules)` | `Entry<TContext>[]` | `(req: NextRequest, event: NextFetchEvent) => Promise<NextResponse \| Response>` | Creates an execution pipeline from an array of rules. |
|
|
175
|
+
| `proxyChain(options, rules)` | `ProxyChainOptions<TContext>, Entry<TContext>[]` | `(req: NextRequest, event: NextFetchEvent) => Promise<NextResponse \| Response>` | Creates an execution pipeline with custom configuration. |
|
|
176
|
+
| `defineProxy(name?, fn)` | `string?, ProxyFn<TContext>` | `DefinedProxy<TContext>` | Creates a reusable rule with path and host scoping capabilities. |
|
|
177
|
+
| `next(options?)` | `NextOptions?` | `NextResult` | Generates a pipeline continuation token with optional header mutations. |
|
|
178
|
+
| `path(pattern, options?)` | `string \| RegExp, PathOptions?` | `RegExp` | Compiles a glob pattern, parameterized route, or regex into a stateless path matcher. |
|
|
179
|
+
| `host(matcher)` | `HostMatcher` | `RegExp` | Compiles a single or list of hostnames/patterns into an anchored host matcher. |
|
|
180
|
+
| `createProxyContext()` | `None` | `ProxyContext` | Instantiates a new Map instance for isolated request context storage. |
|
|
181
|
+
|
|
182
|
+
### Configuration Options (`ProxyChainOptions`)
|
|
183
|
+
|
|
184
|
+
| Option | Type | Default | Description |
|
|
185
|
+
| :--- | :--- | :--- | :--- |
|
|
186
|
+
| `createContext` | `(req: NextRequest, event: NextFetchEvent) => TContext` | `() => new Map()` | Custom factory function for request context instantiation. |
|
|
187
|
+
| `cookieMergeStrategy` | `MergeStrategy` | `"last-write-wins"` | Strategy for resolving conflicting `Set-Cookie` directives across rules. |
|
|
188
|
+
| `headerMergeStrategy` | `MergeStrategy` | `"last-write-wins"` | Strategy for resolving conflicting response headers. |
|
|
189
|
+
| `debug` | `boolean` | `false` | Enables structured single-line execution logs for each rule. |
|
|
190
|
+
| `ruleTimeoutMs` | `number` | `0` | Wall-clock execution budget per rule in milliseconds (`0` disables timeout). |
|
|
191
|
+
| `requestHeaderPolicy` | `RequestHeaderPolicy` | `{}` | Custom policy defining blocked and allowed request header mutations. |
|
|
192
|
+
| `strictOverrideCheck` | `boolean` | `false` | Throws an exception at initialization if platform request overrides are unsupported. |
|
|
193
|
+
| `onError` | `(err, index, req, ctx) => Response \| undefined` | `undefined` | Hook invoked when a rule throws an exception to allow custom error recovery. |
|
|
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`). |
|
|
203
|
+
|
|
204
|
+
### Return Value Protocol
|
|
205
|
+
|
|
206
|
+
A `ProxyFn` rule function must return one of the following:
|
|
207
|
+
|
|
208
|
+
| Return Value | Pipeline Behavior |
|
|
209
|
+
| :--- | :--- |
|
|
210
|
+
| `next({ request?, headers? })` | Continues execution, staging request header mutations and response headers. |
|
|
211
|
+
| `undefined` or `void` | Continues execution without mutating headers or state. |
|
|
212
|
+
| `NextResponse.next()` | Continues execution while extracting any internal request header overrides. |
|
|
213
|
+
| `NextResponse.rewrite(url)` | Continues execution while setting the rewrite target for downstream rules. |
|
|
214
|
+
| `NextResponse.redirect(url)` | Halts pipeline execution immediately and returns the redirect response. |
|
|
215
|
+
| `NextResponse.json(...)` or `Response` | Halts pipeline execution immediately and returns the terminal response. |
|
|
114
216
|
|
|
115
217
|
---
|
|
116
218
|
|
|
117
|
-
##
|
|
118
|
-
|
|
119
|
-
### Core Functions
|
|
219
|
+
## License
|
|
120
220
|
|
|
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.
|
|
221
|
+
MIT
|