@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/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ // src/core/engine.ts
2
+ import { NextRequest, NextResponse as NextResponse2 } from "next/server";
3
+
1
4
  // src/core/constants.ts
2
5
  var NEXT = /* @__PURE__ */ Symbol.for("proxy-chain.next");
3
6
  var DEFAULT_BLOCKED_REQUEST_HEADERS = Object.freeze([
@@ -21,178 +24,19 @@ var DEFAULT_BLOCKED_REQUEST_HEADERS = Object.freeze([
21
24
  "x-middleware-override-headers"
22
25
  ]);
23
26
 
27
+ // src/core/actions.ts
28
+ function next(options = {}) {
29
+ return { [NEXT]: true, ...options };
30
+ }
31
+ function isNextResult(value) {
32
+ return typeof value === "object" && value !== null && NEXT in value;
33
+ }
34
+
24
35
  // src/core/context.ts
25
36
  function createProxyContext() {
26
37
  return /* @__PURE__ */ new Map();
27
38
  }
28
39
 
29
- // src/core/matching.ts
30
- var RESERVED_FN_NAMES = /* @__PURE__ */ new Set(["", "proxy", "anonymous", "run", "next"]);
31
- function extractMeaningfulName(value) {
32
- if (!value || RESERVED_FN_NAMES.has(value)) return void 0;
33
- return value;
34
- }
35
- function toStatelessRegExp(re) {
36
- const flags = re.flags.replace(/[gy]/g, "");
37
- return flags === re.flags ? re : new RegExp(re.source, flags);
38
- }
39
- function escapeRegex(value) {
40
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
41
- }
42
- function host(matcher) {
43
- const list = Array.isArray(matcher) ? matcher : [matcher];
44
- const parts = list.map((m) => {
45
- if (m instanceof RegExp) return `(?:${toStatelessRegExp(m).source})`;
46
- return `(?:${escapeRegex(m.toLowerCase())})`;
47
- });
48
- return toStatelessRegExp(new RegExp(`^(?:${parts.join("|")})$`, "i"));
49
- }
50
- function isScopeObject(value) {
51
- return "host" in value || "path" in value;
52
- }
53
- function segmentToRegex(segment) {
54
- if (segment === "**") return "(?:.*)";
55
- if (segment === "*") return "[^/]+";
56
- if (segment.startsWith(":")) return "[^/]+";
57
- return escapeRegex(segment);
58
- }
59
- function path(pattern, options = {}) {
60
- const { locales = [] } = options;
61
- const localePrefix = locales.length > 0 ? `(?:(?:${locales.map(escapeRegex).join("|")})\\/)?` : "";
62
- if (pattern instanceof RegExp) {
63
- const body2 = pattern.source.replace(/^\^/, "").replace(/\$$/, "");
64
- const needsStart = !pattern.source.startsWith("^");
65
- const needsEnd = !pattern.source.endsWith("$");
66
- return toStatelessRegExp(
67
- new RegExp(
68
- `${needsStart ? "^" : ""}${localePrefix ? `/${localePrefix}` : ""}${body2}${needsEnd ? "$" : ""}`,
69
- pattern.flags
70
- )
71
- );
72
- }
73
- const normalized = (pattern.startsWith("/") ? pattern : `/${pattern}`).replace(/\/$/, "") || "/";
74
- const segments = normalized.slice(1).split("/");
75
- const trailingStar = segments.length > 0 && segments[segments.length - 1] === "*";
76
- const suffix = trailingStar ? "(?:\\/.*)?$" : "$";
77
- const body = trailingStar ? segments.slice(0, -1).map(segmentToRegex).concat("[^/]+").join("\\/") : segments.map(segmentToRegex).join("\\/");
78
- return new RegExp(`^/${localePrefix}${body}${suffix}`);
79
- }
80
- function toPathRegex(matcher, options) {
81
- const list = Array.isArray(matcher) ? matcher : [matcher];
82
- const regexes = list.map((m) => path(m, options));
83
- if (regexes.length === 1) return toStatelessRegExp(regexes[0]);
84
- return toStatelessRegExp(new RegExp(`(?:${regexes.map((r) => `(?:${r.source})`).join("|")})`));
85
- }
86
- function compileScopes(input, pathOptions) {
87
- if (input == null) return void 0;
88
- const list = Array.isArray(input) ? input : [input];
89
- const compilePath = (matcher) => {
90
- if (Array.isArray(matcher)) {
91
- const parts = matcher.map(
92
- (m) => typeof m === "string" ? path(m, pathOptions) : toStatelessRegExp(m)
93
- );
94
- if (parts.length === 1) return parts[0];
95
- return toStatelessRegExp(new RegExp(`(?:${parts.map((r) => `(?:${r.source})`).join("|")})`));
96
- }
97
- return typeof matcher === "string" ? path(matcher, pathOptions) : toStatelessRegExp(matcher);
98
- };
99
- const compiled = [];
100
- for (const item of list) {
101
- if (typeof item === "string" || item instanceof RegExp) {
102
- compiled.push({ path: compilePath(item) });
103
- continue;
104
- }
105
- if (item && typeof item === "object" && isScopeObject(item)) {
106
- if (item.host == null && item.path == null) {
107
- throw new Error("[proxy-chain] scope needs at least `host` or `path`");
108
- }
109
- compiled.push({
110
- host: item.host != null ? host(item.host) : void 0,
111
- path: item.path != null ? compilePath(item.path) : void 0
112
- });
113
- continue;
114
- }
115
- throw new Error("[proxy-chain] invalid include/exclude scope");
116
- }
117
- return compiled;
118
- }
119
- function scopeMatches(pathname, hostname, scope) {
120
- if (scope.host) {
121
- if (!hostname || !scope.host.test(hostname)) return false;
122
- }
123
- if (scope.path) {
124
- if (!scope.path.test(pathname)) return false;
125
- }
126
- return true;
127
- }
128
- function toRule(entry) {
129
- if (typeof entry === "function") {
130
- const name2 = extractMeaningfulName(entry.name);
131
- return name2 ? { run: entry, name: name2 } : { run: entry };
132
- }
133
- const rule = entry;
134
- const runName = typeof rule.run === "function" ? extractMeaningfulName(rule.run.name) : void 0;
135
- const name = extractMeaningfulName(rule.name) ?? runName;
136
- const harden = (scopes) => scopes?.map((s) => ({
137
- host: s.host ? toStatelessRegExp(s.host) : void 0,
138
- path: s.path ? toStatelessRegExp(s.path) : void 0
139
- }));
140
- return {
141
- ...rule,
142
- name: name ?? void 0,
143
- include: harden(rule.include),
144
- exclude: harden(rule.exclude)
145
- };
146
- }
147
- function ruleLabel(rule, index) {
148
- const runName = typeof rule.run === "function" ? extractMeaningfulName(rule.run.name) : void 0;
149
- return extractMeaningfulName(rule.name) ?? runName ?? `rule#${index}`;
150
- }
151
- function withPaths(entry, filter) {
152
- const base = toRule(entry);
153
- const opts = filter.pathOptions;
154
- const include = compileScopes(filter.include, opts);
155
- const exclude = compileScopes(filter.exclude, opts);
156
- const name = filter.name ?? base.name;
157
- assertIncludeExcludeNoOverlap(include, exclude, name ?? "proxy");
158
- return { run: base.run, include, exclude, name };
159
- }
160
- function ruleMatches(pathname, hostname, rule) {
161
- if (rule.exclude?.some((scope) => scopeMatches(pathname, hostname, scope))) return false;
162
- if (rule.include && !rule.include.some((scope) => scopeMatches(pathname, hostname, scope))) {
163
- return false;
164
- }
165
- return true;
166
- }
167
- function pathMatches(pathname, rule) {
168
- return ruleMatches(pathname, null, rule);
169
- }
170
- function normalizeHost(raw) {
171
- if (!raw) return null;
172
- const first = raw.split(",")[0]?.trim().toLowerCase();
173
- if (!first) return null;
174
- if (first.startsWith("[")) {
175
- const end = first.indexOf("]");
176
- return end === -1 ? first : first.slice(1, end);
177
- }
178
- const withoutPort = first.replace(/:\d+$/, "");
179
- return withoutPort || null;
180
- }
181
- function scopeKey(scope) {
182
- return `${scope.host?.source ?? "*"}::${scope.host?.flags ?? ""}|${scope.path?.source ?? "*"}::${scope.path?.flags ?? ""}`;
183
- }
184
- function assertIncludeExcludeNoOverlap(include, exclude, label) {
185
- if (!include?.length || !exclude?.length) return;
186
- const excluded = new Set(exclude.map(scopeKey));
187
- for (const scope of include) {
188
- if (excluded.has(scopeKey(scope))) {
189
- throw new Error(
190
- `[proxy-chain] ${label}: include and exclude share an identical scope. Remove one.`
191
- );
192
- }
193
- }
194
- }
195
-
196
40
  // src/core/headers.ts
197
41
  function normalizeHeaderName(name) {
198
42
  return name.toLowerCase();
@@ -235,15 +79,32 @@ function parseCookieName(setCookie) {
235
79
  return (eq === -1 ? setCookie : setCookie.slice(0, eq)).trim();
236
80
  }
237
81
  function listSetCookie(headers) {
238
- const extended = headers;
239
- if (typeof extended.getSetCookie === "function") return extended.getSetCookie();
82
+ if (typeof headers.getSetCookie === "function") return headers.getSetCookie();
240
83
  const single = headers.get("set-cookie");
241
84
  return single ? [single] : [];
242
85
  }
86
+ function cookieAttribute(setCookie, name) {
87
+ const parts = setCookie.split(";");
88
+ for (let i = 1; i < parts.length; i++) {
89
+ const piece = parts[i];
90
+ if (!piece) continue;
91
+ const eq = piece.indexOf("=");
92
+ const key = (eq === -1 ? piece : piece.slice(0, eq)).trim().toLowerCase();
93
+ if (key !== name) continue;
94
+ return (eq === -1 ? "" : piece.slice(eq + 1)).trim();
95
+ }
96
+ return "";
97
+ }
98
+ function cookieIdentityKey(setCookie) {
99
+ const name = parseCookieName(setCookie);
100
+ const path2 = cookieAttribute(setCookie, "path");
101
+ const domain = cookieAttribute(setCookie, "domain").toLowerCase();
102
+ return `${name};domain=${domain};path=${path2}`;
103
+ }
243
104
  function cookieItemsFromHeaders(headers) {
244
105
  const map = /* @__PURE__ */ new Map();
245
106
  for (const value of listSetCookie(headers)) {
246
- map.set(parseCookieName(value), value);
107
+ map.set(cookieIdentityKey(value), value);
247
108
  }
248
109
  return map;
249
110
  }
@@ -303,7 +164,12 @@ function mergeWrites(writes, strategy, onConflict) {
303
164
  }
304
165
  return resolved;
305
166
  }
306
- function applyMergedToResponse(res, headers, cookies) {
167
+ function applyMergedToResponse(res, headers, cookies, dropHeaders) {
168
+ if (dropHeaders) {
169
+ for (const key of dropHeaders) {
170
+ if (!headers.has(key)) res.headers.delete(key);
171
+ }
172
+ }
307
173
  for (const [key, value] of headers) {
308
174
  res.headers.set(key, value);
309
175
  }
@@ -313,14 +179,6 @@ function applyMergedToResponse(res, headers, cookies) {
313
179
  }
314
180
  }
315
181
 
316
- // src/core/actions.ts
317
- function next(options = {}) {
318
- return { [NEXT]: true, ...options };
319
- }
320
- function isNextResult(value) {
321
- return typeof value === "object" && value !== null && NEXT in value;
322
- }
323
-
324
182
  // src/core/logger.ts
325
183
  var LEVEL_RANK = {
326
184
  debug: 10,
@@ -364,27 +222,180 @@ function formatPipelineLine(input) {
364
222
  }
365
223
  }
366
224
 
367
- // src/core/timeout.ts
368
- var RuleTimeoutError = class extends Error {
369
- ruleName;
370
- timeoutMs;
371
- constructor(ruleName, timeoutMs) {
372
- super(`[proxy-chain] ${ruleName} timed out after ${timeoutMs}ms`);
373
- this.name = "RuleTimeoutError";
374
- this.ruleName = ruleName;
375
- this.timeoutMs = timeoutMs;
376
- }
377
- };
378
- async function withRuleTimeout(promise, timeoutMs, ruleName) {
379
- if (!timeoutMs || timeoutMs <= 0) return promise;
380
- let timer;
381
- const timeout = new Promise((_, reject) => {
382
- timer = setTimeout(() => reject(new RuleTimeoutError(ruleName, timeoutMs)), timeoutMs);
225
+ // src/core/matching.ts
226
+ var RESERVED_FN_NAMES = /* @__PURE__ */ new Set(["", "proxy", "anonymous", "run", "next"]);
227
+ function extractMeaningfulName(value) {
228
+ if (!value || RESERVED_FN_NAMES.has(value)) return void 0;
229
+ return value;
230
+ }
231
+ function toStatelessRegExp(re) {
232
+ const flags = re.flags.replace(/[gy]/g, "");
233
+ return flags === re.flags ? re : new RegExp(re.source, flags);
234
+ }
235
+ function escapeRegex(value) {
236
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
237
+ }
238
+ function hostGlobToRegex(value) {
239
+ return escapeRegex(value.toLowerCase()).replace(/\\\*/g, "[^.]+");
240
+ }
241
+ function host(matcher) {
242
+ const list = Array.isArray(matcher) ? matcher : [matcher];
243
+ const parts = list.map((m) => {
244
+ if (m instanceof RegExp) return `(?:${toStatelessRegExp(m).source})`;
245
+ return `(?:${hostGlobToRegex(m)})`;
383
246
  });
384
- try {
385
- return await Promise.race([promise, timeout]);
386
- } finally {
387
- if (timer !== void 0) clearTimeout(timer);
247
+ return toStatelessRegExp(new RegExp(`^(?:${parts.join("|")})$`, "i"));
248
+ }
249
+ function isScopeObject(value) {
250
+ return "host" in value || "path" in value;
251
+ }
252
+ function segmentToRegex(segment) {
253
+ if (segment === "**") return "(?:.*)";
254
+ if (segment === "*") return "[^/]+";
255
+ if (segment.startsWith(":")) return "[^/]+";
256
+ return escapeRegex(segment);
257
+ }
258
+ function hasEndAnchor(source) {
259
+ if (!source.endsWith("$")) return false;
260
+ let slashes = 0;
261
+ for (let i = source.length - 2; i >= 0 && source[i] === "\\"; i--) slashes++;
262
+ return slashes % 2 === 0;
263
+ }
264
+ function compileRegexPath(pattern) {
265
+ let source = pattern.source;
266
+ const hasStart = source.startsWith("^");
267
+ const hasEnd = hasEndAnchor(source);
268
+ if (hasStart) source = source.slice(1);
269
+ if (hasEnd) source = source.slice(0, -1);
270
+ const wrapFull = !hasStart && !hasEnd;
271
+ const start = hasStart || wrapFull ? "^" : "";
272
+ const end = hasEnd || wrapFull ? "$" : "";
273
+ return toStatelessRegExp(new RegExp(`${start}${source}${end}`, pattern.flags));
274
+ }
275
+ function compileStringPath(pattern) {
276
+ const normalized = (pattern.startsWith("/") ? pattern : `/${pattern}`).replace(/\/$/, "") || "/";
277
+ const segments = normalized === "/" ? [] : normalized.slice(1).split("/");
278
+ const last = segments[segments.length - 1];
279
+ const trailingStar = last === "*";
280
+ const trailingGlobstar = last === "**";
281
+ let pathPattern;
282
+ if (normalized === "/") {
283
+ pathPattern = "/";
284
+ } else if (trailingStar) {
285
+ const prefix = segments.slice(0, -1).map(segmentToRegex).join("/");
286
+ pathPattern = prefix ? `/${prefix}/[^/]+` : `/[^/]+`;
287
+ } else if (trailingGlobstar) {
288
+ const prefix = segments.slice(0, -1).map(segmentToRegex).join("/");
289
+ pathPattern = prefix ? `/${prefix}(?:/.*)?` : `/(?:.*)?`;
290
+ } else {
291
+ pathPattern = `/${segments.map(segmentToRegex).join("/")}`;
292
+ }
293
+ return new RegExp(`^${pathPattern}$`);
294
+ }
295
+ function path(pattern) {
296
+ if (pattern instanceof RegExp) return compileRegexPath(pattern);
297
+ return compileStringPath(pattern);
298
+ }
299
+ function toPathRegex(matcher) {
300
+ const list = Array.isArray(matcher) ? matcher : [matcher];
301
+ const regexes = list.map((m) => path(m));
302
+ if (regexes.length === 1) return toStatelessRegExp(regexes[0]);
303
+ return toStatelessRegExp(new RegExp(`(?:${regexes.map((r) => `(?:${r.source})`).join("|")})`));
304
+ }
305
+ function compileScopes(input) {
306
+ if (input == null) return void 0;
307
+ const list = Array.isArray(input) ? input : [input];
308
+ const compilePath = (matcher) => toPathRegex(matcher);
309
+ const compiled = [];
310
+ for (const item of list) {
311
+ if (typeof item === "string" || item instanceof RegExp) {
312
+ compiled.push({ path: compilePath(item) });
313
+ continue;
314
+ }
315
+ if (item && typeof item === "object" && isScopeObject(item)) {
316
+ if (item.host == null && item.path == null) {
317
+ throw new Error("[proxy-chain] scope needs at least `host` or `path`");
318
+ }
319
+ compiled.push({
320
+ host: item.host != null ? host(item.host) : void 0,
321
+ path: item.path != null ? compilePath(item.path) : void 0
322
+ });
323
+ continue;
324
+ }
325
+ throw new Error("[proxy-chain] invalid include/exclude scope");
326
+ }
327
+ return compiled;
328
+ }
329
+ function scopeMatches(pathname, hostname, scope) {
330
+ if (scope.host) {
331
+ if (!hostname || !scope.host.test(hostname)) return false;
332
+ }
333
+ if (scope.path) {
334
+ if (!scope.path.test(pathname)) return false;
335
+ }
336
+ return true;
337
+ }
338
+ function toRule(entry) {
339
+ if (typeof entry === "function") {
340
+ const name2 = extractMeaningfulName(entry.name);
341
+ return name2 ? { run: entry, name: name2 } : { run: entry };
342
+ }
343
+ const rule = entry;
344
+ const runName = typeof rule.run === "function" ? extractMeaningfulName(rule.run.name) : void 0;
345
+ const name = extractMeaningfulName(rule.name) ?? runName;
346
+ const harden = (scopes) => scopes?.map((s) => ({
347
+ host: s.host ? toStatelessRegExp(s.host) : void 0,
348
+ path: s.path ? toStatelessRegExp(s.path) : void 0
349
+ }));
350
+ return {
351
+ ...rule,
352
+ name: name ?? void 0,
353
+ include: harden(rule.include),
354
+ exclude: harden(rule.exclude)
355
+ };
356
+ }
357
+ function ruleLabel(rule, index) {
358
+ const runName = typeof rule.run === "function" ? extractMeaningfulName(rule.run.name) : void 0;
359
+ return extractMeaningfulName(rule.name) ?? runName ?? `rule#${index}`;
360
+ }
361
+ function withPaths(entry, filter) {
362
+ const base = toRule(entry);
363
+ const include = compileScopes(filter.include);
364
+ const exclude = compileScopes(filter.exclude);
365
+ const name = filter.name ?? base.name;
366
+ assertIncludeExcludeNoOverlap(include, exclude, name ?? "proxy");
367
+ return { run: base.run, include, exclude, name };
368
+ }
369
+ function ruleMatches(pathname, hostname, rule) {
370
+ if (rule.exclude?.some((scope) => scopeMatches(pathname, hostname, scope))) return false;
371
+ if (rule.include && !rule.include.some((scope) => scopeMatches(pathname, hostname, scope))) {
372
+ return false;
373
+ }
374
+ return true;
375
+ }
376
+ function normalizeHost(raw) {
377
+ if (!raw) return null;
378
+ const first = raw.split(",")[0]?.trim().toLowerCase();
379
+ if (!first) return null;
380
+ if (first.startsWith("[")) {
381
+ const end = first.indexOf("]");
382
+ return end === -1 ? first : first.slice(1, end);
383
+ }
384
+ const withoutPort = first.replace(/:\d+$/, "");
385
+ return withoutPort || null;
386
+ }
387
+ function scopeKey(scope) {
388
+ return `${scope.host?.source ?? "*"}::${scope.host?.flags ?? ""}|${scope.path?.source ?? "*"}::${scope.path?.flags ?? ""}`;
389
+ }
390
+ function assertIncludeExcludeNoOverlap(include, exclude, label) {
391
+ if (!include?.length || !exclude?.length) return;
392
+ const excluded = new Set(exclude.map(scopeKey));
393
+ for (const scope of include) {
394
+ if (excluded.has(scopeKey(scope))) {
395
+ throw new Error(
396
+ `[proxy-chain] ${label}: include and exclude share an identical scope. Remove one.`
397
+ );
398
+ }
388
399
  }
389
400
  }
390
401
 
@@ -396,26 +407,42 @@ function safeInternalError() {
396
407
  headers: { "content-type": "text/plain; charset=utf-8" }
397
408
  });
398
409
  }
399
- var cachedOverrideSupported = null;
410
+ var UNSUPPORTED_OVERRIDE_MSG = "[proxy-chain] Warning: platform NextResponse.next() does not support x-middleware-next header overrides.";
411
+ var VERIFY_OVERRIDE_MSG = "[proxy-chain] Warning: unable to verify NextResponse request override mechanism.";
412
+ var cachedOverrideProbe = null;
413
+ var overrideProbeForTests = null;
414
+ function runOverrideProbe() {
415
+ if (overrideProbeForTests) return overrideProbeForTests();
416
+ const testRes = NextResponse.next({
417
+ request: { headers: new Headers({ "x-proxy-chain-probe": "1" }) }
418
+ });
419
+ return testRes.headers.get("x-middleware-next") === "1";
420
+ }
421
+ function throwForCachedProbe(probe) {
422
+ if (probe.kind === "unsupported") throw new Error(UNSUPPORTED_OVERRIDE_MSG);
423
+ throw probe.err instanceof Error ? probe.err : new Error(VERIFY_OVERRIDE_MSG);
424
+ }
425
+ function resolveCachedProbe(probe, strict) {
426
+ if (probe.kind === "supported") return true;
427
+ if (strict) throwForCachedProbe(probe);
428
+ return false;
429
+ }
400
430
  function assertOverrideMechanismSupported(strict = false) {
401
- if (cachedOverrideSupported !== null) return cachedOverrideSupported;
431
+ if (cachedOverrideProbe !== null) return resolveCachedProbe(cachedOverrideProbe, strict);
402
432
  try {
403
- const testRes = NextResponse.next({
404
- request: { headers: new Headers({ "x-proxy-chain-probe": "1" }) }
405
- });
406
- const isSupported = testRes.headers.get("x-middleware-next") === "1";
407
- cachedOverrideSupported = isSupported;
408
- if (!isSupported) {
409
- const msg = "[proxy-chain] Warning: platform NextResponse.next() does not support x-middleware-next header overrides.";
410
- if (strict) throw new Error(msg);
411
- console.warn(msg);
433
+ const isSupported = runOverrideProbe();
434
+ if (isSupported) {
435
+ cachedOverrideProbe = { kind: "supported" };
436
+ return true;
412
437
  }
413
- return isSupported;
438
+ cachedOverrideProbe = { kind: "unsupported" };
439
+ if (strict) throw new Error(UNSUPPORTED_OVERRIDE_MSG);
440
+ console.warn(UNSUPPORTED_OVERRIDE_MSG);
441
+ return false;
414
442
  } catch (err) {
415
- cachedOverrideSupported = false;
416
- const msg = "[proxy-chain] Warning: unable to verify NextResponse request override mechanism.";
417
- if (strict) throw err instanceof Error ? err : new Error(msg);
418
- console.warn(msg, err);
443
+ cachedOverrideProbe = { kind: "error", err };
444
+ if (strict) throw err instanceof Error ? err : new Error(VERIFY_OVERRIDE_MSG);
445
+ console.warn(VERIFY_OVERRIDE_MSG, err);
419
446
  return false;
420
447
  }
421
448
  }
@@ -437,11 +464,12 @@ function resolveRuleResult(res, requestHeaderPolicy, logger, debug) {
437
464
  }
438
465
  if ([...filtered.keys()].length) request = filtered;
439
466
  }
467
+ const headerInit = res.headers ? headersInitToHeaders(res.headers) : void 0;
440
468
  return {
441
469
  kind: "continue",
442
470
  request,
443
- responseHeaders: res.headers ? responseHeaderItems(headersInitToHeaders(res.headers)) : /* @__PURE__ */ new Map(),
444
- cookies: /* @__PURE__ */ new Map()
471
+ responseHeaders: headerInit ? responseHeaderItems(headerInit) : /* @__PURE__ */ new Map(),
472
+ cookies: headerInit ? cookieItemsFromHeaders(headerInit) : /* @__PURE__ */ new Map()
445
473
  };
446
474
  }
447
475
  if (res.headers.get("x-middleware-next") === "1") {
@@ -485,16 +513,57 @@ function finalizeResponse(res, headerWrites, cookieWrites, headerStrategy, cooki
485
513
  const cookies = mergeWrites(cookieWrites, cookieStrategy, (key, winner, loser) => {
486
514
  if (debug) logger.debug({ cookie: key, winner, loser }, "cookie conflict resolved");
487
515
  });
488
- applyMergedToResponse(res, headers, cookies);
516
+ const seenHeaders = /* @__PURE__ */ new Set();
517
+ for (const write of headerWrites) {
518
+ for (const key of write.items.keys()) seenHeaders.add(key);
519
+ }
520
+ const dropHeaders = [];
521
+ for (const key of seenHeaders) {
522
+ if (!headers.has(key)) dropHeaders.push(key);
523
+ }
524
+ applyMergedToResponse(res, headers, cookies, dropHeaders);
489
525
  return res;
490
526
  }
491
527
 
528
+ // src/core/timeout.ts
529
+ var RuleTimeoutError = class extends Error {
530
+ ruleName;
531
+ timeoutMs;
532
+ constructor(ruleName, timeoutMs) {
533
+ super(`[proxy-chain] ${ruleName} timed out after ${timeoutMs}ms`);
534
+ this.name = "RuleTimeoutError";
535
+ this.ruleName = ruleName;
536
+ this.timeoutMs = timeoutMs;
537
+ }
538
+ };
539
+ function assertFiniteRuleTimeoutMs(ruleTimeoutMs) {
540
+ if (ruleTimeoutMs != null && !Number.isFinite(ruleTimeoutMs)) {
541
+ throw new RangeError(
542
+ `[proxy-chain] ruleTimeoutMs must be a finite positive number, got ${ruleTimeoutMs}`
543
+ );
544
+ }
545
+ }
546
+ async function withRuleTimeout(promise, timeoutMs, ruleName) {
547
+ assertFiniteRuleTimeoutMs(timeoutMs);
548
+ if (timeoutMs == null || timeoutMs <= 0) return promise;
549
+ let timer;
550
+ const timeout = new Promise((_, reject) => {
551
+ timer = setTimeout(() => reject(new RuleTimeoutError(ruleName, timeoutMs)), timeoutMs);
552
+ });
553
+ try {
554
+ return await Promise.race([promise, timeout]);
555
+ } finally {
556
+ if (timer !== void 0) clearTimeout(timer);
557
+ void promise.catch(() => {
558
+ });
559
+ }
560
+ }
561
+
492
562
  // src/core/engine.ts
493
- import { NextRequest, NextResponse as NextResponse2 } from "next/server";
494
563
  function isPathFilter(value) {
495
564
  if (value === null || typeof value !== "object") return false;
496
565
  if ("nextUrl" in value) return false;
497
- return "include" in value || "exclude" in value || "name" in value || "pathOptions" in value;
566
+ return "include" in value || "exclude" in value || "name" in value;
498
567
  }
499
568
  function defineProxy(nameOrRun, maybeRun) {
500
569
  const defaultName = typeof nameOrRun === "string" ? nameOrRun : void 0;
@@ -514,25 +583,47 @@ function defineProxy(nameOrRun, maybeRun) {
514
583
  });
515
584
  return proxy;
516
585
  }
517
- function resolveConfig(overrides = {}) {
518
- const { logger, debug, ...rest } = overrides;
519
- return {
520
- createContext: () => createProxyContext(),
586
+ function resolveConfig(overrides, fallbackCreateContext) {
587
+ const { logger, debug, createContext, ...rest } = overrides;
588
+ const config = {
589
+ createContext: createContext ?? fallbackCreateContext,
521
590
  cookieMergeStrategy: "last-write-wins",
522
591
  headerMergeStrategy: "last-write-wins",
523
592
  debug: false,
524
593
  ruleTimeoutMs: 0,
525
594
  requestHeaderPolicy: {},
526
595
  strictOverrideCheck: false,
596
+ hostFolderHeader: void 0,
527
597
  ...rest,
528
598
  ...debug !== void 0 ? { debug } : {},
529
599
  logger: logger ?? createConsoleLogger(debug ? "debug" : "info")
530
600
  };
601
+ assertFiniteRuleTimeoutMs(config.ruleTimeoutMs);
602
+ return config;
603
+ }
604
+ function physicalFolderPrefix(raw) {
605
+ return "/" + raw.replace(/^\/+|\/+$/g, "").split("/").filter((seg) => !/^\(.*\)$/.test(seg)).join("/");
606
+ }
607
+ function applyFolderPrefix(rewriteUrl, folder, baseUrl) {
608
+ try {
609
+ const parsed = new URL(rewriteUrl, baseUrl);
610
+ if (!parsed.pathname.startsWith(folder)) {
611
+ parsed.pathname = folder + (parsed.pathname === "/" ? "" : parsed.pathname);
612
+ return parsed.toString();
613
+ }
614
+ return rewriteUrl;
615
+ } catch {
616
+ if (!rewriteUrl.startsWith(folder)) {
617
+ return folder + (rewriteUrl === "/" ? "" : rewriteUrl);
618
+ }
619
+ return rewriteUrl;
620
+ }
531
621
  }
532
622
  function proxyChain(optionsOrEntries, maybeEntries) {
533
623
  const options = Array.isArray(optionsOrEntries) ? {} : optionsOrEntries;
534
- const entries = Array.isArray(optionsOrEntries) ? optionsOrEntries : maybeEntries;
535
- const config = resolveConfig(options);
624
+ const entries = (Array.isArray(optionsOrEntries) ? optionsOrEntries : maybeEntries) ?? [];
625
+ const fallbackCreateContext = createProxyContext;
626
+ const config = resolveConfig(options, fallbackCreateContext);
536
627
  const rules = entries.map(toRule);
537
628
  const {
538
629
  logger,
@@ -541,7 +632,8 @@ function proxyChain(optionsOrEntries, maybeEntries) {
541
632
  headerMergeStrategy,
542
633
  ruleTimeoutMs,
543
634
  requestHeaderPolicy,
544
- strictOverrideCheck
635
+ strictOverrideCheck,
636
+ hostFolderHeader
545
637
  } = config;
546
638
  assertOverrideMechanismSupported(strictOverrideCheck);
547
639
  for (let i = 0; i < rules.length; i++) {
@@ -607,7 +699,7 @@ function proxyChain(optionsOrEntries, maybeEntries) {
607
699
  if (config.onError) {
608
700
  try {
609
701
  const recovered = await config.onError(err, i, activeReq, ctx);
610
- if (recovered == null) continue;
702
+ if (recovered == null) return safeInternalError();
611
703
  if (isNextResult(recovered)) {
612
704
  raw = recovered;
613
705
  } else if (recovered instanceof Response) {
@@ -646,7 +738,7 @@ function proxyChain(optionsOrEntries, maybeEntries) {
646
738
  debug
647
739
  );
648
740
  } else {
649
- continue;
741
+ return safeInternalError();
650
742
  }
651
743
  } catch (onErrorErr) {
652
744
  logger.error({ rule: label, err: onErrorErr }, "onError handler threw");
@@ -662,6 +754,9 @@ function proxyChain(optionsOrEntries, maybeEntries) {
662
754
  if (outcome.kind === "continue") {
663
755
  if (outcome.rewriteUrl) {
664
756
  pendingRewriteUrl = outcome.rewriteUrl;
757
+ if (currentReq === req) {
758
+ currentReq = new NextRequest(currentReq);
759
+ }
665
760
  try {
666
761
  const parsed = new URL(outcome.rewriteUrl);
667
762
  currentReq.nextUrl.pathname = parsed.pathname;
@@ -714,24 +809,14 @@ function proxyChain(optionsOrEntries, maybeEntries) {
714
809
  })
715
810
  );
716
811
  }
717
- const internalFolder = appliedRequestOverrides.get("x-internal-host-folder");
812
+ const internalFolder = hostFolderHeader ? appliedRequestOverrides.get(hostFolderHeader) : null;
718
813
  if (internalFolder && outcome.response.headers.has("x-middleware-rewrite")) {
719
814
  const rawRewrite = outcome.response.headers.get("x-middleware-rewrite");
720
- const cleanFolder = "/" + internalFolder.replace(/^\/+|\/+$/g, "").split("/").filter((seg) => !/^\(.*\)$/.test(seg)).join("/");
721
- try {
722
- const parsed = new URL(rawRewrite, currentReq.url);
723
- if (!parsed.pathname.startsWith(cleanFolder)) {
724
- parsed.pathname = cleanFolder + (parsed.pathname === "/" ? "" : parsed.pathname);
725
- outcome.response.headers.set("x-middleware-rewrite", parsed.toString());
726
- }
727
- } catch {
728
- if (!rawRewrite.startsWith(cleanFolder)) {
729
- outcome.response.headers.set(
730
- "x-middleware-rewrite",
731
- cleanFolder + (rawRewrite === "/" ? "" : rawRewrite)
732
- );
733
- }
734
- }
815
+ const cleanFolder = physicalFolderPrefix(internalFolder);
816
+ outcome.response.headers.set(
817
+ "x-middleware-rewrite",
818
+ applyFolderPrefix(rawRewrite, cleanFolder, currentReq.url)
819
+ );
735
820
  }
736
821
  return finalizeResponse(
737
822
  outcome.response,
@@ -744,22 +829,12 @@ function proxyChain(optionsOrEntries, maybeEntries) {
744
829
  );
745
830
  }
746
831
  const finalReq = materializeRequest();
747
- const internalHostFolder = appliedRequestOverrides.get("x-internal-host-folder");
832
+ const internalHostFolder = hostFolderHeader ? appliedRequestOverrides.get(hostFolderHeader) : null;
748
833
  let finalRewriteUrl = pendingRewriteUrl;
749
834
  if (internalHostFolder) {
750
- const cleanFolder = "/" + internalHostFolder.replace(/^\/+|\/+$/g, "").split("/").filter((seg) => !/^\(.*\)$/.test(seg)).join("/");
835
+ const cleanFolder = physicalFolderPrefix(internalHostFolder);
751
836
  if (finalRewriteUrl) {
752
- try {
753
- const parsed = new URL(finalRewriteUrl, finalReq.url);
754
- if (!parsed.pathname.startsWith(cleanFolder)) {
755
- parsed.pathname = cleanFolder + (parsed.pathname === "/" ? "" : parsed.pathname);
756
- finalRewriteUrl = parsed.toString();
757
- }
758
- } catch {
759
- if (!finalRewriteUrl.startsWith(cleanFolder)) {
760
- finalRewriteUrl = cleanFolder + (finalRewriteUrl === "/" ? "" : finalRewriteUrl);
761
- }
762
- }
837
+ finalRewriteUrl = applyFolderPrefix(finalRewriteUrl, cleanFolder, finalReq.url);
763
838
  } else {
764
839
  const currentPath = finalReq.nextUrl.pathname;
765
840
  if (!currentPath.startsWith(cleanFolder)) {
@@ -799,42 +874,12 @@ function proxyChain(optionsOrEntries, maybeEntries) {
799
874
  };
800
875
  }
801
876
  export {
802
- DEFAULT_BLOCKED_REQUEST_HEADERS,
803
- NEXT,
804
877
  RuleTimeoutError,
805
- applyMergedToResponse,
806
- assertIncludeExcludeNoOverlap,
807
- assertOverrideMechanismSupported,
808
- compileScopes,
809
- cookieItemsFromHeaders,
810
- createConsoleLogger,
811
878
  createProxyContext,
812
- decodeOverriddenRequestHeaders,
813
879
  defineProxy,
814
- finalizeResponse,
815
- formatPipelineLine,
816
- headersDiffer,
817
- headersInitToHeaders,
818
880
  host,
819
881
  isNextResult,
820
- isRequestHeaderAllowed,
821
- listSetCookie,
822
- mergeRequestHeaderOverrides,
823
- mergeWrites,
824
882
  next,
825
- normalizeHost,
826
- parseCookieName,
827
883
  path,
828
- pathMatches,
829
- proxyChain,
830
- resolveRuleResult,
831
- responseHeaderItems,
832
- ruleLabel,
833
- ruleMatches,
834
- safeInternalError,
835
- toPathRegex,
836
- toRule,
837
- toStatelessRegExp,
838
- withPaths,
839
- withRuleTimeout
884
+ proxyChain
840
885
  };