vinext 1.0.0-beta.1 → 1.0.0-beta.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 +4 -1
- package/dist/build/client-build-config.js +1 -0
- package/dist/build/report.d.ts +8 -1
- package/dist/build/report.js +12 -2
- package/dist/check.js +15 -2
- package/dist/cli.js +3 -2
- package/dist/config/config-matchers.d.ts +13 -37
- package/dist/config/config-matchers.js +31 -121
- package/dist/config/next-config.d.ts +3 -2
- package/dist/config/next-config.js +3 -1
- package/dist/config/request-context.d.ts +14 -0
- package/dist/config/request-context.js +26 -0
- package/dist/entries/app-rsc-entry.js +27 -9
- package/dist/entries/app-rsc-manifest.js +17 -0
- package/dist/entries/pages-server-entry.js +3 -2
- package/dist/index.js +82 -58
- package/dist/init.d.ts +6 -4
- package/dist/init.js +20 -22
- package/dist/plugins/og-assets.js +1 -1
- package/dist/routing/app-route-graph.d.ts +13 -5
- package/dist/routing/app-route-graph.js +59 -13
- package/dist/routing/app-router.js +12 -6
- package/dist/routing/file-matcher.d.ts +11 -22
- package/dist/routing/file-matcher.js +21 -33
- package/dist/routing/pages-router.js +23 -29
- package/dist/routing/route-matching.d.ts +14 -1
- package/dist/routing/route-matching.js +13 -1
- package/dist/routing/route-pattern.d.ts +2 -1
- package/dist/routing/route-pattern.js +7 -4
- package/dist/routing/route-trie.d.ts +5 -1
- package/dist/routing/route-trie.js +5 -2
- package/dist/server/app-browser-entry.js +7 -6
- package/dist/server/app-browser-history-controller.d.ts +1 -1
- package/dist/server/app-browser-history-controller.js +3 -3
- package/dist/server/app-browser-navigation-controller.js +1 -1
- package/dist/server/app-browser-state.js +1 -1
- package/dist/server/app-browser-visible-commit.js +1 -1
- package/dist/server/app-fallback-renderer.d.ts +3 -0
- package/dist/server/app-fallback-renderer.js +4 -2
- package/dist/server/app-middleware.js +2 -1
- package/dist/server/app-page-boundary-render.d.ts +12 -1
- package/dist/server/app-page-boundary-render.js +84 -2
- package/dist/server/app-page-cache-finalizer.d.ts +1 -1
- package/dist/server/app-page-cache.d.ts +1 -1
- package/dist/server/app-page-dispatch.d.ts +11 -2
- package/dist/server/app-page-dispatch.js +14 -17
- package/dist/server/app-page-element-builder.d.ts +7 -2
- package/dist/server/app-page-element-builder.js +112 -27
- package/dist/server/app-page-head.d.ts +54 -2
- package/dist/server/app-page-head.js +164 -78
- package/dist/server/app-page-http-access-fallback-metadata.d.ts +29 -0
- package/dist/server/app-page-http-access-fallback-metadata.js +106 -0
- package/dist/server/app-page-render.d.ts +2 -2
- package/dist/server/app-page-render.js +1 -1
- package/dist/server/app-page-request.d.ts +12 -0
- package/dist/server/app-page-request.js +133 -13
- package/dist/server/app-page-route-wiring.d.ts +9 -0
- package/dist/server/app-page-route-wiring.js +43 -3
- package/dist/server/app-post-middleware-context.d.ts +1 -1
- package/dist/server/app-post-middleware-context.js +1 -1
- package/dist/server/app-route-handler-dispatch.js +31 -17
- package/dist/server/app-route-handler-execution.d.ts +6 -1
- package/dist/server/app-route-handler-execution.js +42 -16
- package/dist/server/app-route-handler-policy.d.ts +2 -0
- package/dist/server/app-route-handler-policy.js +3 -3
- package/dist/server/app-route-module-loader.d.ts +3 -1
- package/dist/server/app-route-module-loader.js +1 -0
- package/dist/server/app-rsc-cache-busting.d.ts +1 -1
- package/dist/server/app-rsc-errors.d.ts +1 -1
- package/dist/server/app-rsc-errors.js +12 -2
- package/dist/server/app-rsc-handler.d.ts +10 -5
- package/dist/server/app-rsc-handler.js +52 -24
- package/dist/server/app-rsc-request-normalization.d.ts +3 -2
- package/dist/server/app-rsc-request-normalization.js +8 -2
- package/dist/server/app-rsc-response-finalizer.d.ts +2 -2
- package/dist/server/app-rsc-response-finalizer.js +5 -11
- package/dist/server/app-rsc-route-matching.d.ts +20 -0
- package/dist/server/app-rsc-route-matching.js +87 -11
- package/dist/server/app-segment-config.js +13 -3
- package/dist/server/app-server-action-execution.d.ts +3 -1
- package/dist/server/app-server-action-execution.js +9 -7
- package/dist/server/app-ssr-entry.js +2 -2
- package/dist/server/app-ssr-stream.d.ts +1 -7
- package/dist/server/app-ssr-stream.js +2 -10
- package/dist/server/config-headers.d.ts +24 -0
- package/dist/server/config-headers.js +52 -0
- package/dist/server/dev-initial-server-error.d.ts +1 -1
- package/dist/server/dev-server.js +29 -13
- package/dist/server/isr-cache.d.ts +1 -1
- package/dist/server/middleware-matcher-pattern.d.ts +22 -0
- package/dist/server/middleware-matcher-pattern.js +219 -0
- package/dist/server/middleware-matcher.d.ts +2 -7
- package/dist/server/middleware-matcher.js +13 -60
- package/dist/server/middleware-path-to-regexp.d.ts +14 -0
- package/dist/server/middleware-path-to-regexp.js +228 -0
- package/dist/server/middleware-runtime.js +4 -3
- package/dist/server/middleware.d.ts +1 -1
- package/dist/server/middleware.js +2 -1
- package/dist/server/pages-data-route.d.ts +11 -1
- package/dist/server/pages-data-route.js +16 -1
- package/dist/server/pages-get-initial-props.d.ts +15 -2
- package/dist/server/pages-get-initial-props.js +16 -6
- package/dist/server/pages-page-data.d.ts +3 -1
- package/dist/server/pages-page-data.js +16 -7
- package/dist/server/pages-page-handler.d.ts +4 -2
- package/dist/server/pages-page-handler.js +16 -17
- package/dist/server/pages-page-response.js +1 -1
- package/dist/server/pages-readiness.d.ts +3 -3
- package/dist/server/pages-request-pipeline.d.ts +1 -0
- package/dist/server/pages-request-pipeline.js +25 -11
- package/dist/server/pages-router-entry.js +6 -0
- package/dist/server/prerender-route-params.js +2 -2
- package/dist/server/prod-server.d.ts +1 -4
- package/dist/server/prod-server.js +36 -23
- package/dist/server/request-pipeline.d.ts +15 -29
- package/dist/server/request-pipeline.js +24 -59
- package/dist/server/rsc-stream-hints.js +122 -18
- package/dist/shims/app.d.ts +1 -1
- package/dist/shims/cache-request-state.d.ts +27 -1
- package/dist/shims/cache-request-state.js +60 -1
- package/dist/shims/cache-runtime.d.ts +12 -1
- package/dist/shims/cache-runtime.js +22 -7
- package/dist/shims/cache.d.ts +5 -5
- package/dist/shims/cache.js +26 -10
- package/dist/shims/document.d.ts +42 -83
- package/dist/shims/document.js +62 -53
- package/dist/shims/dynamic.d.ts +5 -25
- package/dist/shims/dynamic.js +29 -8
- package/dist/shims/error.d.ts +1 -1
- package/dist/shims/error.js +1 -1
- package/dist/shims/fetch-cache.js +12 -3
- package/dist/shims/font-google-base.d.ts +11 -4
- package/dist/shims/font-local.d.ts +10 -3
- package/dist/shims/head.d.ts +13 -2
- package/dist/shims/head.js +3 -3
- package/dist/shims/headers.d.ts +28 -31
- package/dist/shims/headers.js +109 -49
- package/dist/shims/image.d.ts +27 -37
- package/dist/shims/image.js +21 -14
- package/dist/shims/internal/cookie-serialize.d.ts +3 -1
- package/dist/shims/internal/cookie-serialize.js +3 -1
- package/dist/shims/internal/hybrid-client-route-owner.js +3 -1
- package/dist/shims/internal/utils.d.ts +3 -3
- package/dist/shims/legacy-image.d.ts +3 -37
- package/dist/shims/legacy-image.js +6 -16
- package/dist/shims/link.d.ts +15 -22
- package/dist/shims/link.js +20 -11
- package/dist/shims/navigation-context-state.d.ts +1 -1
- package/dist/shims/navigation.d.ts +7 -3
- package/dist/shims/navigation.js +24 -13
- package/dist/shims/navigation.react-server.js +1 -1
- package/dist/shims/next-shims-augmentations.d.ts +1 -0
- package/dist/shims/next-shims-public.d.ts +1 -0
- package/dist/shims/public-shim-map.json.js +103 -0
- package/dist/shims/readonly-url-search-params.d.ts +3 -3
- package/dist/shims/readonly-url-search-params.js +3 -3
- package/dist/shims/request-state-types.d.ts +1 -1
- package/dist/shims/router.d.ts +55 -21
- package/dist/shims/router.js +162 -37
- package/dist/shims/script.d.ts +6 -10
- package/dist/shims/script.js +6 -3
- package/dist/shims/server.d.ts +45 -22
- package/dist/shims/server.js +44 -5
- package/dist/shims/unified-request-context.d.ts +1 -1
- package/dist/shims/unified-request-context.js +2 -0
- package/dist/shims/url-safety.d.ts +1 -1
- package/dist/shims/url-safety.js +2 -2
- package/dist/typegen.d.ts +8 -2
- package/dist/typegen.js +76 -15
- package/dist/utils/domain-locale.d.ts +2 -2
- package/dist/utils/external-url.d.ts +5 -0
- package/dist/utils/external-url.js +7 -0
- package/dist/utils/regex-safety.d.ts +8 -0
- package/dist/utils/regex-safety.js +737 -0
- package/package.json +13 -2
|
@@ -4,6 +4,7 @@ import { createValidFileMatcher, findFileWithExtensions, findFileWithExts } from
|
|
|
4
4
|
import { patternToNextFormat } from "../routing/route-validation.js";
|
|
5
5
|
import { matchRoute } from "../routing/pages-router.js";
|
|
6
6
|
import { NEXTJS_DEPLOYMENT_ID_HEADER } from "./headers.js";
|
|
7
|
+
import { isDangerousScheme } from "../shims/url-safety.js";
|
|
7
8
|
import { importModule, reportRequestError } from "./instrumentation.js";
|
|
8
9
|
import { buildCacheStateHeaders } from "./cache-headers.js";
|
|
9
10
|
import { isUnknownRecord } from "../utils/record.js";
|
|
@@ -175,6 +176,17 @@ function writeGsspRedirect(res, redirect, isDataReq, props) {
|
|
|
175
176
|
const status = redirect.statusCode ?? (redirect.permanent ? 308 : 307);
|
|
176
177
|
let dest = redirect.destination;
|
|
177
178
|
if (!dest.startsWith("http://") && !dest.startsWith("https://")) dest = dest.replace(/^[\\/]+/, "/");
|
|
179
|
+
if (isDangerousScheme(dest)) {
|
|
180
|
+
const deploymentId = process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID;
|
|
181
|
+
const headers = {
|
|
182
|
+
"Cache-Control": "private, no-cache, no-store, max-age=0, must-revalidate",
|
|
183
|
+
"Content-Type": "text/plain; charset=utf-8"
|
|
184
|
+
};
|
|
185
|
+
if (deploymentId) headers[NEXTJS_DEPLOYMENT_ID_HEADER] = deploymentId;
|
|
186
|
+
res.writeHead(500, headers);
|
|
187
|
+
res.end("Invalid redirect destination");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
178
190
|
if (isDataReq) {
|
|
179
191
|
const deploymentId = process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID;
|
|
180
192
|
const dataHeaders = { "Content-Type": "application/json" };
|
|
@@ -376,9 +388,9 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
|
|
|
376
388
|
const parsedResolvedUrl = new URL(localeStrippedUrl, "http://vinext.local");
|
|
377
389
|
const originalRequestSearch = new URL(originalUrl, "http://vinext.local").search;
|
|
378
390
|
const gsspResolvedUrl = parsedResolvedUrl.pathname + originalRequestSearch;
|
|
379
|
-
|
|
391
|
+
let requestAsPath = isDataReq ? gsspResolvedUrl : i18nConfig ? extractLocaleFromUrl$1(originalUrl, i18nConfig, locale).url : originalUrl;
|
|
380
392
|
const userFacingParams = route.isDynamic ? params : null;
|
|
381
|
-
|
|
393
|
+
let query = mergeRouteParamsIntoQuery(parseQueryString(url), params);
|
|
382
394
|
return runWithRequestContext(createRequestContext(), async () => {
|
|
383
395
|
ensureFetchPatch();
|
|
384
396
|
try {
|
|
@@ -396,6 +408,11 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
|
|
|
396
408
|
});
|
|
397
409
|
}
|
|
398
410
|
const pageModule = await importModule(runner, route.filePath);
|
|
411
|
+
const isStaticPropsRender = typeof pageModule.getStaticProps === "function" && typeof pageModule.getServerSideProps !== "function";
|
|
412
|
+
if (isStaticPropsRender) {
|
|
413
|
+
query = mergeRouteParamsIntoQuery({}, params);
|
|
414
|
+
requestAsPath = localeStrippedUrl.split("?")[0];
|
|
415
|
+
}
|
|
399
416
|
const requestPreview = typeof pageModule.getStaticProps === "function" || typeof pageModule.getServerSideProps === "function" ? getPagesPreviewState(req.headers.cookie, { isOnDemandRevalidate: isOnDemandRevalidateRequest(req.headers[PRERENDER_REVALIDATE_HEADER]) }) : {
|
|
400
417
|
data: false,
|
|
401
418
|
shouldClear: false
|
|
@@ -414,7 +431,7 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
|
|
|
414
431
|
}),
|
|
415
432
|
...requestPreviewData === false ? {} : { isPreview: true }
|
|
416
433
|
};
|
|
417
|
-
const navigationIsReady = typeof routerShim.getPagesNavigationIsReadyFromSerializedState === "function" ? routerShim.getPagesNavigationIsReadyFromSerializedState(patternToNextFormat(route.pattern), new URL(url, "http://_").search, pagesNextData) : true;
|
|
434
|
+
const navigationIsReady = isStaticPropsRender ? false : typeof routerShim.getPagesNavigationIsReadyFromSerializedState === "function" ? routerShim.getPagesNavigationIsReadyFromSerializedState(patternToNextFormat(route.pattern), new URL(url, "http://_").search, pagesNextData) : true;
|
|
418
435
|
if (typeof routerShim.setSSRContext === "function") routerShim.setSSRContext({
|
|
419
436
|
pathname: patternToNextFormat(route.pattern),
|
|
420
437
|
query,
|
|
@@ -487,8 +504,8 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
|
|
|
487
504
|
isFallbackRender = (await isrGet(pagesIsrCacheKey(url.split("?")[0])))?.value.value?.kind !== "PAGES";
|
|
488
505
|
if (isFallbackRender && typeof routerShim.setSSRContext === "function") routerShim.setSSRContext({
|
|
489
506
|
pathname: patternToNextFormat(route.pattern),
|
|
490
|
-
query,
|
|
491
|
-
asPath:
|
|
507
|
+
query: {},
|
|
508
|
+
asPath: patternToNextFormat(route.pattern),
|
|
492
509
|
navigationIsReady: false,
|
|
493
510
|
locale: locale ?? currentDefaultLocale,
|
|
494
511
|
locales: i18nConfig?.locales,
|
|
@@ -520,6 +537,7 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
|
|
|
520
537
|
pathname: patternToNextFormat(route.pattern),
|
|
521
538
|
query,
|
|
522
539
|
asPath: requestAsPath,
|
|
540
|
+
router: routerShim.default,
|
|
523
541
|
locale: locale ?? currentDefaultLocale,
|
|
524
542
|
locales: i18nConfig?.locales,
|
|
525
543
|
defaultLocale: currentDefaultLocale
|
|
@@ -692,11 +710,7 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
|
|
|
692
710
|
return typeof routerShim.wrapWithRouterContext === "function" ? routerShim.wrapWithRouterContext(appTree) : appTree;
|
|
693
711
|
},
|
|
694
712
|
Component: pageModule.default,
|
|
695
|
-
router:
|
|
696
|
-
pathname: patternToNextFormat(route.pattern),
|
|
697
|
-
query,
|
|
698
|
-
asPath: requestAsPath
|
|
699
|
-
},
|
|
713
|
+
router: routerShim.default,
|
|
700
714
|
ctx: {
|
|
701
715
|
req: regenReq,
|
|
702
716
|
res: regenRes,
|
|
@@ -771,7 +785,8 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
|
|
|
771
785
|
...pagesNextData.__vinext,
|
|
772
786
|
pageModuleUrl: regenPageUrl,
|
|
773
787
|
appModuleUrl: regenAppUrl,
|
|
774
|
-
hasMiddleware
|
|
788
|
+
hasMiddleware,
|
|
789
|
+
routeUrl: requestAsPath
|
|
775
790
|
}
|
|
776
791
|
};
|
|
777
792
|
const freshHtml = `<!DOCTYPE html><html><head></head><body><div id="__next">${freshBody}</div>${`<script id="__NEXT_DATA__" type="application/json">${safeJsonStringify({
|
|
@@ -966,7 +981,8 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
|
|
|
966
981
|
...pagesNextData.__vinext,
|
|
967
982
|
pageModuleUrl,
|
|
968
983
|
appModuleUrl,
|
|
969
|
-
hasMiddleware
|
|
984
|
+
hasMiddleware,
|
|
985
|
+
routeUrl: requestAsPath
|
|
970
986
|
}
|
|
971
987
|
};
|
|
972
988
|
const hydrationScript = createPagesDevHydrationScript({
|
|
@@ -979,7 +995,7 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
|
|
|
979
995
|
const nextDataScript = `<script id="__NEXT_DATA__" type="application/json"${nonceAttr}>${safeJsonStringify({
|
|
980
996
|
props: renderProps,
|
|
981
997
|
page: patternToNextFormat(route.pattern),
|
|
982
|
-
query: params,
|
|
998
|
+
query: isFallbackRender ? {} : params,
|
|
983
999
|
buildId: process.env.__VINEXT_BUILD_ID,
|
|
984
1000
|
isFallback: isFallbackRender,
|
|
985
1001
|
locale: locale ?? currentDefaultLocale,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { OnRequestErrorContext } from "./instrumentation.js";
|
|
2
2
|
import { RenderObservation } from "./cache-proof.js";
|
|
3
|
+
import { AppRscRenderMode } from "./app-rsc-render-mode.js";
|
|
3
4
|
import { normalizeMountedSlotsHeader } from "./app-mounted-slots-header.js";
|
|
4
5
|
import { CacheHandlerValue, CachedAppPageValue, CachedPagesValue, IncrementalCacheValue } from "../shims/cache-handler.js";
|
|
5
|
-
import { AppRscRenderMode } from "./app-rsc-render-mode.js";
|
|
6
6
|
|
|
7
7
|
//#region src/server/isr-cache.d.ts
|
|
8
8
|
/**
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { HasCondition } from "../config/next-config.js";
|
|
2
|
+
|
|
3
|
+
//#region src/server/middleware-matcher-pattern.d.ts
|
|
4
|
+
type CompiledMiddlewareMatcherPattern = {
|
|
5
|
+
regexp: RegExp;
|
|
6
|
+
error?: never;
|
|
7
|
+
} | {
|
|
8
|
+
regexp?: never;
|
|
9
|
+
error: string;
|
|
10
|
+
kind: "invalid" | "unsafe";
|
|
11
|
+
};
|
|
12
|
+
type MiddlewareMatcherObject = {
|
|
13
|
+
source: string;
|
|
14
|
+
locale?: false;
|
|
15
|
+
has?: HasCondition[];
|
|
16
|
+
missing?: HasCondition[];
|
|
17
|
+
};
|
|
18
|
+
declare function isValidMiddlewareMatcherObjectConfig(value: unknown): value is MiddlewareMatcherObject;
|
|
19
|
+
declare function compileMiddlewareMatcherPattern(source: string): CompiledMiddlewareMatcherPattern;
|
|
20
|
+
declare function validateMiddlewareMatcherPatterns(value: unknown): void;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { CompiledMiddlewareMatcherPattern, MiddlewareMatcherObject, compileMiddlewareMatcherPattern, isValidMiddlewareMatcherObjectConfig, validateMiddlewareMatcherPatterns };
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { analyzeRegexSafety, regexAtomsMayOverlap } from "../utils/regex-safety.js";
|
|
2
|
+
import { middlewarePathTokensToRegExp, normalizeMiddlewarePathTokens, parseMiddlewarePath } from "./middleware-path-to-regexp.js";
|
|
3
|
+
//#region src/server/middleware-matcher-pattern.ts
|
|
4
|
+
const MATCHER_OBJECT_KEYS = /* @__PURE__ */ new Set([
|
|
5
|
+
"source",
|
|
6
|
+
"locale",
|
|
7
|
+
"has",
|
|
8
|
+
"missing"
|
|
9
|
+
]);
|
|
10
|
+
const CONDITION_TYPES_WITH_KEY = /* @__PURE__ */ new Set([
|
|
11
|
+
"header",
|
|
12
|
+
"query",
|
|
13
|
+
"cookie"
|
|
14
|
+
]);
|
|
15
|
+
function invalidConditionReason(value) {
|
|
16
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "has and missing entries must be objects";
|
|
17
|
+
const condition = value;
|
|
18
|
+
const type = condition.type;
|
|
19
|
+
if (typeof type === "string" && CONDITION_TYPES_WITH_KEY.has(type)) {
|
|
20
|
+
for (const key of Object.keys(condition)) if (key !== "type" && key !== "key" && key !== "value") return `condition contains unsupported field "${key}"`;
|
|
21
|
+
if (typeof condition.key !== "string") return `condition type "${type}" requires a string key`;
|
|
22
|
+
if (condition.value !== void 0 && typeof condition.value !== "string") return `condition type "${type}" requires value to be a string`;
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (type === "host") {
|
|
26
|
+
for (const key of Object.keys(condition)) if (key !== "type" && key !== "value") return `host condition contains unsupported field "${key}"`;
|
|
27
|
+
return typeof condition.value === "string" ? null : "host condition requires a string value";
|
|
28
|
+
}
|
|
29
|
+
return "condition type must be header, query, cookie, or host";
|
|
30
|
+
}
|
|
31
|
+
function invalidConditionsReason(value, field) {
|
|
32
|
+
if (value === void 0) return null;
|
|
33
|
+
if (!Array.isArray(value)) return `${field} must be an array`;
|
|
34
|
+
for (const condition of value) {
|
|
35
|
+
const reason = invalidConditionReason(condition);
|
|
36
|
+
if (reason) return `${field} ${reason}`;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function matcherObjectSource(value) {
|
|
41
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { error: "matcher entries must be strings or objects" };
|
|
42
|
+
const matcher = value;
|
|
43
|
+
for (const key of Object.keys(matcher)) if (!MATCHER_OBJECT_KEYS.has(key)) return { error: `matcher object contains unsupported field "${key}"` };
|
|
44
|
+
if (typeof matcher.source !== "string") return { error: "matcher object requires a string source" };
|
|
45
|
+
if (matcher.locale !== void 0 && matcher.locale !== false) return { error: "matcher object locale must be false when provided" };
|
|
46
|
+
const invalidHas = invalidConditionsReason(matcher.has, "has");
|
|
47
|
+
if (invalidHas) return { error: invalidHas };
|
|
48
|
+
const invalidMissing = invalidConditionsReason(matcher.missing, "missing");
|
|
49
|
+
if (invalidMissing) return { error: invalidMissing };
|
|
50
|
+
return { source: matcher.source };
|
|
51
|
+
}
|
|
52
|
+
function isValidMiddlewareMatcherObjectConfig(value) {
|
|
53
|
+
return matcherObjectSource(value).error === void 0;
|
|
54
|
+
}
|
|
55
|
+
function patternMatches(pattern, value) {
|
|
56
|
+
try {
|
|
57
|
+
return new RegExp(`^(?:${pattern})$`).test(value);
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function atomsOverlap(left, right) {
|
|
63
|
+
return regexAtomsMayOverlap(left, right, true);
|
|
64
|
+
}
|
|
65
|
+
function groupCanMatchEmpty(group) {
|
|
66
|
+
if (/^\?(?:[=!]|<[=!])/.test(group)) return true;
|
|
67
|
+
return patternMatches(group.startsWith("?:") ? group.slice(2) : group, "");
|
|
68
|
+
}
|
|
69
|
+
function hasOverlappingSequentialRepetition(pattern) {
|
|
70
|
+
const repeatedAtDepth = [[]];
|
|
71
|
+
const groupStarts = [];
|
|
72
|
+
let depth = 0;
|
|
73
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
74
|
+
const character = pattern[index];
|
|
75
|
+
if (character === "(") {
|
|
76
|
+
groupStarts.push(index);
|
|
77
|
+
depth++;
|
|
78
|
+
repeatedAtDepth[depth] = [];
|
|
79
|
+
if (pattern[index + 1] === "?") {
|
|
80
|
+
index++;
|
|
81
|
+
if (pattern[index + 1] === "<" && /[=!]/.test(pattern[index + 2] ?? "")) index += 2;
|
|
82
|
+
else if (/[=:!]/.test(pattern[index + 1] ?? "")) index++;
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (character === ")") {
|
|
87
|
+
const groupStart = groupStarts.pop();
|
|
88
|
+
repeatedAtDepth[depth] = [];
|
|
89
|
+
depth = Math.max(0, depth - 1);
|
|
90
|
+
if (groupStart !== void 0) {
|
|
91
|
+
const modifier = pattern[index + 1];
|
|
92
|
+
if (!(modifier === "*" || modifier === "?" || modifier === "{" && /^\{0(?:,\d*)?\}/.test(pattern.slice(index + 1))) && !groupCanMatchEmpty(pattern.slice(groupStart + 1, index))) repeatedAtDepth[depth] = [];
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (character === "|") {
|
|
97
|
+
repeatedAtDepth[depth] = [];
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (character === "^" || character === "$") continue;
|
|
101
|
+
if (character === "*" || character === "+" || character === "?" || character === "{") continue;
|
|
102
|
+
let atom = character;
|
|
103
|
+
if (character === "\\") {
|
|
104
|
+
if (index + 1 >= pattern.length) return true;
|
|
105
|
+
atom += pattern[++index];
|
|
106
|
+
} else if (character === "[") {
|
|
107
|
+
let classEnd = index + 1;
|
|
108
|
+
if (pattern[classEnd] === "^") classEnd++;
|
|
109
|
+
while (classEnd < pattern.length && pattern[classEnd] !== "]") {
|
|
110
|
+
if (pattern[classEnd] === "\\") classEnd++;
|
|
111
|
+
classEnd++;
|
|
112
|
+
}
|
|
113
|
+
if (classEnd >= pattern.length) return true;
|
|
114
|
+
atom = pattern.slice(index, classEnd + 1);
|
|
115
|
+
index = classEnd;
|
|
116
|
+
} else if (character === ".") atom = ".";
|
|
117
|
+
const quantifierStart = index + 1;
|
|
118
|
+
const quantifier = pattern[quantifierStart];
|
|
119
|
+
let unbounded = quantifier === "*" || quantifier === "+";
|
|
120
|
+
let canBeEmpty = quantifier === "*";
|
|
121
|
+
let quantifierEnd = quantifierStart;
|
|
122
|
+
if (quantifier === "{") {
|
|
123
|
+
const match = /^\{(\d+),\}/.exec(pattern.slice(quantifierStart));
|
|
124
|
+
if (match) {
|
|
125
|
+
unbounded = true;
|
|
126
|
+
canBeEmpty = Number(match[1]) === 0;
|
|
127
|
+
quantifierEnd += match[0].length - 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!unbounded) {
|
|
131
|
+
if (quantifier !== "?") repeatedAtDepth[depth] = [];
|
|
132
|
+
if (quantifier === "?") index = quantifierStart;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (repeatedAtDepth[depth].some((previous) => atomsOverlap(previous, atom))) return true;
|
|
136
|
+
repeatedAtDepth[depth] = canBeEmpty ? [...repeatedAtDepth[depth], atom] : [atom];
|
|
137
|
+
index = quantifierEnd;
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
function unsafeTokenReason(token) {
|
|
142
|
+
const regexSafetyIssue = analyzeRegexSafety(token.pattern, { ignoreCase: true });
|
|
143
|
+
if (regexSafetyIssue) {
|
|
144
|
+
if (regexSafetyIssue === "analysis budget exceeded") return `parameter "${token.name}" exceeds the regex analysis budget`;
|
|
145
|
+
return `parameter "${token.name}" contains ${regexSafetyIssue}`;
|
|
146
|
+
}
|
|
147
|
+
if (hasOverlappingSequentialRepetition(token.pattern)) return `parameter "${token.name}" contains overlapping sequential repetition`;
|
|
148
|
+
if (token.modifier !== "*" && token.modifier !== "+") return null;
|
|
149
|
+
if (patternMatches(token.pattern, "") || patternMatches(token.pattern, "/")) return `repeated parameter "${token.name}" may match an empty value or path delimiter`;
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
function validateTokens(tokens) {
|
|
153
|
+
for (const token of tokens) {
|
|
154
|
+
if (typeof token === "string") continue;
|
|
155
|
+
const reason = unsafeTokenReason(token);
|
|
156
|
+
if (reason) return reason;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
function compileMiddlewareMatcherPattern(source) {
|
|
161
|
+
if (!source.startsWith("/")) return {
|
|
162
|
+
kind: "invalid",
|
|
163
|
+
error: "source must start with /"
|
|
164
|
+
};
|
|
165
|
+
if (source.length > 4096) return {
|
|
166
|
+
kind: "invalid",
|
|
167
|
+
error: "source exceeds max built length of 4096"
|
|
168
|
+
};
|
|
169
|
+
let tokens;
|
|
170
|
+
try {
|
|
171
|
+
tokens = parseMiddlewarePath(source);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return {
|
|
174
|
+
kind: "invalid",
|
|
175
|
+
error: error instanceof Error ? error.message : "matcher could not be parsed"
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const unsafeReason = validateTokens(tokens);
|
|
179
|
+
if (unsafeReason) return {
|
|
180
|
+
kind: "unsafe",
|
|
181
|
+
error: unsafeReason
|
|
182
|
+
};
|
|
183
|
+
try {
|
|
184
|
+
return { regexp: middlewarePathTokensToRegExp(tokens) };
|
|
185
|
+
} catch {
|
|
186
|
+
const normalizedTokens = normalizeMiddlewarePathTokens(tokens);
|
|
187
|
+
const normalizedUnsafeReason = validateTokens(normalizedTokens);
|
|
188
|
+
if (normalizedUnsafeReason) return {
|
|
189
|
+
kind: "unsafe",
|
|
190
|
+
error: normalizedUnsafeReason
|
|
191
|
+
};
|
|
192
|
+
try {
|
|
193
|
+
return { regexp: middlewarePathTokensToRegExp(normalizedTokens) };
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return {
|
|
196
|
+
kind: "invalid",
|
|
197
|
+
error: error instanceof Error ? error.message : "matcher could not be compiled"
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function validateMiddlewareMatcherPatterns(value) {
|
|
203
|
+
const sources = [];
|
|
204
|
+
if (typeof value === "string") sources.push(value);
|
|
205
|
+
else if (Array.isArray(value)) for (const matcher of value) if (typeof matcher === "string") sources.push(matcher);
|
|
206
|
+
else {
|
|
207
|
+
const result = matcherObjectSource(matcher);
|
|
208
|
+
if (result.error) throw new Error(`Invalid middleware matcher config: ${result.error}.`);
|
|
209
|
+
sources.push(result.source);
|
|
210
|
+
}
|
|
211
|
+
else throw new Error("Invalid middleware matcher config: matcher must be a string or an array of strings or objects.");
|
|
212
|
+
for (const source of sources) {
|
|
213
|
+
const result = compileMiddlewareMatcherPattern(source);
|
|
214
|
+
if (result.regexp) continue;
|
|
215
|
+
throw new Error(`Invalid middleware matcher "${source}": ${result.error}.`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
export { compileMiddlewareMatcherPattern, isValidMiddlewareMatcherObjectConfig, validateMiddlewareMatcherPatterns };
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NextI18nConfig } from "../config/next-config.js";
|
|
2
|
+
import { MiddlewareMatcherObject } from "./middleware-matcher-pattern.js";
|
|
2
3
|
|
|
3
4
|
//#region src/server/middleware-matcher.d.ts
|
|
4
|
-
type MiddlewareMatcherObject = {
|
|
5
|
-
source: string;
|
|
6
|
-
locale?: false;
|
|
7
|
-
has?: HasCondition[];
|
|
8
|
-
missing?: HasCondition[];
|
|
9
|
-
};
|
|
10
5
|
type MatcherConfig = string | Array<string | MiddlewareMatcherObject>;
|
|
11
6
|
declare function matchesMiddleware(pathname: string, matcher: MatcherConfig | undefined, request?: Request, i18nConfig?: NextI18nConfig | null): boolean;
|
|
12
7
|
declare function matchPattern(pathname: string, pattern: string): boolean;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { removeTrailingSlash } from "../utils/base-path.js";
|
|
2
|
-
import {
|
|
2
|
+
import { requestContextFromRequest } from "../config/request-context.js";
|
|
3
|
+
import { checkHasConditions } from "../config/config-matchers.js";
|
|
4
|
+
import { compileMiddlewareMatcherPattern, isValidMiddlewareMatcherObjectConfig } from "./middleware-matcher-pattern.js";
|
|
3
5
|
//#region src/server/middleware-matcher.ts
|
|
4
6
|
const EMPTY_MIDDLEWARE_REQUEST_CONTEXT = {
|
|
5
7
|
headers: new Headers(),
|
|
@@ -12,30 +14,20 @@ const _mwPatternCache = /* @__PURE__ */ new Map();
|
|
|
12
14
|
function matchesMiddleware(pathname, matcher, request, i18nConfig) {
|
|
13
15
|
if (!matcher) return true;
|
|
14
16
|
if (typeof matcher === "string") return matchMatcherPattern(pathname, matcher, i18nConfig);
|
|
15
|
-
if (!Array.isArray(matcher)) return
|
|
17
|
+
if (!Array.isArray(matcher)) return true;
|
|
16
18
|
const requestContext = request ? requestContextFromRequest(request) : EMPTY_MIDDLEWARE_REQUEST_CONTEXT;
|
|
17
19
|
for (const m of matcher) {
|
|
18
20
|
if (typeof m === "string") {
|
|
19
21
|
if (matchMatcherPattern(pathname, m, i18nConfig)) return true;
|
|
20
22
|
continue;
|
|
21
23
|
}
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
24
|
+
if (!isValidMiddlewareMatcherObjectConfig(m)) return true;
|
|
25
|
+
if (!matchObjectMatcher(pathname, m, i18nConfig)) continue;
|
|
26
|
+
if (!checkHasConditions(m.has, m.missing, requestContext)) continue;
|
|
27
|
+
return true;
|
|
27
28
|
}
|
|
28
29
|
return false;
|
|
29
30
|
}
|
|
30
|
-
function isValidMiddlewareMatcherObject(value) {
|
|
31
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
32
|
-
if (!("source" in value) || typeof value.source !== "string") return false;
|
|
33
|
-
for (const key of Object.keys(value)) if (key !== "source" && key !== "locale" && key !== "has" && key !== "missing") return false;
|
|
34
|
-
if ("locale" in value && value.locale !== void 0 && value.locale !== false) return false;
|
|
35
|
-
if ("has" in value && value.has !== void 0 && !Array.isArray(value.has)) return false;
|
|
36
|
-
if ("missing" in value && value.missing !== void 0 && !Array.isArray(value.missing)) return false;
|
|
37
|
-
return true;
|
|
38
|
-
}
|
|
39
31
|
function matchMatcherPattern(pathname, pattern, i18nConfig) {
|
|
40
32
|
if (!i18nConfig) return matchPattern(pathname, pattern);
|
|
41
33
|
return matchPattern(stripLocalePrefix(pathname, i18nConfig) ?? pathname, pattern);
|
|
@@ -58,54 +50,15 @@ function matchPattern(pathname, pattern) {
|
|
|
58
50
|
_mwPatternCache.set(normalizedPattern, cached);
|
|
59
51
|
}
|
|
60
52
|
if (cached === UNSAFE_MATCHER_PATTERN) return true;
|
|
61
|
-
if (cached === null) return removeTrailingSlash(pathname) === normalizedPattern;
|
|
62
53
|
if (cached.test(pathname)) return true;
|
|
63
54
|
return pathname.endsWith("/") && cached.test(removeTrailingSlash(pathname));
|
|
64
55
|
}
|
|
65
|
-
function extractConstraint(str, re) {
|
|
66
|
-
if (str[re.lastIndex] !== "(") return null;
|
|
67
|
-
const start = re.lastIndex + 1;
|
|
68
|
-
let depth = 1;
|
|
69
|
-
let i = start;
|
|
70
|
-
while (i < str.length && depth > 0) {
|
|
71
|
-
if (str[i] === "(") depth++;
|
|
72
|
-
else if (str[i] === ")") depth--;
|
|
73
|
-
i++;
|
|
74
|
-
}
|
|
75
|
-
if (depth !== 0) return null;
|
|
76
|
-
re.lastIndex = i;
|
|
77
|
-
return str.slice(start, i - 1);
|
|
78
|
-
}
|
|
79
56
|
function compileMatcherPattern(pattern) {
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
while ((tok = tokenRe.exec(pattern)) !== null) if (tok[1] !== void 0) {
|
|
86
|
-
const constraint = hasConstraints ? extractConstraint(pattern, tokenRe) : null;
|
|
87
|
-
regexStr += constraint !== null ? `(?:/(${constraint}))?` : "(?:/.*)?";
|
|
88
|
-
} else if (tok[2] !== void 0) {
|
|
89
|
-
const constraint = hasConstraints ? extractConstraint(pattern, tokenRe) : null;
|
|
90
|
-
regexStr += constraint !== null ? `(?:/(${constraint}))` : "(?:/.+)";
|
|
91
|
-
} else if (tok[3] !== void 0) {
|
|
92
|
-
const constraint = hasConstraints ? extractConstraint(pattern, tokenRe) : null;
|
|
93
|
-
const isOptional = pattern[tokenRe.lastIndex] === "?";
|
|
94
|
-
if (isOptional) tokenRe.lastIndex += 1;
|
|
95
|
-
const group = constraint !== null ? `(${constraint})` : "([^/]+)";
|
|
96
|
-
if (isOptional && regexStr.endsWith("/")) regexStr = regexStr.slice(0, -1) + `(?:/${group})?`;
|
|
97
|
-
else if (isOptional) regexStr += `${group}?`;
|
|
98
|
-
else regexStr += group;
|
|
99
|
-
} else if (tok[0] === ".") regexStr += "\\.";
|
|
100
|
-
else regexStr += tok[0];
|
|
101
|
-
return compileMatcherRegExp("^" + regexStr + "$", pattern);
|
|
102
|
-
}
|
|
103
|
-
function compileMatcherRegExp(regexPattern, sourcePattern) {
|
|
104
|
-
if (!isSafeRegex(regexPattern)) {
|
|
105
|
-
console.warn(`[vinext] Rejecting potentially unsafe middleware matcher (ReDoS risk): ${sourcePattern}\n Middleware will run for all paths to avoid bypassing request guards.\n Simplify the matcher to avoid nested repetition.`);
|
|
106
|
-
return UNSAFE_MATCHER_PATTERN;
|
|
107
|
-
}
|
|
108
|
-
return safeRegExp(regexPattern);
|
|
57
|
+
const result = compileMiddlewareMatcherPattern(pattern);
|
|
58
|
+
if (result.regexp) return result.regexp;
|
|
59
|
+
const problem = result.kind === "unsafe" ? "potentially unsafe" : "invalid";
|
|
60
|
+
console.warn(`[vinext] Rejecting ${problem} middleware matcher: ${pattern}\n ${result.error}.\n Middleware will run for all paths to avoid bypassing request guards.`);
|
|
61
|
+
return UNSAFE_MATCHER_PATTERN;
|
|
109
62
|
}
|
|
110
63
|
//#endregion
|
|
111
64
|
export { matchPattern, matchesMiddleware };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/server/middleware-path-to-regexp.d.ts
|
|
2
|
+
type MiddlewarePathToken = string | MiddlewarePathKey;
|
|
3
|
+
type MiddlewarePathKey = {
|
|
4
|
+
name: string | number;
|
|
5
|
+
prefix: string;
|
|
6
|
+
suffix: string;
|
|
7
|
+
pattern: string;
|
|
8
|
+
modifier: string;
|
|
9
|
+
};
|
|
10
|
+
declare function parseMiddlewarePath(value: string): MiddlewarePathToken[];
|
|
11
|
+
declare function normalizeMiddlewarePathTokens(tokens: MiddlewarePathToken[]): MiddlewarePathToken[];
|
|
12
|
+
declare function middlewarePathTokensToRegExp(tokens: MiddlewarePathToken[]): RegExp;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { MiddlewarePathKey, MiddlewarePathToken, middlewarePathTokensToRegExp, normalizeMiddlewarePathTokens, parseMiddlewarePath };
|