@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/CHANGELOG.md +55 -0
- package/LICENSE +21 -0
- package/README.md +165 -146
- 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 +19 -12
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
|
-
|
|
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(
|
|
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/
|
|
368
|
-
var
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
|
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 (
|
|
431
|
+
if (cachedOverrideProbe !== null) return resolveCachedProbe(cachedOverrideProbe, strict);
|
|
402
432
|
try {
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
520
|
-
createContext:
|
|
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
|
|
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)
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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(
|
|
832
|
+
const internalHostFolder = hostFolderHeader ? appliedRequestOverrides.get(hostFolderHeader) : null;
|
|
748
833
|
let finalRewriteUrl = pendingRewriteUrl;
|
|
749
834
|
if (internalHostFolder) {
|
|
750
|
-
const cleanFolder =
|
|
835
|
+
const cleanFolder = physicalFolderPrefix(internalHostFolder);
|
|
751
836
|
if (finalRewriteUrl) {
|
|
752
|
-
|
|
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
|
-
|
|
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
|
};
|