@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.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -145
  3. 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
- 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.
3
+ Composable request pipeline for Next.js middleware and proxy.
4
4
 
5
- ---
6
-
7
- ## Overview & Motivation
5
+ [![npm version](https://img.shields.io/npm/v/@klnap/next-proxy-chain)](https://www.npmjs.com/package/@klnap/next-proxy-chain)
6
+ [![npm downloads](https://img.shields.io/npm/dm/@klnap/next-proxy-chain)](https://www.npmjs.com/package/@klnap/next-proxy-chain)
7
+ [![license](https://img.shields.io/npm/l/@klnap/next-proxy-chain)](https://github.com/klnap/next-proxy-chain/blob/main/LICENSE)
8
8
 
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:
9
+ ---
10
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-*`).
11
+ ## Features
16
12
 
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`).
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
- # bun
30
- bun add @klnap/next-proxy-chain
31
+ Or with your preferred package manager:
31
32
 
32
- # pnpm
33
+ ```bash
33
34
  pnpm add @klnap/next-proxy-chain
34
-
35
- # yarn
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
- ## Core Architecture
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 requestIdProxy = defineProxy("requestId", (req) => {
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
- export const middleware = proxyChain([
113
- requestIdProxy,
114
- authProxy({ include: path("/dashboard/*") }),
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
- ### Next.js 16 Integration (`proxy.ts`)
75
+ ---
76
+
77
+ ## Routing & Scoping
123
78
 
124
- In Next.js 16, export the proxy chain as default or named `proxy` export:
79
+ Scope rules to specific paths, hosts, or combinations using `defineProxy`:
125
80
 
126
81
  ```typescript
127
- // proxy.ts
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 multiTenantProxy = defineProxy("multiTenant", (req, ctx) => {
132
- const hostname = req.headers.get("host") ?? ""
84
+ const apiAuth = defineProxy("apiAuth", async (req, ctx) => {
85
+ // Executed only for /api/* requests except /api/public/*
86
+ return next()
87
+ })
133
88
 
134
- if (hostname.startsWith("admin.")) {
135
- ctx.set("tenant", "admin")
136
- return next({
137
- request: { "x-tenant-id": "admin-portal" },
138
- })
139
- }
89
+ // Scoped rule configuration
90
+ const scopedRule = apiAuth({
91
+ include: path("/api/*"),
92
+ exclude: path("/api/public/*"),
93
+ })
140
94
 
141
- if (hostname.startsWith("app.")) {
142
- ctx.set("tenant", "app")
143
- return next({
144
- request: { "x-tenant-id": "customer-app" },
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 securityHeadersProxy = defineProxy("securityHeaders", () => {
152
- return next({
153
- headers: {
154
- "x-content-type-options": "nosniff",
155
- "x-frame-options": "DENY",
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
- export default proxyChain({ debug: process.env.NODE_ENV !== "production" }, [
161
- multiTenantProxy,
162
- securityHeadersProxy,
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 | 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. |
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: 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`). |
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` rule function must return one of the following:
182
+ A rule (`ProxyFn`) can return:
207
183
 
208
- | Return Value | Pipeline Behavior |
184
+ | Return Value | Behavior |
209
185
  | :--- | :--- |
210
- | `next({ request?, headers? })` | Continues execution, staging request header mutations and response headers. |
211
- | `undefined` or `void` | Continues execution without mutating headers or state. |
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)` | 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. |
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.2",
4
- "description": "Composable request-chaining and proxy engine for Next.js 15 (middleware.ts) and Next.js 16 (proxy.ts) with deterministic header mutation, isolated per-request context, and pure Web API core.",
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
  ],