@klnap/next-proxy-chain 1.0.0 → 1.0.2
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/README.md +174 -79
- package/dist/index.cjs +196 -174
- package/dist/index.d.cts +255 -35
- package/dist/index.d.ts +255 -35
- package/dist/index.js +443 -81
- package/package.json +16 -12
- package/dist/chunk-PTAOIFNB.js +0 -412
- package/dist/core.cjs +0 -467
- package/dist/core.d.cts +0 -210
- package/dist/core.d.ts +0 -210
- package/dist/core.d.ts.map +0 -1
- package/dist/core.js +0 -68
- package/dist/core.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,78 +1,395 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
parseCookieName,
|
|
24
|
-
path,
|
|
25
|
-
pathMatches,
|
|
26
|
-
responseHeaderItems,
|
|
27
|
-
ruleLabel,
|
|
28
|
-
ruleMatches,
|
|
29
|
-
toPathRegex,
|
|
30
|
-
toRule,
|
|
31
|
-
toStatelessRegExp,
|
|
32
|
-
withPaths,
|
|
33
|
-
withRuleTimeout
|
|
34
|
-
} from "./chunk-PTAOIFNB.js";
|
|
1
|
+
// src/core/constants.ts
|
|
2
|
+
var NEXT = /* @__PURE__ */ Symbol.for("proxy-chain.next");
|
|
3
|
+
var DEFAULT_BLOCKED_REQUEST_HEADERS = Object.freeze([
|
|
4
|
+
"host",
|
|
5
|
+
"connection",
|
|
6
|
+
"keep-alive",
|
|
7
|
+
"proxy-authenticate",
|
|
8
|
+
"proxy-authorization",
|
|
9
|
+
"te",
|
|
10
|
+
"trailer",
|
|
11
|
+
"transfer-encoding",
|
|
12
|
+
"upgrade",
|
|
13
|
+
"content-length",
|
|
14
|
+
"cookie",
|
|
15
|
+
"authorization",
|
|
16
|
+
"x-forwarded-host",
|
|
17
|
+
"x-forwarded-for",
|
|
18
|
+
"x-forwarded-proto",
|
|
19
|
+
"x-real-ip",
|
|
20
|
+
"x-middleware-next",
|
|
21
|
+
"x-middleware-override-headers"
|
|
22
|
+
]);
|
|
35
23
|
|
|
36
|
-
// src/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (value === null || typeof value !== "object") return false;
|
|
40
|
-
if ("nextUrl" in value) return false;
|
|
41
|
-
return "include" in value || "exclude" in value || "name" in value || "pathOptions" in value;
|
|
24
|
+
// src/core/context.ts
|
|
25
|
+
function createProxyContext() {
|
|
26
|
+
return /* @__PURE__ */ new Map();
|
|
42
27
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
28
|
+
|
|
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
|
|
51
112
|
});
|
|
113
|
+
continue;
|
|
52
114
|
}
|
|
53
|
-
|
|
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
|
+
// src/core/headers.ts
|
|
197
|
+
function normalizeHeaderName(name) {
|
|
198
|
+
return name.toLowerCase();
|
|
199
|
+
}
|
|
200
|
+
function headersInitToHeaders(init) {
|
|
201
|
+
return new Headers(init);
|
|
202
|
+
}
|
|
203
|
+
function headersDiffer(base, overrides) {
|
|
204
|
+
for (const [key, value] of overrides.entries()) {
|
|
205
|
+
if (base.get(key) !== value) return true;
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
function isRequestHeaderAllowed(name, policy = {}) {
|
|
210
|
+
const key = normalizeHeaderName(name);
|
|
211
|
+
const allowed = new Set((policy.allowed ?? []).map(normalizeHeaderName));
|
|
212
|
+
if (allowed.has(key)) return true;
|
|
213
|
+
if (key.startsWith("x-middleware-")) return false;
|
|
214
|
+
const blocked = /* @__PURE__ */ new Set([
|
|
215
|
+
...DEFAULT_BLOCKED_REQUEST_HEADERS.map(normalizeHeaderName),
|
|
216
|
+
...(policy.blocked ?? []).map(normalizeHeaderName)
|
|
217
|
+
]);
|
|
218
|
+
return !blocked.has(key);
|
|
219
|
+
}
|
|
220
|
+
function mergeRequestHeaderOverrides(target, overrides, policy = {}) {
|
|
221
|
+
const dropped = [];
|
|
222
|
+
let applied = 0;
|
|
223
|
+
overrides.forEach((value, key) => {
|
|
224
|
+
if (!isRequestHeaderAllowed(key, policy)) {
|
|
225
|
+
dropped.push(key);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
target.set(key, value);
|
|
229
|
+
applied++;
|
|
54
230
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
231
|
+
return { applied, dropped };
|
|
232
|
+
}
|
|
233
|
+
function parseCookieName(setCookie) {
|
|
234
|
+
const eq = setCookie.indexOf("=");
|
|
235
|
+
return (eq === -1 ? setCookie : setCookie.slice(0, eq)).trim();
|
|
236
|
+
}
|
|
237
|
+
function listSetCookie(headers) {
|
|
238
|
+
const extended = headers;
|
|
239
|
+
if (typeof extended.getSetCookie === "function") return extended.getSetCookie();
|
|
240
|
+
const single = headers.get("set-cookie");
|
|
241
|
+
return single ? [single] : [];
|
|
242
|
+
}
|
|
243
|
+
function cookieItemsFromHeaders(headers) {
|
|
244
|
+
const map = /* @__PURE__ */ new Map();
|
|
245
|
+
for (const value of listSetCookie(headers)) {
|
|
246
|
+
map.set(parseCookieName(value), value);
|
|
247
|
+
}
|
|
248
|
+
return map;
|
|
249
|
+
}
|
|
250
|
+
function responseHeaderItems(headers) {
|
|
251
|
+
const map = /* @__PURE__ */ new Map();
|
|
252
|
+
headers.forEach((value, key) => {
|
|
253
|
+
const lower = key.toLowerCase();
|
|
254
|
+
if (lower === "set-cookie") return;
|
|
255
|
+
if (lower.startsWith("x-middleware-next") || lower.startsWith("x-middleware-rewrite") || lower.startsWith("x-middleware-override-headers") || lower.startsWith("x-middleware-request-")) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
map.set(key, value);
|
|
58
259
|
});
|
|
59
|
-
return
|
|
260
|
+
return map;
|
|
60
261
|
}
|
|
61
|
-
function
|
|
62
|
-
const
|
|
262
|
+
function decodeOverriddenRequestHeaders(headers) {
|
|
263
|
+
const result = new Headers();
|
|
264
|
+
const overrideHeaderNames = headers.get("x-middleware-override-headers");
|
|
265
|
+
if (overrideHeaderNames) {
|
|
266
|
+
for (const name of overrideHeaderNames.split(",")) {
|
|
267
|
+
const trimmed = name.trim();
|
|
268
|
+
const val = headers.get(`x-middleware-request-${trimmed}`);
|
|
269
|
+
if (val !== null) {
|
|
270
|
+
result.set(trimmed, val);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
function mergeWrites(writes, strategy, onConflict) {
|
|
277
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
278
|
+
const setBy = /* @__PURE__ */ new Map();
|
|
279
|
+
for (const write of writes) {
|
|
280
|
+
for (const [key, value] of write.items) {
|
|
281
|
+
const existing = resolved.get(key);
|
|
282
|
+
if (existing === void 0) {
|
|
283
|
+
resolved.set(key, value);
|
|
284
|
+
setBy.set(key, write.ruleName);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const previousRule = setBy.get(key);
|
|
288
|
+
if (typeof strategy === "function") {
|
|
289
|
+
const result = strategy(key, existing, value);
|
|
290
|
+
if (result === null || result === void 0) resolved.delete(key);
|
|
291
|
+
else resolved.set(key, result);
|
|
292
|
+
setBy.set(key, write.ruleName);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (strategy === "first-write-wins") {
|
|
296
|
+
onConflict?.(key, previousRule, write.ruleName);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
onConflict?.(key, write.ruleName, previousRule);
|
|
300
|
+
resolved.set(key, value);
|
|
301
|
+
setBy.set(key, write.ruleName);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return resolved;
|
|
305
|
+
}
|
|
306
|
+
function applyMergedToResponse(res, headers, cookies) {
|
|
307
|
+
for (const [key, value] of headers) {
|
|
308
|
+
res.headers.set(key, value);
|
|
309
|
+
}
|
|
310
|
+
res.headers.delete("set-cookie");
|
|
311
|
+
for (const cookie of cookies.values()) {
|
|
312
|
+
res.headers.append("set-cookie", cookie);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
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
|
+
// src/core/logger.ts
|
|
325
|
+
var LEVEL_RANK = {
|
|
326
|
+
debug: 10,
|
|
327
|
+
info: 20,
|
|
328
|
+
warn: 30,
|
|
329
|
+
error: 40
|
|
330
|
+
};
|
|
331
|
+
function createConsoleLogger(minLevel = "info") {
|
|
332
|
+
const min = LEVEL_RANK[minLevel];
|
|
333
|
+
const write = (level) => (obj, msg) => {
|
|
334
|
+
if (LEVEL_RANK[level] < min) return;
|
|
335
|
+
if (typeof obj === "string") {
|
|
336
|
+
console[level](obj);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const message = msg ?? "";
|
|
340
|
+
if (message) console[level](`[proxy-chain] ${message}`, obj);
|
|
341
|
+
else console[level]("[proxy-chain]", obj);
|
|
342
|
+
};
|
|
63
343
|
return {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
ruleTimeoutMs: 0,
|
|
69
|
-
requestHeaderPolicy: {},
|
|
70
|
-
strictOverrideCheck: false,
|
|
71
|
-
...rest,
|
|
72
|
-
...debug !== void 0 ? { debug } : {},
|
|
73
|
-
logger: logger ?? createConsoleLogger(debug ? "debug" : "info")
|
|
344
|
+
debug: write("debug"),
|
|
345
|
+
info: write("info"),
|
|
346
|
+
warn: write("warn"),
|
|
347
|
+
error: write("error")
|
|
74
348
|
};
|
|
75
349
|
}
|
|
350
|
+
function formatDuration(ms) {
|
|
351
|
+
return `${ms.toFixed(2)}ms`;
|
|
352
|
+
}
|
|
353
|
+
function formatPipelineLine(input) {
|
|
354
|
+
const trace = input.traceId ? ` [${input.traceId.slice(0, 8)}]` : "";
|
|
355
|
+
const req = `${input.method} ${input.path}${trace}`;
|
|
356
|
+
const { name, outcome } = input;
|
|
357
|
+
switch (outcome.kind) {
|
|
358
|
+
case "passed":
|
|
359
|
+
return `[proxy-chain] \xB7 ${req} | ${name} (${formatDuration(outcome.duration)}) -> passed`;
|
|
360
|
+
case "next":
|
|
361
|
+
return `[proxy-chain] ~ ${req} | ${name} (${formatDuration(outcome.duration)}) -> headers mutated (${outcome.headers})`;
|
|
362
|
+
case "stop":
|
|
363
|
+
return `[proxy-chain] \xD7 ${req} | ${name} (${formatDuration(outcome.duration)}) -> stop ${outcome.status}`;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
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);
|
|
383
|
+
});
|
|
384
|
+
try {
|
|
385
|
+
return await Promise.race([promise, timeout]);
|
|
386
|
+
} finally {
|
|
387
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/core/pipeline.ts
|
|
392
|
+
import { NextResponse } from "next/server";
|
|
76
393
|
function safeInternalError() {
|
|
77
394
|
return new NextResponse("Internal Server Error", {
|
|
78
395
|
status: 500,
|
|
@@ -161,6 +478,57 @@ function resolveRuleResult(res, requestHeaderPolicy, logger, debug) {
|
|
|
161
478
|
}
|
|
162
479
|
return { kind: "stop", response: res };
|
|
163
480
|
}
|
|
481
|
+
function finalizeResponse(res, headerWrites, cookieWrites, headerStrategy, cookieStrategy, logger, debug) {
|
|
482
|
+
const headers = mergeWrites(headerWrites, headerStrategy, (key, winner, loser) => {
|
|
483
|
+
if (debug) logger.debug({ header: key, winner, loser }, "header conflict resolved");
|
|
484
|
+
});
|
|
485
|
+
const cookies = mergeWrites(cookieWrites, cookieStrategy, (key, winner, loser) => {
|
|
486
|
+
if (debug) logger.debug({ cookie: key, winner, loser }, "cookie conflict resolved");
|
|
487
|
+
});
|
|
488
|
+
applyMergedToResponse(res, headers, cookies);
|
|
489
|
+
return res;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/core/engine.ts
|
|
493
|
+
import { NextRequest, NextResponse as NextResponse2 } from "next/server";
|
|
494
|
+
function isPathFilter(value) {
|
|
495
|
+
if (value === null || typeof value !== "object") return false;
|
|
496
|
+
if ("nextUrl" in value) return false;
|
|
497
|
+
return "include" in value || "exclude" in value || "name" in value || "pathOptions" in value;
|
|
498
|
+
}
|
|
499
|
+
function defineProxy(nameOrRun, maybeRun) {
|
|
500
|
+
const defaultName = typeof nameOrRun === "string" ? nameOrRun : void 0;
|
|
501
|
+
const run = typeof nameOrRun === "function" ? nameOrRun : maybeRun;
|
|
502
|
+
const proxy = ((reqOrFilter, eventOrCtx, maybeEvent) => {
|
|
503
|
+
if (isPathFilter(reqOrFilter)) {
|
|
504
|
+
return withPaths(run, {
|
|
505
|
+
...reqOrFilter,
|
|
506
|
+
name: reqOrFilter.name ?? defaultName
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return run(reqOrFilter, eventOrCtx, maybeEvent);
|
|
510
|
+
});
|
|
511
|
+
Object.defineProperty(proxy, "name", {
|
|
512
|
+
value: defaultName ?? "",
|
|
513
|
+
configurable: true
|
|
514
|
+
});
|
|
515
|
+
return proxy;
|
|
516
|
+
}
|
|
517
|
+
function resolveConfig(overrides = {}) {
|
|
518
|
+
const { logger, debug, ...rest } = overrides;
|
|
519
|
+
return {
|
|
520
|
+
createContext: () => createProxyContext(),
|
|
521
|
+
cookieMergeStrategy: "last-write-wins",
|
|
522
|
+
headerMergeStrategy: "last-write-wins",
|
|
523
|
+
debug: false,
|
|
524
|
+
ruleTimeoutMs: 0,
|
|
525
|
+
requestHeaderPolicy: {},
|
|
526
|
+
strictOverrideCheck: false,
|
|
527
|
+
...rest,
|
|
528
|
+
...debug !== void 0 ? { debug } : {},
|
|
529
|
+
logger: logger ?? createConsoleLogger(debug ? "debug" : "info")
|
|
530
|
+
};
|
|
531
|
+
}
|
|
164
532
|
function proxyChain(optionsOrEntries, maybeEntries) {
|
|
165
533
|
const options = Array.isArray(optionsOrEntries) ? {} : optionsOrEntries;
|
|
166
534
|
const entries = Array.isArray(optionsOrEntries) ? optionsOrEntries : maybeEntries;
|
|
@@ -212,8 +580,9 @@ function proxyChain(optionsOrEntries, maybeEntries) {
|
|
|
212
580
|
const appliedRequestOverrides = new Headers();
|
|
213
581
|
const queueRequestOverrides = (overrides) => {
|
|
214
582
|
if (!pendingRequestOverrides) pendingRequestOverrides = new Headers();
|
|
215
|
-
|
|
583
|
+
const target = pendingRequestOverrides;
|
|
216
584
|
overrides.forEach((v, k) => {
|
|
585
|
+
target.set(k, v);
|
|
217
586
|
appliedRequestOverrides.set(k, v);
|
|
218
587
|
});
|
|
219
588
|
};
|
|
@@ -267,7 +636,7 @@ function proxyChain(optionsOrEntries, maybeEntries) {
|
|
|
267
636
|
})
|
|
268
637
|
);
|
|
269
638
|
}
|
|
270
|
-
return
|
|
639
|
+
return finalizeResponse(
|
|
271
640
|
recovered,
|
|
272
641
|
headerWrites,
|
|
273
642
|
cookieWrites,
|
|
@@ -364,7 +733,7 @@ function proxyChain(optionsOrEntries, maybeEntries) {
|
|
|
364
733
|
}
|
|
365
734
|
}
|
|
366
735
|
}
|
|
367
|
-
return
|
|
736
|
+
return finalizeResponse(
|
|
368
737
|
outcome.response,
|
|
369
738
|
headerWrites,
|
|
370
739
|
cookieWrites,
|
|
@@ -403,11 +772,11 @@ function proxyChain(optionsOrEntries, maybeEntries) {
|
|
|
403
772
|
let final;
|
|
404
773
|
if (finalRewriteUrl) {
|
|
405
774
|
const rewriteTarget = new URL(finalRewriteUrl, finalReq.url);
|
|
406
|
-
final =
|
|
775
|
+
final = NextResponse2.rewrite(rewriteTarget, { request: { headers: finalReq.headers } });
|
|
407
776
|
} else {
|
|
408
|
-
final =
|
|
777
|
+
final = NextResponse2.next({ request: { headers: finalReq.headers } });
|
|
409
778
|
}
|
|
410
|
-
return
|
|
779
|
+
return finalizeResponse(
|
|
411
780
|
final,
|
|
412
781
|
headerWrites,
|
|
413
782
|
cookieWrites,
|
|
@@ -429,16 +798,6 @@ function proxyChain(optionsOrEntries, maybeEntries) {
|
|
|
429
798
|
}
|
|
430
799
|
};
|
|
431
800
|
}
|
|
432
|
-
function finalize(res, headerWrites, cookieWrites, headerStrategy, cookieStrategy, logger, debug) {
|
|
433
|
-
const headers = mergeWrites(headerWrites, headerStrategy, (key, winner, loser) => {
|
|
434
|
-
if (debug) logger.debug({ header: key, winner, loser }, "header conflict resolved");
|
|
435
|
-
});
|
|
436
|
-
const cookies = mergeWrites(cookieWrites, cookieStrategy, (key, winner, loser) => {
|
|
437
|
-
if (debug) logger.debug({ cookie: key, winner, loser }, "cookie conflict resolved");
|
|
438
|
-
});
|
|
439
|
-
applyMergedToResponse(res, headers, cookies);
|
|
440
|
-
return res;
|
|
441
|
-
}
|
|
442
801
|
export {
|
|
443
802
|
DEFAULT_BLOCKED_REQUEST_HEADERS,
|
|
444
803
|
NEXT,
|
|
@@ -452,8 +811,10 @@ export {
|
|
|
452
811
|
createProxyContext,
|
|
453
812
|
decodeOverriddenRequestHeaders,
|
|
454
813
|
defineProxy,
|
|
814
|
+
finalizeResponse,
|
|
455
815
|
formatPipelineLine,
|
|
456
816
|
headersDiffer,
|
|
817
|
+
headersInitToHeaders,
|
|
457
818
|
host,
|
|
458
819
|
isNextResult,
|
|
459
820
|
isRequestHeaderAllowed,
|
|
@@ -466,6 +827,7 @@ export {
|
|
|
466
827
|
path,
|
|
467
828
|
pathMatches,
|
|
468
829
|
proxyChain,
|
|
830
|
+
resolveRuleResult,
|
|
469
831
|
responseHeaderItems,
|
|
470
832
|
ruleLabel,
|
|
471
833
|
ruleMatches,
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@klnap/next-proxy-chain",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Composable Next.js 16
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Composable request-chaining and proxy engine for Next.js 15 (middleware.ts) and Next.js 16 (proxy.ts) with deterministic header mutation, isolated per-request context, and pure Web API core.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "klnap",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"sideEffects": false,
|
|
9
9
|
"keywords": [
|
|
10
10
|
"nextjs",
|
|
11
|
+
"nextjs15",
|
|
11
12
|
"nextjs16",
|
|
12
13
|
"proxy",
|
|
13
14
|
"middleware",
|
|
@@ -26,10 +27,10 @@
|
|
|
26
27
|
"default": "./dist/index.js"
|
|
27
28
|
},
|
|
28
29
|
"./core": {
|
|
29
|
-
"types": "./dist/
|
|
30
|
-
"import": "./dist/
|
|
31
|
-
"require": "./dist/
|
|
32
|
-
"default": "./dist/
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js",
|
|
32
|
+
"require": "./dist/index.cjs",
|
|
33
|
+
"default": "./dist/index.js"
|
|
33
34
|
}
|
|
34
35
|
},
|
|
35
36
|
"main": "./dist/index.cjs",
|
|
@@ -39,23 +40,26 @@
|
|
|
39
40
|
"access": "public"
|
|
40
41
|
},
|
|
41
42
|
"scripts": {
|
|
42
|
-
"build": "tsup src/index.ts
|
|
43
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
43
44
|
"typecheck": "tsc -p tsconfig.json",
|
|
44
45
|
"test": "bun test",
|
|
45
46
|
"test:watch": "bun test --watch",
|
|
46
|
-
"clean": "rm -rf dist .turbo coverage
|
|
47
|
+
"clean": "rm -rf dist .turbo coverage",
|
|
48
|
+
"prepublishOnly": "bun run clean && bun run typecheck && bun test && bun run build"
|
|
47
49
|
},
|
|
48
50
|
"peerDependencies": {
|
|
49
|
-
"next": ">=15.0.0
|
|
51
|
+
"next": ">=15.0.0"
|
|
50
52
|
},
|
|
51
53
|
"devDependencies": {
|
|
54
|
+
"@types/bun": "^1.4.0",
|
|
52
55
|
"@types/node": "^20",
|
|
53
56
|
"next": "16.3.2",
|
|
54
57
|
"tsup": "^8.4.0",
|
|
55
|
-
"typescript": "^5"
|
|
58
|
+
"typescript": "^5",
|
|
59
|
+
"vitest": "^4.1.11"
|
|
56
60
|
},
|
|
57
61
|
"engines": {
|
|
58
|
-
"node": ">=
|
|
59
|
-
"bun": ">=1.
|
|
62
|
+
"node": ">=20.0.0",
|
|
63
|
+
"bun": ">=1.0.0"
|
|
60
64
|
}
|
|
61
65
|
}
|