@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 CHANGED
@@ -1,126 +1,221 @@
1
- # `@repo/next-proxy-chain`
1
+ # @klnap/next-proxy-chain
2
2
 
3
- Composable Next.js Edge proxy pipeline with pure Web API core, pass-through detection, context threading, header/cookie accumulation, and zero external runtime dependencies.
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
- ## Key Features
7
+ ## Overview & Motivation
8
8
 
9
- - **Linear Pipeline Composition**: Order middleware steps clearly without nested wrapper hell.
10
- - **Pass-through Interoperability**: Transparently supports `NextResponse.next()` from third-party middlewares via `x-middleware-next: 1` detection.
11
- - **Per-request Context**: Type-safe, isolated `Map` context (`TContext`) threaded across all rules.
12
- - **Header & Cookie Accumulation**: Collects and merges request headers, response headers, and cookies across rules before writing to the outgoing response.
13
- - **Host & Path Scoping**: Declarative `include` / `exclude` routing rules with glob, RegExp, and next-intl locale support.
14
- - **Header Injection & Leak Protection**: Automatically filters internal `x-middleware-*` transport headers from client responses.
15
- - **100% Edge-Safe**: Built purely on standard Web APIs (`Headers`, `Map`, `Response`, `Request`).
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
- ## Quick Start
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
- ### 1. Define Reusable Proxies
35
+ # yarn
36
+ yarn add @klnap/next-proxy-chain
37
+ ```
38
+
39
+ ---
22
40
 
23
- ```ts
24
- import { defineProxy, next } from "@repo/next-proxy-chain"
41
+ ## Core Architecture
25
42
 
26
- // Request ID injector
27
- export const requestIdProxy = defineProxy("requestId", (req) => {
28
- const id = crypto.randomUUID()
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
- // Authentication guard
36
- export const authProxy = defineProxy("auth", async (req, ctx) => {
37
- const token = req.cookies.get("auth-token")?.value
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
- ctx.set("userId", "user_123")
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 isDev = process.env.NODE_ENV !== "production"
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: "/((?!api|_next|.*\\..*).*)",
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
- ## Scope Matching Rules
124
+ In Next.js 16, export the proxy chain as default or named `proxy` export:
73
125
 
74
- Scoping supports globs, RegExps, host matching, and combinations:
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
- ```ts
77
- // Path on any host
78
- include: path("/admin/*")
131
+ const multiTenantProxy = defineProxy("multiTenant", (req, ctx) => {
132
+ const hostname = req.headers.get("host") ?? ""
79
133
 
80
- // Any path on a specific host
81
- include: { host: "shop.localhost" }
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
- // Specific path on a specific host
84
- include: { host: "shop.localhost", path: "/checkout/*" }
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
- // OR combinations
87
- include: [
88
- path("/health"),
89
- { host: "shop.localhost", path: "/checkout/*" },
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
- // Exclude always wins
93
- exclude: { host: "museum.localhost" }
160
+ export default proxyChain({ debug: process.env.NODE_ENV !== "production" }, [
161
+ multiTenantProxy,
162
+ securityHeadersProxy,
163
+ ])
94
164
  ```
95
165
 
96
166
  ---
97
167
 
98
- ## Error Handling
168
+ ## API Reference
99
169
 
100
- By default, any unhandled error inside a rule logs safely and returns an HTTP 500 (`safeInternalError`). You can supply a custom `onError` handler:
170
+ ### Core Functions
101
171
 
102
- ```ts
103
- export default proxyChain(
104
- {
105
- onError: (err, ruleIndex, req, ctx) => {
106
- return new NextResponse("Service Unavailable", { status: 503 })
107
- },
108
- },
109
- [
110
- // rules...
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
- ## API Reference
118
-
119
- ### Core Functions
219
+ ## License
120
220
 
121
- - `proxyChain(options?, rules)`: Combines rules into an Edge proxy handler.
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