@klnap/next-proxy-chain 1.0.2 → 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.cjs CHANGED
@@ -20,47 +20,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- DEFAULT_BLOCKED_REQUEST_HEADERS: () => DEFAULT_BLOCKED_REQUEST_HEADERS,
24
- NEXT: () => NEXT,
25
23
  RuleTimeoutError: () => RuleTimeoutError,
26
- applyMergedToResponse: () => applyMergedToResponse,
27
- assertIncludeExcludeNoOverlap: () => assertIncludeExcludeNoOverlap,
28
- assertOverrideMechanismSupported: () => assertOverrideMechanismSupported,
29
- compileScopes: () => compileScopes,
30
- cookieItemsFromHeaders: () => cookieItemsFromHeaders,
31
- createConsoleLogger: () => createConsoleLogger,
32
24
  createProxyContext: () => createProxyContext,
33
- decodeOverriddenRequestHeaders: () => decodeOverriddenRequestHeaders,
34
25
  defineProxy: () => defineProxy,
35
- finalizeResponse: () => finalizeResponse,
36
- formatPipelineLine: () => formatPipelineLine,
37
- headersDiffer: () => headersDiffer,
38
- headersInitToHeaders: () => headersInitToHeaders,
39
26
  host: () => host,
40
27
  isNextResult: () => isNextResult,
41
- isRequestHeaderAllowed: () => isRequestHeaderAllowed,
42
- listSetCookie: () => listSetCookie,
43
- mergeRequestHeaderOverrides: () => mergeRequestHeaderOverrides,
44
- mergeWrites: () => mergeWrites,
45
28
  next: () => next,
46
- normalizeHost: () => normalizeHost,
47
- parseCookieName: () => parseCookieName,
48
29
  path: () => path,
49
- pathMatches: () => pathMatches,
50
- proxyChain: () => proxyChain,
51
- resolveRuleResult: () => resolveRuleResult,
52
- responseHeaderItems: () => responseHeaderItems,
53
- ruleLabel: () => ruleLabel,
54
- ruleMatches: () => ruleMatches,
55
- safeInternalError: () => safeInternalError,
56
- toPathRegex: () => toPathRegex,
57
- toRule: () => toRule,
58
- toStatelessRegExp: () => toStatelessRegExp,
59
- withPaths: () => withPaths,
60
- withRuleTimeout: () => withRuleTimeout
30
+ proxyChain: () => proxyChain
61
31
  });
62
32
  module.exports = __toCommonJS(index_exports);
63
33
 
34
+ // src/core/engine.ts
35
+ var import_server2 = require("next/server");
36
+
64
37
  // src/core/constants.ts
65
38
  var NEXT = /* @__PURE__ */ Symbol.for("proxy-chain.next");
66
39
  var DEFAULT_BLOCKED_REQUEST_HEADERS = Object.freeze([
@@ -84,178 +57,19 @@ var DEFAULT_BLOCKED_REQUEST_HEADERS = Object.freeze([
84
57
  "x-middleware-override-headers"
85
58
  ]);
86
59
 
60
+ // src/core/actions.ts
61
+ function next(options = {}) {
62
+ return { [NEXT]: true, ...options };
63
+ }
64
+ function isNextResult(value) {
65
+ return typeof value === "object" && value !== null && NEXT in value;
66
+ }
67
+
87
68
  // src/core/context.ts
88
69
  function createProxyContext() {
89
70
  return /* @__PURE__ */ new Map();
90
71
  }
91
72
 
92
- // src/core/matching.ts
93
- var RESERVED_FN_NAMES = /* @__PURE__ */ new Set(["", "proxy", "anonymous", "run", "next"]);
94
- function extractMeaningfulName(value) {
95
- if (!value || RESERVED_FN_NAMES.has(value)) return void 0;
96
- return value;
97
- }
98
- function toStatelessRegExp(re) {
99
- const flags = re.flags.replace(/[gy]/g, "");
100
- return flags === re.flags ? re : new RegExp(re.source, flags);
101
- }
102
- function escapeRegex(value) {
103
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
104
- }
105
- function host(matcher) {
106
- const list = Array.isArray(matcher) ? matcher : [matcher];
107
- const parts = list.map((m) => {
108
- if (m instanceof RegExp) return `(?:${toStatelessRegExp(m).source})`;
109
- return `(?:${escapeRegex(m.toLowerCase())})`;
110
- });
111
- return toStatelessRegExp(new RegExp(`^(?:${parts.join("|")})$`, "i"));
112
- }
113
- function isScopeObject(value) {
114
- return "host" in value || "path" in value;
115
- }
116
- function segmentToRegex(segment) {
117
- if (segment === "**") return "(?:.*)";
118
- if (segment === "*") return "[^/]+";
119
- if (segment.startsWith(":")) return "[^/]+";
120
- return escapeRegex(segment);
121
- }
122
- function path(pattern, options = {}) {
123
- const { locales = [] } = options;
124
- const localePrefix = locales.length > 0 ? `(?:(?:${locales.map(escapeRegex).join("|")})\\/)?` : "";
125
- if (pattern instanceof RegExp) {
126
- const body2 = pattern.source.replace(/^\^/, "").replace(/\$$/, "");
127
- const needsStart = !pattern.source.startsWith("^");
128
- const needsEnd = !pattern.source.endsWith("$");
129
- return toStatelessRegExp(
130
- new RegExp(
131
- `${needsStart ? "^" : ""}${localePrefix ? `/${localePrefix}` : ""}${body2}${needsEnd ? "$" : ""}`,
132
- pattern.flags
133
- )
134
- );
135
- }
136
- const normalized = (pattern.startsWith("/") ? pattern : `/${pattern}`).replace(/\/$/, "") || "/";
137
- const segments = normalized.slice(1).split("/");
138
- const trailingStar = segments.length > 0 && segments[segments.length - 1] === "*";
139
- const suffix = trailingStar ? "(?:\\/.*)?$" : "$";
140
- const body = trailingStar ? segments.slice(0, -1).map(segmentToRegex).concat("[^/]+").join("\\/") : segments.map(segmentToRegex).join("\\/");
141
- return new RegExp(`^/${localePrefix}${body}${suffix}`);
142
- }
143
- function toPathRegex(matcher, options) {
144
- const list = Array.isArray(matcher) ? matcher : [matcher];
145
- const regexes = list.map((m) => path(m, options));
146
- if (regexes.length === 1) return toStatelessRegExp(regexes[0]);
147
- return toStatelessRegExp(new RegExp(`(?:${regexes.map((r) => `(?:${r.source})`).join("|")})`));
148
- }
149
- function compileScopes(input, pathOptions) {
150
- if (input == null) return void 0;
151
- const list = Array.isArray(input) ? input : [input];
152
- const compilePath = (matcher) => {
153
- if (Array.isArray(matcher)) {
154
- const parts = matcher.map(
155
- (m) => typeof m === "string" ? path(m, pathOptions) : toStatelessRegExp(m)
156
- );
157
- if (parts.length === 1) return parts[0];
158
- return toStatelessRegExp(new RegExp(`(?:${parts.map((r) => `(?:${r.source})`).join("|")})`));
159
- }
160
- return typeof matcher === "string" ? path(matcher, pathOptions) : toStatelessRegExp(matcher);
161
- };
162
- const compiled = [];
163
- for (const item of list) {
164
- if (typeof item === "string" || item instanceof RegExp) {
165
- compiled.push({ path: compilePath(item) });
166
- continue;
167
- }
168
- if (item && typeof item === "object" && isScopeObject(item)) {
169
- if (item.host == null && item.path == null) {
170
- throw new Error("[proxy-chain] scope needs at least `host` or `path`");
171
- }
172
- compiled.push({
173
- host: item.host != null ? host(item.host) : void 0,
174
- path: item.path != null ? compilePath(item.path) : void 0
175
- });
176
- continue;
177
- }
178
- throw new Error("[proxy-chain] invalid include/exclude scope");
179
- }
180
- return compiled;
181
- }
182
- function scopeMatches(pathname, hostname, scope) {
183
- if (scope.host) {
184
- if (!hostname || !scope.host.test(hostname)) return false;
185
- }
186
- if (scope.path) {
187
- if (!scope.path.test(pathname)) return false;
188
- }
189
- return true;
190
- }
191
- function toRule(entry) {
192
- if (typeof entry === "function") {
193
- const name2 = extractMeaningfulName(entry.name);
194
- return name2 ? { run: entry, name: name2 } : { run: entry };
195
- }
196
- const rule = entry;
197
- const runName = typeof rule.run === "function" ? extractMeaningfulName(rule.run.name) : void 0;
198
- const name = extractMeaningfulName(rule.name) ?? runName;
199
- const harden = (scopes) => scopes?.map((s) => ({
200
- host: s.host ? toStatelessRegExp(s.host) : void 0,
201
- path: s.path ? toStatelessRegExp(s.path) : void 0
202
- }));
203
- return {
204
- ...rule,
205
- name: name ?? void 0,
206
- include: harden(rule.include),
207
- exclude: harden(rule.exclude)
208
- };
209
- }
210
- function ruleLabel(rule, index) {
211
- const runName = typeof rule.run === "function" ? extractMeaningfulName(rule.run.name) : void 0;
212
- return extractMeaningfulName(rule.name) ?? runName ?? `rule#${index}`;
213
- }
214
- function withPaths(entry, filter) {
215
- const base = toRule(entry);
216
- const opts = filter.pathOptions;
217
- const include = compileScopes(filter.include, opts);
218
- const exclude = compileScopes(filter.exclude, opts);
219
- const name = filter.name ?? base.name;
220
- assertIncludeExcludeNoOverlap(include, exclude, name ?? "proxy");
221
- return { run: base.run, include, exclude, name };
222
- }
223
- function ruleMatches(pathname, hostname, rule) {
224
- if (rule.exclude?.some((scope) => scopeMatches(pathname, hostname, scope))) return false;
225
- if (rule.include && !rule.include.some((scope) => scopeMatches(pathname, hostname, scope))) {
226
- return false;
227
- }
228
- return true;
229
- }
230
- function pathMatches(pathname, rule) {
231
- return ruleMatches(pathname, null, rule);
232
- }
233
- function normalizeHost(raw) {
234
- if (!raw) return null;
235
- const first = raw.split(",")[0]?.trim().toLowerCase();
236
- if (!first) return null;
237
- if (first.startsWith("[")) {
238
- const end = first.indexOf("]");
239
- return end === -1 ? first : first.slice(1, end);
240
- }
241
- const withoutPort = first.replace(/:\d+$/, "");
242
- return withoutPort || null;
243
- }
244
- function scopeKey(scope) {
245
- return `${scope.host?.source ?? "*"}::${scope.host?.flags ?? ""}|${scope.path?.source ?? "*"}::${scope.path?.flags ?? ""}`;
246
- }
247
- function assertIncludeExcludeNoOverlap(include, exclude, label) {
248
- if (!include?.length || !exclude?.length) return;
249
- const excluded = new Set(exclude.map(scopeKey));
250
- for (const scope of include) {
251
- if (excluded.has(scopeKey(scope))) {
252
- throw new Error(
253
- `[proxy-chain] ${label}: include and exclude share an identical scope. Remove one.`
254
- );
255
- }
256
- }
257
- }
258
-
259
73
  // src/core/headers.ts
260
74
  function normalizeHeaderName(name) {
261
75
  return name.toLowerCase();
@@ -298,15 +112,32 @@ function parseCookieName(setCookie) {
298
112
  return (eq === -1 ? setCookie : setCookie.slice(0, eq)).trim();
299
113
  }
300
114
  function listSetCookie(headers) {
301
- const extended = headers;
302
- if (typeof extended.getSetCookie === "function") return extended.getSetCookie();
115
+ if (typeof headers.getSetCookie === "function") return headers.getSetCookie();
303
116
  const single = headers.get("set-cookie");
304
117
  return single ? [single] : [];
305
118
  }
119
+ function cookieAttribute(setCookie, name) {
120
+ const parts = setCookie.split(";");
121
+ for (let i = 1; i < parts.length; i++) {
122
+ const piece = parts[i];
123
+ if (!piece) continue;
124
+ const eq = piece.indexOf("=");
125
+ const key = (eq === -1 ? piece : piece.slice(0, eq)).trim().toLowerCase();
126
+ if (key !== name) continue;
127
+ return (eq === -1 ? "" : piece.slice(eq + 1)).trim();
128
+ }
129
+ return "";
130
+ }
131
+ function cookieIdentityKey(setCookie) {
132
+ const name = parseCookieName(setCookie);
133
+ const path2 = cookieAttribute(setCookie, "path");
134
+ const domain = cookieAttribute(setCookie, "domain").toLowerCase();
135
+ return `${name};domain=${domain};path=${path2}`;
136
+ }
306
137
  function cookieItemsFromHeaders(headers) {
307
138
  const map = /* @__PURE__ */ new Map();
308
139
  for (const value of listSetCookie(headers)) {
309
- map.set(parseCookieName(value), value);
140
+ map.set(cookieIdentityKey(value), value);
310
141
  }
311
142
  return map;
312
143
  }
@@ -366,7 +197,12 @@ function mergeWrites(writes, strategy, onConflict) {
366
197
  }
367
198
  return resolved;
368
199
  }
369
- function applyMergedToResponse(res, headers, cookies) {
200
+ function applyMergedToResponse(res, headers, cookies, dropHeaders) {
201
+ if (dropHeaders) {
202
+ for (const key of dropHeaders) {
203
+ if (!headers.has(key)) res.headers.delete(key);
204
+ }
205
+ }
370
206
  for (const [key, value] of headers) {
371
207
  res.headers.set(key, value);
372
208
  }
@@ -376,14 +212,6 @@ function applyMergedToResponse(res, headers, cookies) {
376
212
  }
377
213
  }
378
214
 
379
- // src/core/actions.ts
380
- function next(options = {}) {
381
- return { [NEXT]: true, ...options };
382
- }
383
- function isNextResult(value) {
384
- return typeof value === "object" && value !== null && NEXT in value;
385
- }
386
-
387
215
  // src/core/logger.ts
388
216
  var LEVEL_RANK = {
389
217
  debug: 10,
@@ -427,27 +255,180 @@ function formatPipelineLine(input) {
427
255
  }
428
256
  }
429
257
 
430
- // src/core/timeout.ts
431
- var RuleTimeoutError = class extends Error {
432
- ruleName;
433
- timeoutMs;
434
- constructor(ruleName, timeoutMs) {
435
- super(`[proxy-chain] ${ruleName} timed out after ${timeoutMs}ms`);
436
- this.name = "RuleTimeoutError";
437
- this.ruleName = ruleName;
438
- this.timeoutMs = timeoutMs;
439
- }
440
- };
441
- async function withRuleTimeout(promise, timeoutMs, ruleName) {
442
- if (!timeoutMs || timeoutMs <= 0) return promise;
443
- let timer;
444
- const timeout = new Promise((_, reject) => {
445
- timer = setTimeout(() => reject(new RuleTimeoutError(ruleName, timeoutMs)), timeoutMs);
258
+ // src/core/matching.ts
259
+ var RESERVED_FN_NAMES = /* @__PURE__ */ new Set(["", "proxy", "anonymous", "run", "next"]);
260
+ function extractMeaningfulName(value) {
261
+ if (!value || RESERVED_FN_NAMES.has(value)) return void 0;
262
+ return value;
263
+ }
264
+ function toStatelessRegExp(re) {
265
+ const flags = re.flags.replace(/[gy]/g, "");
266
+ return flags === re.flags ? re : new RegExp(re.source, flags);
267
+ }
268
+ function escapeRegex(value) {
269
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
270
+ }
271
+ function hostGlobToRegex(value) {
272
+ return escapeRegex(value.toLowerCase()).replace(/\\\*/g, "[^.]+");
273
+ }
274
+ function host(matcher) {
275
+ const list = Array.isArray(matcher) ? matcher : [matcher];
276
+ const parts = list.map((m) => {
277
+ if (m instanceof RegExp) return `(?:${toStatelessRegExp(m).source})`;
278
+ return `(?:${hostGlobToRegex(m)})`;
446
279
  });
447
- try {
448
- return await Promise.race([promise, timeout]);
449
- } finally {
450
- if (timer !== void 0) clearTimeout(timer);
280
+ return toStatelessRegExp(new RegExp(`^(?:${parts.join("|")})$`, "i"));
281
+ }
282
+ function isScopeObject(value) {
283
+ return "host" in value || "path" in value;
284
+ }
285
+ function segmentToRegex(segment) {
286
+ if (segment === "**") return "(?:.*)";
287
+ if (segment === "*") return "[^/]+";
288
+ if (segment.startsWith(":")) return "[^/]+";
289
+ return escapeRegex(segment);
290
+ }
291
+ function hasEndAnchor(source) {
292
+ if (!source.endsWith("$")) return false;
293
+ let slashes = 0;
294
+ for (let i = source.length - 2; i >= 0 && source[i] === "\\"; i--) slashes++;
295
+ return slashes % 2 === 0;
296
+ }
297
+ function compileRegexPath(pattern) {
298
+ let source = pattern.source;
299
+ const hasStart = source.startsWith("^");
300
+ const hasEnd = hasEndAnchor(source);
301
+ if (hasStart) source = source.slice(1);
302
+ if (hasEnd) source = source.slice(0, -1);
303
+ const wrapFull = !hasStart && !hasEnd;
304
+ const start = hasStart || wrapFull ? "^" : "";
305
+ const end = hasEnd || wrapFull ? "$" : "";
306
+ return toStatelessRegExp(new RegExp(`${start}${source}${end}`, pattern.flags));
307
+ }
308
+ function compileStringPath(pattern) {
309
+ const normalized = (pattern.startsWith("/") ? pattern : `/${pattern}`).replace(/\/$/, "") || "/";
310
+ const segments = normalized === "/" ? [] : normalized.slice(1).split("/");
311
+ const last = segments[segments.length - 1];
312
+ const trailingStar = last === "*";
313
+ const trailingGlobstar = last === "**";
314
+ let pathPattern;
315
+ if (normalized === "/") {
316
+ pathPattern = "/";
317
+ } else if (trailingStar) {
318
+ const prefix = segments.slice(0, -1).map(segmentToRegex).join("/");
319
+ pathPattern = prefix ? `/${prefix}/[^/]+` : `/[^/]+`;
320
+ } else if (trailingGlobstar) {
321
+ const prefix = segments.slice(0, -1).map(segmentToRegex).join("/");
322
+ pathPattern = prefix ? `/${prefix}(?:/.*)?` : `/(?:.*)?`;
323
+ } else {
324
+ pathPattern = `/${segments.map(segmentToRegex).join("/")}`;
325
+ }
326
+ return new RegExp(`^${pathPattern}$`);
327
+ }
328
+ function path(pattern) {
329
+ if (pattern instanceof RegExp) return compileRegexPath(pattern);
330
+ return compileStringPath(pattern);
331
+ }
332
+ function toPathRegex(matcher) {
333
+ const list = Array.isArray(matcher) ? matcher : [matcher];
334
+ const regexes = list.map((m) => path(m));
335
+ if (regexes.length === 1) return toStatelessRegExp(regexes[0]);
336
+ return toStatelessRegExp(new RegExp(`(?:${regexes.map((r) => `(?:${r.source})`).join("|")})`));
337
+ }
338
+ function compileScopes(input) {
339
+ if (input == null) return void 0;
340
+ const list = Array.isArray(input) ? input : [input];
341
+ const compilePath = (matcher) => toPathRegex(matcher);
342
+ const compiled = [];
343
+ for (const item of list) {
344
+ if (typeof item === "string" || item instanceof RegExp) {
345
+ compiled.push({ path: compilePath(item) });
346
+ continue;
347
+ }
348
+ if (item && typeof item === "object" && isScopeObject(item)) {
349
+ if (item.host == null && item.path == null) {
350
+ throw new Error("[proxy-chain] scope needs at least `host` or `path`");
351
+ }
352
+ compiled.push({
353
+ host: item.host != null ? host(item.host) : void 0,
354
+ path: item.path != null ? compilePath(item.path) : void 0
355
+ });
356
+ continue;
357
+ }
358
+ throw new Error("[proxy-chain] invalid include/exclude scope");
359
+ }
360
+ return compiled;
361
+ }
362
+ function scopeMatches(pathname, hostname, scope) {
363
+ if (scope.host) {
364
+ if (!hostname || !scope.host.test(hostname)) return false;
365
+ }
366
+ if (scope.path) {
367
+ if (!scope.path.test(pathname)) return false;
368
+ }
369
+ return true;
370
+ }
371
+ function toRule(entry) {
372
+ if (typeof entry === "function") {
373
+ const name2 = extractMeaningfulName(entry.name);
374
+ return name2 ? { run: entry, name: name2 } : { run: entry };
375
+ }
376
+ const rule = entry;
377
+ const runName = typeof rule.run === "function" ? extractMeaningfulName(rule.run.name) : void 0;
378
+ const name = extractMeaningfulName(rule.name) ?? runName;
379
+ const harden = (scopes) => scopes?.map((s) => ({
380
+ host: s.host ? toStatelessRegExp(s.host) : void 0,
381
+ path: s.path ? toStatelessRegExp(s.path) : void 0
382
+ }));
383
+ return {
384
+ ...rule,
385
+ name: name ?? void 0,
386
+ include: harden(rule.include),
387
+ exclude: harden(rule.exclude)
388
+ };
389
+ }
390
+ function ruleLabel(rule, index) {
391
+ const runName = typeof rule.run === "function" ? extractMeaningfulName(rule.run.name) : void 0;
392
+ return extractMeaningfulName(rule.name) ?? runName ?? `rule#${index}`;
393
+ }
394
+ function withPaths(entry, filter) {
395
+ const base = toRule(entry);
396
+ const include = compileScopes(filter.include);
397
+ const exclude = compileScopes(filter.exclude);
398
+ const name = filter.name ?? base.name;
399
+ assertIncludeExcludeNoOverlap(include, exclude, name ?? "proxy");
400
+ return { run: base.run, include, exclude, name };
401
+ }
402
+ function ruleMatches(pathname, hostname, rule) {
403
+ if (rule.exclude?.some((scope) => scopeMatches(pathname, hostname, scope))) return false;
404
+ if (rule.include && !rule.include.some((scope) => scopeMatches(pathname, hostname, scope))) {
405
+ return false;
406
+ }
407
+ return true;
408
+ }
409
+ function normalizeHost(raw) {
410
+ if (!raw) return null;
411
+ const first = raw.split(",")[0]?.trim().toLowerCase();
412
+ if (!first) return null;
413
+ if (first.startsWith("[")) {
414
+ const end = first.indexOf("]");
415
+ return end === -1 ? first : first.slice(1, end);
416
+ }
417
+ const withoutPort = first.replace(/:\d+$/, "");
418
+ return withoutPort || null;
419
+ }
420
+ function scopeKey(scope) {
421
+ return `${scope.host?.source ?? "*"}::${scope.host?.flags ?? ""}|${scope.path?.source ?? "*"}::${scope.path?.flags ?? ""}`;
422
+ }
423
+ function assertIncludeExcludeNoOverlap(include, exclude, label) {
424
+ if (!include?.length || !exclude?.length) return;
425
+ const excluded = new Set(exclude.map(scopeKey));
426
+ for (const scope of include) {
427
+ if (excluded.has(scopeKey(scope))) {
428
+ throw new Error(
429
+ `[proxy-chain] ${label}: include and exclude share an identical scope. Remove one.`
430
+ );
431
+ }
451
432
  }
452
433
  }
453
434
 
@@ -459,26 +440,42 @@ function safeInternalError() {
459
440
  headers: { "content-type": "text/plain; charset=utf-8" }
460
441
  });
461
442
  }
462
- var cachedOverrideSupported = null;
443
+ var UNSUPPORTED_OVERRIDE_MSG = "[proxy-chain] Warning: platform NextResponse.next() does not support x-middleware-next header overrides.";
444
+ var VERIFY_OVERRIDE_MSG = "[proxy-chain] Warning: unable to verify NextResponse request override mechanism.";
445
+ var cachedOverrideProbe = null;
446
+ var overrideProbeForTests = null;
447
+ function runOverrideProbe() {
448
+ if (overrideProbeForTests) return overrideProbeForTests();
449
+ const testRes = import_server.NextResponse.next({
450
+ request: { headers: new Headers({ "x-proxy-chain-probe": "1" }) }
451
+ });
452
+ return testRes.headers.get("x-middleware-next") === "1";
453
+ }
454
+ function throwForCachedProbe(probe) {
455
+ if (probe.kind === "unsupported") throw new Error(UNSUPPORTED_OVERRIDE_MSG);
456
+ throw probe.err instanceof Error ? probe.err : new Error(VERIFY_OVERRIDE_MSG);
457
+ }
458
+ function resolveCachedProbe(probe, strict) {
459
+ if (probe.kind === "supported") return true;
460
+ if (strict) throwForCachedProbe(probe);
461
+ return false;
462
+ }
463
463
  function assertOverrideMechanismSupported(strict = false) {
464
- if (cachedOverrideSupported !== null) return cachedOverrideSupported;
464
+ if (cachedOverrideProbe !== null) return resolveCachedProbe(cachedOverrideProbe, strict);
465
465
  try {
466
- const testRes = import_server.NextResponse.next({
467
- request: { headers: new Headers({ "x-proxy-chain-probe": "1" }) }
468
- });
469
- const isSupported = testRes.headers.get("x-middleware-next") === "1";
470
- cachedOverrideSupported = isSupported;
471
- if (!isSupported) {
472
- const msg = "[proxy-chain] Warning: platform NextResponse.next() does not support x-middleware-next header overrides.";
473
- if (strict) throw new Error(msg);
474
- console.warn(msg);
466
+ const isSupported = runOverrideProbe();
467
+ if (isSupported) {
468
+ cachedOverrideProbe = { kind: "supported" };
469
+ return true;
475
470
  }
476
- return isSupported;
471
+ cachedOverrideProbe = { kind: "unsupported" };
472
+ if (strict) throw new Error(UNSUPPORTED_OVERRIDE_MSG);
473
+ console.warn(UNSUPPORTED_OVERRIDE_MSG);
474
+ return false;
477
475
  } catch (err) {
478
- cachedOverrideSupported = false;
479
- const msg = "[proxy-chain] Warning: unable to verify NextResponse request override mechanism.";
480
- if (strict) throw err instanceof Error ? err : new Error(msg);
481
- console.warn(msg, err);
476
+ cachedOverrideProbe = { kind: "error", err };
477
+ if (strict) throw err instanceof Error ? err : new Error(VERIFY_OVERRIDE_MSG);
478
+ console.warn(VERIFY_OVERRIDE_MSG, err);
482
479
  return false;
483
480
  }
484
481
  }
@@ -500,11 +497,12 @@ function resolveRuleResult(res, requestHeaderPolicy, logger, debug) {
500
497
  }
501
498
  if ([...filtered.keys()].length) request = filtered;
502
499
  }
500
+ const headerInit = res.headers ? headersInitToHeaders(res.headers) : void 0;
503
501
  return {
504
502
  kind: "continue",
505
503
  request,
506
- responseHeaders: res.headers ? responseHeaderItems(headersInitToHeaders(res.headers)) : /* @__PURE__ */ new Map(),
507
- cookies: /* @__PURE__ */ new Map()
504
+ responseHeaders: headerInit ? responseHeaderItems(headerInit) : /* @__PURE__ */ new Map(),
505
+ cookies: headerInit ? cookieItemsFromHeaders(headerInit) : /* @__PURE__ */ new Map()
508
506
  };
509
507
  }
510
508
  if (res.headers.get("x-middleware-next") === "1") {
@@ -548,16 +546,57 @@ function finalizeResponse(res, headerWrites, cookieWrites, headerStrategy, cooki
548
546
  const cookies = mergeWrites(cookieWrites, cookieStrategy, (key, winner, loser) => {
549
547
  if (debug) logger.debug({ cookie: key, winner, loser }, "cookie conflict resolved");
550
548
  });
551
- applyMergedToResponse(res, headers, cookies);
549
+ const seenHeaders = /* @__PURE__ */ new Set();
550
+ for (const write of headerWrites) {
551
+ for (const key of write.items.keys()) seenHeaders.add(key);
552
+ }
553
+ const dropHeaders = [];
554
+ for (const key of seenHeaders) {
555
+ if (!headers.has(key)) dropHeaders.push(key);
556
+ }
557
+ applyMergedToResponse(res, headers, cookies, dropHeaders);
552
558
  return res;
553
559
  }
554
560
 
561
+ // src/core/timeout.ts
562
+ var RuleTimeoutError = class extends Error {
563
+ ruleName;
564
+ timeoutMs;
565
+ constructor(ruleName, timeoutMs) {
566
+ super(`[proxy-chain] ${ruleName} timed out after ${timeoutMs}ms`);
567
+ this.name = "RuleTimeoutError";
568
+ this.ruleName = ruleName;
569
+ this.timeoutMs = timeoutMs;
570
+ }
571
+ };
572
+ function assertFiniteRuleTimeoutMs(ruleTimeoutMs) {
573
+ if (ruleTimeoutMs != null && !Number.isFinite(ruleTimeoutMs)) {
574
+ throw new RangeError(
575
+ `[proxy-chain] ruleTimeoutMs must be a finite positive number, got ${ruleTimeoutMs}`
576
+ );
577
+ }
578
+ }
579
+ async function withRuleTimeout(promise, timeoutMs, ruleName) {
580
+ assertFiniteRuleTimeoutMs(timeoutMs);
581
+ if (timeoutMs == null || timeoutMs <= 0) return promise;
582
+ let timer;
583
+ const timeout = new Promise((_, reject) => {
584
+ timer = setTimeout(() => reject(new RuleTimeoutError(ruleName, timeoutMs)), timeoutMs);
585
+ });
586
+ try {
587
+ return await Promise.race([promise, timeout]);
588
+ } finally {
589
+ if (timer !== void 0) clearTimeout(timer);
590
+ void promise.catch(() => {
591
+ });
592
+ }
593
+ }
594
+
555
595
  // src/core/engine.ts
556
- var import_server2 = require("next/server");
557
596
  function isPathFilter(value) {
558
597
  if (value === null || typeof value !== "object") return false;
559
598
  if ("nextUrl" in value) return false;
560
- return "include" in value || "exclude" in value || "name" in value || "pathOptions" in value;
599
+ return "include" in value || "exclude" in value || "name" in value;
561
600
  }
562
601
  function defineProxy(nameOrRun, maybeRun) {
563
602
  const defaultName = typeof nameOrRun === "string" ? nameOrRun : void 0;
@@ -577,25 +616,47 @@ function defineProxy(nameOrRun, maybeRun) {
577
616
  });
578
617
  return proxy;
579
618
  }
580
- function resolveConfig(overrides = {}) {
581
- const { logger, debug, ...rest } = overrides;
582
- return {
583
- createContext: () => createProxyContext(),
619
+ function resolveConfig(overrides, fallbackCreateContext) {
620
+ const { logger, debug, createContext, ...rest } = overrides;
621
+ const config = {
622
+ createContext: createContext ?? fallbackCreateContext,
584
623
  cookieMergeStrategy: "last-write-wins",
585
624
  headerMergeStrategy: "last-write-wins",
586
625
  debug: false,
587
626
  ruleTimeoutMs: 0,
588
627
  requestHeaderPolicy: {},
589
628
  strictOverrideCheck: false,
629
+ hostFolderHeader: void 0,
590
630
  ...rest,
591
631
  ...debug !== void 0 ? { debug } : {},
592
632
  logger: logger ?? createConsoleLogger(debug ? "debug" : "info")
593
633
  };
634
+ assertFiniteRuleTimeoutMs(config.ruleTimeoutMs);
635
+ return config;
636
+ }
637
+ function physicalFolderPrefix(raw) {
638
+ return "/" + raw.replace(/^\/+|\/+$/g, "").split("/").filter((seg) => !/^\(.*\)$/.test(seg)).join("/");
639
+ }
640
+ function applyFolderPrefix(rewriteUrl, folder, baseUrl) {
641
+ try {
642
+ const parsed = new URL(rewriteUrl, baseUrl);
643
+ if (!parsed.pathname.startsWith(folder)) {
644
+ parsed.pathname = folder + (parsed.pathname === "/" ? "" : parsed.pathname);
645
+ return parsed.toString();
646
+ }
647
+ return rewriteUrl;
648
+ } catch {
649
+ if (!rewriteUrl.startsWith(folder)) {
650
+ return folder + (rewriteUrl === "/" ? "" : rewriteUrl);
651
+ }
652
+ return rewriteUrl;
653
+ }
594
654
  }
595
655
  function proxyChain(optionsOrEntries, maybeEntries) {
596
656
  const options = Array.isArray(optionsOrEntries) ? {} : optionsOrEntries;
597
- const entries = Array.isArray(optionsOrEntries) ? optionsOrEntries : maybeEntries;
598
- const config = resolveConfig(options);
657
+ const entries = (Array.isArray(optionsOrEntries) ? optionsOrEntries : maybeEntries) ?? [];
658
+ const fallbackCreateContext = createProxyContext;
659
+ const config = resolveConfig(options, fallbackCreateContext);
599
660
  const rules = entries.map(toRule);
600
661
  const {
601
662
  logger,
@@ -604,7 +665,8 @@ function proxyChain(optionsOrEntries, maybeEntries) {
604
665
  headerMergeStrategy,
605
666
  ruleTimeoutMs,
606
667
  requestHeaderPolicy,
607
- strictOverrideCheck
668
+ strictOverrideCheck,
669
+ hostFolderHeader
608
670
  } = config;
609
671
  assertOverrideMechanismSupported(strictOverrideCheck);
610
672
  for (let i = 0; i < rules.length; i++) {
@@ -670,7 +732,7 @@ function proxyChain(optionsOrEntries, maybeEntries) {
670
732
  if (config.onError) {
671
733
  try {
672
734
  const recovered = await config.onError(err, i, activeReq, ctx);
673
- if (recovered == null) continue;
735
+ if (recovered == null) return safeInternalError();
674
736
  if (isNextResult(recovered)) {
675
737
  raw = recovered;
676
738
  } else if (recovered instanceof Response) {
@@ -709,7 +771,7 @@ function proxyChain(optionsOrEntries, maybeEntries) {
709
771
  debug
710
772
  );
711
773
  } else {
712
- continue;
774
+ return safeInternalError();
713
775
  }
714
776
  } catch (onErrorErr) {
715
777
  logger.error({ rule: label, err: onErrorErr }, "onError handler threw");
@@ -725,6 +787,9 @@ function proxyChain(optionsOrEntries, maybeEntries) {
725
787
  if (outcome.kind === "continue") {
726
788
  if (outcome.rewriteUrl) {
727
789
  pendingRewriteUrl = outcome.rewriteUrl;
790
+ if (currentReq === req) {
791
+ currentReq = new import_server2.NextRequest(currentReq);
792
+ }
728
793
  try {
729
794
  const parsed = new URL(outcome.rewriteUrl);
730
795
  currentReq.nextUrl.pathname = parsed.pathname;
@@ -777,24 +842,14 @@ function proxyChain(optionsOrEntries, maybeEntries) {
777
842
  })
778
843
  );
779
844
  }
780
- const internalFolder = appliedRequestOverrides.get("x-internal-host-folder");
845
+ const internalFolder = hostFolderHeader ? appliedRequestOverrides.get(hostFolderHeader) : null;
781
846
  if (internalFolder && outcome.response.headers.has("x-middleware-rewrite")) {
782
847
  const rawRewrite = outcome.response.headers.get("x-middleware-rewrite");
783
- const cleanFolder = "/" + internalFolder.replace(/^\/+|\/+$/g, "").split("/").filter((seg) => !/^\(.*\)$/.test(seg)).join("/");
784
- try {
785
- const parsed = new URL(rawRewrite, currentReq.url);
786
- if (!parsed.pathname.startsWith(cleanFolder)) {
787
- parsed.pathname = cleanFolder + (parsed.pathname === "/" ? "" : parsed.pathname);
788
- outcome.response.headers.set("x-middleware-rewrite", parsed.toString());
789
- }
790
- } catch {
791
- if (!rawRewrite.startsWith(cleanFolder)) {
792
- outcome.response.headers.set(
793
- "x-middleware-rewrite",
794
- cleanFolder + (rawRewrite === "/" ? "" : rawRewrite)
795
- );
796
- }
797
- }
848
+ const cleanFolder = physicalFolderPrefix(internalFolder);
849
+ outcome.response.headers.set(
850
+ "x-middleware-rewrite",
851
+ applyFolderPrefix(rawRewrite, cleanFolder, currentReq.url)
852
+ );
798
853
  }
799
854
  return finalizeResponse(
800
855
  outcome.response,
@@ -807,22 +862,12 @@ function proxyChain(optionsOrEntries, maybeEntries) {
807
862
  );
808
863
  }
809
864
  const finalReq = materializeRequest();
810
- const internalHostFolder = appliedRequestOverrides.get("x-internal-host-folder");
865
+ const internalHostFolder = hostFolderHeader ? appliedRequestOverrides.get(hostFolderHeader) : null;
811
866
  let finalRewriteUrl = pendingRewriteUrl;
812
867
  if (internalHostFolder) {
813
- const cleanFolder = "/" + internalHostFolder.replace(/^\/+|\/+$/g, "").split("/").filter((seg) => !/^\(.*\)$/.test(seg)).join("/");
868
+ const cleanFolder = physicalFolderPrefix(internalHostFolder);
814
869
  if (finalRewriteUrl) {
815
- try {
816
- const parsed = new URL(finalRewriteUrl, finalReq.url);
817
- if (!parsed.pathname.startsWith(cleanFolder)) {
818
- parsed.pathname = cleanFolder + (parsed.pathname === "/" ? "" : parsed.pathname);
819
- finalRewriteUrl = parsed.toString();
820
- }
821
- } catch {
822
- if (!finalRewriteUrl.startsWith(cleanFolder)) {
823
- finalRewriteUrl = cleanFolder + (finalRewriteUrl === "/" ? "" : finalRewriteUrl);
824
- }
825
- }
870
+ finalRewriteUrl = applyFolderPrefix(finalRewriteUrl, cleanFolder, finalReq.url);
826
871
  } else {
827
872
  const currentPath = finalReq.nextUrl.pathname;
828
873
  if (!currentPath.startsWith(cleanFolder)) {
@@ -863,42 +908,12 @@ function proxyChain(optionsOrEntries, maybeEntries) {
863
908
  }
864
909
  // Annotate the CommonJS export names for ESM import in node:
865
910
  0 && (module.exports = {
866
- DEFAULT_BLOCKED_REQUEST_HEADERS,
867
- NEXT,
868
911
  RuleTimeoutError,
869
- applyMergedToResponse,
870
- assertIncludeExcludeNoOverlap,
871
- assertOverrideMechanismSupported,
872
- compileScopes,
873
- cookieItemsFromHeaders,
874
- createConsoleLogger,
875
912
  createProxyContext,
876
- decodeOverriddenRequestHeaders,
877
913
  defineProxy,
878
- finalizeResponse,
879
- formatPipelineLine,
880
- headersDiffer,
881
- headersInitToHeaders,
882
914
  host,
883
915
  isNextResult,
884
- isRequestHeaderAllowed,
885
- listSetCookie,
886
- mergeRequestHeaderOverrides,
887
- mergeWrites,
888
916
  next,
889
- normalizeHost,
890
- parseCookieName,
891
917
  path,
892
- pathMatches,
893
- proxyChain,
894
- resolveRuleResult,
895
- responseHeaderItems,
896
- ruleLabel,
897
- ruleMatches,
898
- safeInternalError,
899
- toPathRegex,
900
- toRule,
901
- toStatelessRegExp,
902
- withPaths,
903
- withRuleTimeout
918
+ proxyChain
904
919
  });