@klnap/next-proxy-chain 1.0.3 → 1.0.4
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/CHANGELOG.md +55 -0
- package/README.md +67 -32
- package/dist/index.cjs +335 -320
- package/dist/index.d.cts +65 -169
- package/dist/index.d.ts +65 -169
- package/dist/index.js +334 -289
- package/package.json +14 -11
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@klnap/next-proxy-chain` will be documented in this file.
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [1.0.4] - 2026-08-27
|
|
10
|
+
|
|
11
|
+
### Breaking
|
|
12
|
+
|
|
13
|
+
- `onError` returning `undefined` / `void` / `null` now **fails closed** with a 500. Continue only by returning `next()`.
|
|
14
|
+
- `x-internal-host-folder` is no longer a magic routing header. Enable it with `hostFolderHeader: "x-internal-host-folder"`.
|
|
15
|
+
- Public exports are limited to the documented API. Internals (`mergeWrites`, `compileScopes`, `withRuleTimeout`, `./core`, …) are no longer exported.
|
|
16
|
+
- Custom `TContext` requires `createContext`. `proxyChain<{ user: string }>(rules)` without a factory is a type error.
|
|
17
|
+
- Removed `PathOptions` and `pathOptions`; `path()` no longer accepts a `locales` option. Match locale-prefixed routes in the pattern string or RegExp.
|
|
18
|
+
- `ruleTimeoutMs`: passing `NaN`, `Infinity`, or `-Infinity` now throws a `RangeError` at proxy-chain configuration time instead of silently disabling the timeout. If you were relying on an unvalidated computed value (e.g. from `parseInt` without a fallback) resulting in a disabled timeout, this is a breaking change — validate your config value before passing it.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- `path()` no longer drops `^` / `$` from user RegExp.
|
|
23
|
+
- Trailing `*` matches exactly one segment; `**` matches nested paths.
|
|
24
|
+
- `compileScopes()` runs RegExp matchers through `path()`.
|
|
25
|
+
- `next({ headers: { "set-cookie": … } })` reaches the final response.
|
|
26
|
+
- Cookies with the same name and different `Path` / `Domain` are not collapsed.
|
|
27
|
+
- Custom header merge returning `null` deletes the header from the terminal response.
|
|
28
|
+
- Rewrite continuation no longer mutates the incoming `NextRequest`.
|
|
29
|
+
- Timed-out rules no longer leave an unhandled rejection.
|
|
30
|
+
- `host("*.example.com")` matches one DNS label.
|
|
31
|
+
- `strictOverrideCheck` cache no longer pins the first call’s strictness for later calls.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- `headerMergeStrategy` applies only to **response** headers. Request overrides stay last-write-wins.
|
|
36
|
+
- Package metadata: `repository`, `bugs`, `homepage`.
|
|
37
|
+
- Tests run on `bun:test`; removed Typedoc site and monorepo tooling from the package repo.
|
|
38
|
+
|
|
39
|
+
## [1.0.3] - 2026-08-26
|
|
40
|
+
|
|
41
|
+
npm patch. No detailed notes were kept for 1.0.1–1.0.3 at publish time.
|
|
42
|
+
|
|
43
|
+
## [1.0.0] - 2026-08-23
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- Composable Edge-safe proxy pipeline via `proxyChain()`.
|
|
48
|
+
- Transparent Next.js pass-through detection on `x-middleware-next: 1` with request override extraction.
|
|
49
|
+
- Automatic filtering of internal `x-middleware-*` transport headers from client responses.
|
|
50
|
+
- Request header blocklist preventing rule injection of sensitive headers (`host`, `authorization`, `x-middleware-*`).
|
|
51
|
+
- Context threading via isolated per-request `Map` instances (`TContext`).
|
|
52
|
+
- Header and cookie accumulation with `last-write-wins`, `first-write-wins`, and custom merge strategies.
|
|
53
|
+
- Path and host scoping (`include`, `exclude`) with construction-time overlap validation.
|
|
54
|
+
- Cached startup self-test for `NextResponse.next()` request overrides.
|
|
55
|
+
- Error isolation with safe 500 fallback.
|
package/README.md
CHANGED
|
@@ -16,9 +16,9 @@ Composable request pipeline for Next.js middleware and proxy.
|
|
|
16
16
|
- **Deterministic Header & Cookie Merging**: Propagate request header overrides downstream, deduplicate `Set-Cookie` headers, and merge response headers with configurable conflict strategies.
|
|
17
17
|
- **Route & Host Scoping**: Declarative path matching (globs, parameters, regex) and host filtering with `include` and `exclude` rules.
|
|
18
18
|
- **Safe Pattern Matching**: Normalizes stateful regular expressions to avoid `lastIndex`-related matching issues.
|
|
19
|
-
- **Explicit Flow Control**: Return `next({ request, headers })` to
|
|
20
|
-
- **Timeouts & Error Recovery**: Optional per-rule timeouts (`ruleTimeoutMs`)
|
|
21
|
-
- **Zero Runtime Dependencies**: Fully typed in TypeScript with no runtime dependencies.
|
|
19
|
+
- **Explicit Flow Control**: Return `next({ request, headers })` to continue, or a terminal `Response` / `NextResponse.redirect` / `NextResponse.json` to stop. `NextResponse.rewrite` **continues** the pipeline (rewrite is applied at the end unless a later rule stops).
|
|
20
|
+
- **Timeouts & Error Recovery**: Optional per-rule timeouts (`ruleTimeoutMs`). `onError` is fail-closed: returning nothing yields 500; return `next()` to continue; return a `Response` to stop.
|
|
21
|
+
- **Zero Runtime Dependencies**: Fully typed in TypeScript with no runtime dependencies (`next` is a peer).
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
@@ -103,10 +103,16 @@ const adminRule = apiAuth({
|
|
|
103
103
|
|
|
104
104
|
### Path Patterns
|
|
105
105
|
|
|
106
|
-
-
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
-
|
|
106
|
+
- `path("/api/*")` — exactly one segment (`/api/users` yes, `/api/users/123` no)
|
|
107
|
+
- `path("/docs/**")` — the prefix plus zero or more extra segments (`/docs`, `/docs/a/b`)
|
|
108
|
+
- `path("/users/:id")` — one segment (no capture groups; matching only)
|
|
109
|
+
- `path(/^\/foo$/)` — user anchors are kept (`/foo` yes, `/x/foo` no)
|
|
110
|
+
|
|
111
|
+
### Host Patterns
|
|
112
|
+
|
|
113
|
+
- `host("admin.example.com")` — exact hostname (case-insensitive, port stripped at match time)
|
|
114
|
+
- `host("*.example.com")` — one DNS label (`foo.example.com` yes, `example.com` no, `a.b.example.com` no)
|
|
115
|
+
- `host(/shop\.localhost$/)` — raw RegExp
|
|
110
116
|
|
|
111
117
|
---
|
|
112
118
|
|
|
@@ -114,40 +120,51 @@ const adminRule = apiAuth({
|
|
|
114
120
|
|
|
115
121
|
### Request Context
|
|
116
122
|
|
|
117
|
-
|
|
123
|
+
Default context is an isolated `Map<string, unknown>` per request. For a typed object, pass `createContext` — TypeScript then follows the factory, not a fake generic:
|
|
118
124
|
|
|
119
125
|
```typescript
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
type Ctx = { user?: { id: string; role: string } }
|
|
127
|
+
|
|
128
|
+
const enrichUser = defineProxy<Ctx>("enrichUser", async (req, ctx) => {
|
|
129
|
+
ctx.user = { id: "usr_1", role: "admin" }
|
|
122
130
|
return next()
|
|
123
131
|
})
|
|
124
132
|
|
|
125
|
-
const checkPermission = defineProxy("checkPermission", (req, ctx) => {
|
|
126
|
-
|
|
127
|
-
if (user?.role !== "admin") {
|
|
133
|
+
const checkPermission = defineProxy<Ctx>("checkPermission", (req, ctx) => {
|
|
134
|
+
if (ctx.user?.role !== "admin") {
|
|
128
135
|
return new Response("Forbidden", { status: 403 })
|
|
129
136
|
}
|
|
130
137
|
return next()
|
|
131
138
|
})
|
|
139
|
+
|
|
140
|
+
export default proxyChain({ createContext: (): Ctx => ({}) }, [enrichUser, checkPermission])
|
|
132
141
|
```
|
|
133
142
|
|
|
143
|
+
Without `createContext`, `ctx` is `ProxyContext` (`Map`). Do not write `proxyChain<{ user: string }>(rules)` — that is a type error.
|
|
144
|
+
|
|
134
145
|
### Request and Response Headers
|
|
135
146
|
|
|
136
|
-
Use `next()` to inject request headers (visible to downstream rules and route handlers) and response headers:
|
|
147
|
+
Use `next()` to inject **request** headers (visible to downstream rules and route handlers) and **response** headers (including `Set-Cookie`):
|
|
137
148
|
|
|
138
149
|
```typescript
|
|
139
150
|
return next({
|
|
140
|
-
// Downstream request headers (NextRequest & server components)
|
|
151
|
+
// Downstream request headers (NextRequest & server components). Always last-write-wins.
|
|
141
152
|
request: {
|
|
142
153
|
"x-user-id": "usr_123",
|
|
143
154
|
},
|
|
144
|
-
// Final outgoing response headers
|
|
155
|
+
// Final outgoing response headers / Set-Cookie
|
|
145
156
|
headers: {
|
|
146
157
|
"cache-control": "no-store",
|
|
158
|
+
"set-cookie": "session=abc; Path=/",
|
|
147
159
|
},
|
|
148
160
|
})
|
|
149
161
|
```
|
|
150
162
|
|
|
163
|
+
- `headerMergeStrategy` / `cookieMergeStrategy` apply to **response** headers and cookies only.
|
|
164
|
+
- Request overrides ignore those strategies (last write wins).
|
|
165
|
+
- Cookies are keyed by **name + domain + path**, so `session; Path=/` and `session; Path=/admin` both survive.
|
|
166
|
+
- Custom merge `return null` removes that response header from the final `Response`.
|
|
167
|
+
|
|
151
168
|
---
|
|
152
169
|
|
|
153
170
|
## API Reference
|
|
@@ -160,22 +177,40 @@ return next({
|
|
|
160
177
|
| `proxyChain(options, rules)` | `(options: ProxyChainOptions, rules: Entry[]) => Handler` | Creates a handler with custom pipeline options. |
|
|
161
178
|
| `defineProxy(name?, fn)` | `(name?: string, fn: ProxyFn) => DefinedProxy` | Defines a reusable proxy rule with scoping capabilities. |
|
|
162
179
|
| `next(options?)` | `(options?: NextOptions) => NextResult` | Returns a continuation token with optional header mutations. |
|
|
163
|
-
| `path(pattern
|
|
164
|
-
| `host(matcher)` | `(matcher: HostMatcher) => RegExp` | Compiles hostname(s) into an anchored `RegExp
|
|
180
|
+
| `path(pattern)` | `(pattern: string \| RegExp) => RegExp` | Compiles a path pattern into a stateless `RegExp`. |
|
|
181
|
+
| `host(matcher)` | `(matcher: HostMatcher) => RegExp` | Compiles hostname(s) into an anchored `RegExp` (`*` = one DNS label). |
|
|
165
182
|
| `createProxyContext()` | `() => ProxyContext` | Creates a new `Map<string, unknown>` context instance. |
|
|
183
|
+
| `isNextResult(value)` | `(value: unknown) => boolean` | Type guard for `next()` continuation tokens. |
|
|
184
|
+
| `RuleTimeoutError` | `Error` | Thrown internally when `ruleTimeoutMs` is exceeded (catchable in `onError`). |
|
|
166
185
|
|
|
167
186
|
### Configuration Options (`ProxyChainOptions`)
|
|
168
187
|
|
|
169
188
|
| Option | Type | Default | Description |
|
|
170
189
|
| :--- | :--- | :--- | :--- |
|
|
171
|
-
| `createContext` | `(req, event) => TContext` | `() => new Map()` | Factory for custom per-request context
|
|
172
|
-
| `headerMergeStrategy` | `"last-write-wins" \| "first-write-wins" \| Function` | `"last-write-wins"` |
|
|
173
|
-
| `cookieMergeStrategy` | `"last-write-wins" \| "first-write-wins" \| Function` | `"last-write-wins"` |
|
|
174
|
-
| `ruleTimeoutMs` | `number` | `0` | Wall-clock
|
|
175
|
-
| `onError` | `(err, index, req, ctx) => Response \| void` | `undefined` |
|
|
176
|
-
| `requestHeaderPolicy` | `{ blocked?: string[], allowed?: string[] }` | `{}` |
|
|
177
|
-
| `debug` | `boolean` | `false` |
|
|
178
|
-
| `logger` | `ChainLogger` | `console` | Custom logger
|
|
190
|
+
| `createContext` | `(req, event) => TContext` | `() => new Map()` | Factory for custom per-request context. Required for a non-`Map` `TContext`. |
|
|
191
|
+
| `headerMergeStrategy` | `"last-write-wins" \| "first-write-wins" \| Function` | `"last-write-wins"` | Conflict strategy for **response** headers only. |
|
|
192
|
+
| `cookieMergeStrategy` | `"last-write-wins" \| "first-write-wins" \| Function` | `"last-write-wins"` | Conflict strategy for `Set-Cookie` (same name+domain+path). |
|
|
193
|
+
| `ruleTimeoutMs` | `number` | `0` | Wall-clock timeout per rule in ms (`0` disables). Does not abort the rule function; leftover rejections are swallowed. |
|
|
194
|
+
| `onError` | `(err, index, req, ctx) => Response \| NextResult \| void` | `undefined` | Recovery hook. See **Errors** below. |
|
|
195
|
+
| `requestHeaderPolicy` | `{ blocked?: string[], allowed?: string[] }` | `{}` | Blocklist/allowlist for **request** header mutations. |
|
|
196
|
+
| `debug` | `boolean` | `false` | Structured one-line logs per rule. |
|
|
197
|
+
| `logger` | `ChainLogger` | `console` | Custom logger (`debug`, `info`, `warn`, `error`). |
|
|
198
|
+
| `strictOverrideCheck` | `boolean` | `false` | Throw at init if `NextResponse.next()` request overrides look unsupported. |
|
|
199
|
+
| `hostFolderHeader` | `string` | unset | Opt-in: request header name that prefixes rewrites with a physical folder (e.g. `"x-internal-host-folder"`). Off by default. |
|
|
200
|
+
|
|
201
|
+
### Errors (`onError`)
|
|
202
|
+
|
|
203
|
+
| `onError` returns | Behavior |
|
|
204
|
+
| :--- | :--- |
|
|
205
|
+
| `Response` | Stop and return that response (headers/cookies still merged). |
|
|
206
|
+
| `next()` | Continue to the next rule (rule-level errors only). |
|
|
207
|
+
| `undefined` / `void` / `null` | **Fail closed** — `500 Internal Server Error`. |
|
|
208
|
+
| throws | `500 Internal Server Error`. |
|
|
209
|
+
| omitted | Rule throw → `500`. |
|
|
210
|
+
|
|
211
|
+
`ruleIndex` is `0`-based. `-1` means an engine-level error after context creation; only a `Response` is honored (the pipeline cannot continue).
|
|
212
|
+
|
|
213
|
+
A logging-only `onError: (err) => { console.error(err) }` does **not** skip the failure.
|
|
179
214
|
|
|
180
215
|
### Return Value Protocol
|
|
181
216
|
|
|
@@ -183,12 +218,12 @@ A rule (`ProxyFn`) can return:
|
|
|
183
218
|
|
|
184
219
|
| Return Value | Behavior |
|
|
185
220
|
| :--- | :--- |
|
|
186
|
-
| `next({ request?, headers? })` | Continues
|
|
187
|
-
| `undefined` / `void` | Continues
|
|
188
|
-
| `NextResponse.next()` | Continues
|
|
189
|
-
| `NextResponse.rewrite(url)` | Stages a
|
|
190
|
-
| `NextResponse.redirect(url)` | Stops
|
|
191
|
-
| `Response` / `NextResponse.json()` | Stops
|
|
221
|
+
| `next({ request?, headers? })` | Continues. Queues request overrides, response headers, and `Set-Cookie`. |
|
|
222
|
+
| `undefined` / `void` | Continues without mutations. |
|
|
223
|
+
| `NextResponse.next()` | Continues; extracts Next.js request override headers and cookies. |
|
|
224
|
+
| `NextResponse.rewrite(url)` | **Continues**. Stages a rewrite for later rules; the incoming `NextRequest` is not mutated. The rewrite is applied when the chain finishes unless a later rule returns a terminal response. |
|
|
225
|
+
| `NextResponse.redirect(url)` | Stops immediately with the redirect. |
|
|
226
|
+
| `Response` / `NextResponse.json()` | Stops immediately with that response. |
|
|
192
227
|
|
|
193
228
|
---
|
|
194
229
|
|