@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.
Files changed (2) hide show
  1. package/README.md +177 -53
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,52 +1,91 @@
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
+ > 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
+ [![npm version](https://img.shields.io/npm/v/@klnap/next-proxy-chain.svg)](https://www.npmjs.com/package/@klnap/next-proxy-chain)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6.svg)](https://www.typescriptlang.org/)
8
+ [![Runtime: Edge / Node / Bun / Deno](https://img.shields.io/badge/Runtime-Edge%20%7C%20Node%20%7C%20Bun%20%7C%20Deno-black.svg)](https://bun.sh)
4
9
 
5
10
  ---
6
11
 
7
- ## Key Features
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
- - **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`).
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 Reusable Proxies
46
+ ### 1. Define Modular Proxy Rules
47
+
48
+ Create self-contained proxy rules in your application:
22
49
 
23
- ```ts
24
- import { defineProxy, next } from "@repo/next-proxy-chain"
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 id = crypto.randomUUID()
55
+ const requestId = req.headers.get("x-request-id") ?? crypto.randomUUID()
56
+
29
57
  return next({
30
- request: { "x-request-id": id },
31
- headers: { "x-request-id": 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("auth-token")?.value
70
+ const token = req.cookies.get("session_token")?.value
71
+
38
72
  if (!token) {
39
- return NextResponse.redirect(new URL("/login", req.url))
73
+ const loginUrl = new URL("/login", req.url)
74
+ loginUrl.searchParams.set("from", req.nextUrl.pathname)
75
+ return NextResponse.redirect(loginUrl)
40
76
  }
41
- ctx.set("userId", "user_123")
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 the Pipeline in `proxy.ts`
84
+ ### 2. Assemble Pipeline in `src/proxy.ts` (Next.js 16)
47
85
 
48
- ```ts
49
- import { proxyChain, path } from "@repo/next-proxy-chain"
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
- ## Scope Matching Rules
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
- Scoping supports globs, RegExps, host matching, and combinations:
155
+ When multiple rules in the chain mutate headers or cookies, `@klnap/next-proxy-chain` merges them deterministically:
75
156
 
76
- ```ts
77
- // Path on any host
78
- include: path("/admin/*")
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
- // Any path on a specific host
81
- include: { host: "shop.localhost" }
161
+ ```typescript
162
+ import { defineProxy, next } from "@klnap/next-proxy-chain"
82
163
 
83
- // Specific path on a specific host
84
- include: { host: "shop.localhost", path: "/checkout/*" }
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
- // OR combinations
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
- // Exclude always wins
93
- exclude: { host: "museum.localhost" }
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
- By default, any unhandled error inside a rule logs safely and returns an HTTP 500 (`safeInternalError`). You can supply a custom `onError` handler:
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
- onError: (err, ruleIndex, req, ctx) => {
106
- return new NextResponse("Service Unavailable", { status: 503 })
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
- ### Core Functions
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
- - `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.
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.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": ">=15.0.0 || >=16.0.0"
49
+ "next": ">=16.0.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^20",