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.
Files changed (175) hide show
  1. package/README.md +4 -1
  2. package/dist/build/client-build-config.js +1 -0
  3. package/dist/build/report.d.ts +8 -1
  4. package/dist/build/report.js +12 -2
  5. package/dist/check.js +15 -2
  6. package/dist/cli.js +3 -2
  7. package/dist/config/config-matchers.d.ts +13 -37
  8. package/dist/config/config-matchers.js +31 -121
  9. package/dist/config/next-config.d.ts +3 -2
  10. package/dist/config/next-config.js +3 -1
  11. package/dist/config/request-context.d.ts +14 -0
  12. package/dist/config/request-context.js +26 -0
  13. package/dist/entries/app-rsc-entry.js +27 -9
  14. package/dist/entries/app-rsc-manifest.js +17 -0
  15. package/dist/entries/pages-server-entry.js +3 -2
  16. package/dist/index.js +82 -58
  17. package/dist/init.d.ts +6 -4
  18. package/dist/init.js +20 -22
  19. package/dist/plugins/og-assets.js +1 -1
  20. package/dist/routing/app-route-graph.d.ts +13 -5
  21. package/dist/routing/app-route-graph.js +59 -13
  22. package/dist/routing/app-router.js +12 -6
  23. package/dist/routing/file-matcher.d.ts +11 -22
  24. package/dist/routing/file-matcher.js +21 -33
  25. package/dist/routing/pages-router.js +23 -29
  26. package/dist/routing/route-matching.d.ts +14 -1
  27. package/dist/routing/route-matching.js +13 -1
  28. package/dist/routing/route-pattern.d.ts +2 -1
  29. package/dist/routing/route-pattern.js +7 -4
  30. package/dist/routing/route-trie.d.ts +5 -1
  31. package/dist/routing/route-trie.js +5 -2
  32. package/dist/server/app-browser-entry.js +7 -6
  33. package/dist/server/app-browser-history-controller.d.ts +1 -1
  34. package/dist/server/app-browser-history-controller.js +3 -3
  35. package/dist/server/app-browser-navigation-controller.js +1 -1
  36. package/dist/server/app-browser-state.js +1 -1
  37. package/dist/server/app-browser-visible-commit.js +1 -1
  38. package/dist/server/app-fallback-renderer.d.ts +3 -0
  39. package/dist/server/app-fallback-renderer.js +4 -2
  40. package/dist/server/app-middleware.js +2 -1
  41. package/dist/server/app-page-boundary-render.d.ts +12 -1
  42. package/dist/server/app-page-boundary-render.js +84 -2
  43. package/dist/server/app-page-cache-finalizer.d.ts +1 -1
  44. package/dist/server/app-page-cache.d.ts +1 -1
  45. package/dist/server/app-page-dispatch.d.ts +11 -2
  46. package/dist/server/app-page-dispatch.js +14 -17
  47. package/dist/server/app-page-element-builder.d.ts +7 -2
  48. package/dist/server/app-page-element-builder.js +112 -27
  49. package/dist/server/app-page-head.d.ts +54 -2
  50. package/dist/server/app-page-head.js +164 -78
  51. package/dist/server/app-page-http-access-fallback-metadata.d.ts +29 -0
  52. package/dist/server/app-page-http-access-fallback-metadata.js +106 -0
  53. package/dist/server/app-page-render.d.ts +2 -2
  54. package/dist/server/app-page-render.js +1 -1
  55. package/dist/server/app-page-request.d.ts +12 -0
  56. package/dist/server/app-page-request.js +133 -13
  57. package/dist/server/app-page-route-wiring.d.ts +9 -0
  58. package/dist/server/app-page-route-wiring.js +43 -3
  59. package/dist/server/app-post-middleware-context.d.ts +1 -1
  60. package/dist/server/app-post-middleware-context.js +1 -1
  61. package/dist/server/app-route-handler-dispatch.js +31 -17
  62. package/dist/server/app-route-handler-execution.d.ts +6 -1
  63. package/dist/server/app-route-handler-execution.js +42 -16
  64. package/dist/server/app-route-handler-policy.d.ts +2 -0
  65. package/dist/server/app-route-handler-policy.js +3 -3
  66. package/dist/server/app-route-module-loader.d.ts +3 -1
  67. package/dist/server/app-route-module-loader.js +1 -0
  68. package/dist/server/app-rsc-cache-busting.d.ts +1 -1
  69. package/dist/server/app-rsc-errors.d.ts +1 -1
  70. package/dist/server/app-rsc-errors.js +12 -2
  71. package/dist/server/app-rsc-handler.d.ts +10 -5
  72. package/dist/server/app-rsc-handler.js +52 -24
  73. package/dist/server/app-rsc-request-normalization.d.ts +3 -2
  74. package/dist/server/app-rsc-request-normalization.js +8 -2
  75. package/dist/server/app-rsc-response-finalizer.d.ts +2 -2
  76. package/dist/server/app-rsc-response-finalizer.js +5 -11
  77. package/dist/server/app-rsc-route-matching.d.ts +20 -0
  78. package/dist/server/app-rsc-route-matching.js +87 -11
  79. package/dist/server/app-segment-config.js +13 -3
  80. package/dist/server/app-server-action-execution.d.ts +3 -1
  81. package/dist/server/app-server-action-execution.js +9 -7
  82. package/dist/server/app-ssr-entry.js +2 -2
  83. package/dist/server/app-ssr-stream.d.ts +1 -7
  84. package/dist/server/app-ssr-stream.js +2 -10
  85. package/dist/server/config-headers.d.ts +24 -0
  86. package/dist/server/config-headers.js +52 -0
  87. package/dist/server/dev-initial-server-error.d.ts +1 -1
  88. package/dist/server/dev-server.js +29 -13
  89. package/dist/server/isr-cache.d.ts +1 -1
  90. package/dist/server/middleware-matcher-pattern.d.ts +22 -0
  91. package/dist/server/middleware-matcher-pattern.js +219 -0
  92. package/dist/server/middleware-matcher.d.ts +2 -7
  93. package/dist/server/middleware-matcher.js +13 -60
  94. package/dist/server/middleware-path-to-regexp.d.ts +14 -0
  95. package/dist/server/middleware-path-to-regexp.js +228 -0
  96. package/dist/server/middleware-runtime.js +4 -3
  97. package/dist/server/middleware.d.ts +1 -1
  98. package/dist/server/middleware.js +2 -1
  99. package/dist/server/pages-data-route.d.ts +11 -1
  100. package/dist/server/pages-data-route.js +16 -1
  101. package/dist/server/pages-get-initial-props.d.ts +15 -2
  102. package/dist/server/pages-get-initial-props.js +16 -6
  103. package/dist/server/pages-page-data.d.ts +3 -1
  104. package/dist/server/pages-page-data.js +16 -7
  105. package/dist/server/pages-page-handler.d.ts +4 -2
  106. package/dist/server/pages-page-handler.js +16 -17
  107. package/dist/server/pages-page-response.js +1 -1
  108. package/dist/server/pages-readiness.d.ts +3 -3
  109. package/dist/server/pages-request-pipeline.d.ts +1 -0
  110. package/dist/server/pages-request-pipeline.js +25 -11
  111. package/dist/server/pages-router-entry.js +6 -0
  112. package/dist/server/prerender-route-params.js +2 -2
  113. package/dist/server/prod-server.d.ts +1 -4
  114. package/dist/server/prod-server.js +36 -23
  115. package/dist/server/request-pipeline.d.ts +15 -29
  116. package/dist/server/request-pipeline.js +24 -59
  117. package/dist/server/rsc-stream-hints.js +122 -18
  118. package/dist/shims/app.d.ts +1 -1
  119. package/dist/shims/cache-request-state.d.ts +27 -1
  120. package/dist/shims/cache-request-state.js +60 -1
  121. package/dist/shims/cache-runtime.d.ts +12 -1
  122. package/dist/shims/cache-runtime.js +22 -7
  123. package/dist/shims/cache.d.ts +5 -5
  124. package/dist/shims/cache.js +26 -10
  125. package/dist/shims/document.d.ts +42 -83
  126. package/dist/shims/document.js +62 -53
  127. package/dist/shims/dynamic.d.ts +5 -25
  128. package/dist/shims/dynamic.js +29 -8
  129. package/dist/shims/error.d.ts +1 -1
  130. package/dist/shims/error.js +1 -1
  131. package/dist/shims/fetch-cache.js +12 -3
  132. package/dist/shims/font-google-base.d.ts +11 -4
  133. package/dist/shims/font-local.d.ts +10 -3
  134. package/dist/shims/head.d.ts +13 -2
  135. package/dist/shims/head.js +3 -3
  136. package/dist/shims/headers.d.ts +28 -31
  137. package/dist/shims/headers.js +109 -49
  138. package/dist/shims/image.d.ts +27 -37
  139. package/dist/shims/image.js +21 -14
  140. package/dist/shims/internal/cookie-serialize.d.ts +3 -1
  141. package/dist/shims/internal/cookie-serialize.js +3 -1
  142. package/dist/shims/internal/hybrid-client-route-owner.js +3 -1
  143. package/dist/shims/internal/utils.d.ts +3 -3
  144. package/dist/shims/legacy-image.d.ts +3 -37
  145. package/dist/shims/legacy-image.js +6 -16
  146. package/dist/shims/link.d.ts +15 -22
  147. package/dist/shims/link.js +20 -11
  148. package/dist/shims/navigation-context-state.d.ts +1 -1
  149. package/dist/shims/navigation.d.ts +7 -3
  150. package/dist/shims/navigation.js +24 -13
  151. package/dist/shims/navigation.react-server.js +1 -1
  152. package/dist/shims/next-shims-augmentations.d.ts +1 -0
  153. package/dist/shims/next-shims-public.d.ts +1 -0
  154. package/dist/shims/public-shim-map.json.js +103 -0
  155. package/dist/shims/readonly-url-search-params.d.ts +3 -3
  156. package/dist/shims/readonly-url-search-params.js +3 -3
  157. package/dist/shims/request-state-types.d.ts +1 -1
  158. package/dist/shims/router.d.ts +55 -21
  159. package/dist/shims/router.js +162 -37
  160. package/dist/shims/script.d.ts +6 -10
  161. package/dist/shims/script.js +6 -3
  162. package/dist/shims/server.d.ts +45 -22
  163. package/dist/shims/server.js +44 -5
  164. package/dist/shims/unified-request-context.d.ts +1 -1
  165. package/dist/shims/unified-request-context.js +2 -0
  166. package/dist/shims/url-safety.d.ts +1 -1
  167. package/dist/shims/url-safety.js +2 -2
  168. package/dist/typegen.d.ts +8 -2
  169. package/dist/typegen.js +76 -15
  170. package/dist/utils/domain-locale.d.ts +2 -2
  171. package/dist/utils/external-url.d.ts +5 -0
  172. package/dist/utils/external-url.js +7 -0
  173. package/dist/utils/regex-safety.d.ts +8 -0
  174. package/dist/utils/regex-safety.js +737 -0
  175. 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
- const requestAsPath = isDataReq ? gsspResolvedUrl : i18nConfig ? extractLocaleFromUrl$1(originalUrl, i18nConfig, locale).url : originalUrl;
391
+ let requestAsPath = isDataReq ? gsspResolvedUrl : i18nConfig ? extractLocaleFromUrl$1(originalUrl, i18nConfig, locale).url : originalUrl;
380
392
  const userFacingParams = route.isDynamic ? params : null;
381
- const query = mergeRouteParamsIntoQuery(parseQueryString(url), params);
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: requestAsPath,
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 { HasCondition, NextI18nConfig } from "../config/next-config.js";
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 { checkHasConditions, isSafeRegex, requestContextFromRequest, safeRegExp } from "../config/config-matchers.js";
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 false;
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 (isValidMiddlewareMatcherObject(m)) {
23
- if (!matchObjectMatcher(pathname, m, i18nConfig)) continue;
24
- if (!checkHasConditions(m.has, m.missing, requestContext)) continue;
25
- return true;
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 hasConstraints = /:[\w-]+[*+]?\(/.test(pattern);
81
- if (!hasConstraints && (pattern.includes("(") || pattern.includes("\\"))) return compileMatcherRegExp("^" + pattern + "$", pattern);
82
- let regexStr = "";
83
- const tokenRe = /\/:([\w-]+)\*|\/:([\w-]+)\+|:([\w-]+)|[.]|[^/:.]+|./g;
84
- let tok;
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 };