@timber-js/app 0.2.0-alpha.147 → 0.2.0-alpha.149

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 (277) hide show
  1. package/LICENSE +8 -0
  2. package/dist/_chunks/{actions-OIZ8siQu.js → actions-a0RczDnB.js} +8 -34
  3. package/dist/_chunks/{actions-OIZ8siQu.js.map → actions-a0RczDnB.js.map} +1 -1
  4. package/dist/_chunks/cache-api-Co-yI-gP.js +424 -0
  5. package/dist/_chunks/cache-api-Co-yI-gP.js.map +1 -0
  6. package/dist/_chunks/{metadata-routes-BjepjefT.js → canonicalize-P41GR6tY.js} +65 -2
  7. package/dist/_chunks/canonicalize-P41GR6tY.js.map +1 -0
  8. package/dist/_chunks/{cli-schema-sync-CQaxupHv.js → cli-schema-sync-DJDdyEae.js} +111 -260
  9. package/dist/_chunks/cli-schema-sync-DJDdyEae.js.map +1 -0
  10. package/dist/_chunks/cli-schema-sync-D_IhHEUW.js +2 -0
  11. package/dist/_chunks/{define-DMVhsipD.js → define-Bssfp6ot.js} +3 -33
  12. package/dist/_chunks/define-Bssfp6ot.js.map +1 -0
  13. package/dist/_chunks/{define-C3RvHQPK.js → define-CFmvb4Bt.js} +15 -7
  14. package/dist/_chunks/define-CFmvb4Bt.js.map +1 -0
  15. package/dist/_chunks/{href-validation-DyZIzfMJ.js → href-validation-FiA_rToW.js} +2 -1
  16. package/dist/_chunks/{href-validation-DyZIzfMJ.js.map → href-validation-FiA_rToW.js.map} +1 -1
  17. package/dist/_chunks/{tracing-M_zKubhk.js → logger-CM0clugc.js} +394 -107
  18. package/dist/_chunks/logger-CM0clugc.js.map +1 -0
  19. package/dist/_chunks/navigation-root-bUDZLw74.js.map +1 -1
  20. package/dist/_chunks/plugin-context-DurqFBuY.js +2 -0
  21. package/dist/_chunks/plugin-context-GfDWsdd2.js +268 -0
  22. package/dist/_chunks/plugin-context-GfDWsdd2.js.map +1 -0
  23. package/dist/_chunks/{resolve-schema-Dj8tQloi.js → resolve-schema-3iUvBV5T.js} +3 -30
  24. package/dist/_chunks/resolve-schema-3iUvBV5T.js.map +1 -0
  25. package/dist/_chunks/{schema-bridge-DK_89h8O.js → schema-bridge-BY3QLBL7.js} +35 -2
  26. package/dist/_chunks/schema-bridge-BY3QLBL7.js.map +1 -0
  27. package/dist/_chunks/tree-match-D2l830j2.js +102 -0
  28. package/dist/_chunks/tree-match-D2l830j2.js.map +1 -0
  29. package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +1 -1
  30. package/dist/_chunks/{walkers-Ck4Y45XN.js → walkers-DvWDj453.js} +107 -82
  31. package/dist/_chunks/walkers-DvWDj453.js.map +1 -0
  32. package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
  33. package/dist/adapters/cloudflare-kv-cache.js +36 -14
  34. package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
  35. package/dist/adapters/compress-module.d.ts +5 -4
  36. package/dist/adapters/compress-module.d.ts.map +1 -1
  37. package/dist/adapters/nitro.d.ts +1 -3
  38. package/dist/adapters/nitro.d.ts.map +1 -1
  39. package/dist/adapters/nitro.js +14 -134
  40. package/dist/adapters/nitro.js.map +1 -1
  41. package/dist/cache/cache-api.d.ts.map +1 -1
  42. package/dist/cache/index.d.ts +3 -1
  43. package/dist/cache/index.d.ts.map +1 -1
  44. package/dist/cache/index.js +20 -235
  45. package/dist/cache/index.js.map +1 -1
  46. package/dist/cache/invalidation-epoch.d.ts +43 -0
  47. package/dist/cache/invalidation-epoch.d.ts.map +1 -0
  48. package/dist/cache/redis-handler.d.ts +13 -0
  49. package/dist/cache/redis-handler.d.ts.map +1 -1
  50. package/dist/cache/timber-cache.d.ts +17 -9
  51. package/dist/cache/timber-cache.d.ts.map +1 -1
  52. package/dist/cli-schema-sync.d.ts +21 -1
  53. package/dist/cli-schema-sync.d.ts.map +1 -1
  54. package/dist/cli.d.ts +12 -0
  55. package/dist/cli.d.ts.map +1 -1
  56. package/dist/cli.js +26 -31
  57. package/dist/cli.js.map +1 -1
  58. package/dist/client/browser-entry/hmr.d.ts.map +1 -1
  59. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  60. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  61. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  62. package/dist/client/history.d.ts +10 -17
  63. package/dist/client/history.d.ts.map +1 -1
  64. package/dist/client/index.js +1 -1
  65. package/dist/client/index.js.map +1 -1
  66. package/dist/client/internal.js +30 -33
  67. package/dist/client/internal.js.map +1 -1
  68. package/dist/client/navigation-api.d.ts +9 -2
  69. package/dist/client/navigation-api.d.ts.map +1 -1
  70. package/dist/client/navigation-context.d.ts +0 -11
  71. package/dist/client/navigation-context.d.ts.map +1 -1
  72. package/dist/client/navigation-root.d.ts +0 -5
  73. package/dist/client/navigation-root.d.ts.map +1 -1
  74. package/dist/client/router.d.ts.map +1 -1
  75. package/dist/client/rsc-fetch.d.ts.map +1 -1
  76. package/dist/client/use-search-params.d.ts +10 -5
  77. package/dist/client/use-search-params.d.ts.map +1 -1
  78. package/dist/codec.js +1 -1
  79. package/dist/config-validation.d.ts.map +1 -1
  80. package/dist/cookies/define-cookie.d.ts.map +1 -1
  81. package/dist/cookies/index.js +2 -4
  82. package/dist/cookies/index.js.map +1 -1
  83. package/dist/dev-tools/dev-page-shell.d.ts +1 -0
  84. package/dist/dev-tools/dev-page-shell.d.ts.map +1 -1
  85. package/dist/dev-tools/hydration-overlay.d.ts +11 -0
  86. package/dist/dev-tools/hydration-overlay.d.ts.map +1 -0
  87. package/dist/dev-tools/index.d.ts +1 -2
  88. package/dist/dev-tools/index.d.ts.map +1 -1
  89. package/dist/fonts/ast-walk.d.ts +68 -0
  90. package/dist/fonts/ast-walk.d.ts.map +1 -0
  91. package/dist/fonts/ast.d.ts +23 -11
  92. package/dist/fonts/ast.d.ts.map +1 -1
  93. package/dist/fonts/bundle.d.ts +3 -1
  94. package/dist/fonts/bundle.d.ts.map +1 -1
  95. package/dist/fonts/dev-middleware.d.ts +1 -1
  96. package/dist/fonts/dev-middleware.d.ts.map +1 -1
  97. package/dist/fonts/fallbacks.d.ts +0 -7
  98. package/dist/fonts/fallbacks.d.ts.map +1 -1
  99. package/dist/fonts/google.d.ts +4 -2
  100. package/dist/fonts/google.d.ts.map +1 -1
  101. package/dist/fonts/local.d.ts +3 -3
  102. package/dist/fonts/local.d.ts.map +1 -1
  103. package/dist/fonts/paths.d.ts +43 -0
  104. package/dist/fonts/paths.d.ts.map +1 -0
  105. package/dist/fonts/pipeline.d.ts +7 -21
  106. package/dist/fonts/pipeline.d.ts.map +1 -1
  107. package/dist/fonts/transform.d.ts.map +1 -1
  108. package/dist/fonts/virtual-modules.d.ts +1 -18
  109. package/dist/fonts/virtual-modules.d.ts.map +1 -1
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +610 -406
  112. package/dist/index.js.map +1 -1
  113. package/dist/params/index.js +1 -1
  114. package/dist/params/resolve-schema.d.ts.map +1 -1
  115. package/dist/plugin-context.d.ts +28 -5
  116. package/dist/plugin-context.d.ts.map +1 -1
  117. package/dist/plugins/cache.d.ts +13 -5
  118. package/dist/plugins/cache.d.ts.map +1 -1
  119. package/dist/plugins/dev-server.d.ts.map +1 -1
  120. package/dist/plugins/fonts.d.ts +1 -2
  121. package/dist/plugins/fonts.d.ts.map +1 -1
  122. package/dist/plugins/routing.d.ts.map +1 -1
  123. package/dist/plugins/server-bundle.d.ts.map +1 -1
  124. package/dist/routing/codegen.d.ts.map +1 -1
  125. package/dist/routing/convention-lint.d.ts.map +1 -1
  126. package/dist/routing/file-cache.d.ts +8 -0
  127. package/dist/routing/file-cache.d.ts.map +1 -0
  128. package/dist/routing/index.js +2 -2
  129. package/dist/routing/interception.d.ts +0 -5
  130. package/dist/routing/interception.d.ts.map +1 -1
  131. package/dist/routing/manifest-codegen.d.ts +32 -0
  132. package/dist/routing/manifest-codegen.d.ts.map +1 -0
  133. package/dist/routing/scanner.d.ts.map +1 -1
  134. package/dist/schema-bridge.d.ts +15 -0
  135. package/dist/schema-bridge.d.ts.map +1 -1
  136. package/dist/search-params/define.d.ts.map +1 -1
  137. package/dist/search-params/index.js +7 -3
  138. package/dist/search-params/index.js.map +1 -1
  139. package/dist/search-params/wrappers.d.ts.map +1 -1
  140. package/dist/segment-params/define.d.ts.map +1 -1
  141. package/dist/segment-params/index.js +1 -1
  142. package/dist/server/access-gate.d.ts.map +1 -1
  143. package/dist/server/actions.d.ts +0 -14
  144. package/dist/server/actions.d.ts.map +1 -1
  145. package/dist/server/build-manifest.d.ts +0 -29
  146. package/dist/server/build-manifest.d.ts.map +1 -1
  147. package/dist/server/canonicalize.d.ts +10 -0
  148. package/dist/server/canonicalize.d.ts.map +1 -1
  149. package/dist/server/deny-renderer.d.ts +1 -1
  150. package/dist/server/deny-renderer.d.ts.map +1 -1
  151. package/dist/server/html-injector-core.d.ts +8 -0
  152. package/dist/server/html-injector-core.d.ts.map +1 -1
  153. package/dist/server/html-injectors.d.ts.map +1 -1
  154. package/dist/server/index.js +2 -2
  155. package/dist/server/internal.d.ts +1 -5
  156. package/dist/server/internal.d.ts.map +1 -1
  157. package/dist/server/internal.js +12 -171
  158. package/dist/server/internal.js.map +1 -1
  159. package/dist/server/node-stream-transforms.d.ts +0 -11
  160. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  161. package/dist/server/render-utils.d.ts +25 -0
  162. package/dist/server/render-utils.d.ts.map +1 -0
  163. package/dist/server/route-element-builder.d.ts +11 -25
  164. package/dist/server/route-element-builder.d.ts.map +1 -1
  165. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  166. package/dist/server/rsc-entry/helpers.d.ts +2 -18
  167. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  168. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  169. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  170. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  171. package/dist/server/ssr-render.d.ts.map +1 -1
  172. package/dist/shared/href-validation.d.ts.map +1 -1
  173. package/dist/shims/image.d.ts +15 -15
  174. package/package.json +7 -8
  175. package/src/adapters/cloudflare-kv-cache.ts +56 -19
  176. package/src/adapters/compress-module.ts +27 -118
  177. package/src/adapters/nitro.ts +3 -41
  178. package/src/adapters/raw-imports.d.ts +5 -0
  179. package/src/cache/cache-api.ts +9 -1
  180. package/src/cache/index.ts +5 -2
  181. package/src/cache/invalidation-epoch.ts +93 -0
  182. package/src/cache/redis-handler.ts +33 -2
  183. package/src/cache/timber-cache.ts +109 -53
  184. package/src/cli-schema-sync.ts +123 -30
  185. package/src/cli.ts +33 -37
  186. package/src/client/browser-entry/hmr.ts +0 -11
  187. package/src/client/browser-entry/hydrate.ts +39 -5
  188. package/src/client/browser-entry/post-hydration.ts +6 -15
  189. package/src/client/browser-entry/router-init.ts +98 -3
  190. package/src/client/history.ts +10 -24
  191. package/src/client/navigation-api.ts +14 -9
  192. package/src/client/navigation-context.ts +1 -21
  193. package/src/client/navigation-root.tsx +0 -8
  194. package/src/client/router.ts +18 -8
  195. package/src/client/rsc-fetch.ts +7 -2
  196. package/src/client/use-search-params.ts +10 -5
  197. package/src/config-validation.ts +33 -25
  198. package/src/cookies/define-cookie.ts +2 -18
  199. package/src/dev-tools/dev-page-shell.ts +6 -2
  200. package/src/dev-tools/hydration-overlay.ts +158 -0
  201. package/src/dev-tools/index.ts +3 -16
  202. package/src/fonts/ast-walk.ts +235 -0
  203. package/src/fonts/ast.ts +91 -249
  204. package/src/fonts/bundle.ts +16 -9
  205. package/src/fonts/dev-middleware.ts +16 -5
  206. package/src/fonts/fallbacks.ts +0 -23
  207. package/src/fonts/google.ts +7 -3
  208. package/src/fonts/local.ts +7 -6
  209. package/src/fonts/paths.ts +58 -0
  210. package/src/fonts/pipeline.ts +27 -48
  211. package/src/fonts/transform.ts +27 -0
  212. package/src/fonts/virtual-modules.ts +1 -57
  213. package/src/index.ts +5 -11
  214. package/src/params/resolve-schema.ts +2 -34
  215. package/src/plugin-context.ts +95 -17
  216. package/src/plugins/cache.ts +64 -17
  217. package/src/plugins/dev-server.ts +61 -9
  218. package/src/plugins/fonts.ts +21 -6
  219. package/src/plugins/routing.ts +5 -348
  220. package/src/plugins/server-bundle.ts +1 -0
  221. package/src/routing/codegen.ts +3 -2
  222. package/src/routing/convention-lint.ts +4 -3
  223. package/src/routing/file-cache.ts +23 -0
  224. package/src/routing/interception.ts +2 -21
  225. package/src/routing/manifest-codegen.ts +290 -0
  226. package/src/routing/scanner.ts +5 -17
  227. package/src/routing/schema-validation.ts +2 -2
  228. package/src/schema-bridge.ts +63 -2
  229. package/src/search-params/define.ts +34 -2
  230. package/src/search-params/wrappers.ts +21 -3
  231. package/src/segment-params/define.ts +2 -62
  232. package/src/server/access-gate.tsx +8 -0
  233. package/src/server/actions.ts +8 -31
  234. package/src/server/build-manifest.ts +0 -56
  235. package/src/server/canonicalize.ts +6 -3
  236. package/src/server/deny-renderer.ts +6 -0
  237. package/src/server/html-injector-core.ts +19 -0
  238. package/src/server/html-injectors.ts +14 -4
  239. package/src/server/internal.ts +1 -18
  240. package/src/server/node-stream-transforms.ts +5 -72
  241. package/src/server/render-utils.ts +31 -0
  242. package/src/server/route-element-builder.ts +77 -47
  243. package/src/server/rsc-entry/error-renderer.ts +1 -0
  244. package/src/server/rsc-entry/helpers.ts +6 -34
  245. package/src/server/rsc-entry/index.ts +11 -0
  246. package/src/server/rsc-entry/render-route.ts +7 -9
  247. package/src/server/rsc-entry/rsc-payload.ts +2 -1
  248. package/src/server/rsc-entry/ssr-renderer.ts +2 -2
  249. package/src/server/ssr-render.ts +1 -11
  250. package/src/shared/href-validation.ts +8 -0
  251. package/dist/_chunks/cli-schema-sync-CBiMEtqa.js +0 -2
  252. package/dist/_chunks/cli-schema-sync-CQaxupHv.js.map +0 -1
  253. package/dist/_chunks/define-C3RvHQPK.js.map +0 -1
  254. package/dist/_chunks/define-DMVhsipD.js.map +0 -1
  255. package/dist/_chunks/handler-store-BVDgyTh7.js +0 -78
  256. package/dist/_chunks/handler-store-BVDgyTh7.js.map +0 -1
  257. package/dist/_chunks/logger-Dt-jQaBO.js +0 -291
  258. package/dist/_chunks/logger-Dt-jQaBO.js.map +0 -1
  259. package/dist/_chunks/metadata-routes-BjepjefT.js.map +0 -1
  260. package/dist/_chunks/resolve-schema-Dj8tQloi.js.map +0 -1
  261. package/dist/_chunks/schema-bridge-DK_89h8O.js.map +0 -1
  262. package/dist/_chunks/tracing-M_zKubhk.js.map +0 -1
  263. package/dist/_chunks/walkers-Ck4Y45XN.js.map +0 -1
  264. package/dist/_chunks/warnings-CNPXaypM.js +0 -125
  265. package/dist/_chunks/warnings-CNPXaypM.js.map +0 -1
  266. package/dist/client/link-pending-store.d.ts +0 -65
  267. package/dist/client/link-pending-store.d.ts.map +0 -1
  268. package/dist/client/nav-link-store.d.ts +0 -27
  269. package/dist/client/nav-link-store.d.ts.map +0 -1
  270. package/dist/dev-tools/warnings.d.ts +0 -109
  271. package/dist/dev-tools/warnings.d.ts.map +0 -1
  272. package/dist/server/flush.d.ts +0 -74
  273. package/dist/server/flush.d.ts.map +0 -1
  274. package/src/client/link-pending-store.ts +0 -116
  275. package/src/client/nav-link-store.ts +0 -37
  276. package/src/dev-tools/warnings.ts +0 -238
  277. package/src/server/flush.ts +0 -183
@@ -14,7 +14,7 @@
14
14
  * See design/08-forms-and-actions.md
15
15
  */
16
16
 
17
- import { getCacheHandler } from '../cache/handler-store';
17
+ import { cache } from '../cache/cache-api';
18
18
  import { DenySignal, RedirectSignal } from './primitives';
19
19
  import { withSpan } from './tracing';
20
20
  import { revalidationAls, type RevalidationState } from './als-registry.js';
@@ -161,12 +161,14 @@ export async function executeAction(
161
161
  }
162
162
  });
163
163
 
164
- // Process tag invalidation via the module-level cache handler singleton.
165
- // setCacheHandler() is called at boot from rsc-entry when timber.config.ts
166
- // provides a cacheHandler; otherwise falls back to in-memory LRU (TIM-599).
164
+ // Process tag invalidation through cache.invalidate so it records the
165
+ // invalidation epoch — an in-flight cached fn must not re-store data this
166
+ // mutation just invalidated (TIM-1028). cache.invalidate resolves the
167
+ // module-level handler singleton: setCacheHandler() is called at boot from
168
+ // rsc-entry when timber.config.ts provides a cacheHandler; otherwise falls
169
+ // back to in-memory LRU (TIM-599).
167
170
  if (state.tags.length > 0) {
168
- const handler = getCacheHandler();
169
- await Promise.all(state.tags.map((tag) => handler.invalidate({ tag })));
171
+ await Promise.all(state.tags.map((tag) => cache.invalidate({ tag })));
170
172
  }
171
173
 
172
174
  // Process path revalidation — build element tree (not yet serialized)
@@ -212,28 +214,3 @@ export async function executeAction(
212
214
  ...(redirectTo ? { redirectTo, redirectStatus } : {}),
213
215
  };
214
216
  }
215
-
216
- /**
217
- * Build an HTTP Response for a no-JS form submission.
218
- * Standard POST → 302 redirect pattern.
219
- *
220
- * @param redirectPath - Where to redirect after the action executes.
221
- */
222
- export function buildNoJsResponse(redirectPath: string, status: number = 302): Response {
223
- return new Response(null, {
224
- status,
225
- headers: { Location: redirectPath },
226
- });
227
- }
228
-
229
- /**
230
- * Detect whether the incoming request is an RSC action request (with JS)
231
- * or a plain HTML form POST (no JS).
232
- *
233
- * RSC action requests use Accept: text/x-component or Content-Type: text/x-component.
234
- */
235
- export function isRscActionRequest(req: Request): boolean {
236
- const accept = req.headers.get('Accept') ?? '';
237
- const contentType = req.headers.get('Content-Type') ?? '';
238
- return accept.includes('text/x-component') || contentType.includes('text/x-component');
239
- }
@@ -101,19 +101,6 @@ export function buildCssLinkTags(cssUrls: string[]): string {
101
101
  return cssUrls.map((url) => `<link rel="stylesheet" href="${url}">`).join('');
102
102
  }
103
103
 
104
- /**
105
- * Generate a Link header value for CSS preload hints.
106
- *
107
- * Cloudflare CDN automatically converts Link headers with rel=preload
108
- * into 103 Early Hints responses. This avoids platform-specific 103
109
- * sending code.
110
- *
111
- * Example output: `</assets/root.css>; as=style; rel=preload, </assets/page.css>; as=style; rel=preload`
112
- */
113
- export function buildLinkHeaders(cssUrls: string[]): string {
114
- return cssUrls.map((url) => `<${url}>; as=style; rel=preload`).join(', ');
115
- }
116
-
117
104
  // ─── Font utilities ──────────────────────────────────────────────────────
118
105
 
119
106
  /**
@@ -161,44 +148,8 @@ export function buildFontPreloadTags(fonts: ManifestFontEntry[]): string {
161
148
  .join('');
162
149
  }
163
150
 
164
- /**
165
- * Generate Link header values for font preload hints.
166
- *
167
- * Cloudflare CDN converts Link headers with rel=preload into 103 Early Hints.
168
- *
169
- * Example: `</fonts/inter.woff2>; as=font; rel=preload; crossorigin`
170
- */
171
- export function buildFontLinkHeaders(fonts: ManifestFontEntry[]): string {
172
- return fonts.map((f) => `<${f.href}>; as=font; rel=preload; crossorigin`).join(', ');
173
- }
174
-
175
151
  // ─── JS chunk utilities ──────────────────────────────────────────────────
176
152
 
177
- /**
178
- * Collect JS chunk URLs for a matched route's segment chain.
179
- *
180
- * Walks segments root → leaf, collecting the JS chunk for each layout
181
- * and page. Deduplicates while preserving order.
182
- */
183
- export function collectRouteJs(segments: SegmentWithFiles[], manifest: BuildManifest): string[] {
184
- const seen = new Set<string>();
185
- const result: string[] = [];
186
-
187
- for (const segment of segments) {
188
- for (const file of [segment.layout, segment.page]) {
189
- if (!file) continue;
190
- const jsUrl = manifest.js[file.filePath];
191
- if (!jsUrl) continue;
192
- if (!seen.has(jsUrl)) {
193
- seen.add(jsUrl);
194
- result.push(jsUrl);
195
- }
196
- }
197
- }
198
-
199
- return result;
200
- }
201
-
202
153
  /**
203
154
  * Collect modulepreload URLs for a matched route's segment chain.
204
155
  *
@@ -238,10 +189,3 @@ export function collectRouteModulepreloads(
238
189
  export function buildModulepreloadTags(urls: string[]): string {
239
190
  return urls.map((url) => `<link rel="modulepreload" href="${url}">`).join('');
240
191
  }
241
-
242
- /**
243
- * Generate a <script type="module"> tag for a JS entry point.
244
- */
245
- export function buildEntryScriptTag(url: string): string {
246
- return `<script type="module" src="${url}"></script>`;
247
- }
@@ -13,11 +13,14 @@ export type CanonicalizeResult = { ok: true; pathname: string } | { ok: false; s
13
13
  /**
14
14
  * Encoded separators that produce a 400 rejection.
15
15
  * %2f (/) and %5c (\) cause path-confusion attacks.
16
+ *
17
+ * Shared between the runtime canonicalizer and the build-time route scanner
18
+ * to ensure both enforce identical security rules. See design/13-security.md.
16
19
  */
17
- const ENCODED_SEPARATOR_RE = /%2f|%5c/i;
20
+ export const ENCODED_SEPARATOR_RE = /%2f|%5c/i;
18
21
 
19
- /** Null byte — rejected. */
20
- const NULL_BYTE_RE = /%00/i;
22
+ /** Null byte — rejected. Shared with the route scanner. */
23
+ export const NULL_BYTE_RE = /%00/i;
21
24
 
22
25
  /**
23
26
  * Canonicalize a URL pathname.
@@ -20,6 +20,7 @@ import { renderToReadableStream } from '../rsc-runtime/rsc.js';
20
20
 
21
21
  import { DenySignal } from './primitives.js';
22
22
  import { renderDenyEntry, type DenyPageEntry } from './deny-boundary.js';
23
+ import { getCookiesForSsr } from './cookie-context.js';
23
24
  import { logRenderError } from './logger.js';
24
25
  import { loadModule } from './safe-load.js';
25
26
  import { isMdxFilePath } from './utils/mdx-file.js';
@@ -184,6 +185,7 @@ export async function renderDenyPage(
184
185
 
185
186
  // Render the error page to a fresh RSC Flight stream.
186
187
  const rscStream = renderToReadableStream(element, {
188
+ signal: req.signal,
187
189
  onError(error: unknown) {
188
190
  logRenderError({ method: req.method, path: new URL(req.url).pathname, error });
189
191
  },
@@ -201,6 +203,8 @@ export async function renderDenyPage(
201
203
  headHtml: headHtml + flightInitScript(),
202
204
  bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
203
205
  rscStream: inlineStream,
206
+ signal: req.signal,
207
+ cookies: getCookiesForSsr(),
204
208
  };
205
209
 
206
210
  return callSsr(ssrStream, navContext);
@@ -216,6 +220,7 @@ export async function renderDenyPageAsRsc(
216
220
  deny: DenySignal,
217
221
  segments: ManifestSegmentNode[],
218
222
  layoutComponents: LayoutEntry[],
223
+ req: Request,
219
224
  responseHeaders: Headers,
220
225
  createDebugChannelSink: DebugChannelFactory
221
226
  ): Promise<Response> {
@@ -251,6 +256,7 @@ export async function renderDenyPageAsRsc(
251
256
  }
252
257
 
253
258
  const rscStream = renderToReadableStream(element, {
259
+ signal: req.signal,
254
260
  onError(error: unknown) {
255
261
  console.error('[timber] Error page RSC render error:', error);
256
262
  },
@@ -302,6 +302,7 @@ export class FlightInjectorCore {
302
302
  private readonly errorSignal?: AbortSignal;
303
303
  private readonly decoder = new TextDecoder('utf-8', { fatal: true });
304
304
  private pullPromise: Promise<void> | null = null;
305
+ private _aborted = false;
305
306
 
306
307
  constructor(
307
308
  rscStream: ReadableStream<Uint8Array>,
@@ -363,6 +364,22 @@ export class FlightInjectorCore {
363
364
  return this.machine.state.phase === 'error' ? this.machine.state.error : null;
364
365
  }
365
366
 
367
+ /**
368
+ * Abort the pull loop and release the RSC reader. Called by stream
369
+ * wrappers when the HTML pipeline is torn down (Node `destroy()`,
370
+ * Web `cancel()`). Safe to call multiple times.
371
+ */
372
+ abort(reason?: unknown): void {
373
+ if (this._aborted) return;
374
+ this._aborted = true;
375
+ this.rscReader.cancel(reason).catch(() => {});
376
+ this.pending.length = 0;
377
+ }
378
+
379
+ get isAborted(): boolean {
380
+ return this._aborted;
381
+ }
382
+
366
383
  private async runPullLoop(onYieldDrain: () => void): Promise<void> {
367
384
  // Yield once so the first HTML shell chunk flows through the wrapper
368
385
  // before we start reading RSC data.
@@ -370,6 +387,8 @@ export class FlightInjectorCore {
370
387
 
371
388
  try {
372
389
  for (;;) {
390
+ if (this._aborted) return;
391
+
373
392
  // Guard each read with a timeout so a permanently hung RSC stream
374
393
  // eventually aborts. See design/02 §"Streaming Constraints".
375
394
  const readPromise = this.rscReader.read();
@@ -271,8 +271,11 @@ function createFlightInjectionTransform(
271
271
  if (_controller) drain(_controller);
272
272
  };
273
273
 
274
- return new TransformStream<Uint8Array, Uint8Array>({
275
- transform(chunk, controller) {
274
+ // The cast is needed because TypeScript's lib types don't include the
275
+ // `cancel` method on Transformer, even though it's part of the WHATWG
276
+ // Streams spec and supported in all target runtimes.
277
+ const transformer = {
278
+ transform(chunk: Uint8Array, controller: TransformStreamDefaultController<Uint8Array>) {
276
279
  _controller = controller;
277
280
 
278
281
  if (core.isInit) {
@@ -287,7 +290,7 @@ function createFlightInjectionTransform(
287
290
  controller.enqueue(chunk);
288
291
  if (core.pending.length > 0) drain(controller);
289
292
  },
290
- flush(controller) {
293
+ flush(controller: TransformStreamDefaultController<Uint8Array>) {
291
294
  // All HTML chunks emitted. Pull loop stops yielding.
292
295
  core.notifyHtmlDone();
293
296
 
@@ -299,7 +302,14 @@ function createFlightInjectionTransform(
299
302
  }
300
303
  return core.ensurePullLoop(onYieldDrain).then(finish);
301
304
  },
302
- });
305
+ cancel(reason: unknown) {
306
+ core.abort(reason ?? 'transform cancelled');
307
+ },
308
+ };
309
+
310
+ return new TransformStream<Uint8Array, Uint8Array>(
311
+ transformer as Transformer<Uint8Array, Uint8Array>
312
+ );
303
313
  }
304
314
 
305
315
  /**
@@ -86,10 +86,6 @@ export type {
86
86
  SlotDeniedKind,
87
87
  } from './status-code-resolver.js';
88
88
 
89
- // ── Flush Controller ─────────────────────────────────────────────────────
90
- export { flushResponse } from './flush.js';
91
- export type { FlushOptions, FlushResult, RenderFn, RenderResult } from './flush.js';
92
-
93
89
  // ── CSRF Protection ──────────────────────────────────────────────────────
94
90
  export { validateCsrf } from './csrf.js';
95
91
  export type { CsrfConfig, CsrfResult } from './csrf.js';
@@ -123,7 +119,7 @@ export type {
123
119
  } from './metadata-routes.js';
124
120
 
125
121
  // ── Server Actions (pipeline internals) ──────────────────────────────────
126
- export { executeAction, buildNoJsResponse, isRscActionRequest } from './actions.js';
122
+ export { executeAction } from './actions.js';
127
123
  export type {
128
124
  RevalidateRenderer,
129
125
  RevalidationState,
@@ -160,19 +156,6 @@ export type {
160
156
  InstrumentationErrorContext,
161
157
  } from './instrumentation.js';
162
158
 
163
- // ── Dev Warnings ─────────────────────────────────────────────────────────
164
- export {
165
- warnSuspenseWrappingChildren,
166
- warnDenyInSuspense,
167
- warnRedirectInSuspense,
168
- warnRedirectInAccess,
169
- warnStaticRequestApi,
170
- warnSlowSlotWithoutSuspense,
171
- setViteServer,
172
- WarningId,
173
- } from '../dev-tools/warnings.js';
174
- export type { DevWarningConfig } from '../dev-tools/warnings.js';
175
-
176
159
  // ── Route Handler ────────────────────────────────────────────────────────
177
160
  export { handleRouteRequest, resolveAllowedMethods } from './route-handler.js';
178
161
  export type { RouteModule, RouteHandler, HttpMethod } from './route-handler.js';
@@ -20,7 +20,6 @@
20
20
 
21
21
  import { PassThrough, Transform, type Readable } from 'node:stream';
22
22
  import { pipeline } from 'node:stream/promises';
23
- import { createGzip, constants } from 'node:zlib';
24
23
 
25
24
  import {
26
25
  BufferAggregator,
@@ -35,6 +34,7 @@ import {
35
34
  } from './html-injector-core.js';
36
35
  import { logStreamingError } from './logger.js';
37
36
  import { createTickCoalescer } from './coalesce-tick.js';
37
+ import { isAbortError, NOINDEX_SCRIPT } from './render-utils.js';
38
38
 
39
39
  // ─── Buffered Transform ──────────────────────────────────────────────────────
40
40
 
@@ -229,6 +229,10 @@ export function createNodeFlightInjector(
229
229
  if (wasInit) core.ensurePullLoop(drain);
230
230
  callback();
231
231
  },
232
+ destroy(err, callback) {
233
+ core.abort(err ?? 'pipeline destroyed');
234
+ callback(err);
235
+ },
232
236
  flush(callback) {
233
237
  // All HTML chunks have been emitted. Pull loop stops yielding.
234
238
  core.notifyHtmlDone();
@@ -263,15 +267,6 @@ export function createNodeFlightInjector(
263
267
 
264
268
  // ─── Error Handling ──────────────────────────────────────────────────────────
265
269
 
266
- const NOINDEX_SCRIPT =
267
- '<script>document.head.appendChild(Object.assign(document.createElement("meta"),{name:"robots",content:"noindex"}))</script>';
268
-
269
- function isAbortError(error: unknown): boolean {
270
- if (error instanceof DOMException && error.name === 'AbortError') return true;
271
- if (error instanceof Error && error.name === 'AbortError') return true;
272
- return false;
273
- }
274
-
275
270
  /**
276
271
  * Run the Node SSR post-processing pipeline with post-shell error handling.
277
272
  *
@@ -334,65 +329,3 @@ export function runNodePipelineWithErrorHandling(
334
329
 
335
330
  return output;
336
331
  }
337
-
338
- // ─── Compression ─────────────────────────────────────────────────────────────
339
-
340
- const COMPRESSIBLE_TYPES = new Set([
341
- 'text/html',
342
- 'text/css',
343
- 'text/plain',
344
- 'text/xml',
345
- 'text/javascript',
346
- 'text/x-component',
347
- 'application/json',
348
- 'application/javascript',
349
- 'application/xml',
350
- 'application/xhtml+xml',
351
- 'application/rss+xml',
352
- 'image/svg+xml',
353
- ]);
354
-
355
- /**
356
- * Create a Node.js gzip Transform using native node:zlib.
357
- *
358
- * Uses `createGzip()` which is backed by C++ zlib — significantly faster
359
- * than the Web Streams `CompressionStream` API (which is a JS wrapper
360
- * around the same zlib but with per-chunk Promise overhead).
361
- *
362
- * Returns null if the response shouldn't be compressed (wrong content type,
363
- * client doesn't accept gzip, already encoded, etc.).
364
- */
365
- export function createNodeGzipCompressor(
366
- requestHeaders: Headers,
367
- responseHeaders: Headers
368
- ): Transform | null {
369
- // Check Accept-Encoding
370
- const acceptEncoding = requestHeaders.get('accept-encoding') || '';
371
- if (!acceptEncoding.includes('gzip')) return null;
372
-
373
- // Check content type is compressible
374
- const contentType = responseHeaders.get('content-type') || '';
375
- const mimeType = contentType.split(';')[0].trim().toLowerCase();
376
- if (!COMPRESSIBLE_TYPES.has(mimeType)) return null;
377
-
378
- // Don't double-compress
379
- if (responseHeaders.has('content-encoding')) return null;
380
-
381
- // Set response headers for gzip
382
- responseHeaders.set('content-encoding', 'gzip');
383
- responseHeaders.delete('content-length');
384
- const existingVary = responseHeaders.get('vary');
385
- if (existingVary) {
386
- if (!existingVary.toLowerCase().includes('accept-encoding')) {
387
- responseHeaders.set('vary', existingVary + ', Accept-Encoding');
388
- }
389
- } else {
390
- responseHeaders.set('vary', 'Accept-Encoding');
391
- }
392
-
393
- // Z_SYNC_FLUSH ensures each chunk is flushed to the output immediately.
394
- // Without it, gzip buffers internally and the browser doesn't receive
395
- // the HTML shell until the gzip stream closes — breaking streaming.
396
- // ~2–5% size overhead vs Z_NO_FLUSH but preserves correct streaming.
397
- return createGzip({ flush: constants.Z_SYNC_FLUSH });
398
- }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Shared render-phase utilities — used by both the Node.js and Web
3
+ * stream wrappers. Pure functions only; no stream-shape dependencies.
4
+ *
5
+ * Consolidated from duplicated copies in node-stream-transforms.ts,
6
+ * ssr-render.ts, and rsc-entry/helpers.ts. See TIM-1071 item 1.
7
+ */
8
+
9
+ /**
10
+ * Check if an error is an abort error (connection closed by client).
11
+ *
12
+ * When the browser aborts a request (page refresh, navigation away),
13
+ * the AbortSignal fires and React/streams throw an AbortError. This
14
+ * is not an application error — suppress it from error boundaries and logs.
15
+ */
16
+ export function isAbortError(error: unknown): boolean {
17
+ if (error instanceof DOMException && error.name === 'AbortError') return true;
18
+ if (error instanceof Error && error.name === 'AbortError') return true;
19
+ return false;
20
+ }
21
+
22
+ /**
23
+ * Inline script that adds `<meta name="robots" content="noindex">` to the
24
+ * document head. Injected when a render-phase error replaces the shell so
25
+ * search engines don't index error pages.
26
+ *
27
+ * MUST be byte-identical across all stream wrappers (Node and Web) to
28
+ * ensure consistent Content-Length headers.
29
+ */
30
+ export const NOINDEX_SCRIPT =
31
+ '<script>document.head.appendChild(Object.assign(document.createElement("meta"),{name:"robots",content:"noindex"}))</script>';
@@ -6,12 +6,11 @@
6
6
  * rsc-entry.ts under the 500-line limit.
7
7
  *
8
8
  * This module handles:
9
- * 1. Loading page/layout components from the segment chain
10
- * 2. Collecting access.ts checks (executed inside render by AccessPreRunner)
11
- * 3. Resolving metadata (static object or async function, both exported as `metadata`)
9
+ * 1. Running access.ts checks eagerly to gate metadata resolution (TIM-1027)
10
+ * 2. Loading page/layout components from the segment chain
11
+ * 3. Resolving metadata (skipped for denied segments to prevent side effects)
12
12
  * 4. Building the React element tree (page → error boundaries → access gates → layouts)
13
13
  * 5. Resolving parallel slots
14
- * 6. Wrapping the tree with AccessPreRunner for React.cache-scoped access checks
15
14
  *
16
15
  * See design/02-rendering-pipeline.md, design/04-authorization.md
17
16
  */
@@ -154,24 +153,6 @@ export interface RouteElementResult {
154
153
  skippedSegments: string[];
155
154
  }
156
155
 
157
- /**
158
- * Wraps a DenySignal or RedirectSignal with the layout components loaded
159
- * so far, enabling the caller to render deny pages inside the layout shell.
160
- *
161
- * @deprecated No longer thrown by buildRouteElement since TIM-662. Access
162
- * checks now run inside AccessPreRunner during renderToReadableStream, and
163
- * signals are caught by onError. Kept for backward compat with external code.
164
- */
165
- export class RouteSignalWithContext extends Error {
166
- constructor(
167
- public readonly signal: DenySignal | RedirectSignal,
168
- public readonly layoutComponents: LayoutComponentEntry[],
169
- public readonly segments: ManifestSegmentNode[]
170
- ) {
171
- super(signal.message);
172
- }
173
- }
174
-
175
156
  // ─── Module Processing Helpers ─────────────────────────────────────────────
176
157
 
177
158
  /**
@@ -235,16 +216,18 @@ function extractDeferSuspenseFor(mod: Record<string, unknown>, current: number):
235
216
  /**
236
217
  * Build a React element tree from a matched route.
237
218
  *
238
- * Loads modules, runs access checks, resolves metadata, and constructs
239
- * the element tree. DenySignal and RedirectSignal propagate to the caller
240
- * for HTTP-level handling.
219
+ * Runs access checks eagerly to gate metadata resolution, then loads
220
+ * modules, resolves metadata (skipping denied segments), and constructs
221
+ * the element tree. DenySignal and RedirectSignal from access are stored
222
+ * as AccessGate verdicts for synchronous replay during render.
241
223
  *
242
224
  * Does NOT serialize to RSC Flight — the caller decides whether to render
243
225
  * to a stream or use the element directly (e.g., for action revalidation).
244
226
  *
245
- * Access checks are collected but NOT executed here. They run inside
246
- * AccessPreRunner during renderToReadableStream so that access.ts and
247
- * render components share the same React.cache scope (TIM-662).
227
+ * For passing segments, AccessGate re-runs access during render so that
228
+ * React.cache is populated for layout dedup (TIM-662). For denied
229
+ * segments, the pre-computed verdict is replayed — no re-execution,
230
+ * no metadata side effects, no head leak (TIM-1027).
248
231
  */
249
232
  export async function buildRouteElement(
250
233
  req: Request,
@@ -255,7 +238,46 @@ export async function buildRouteElement(
255
238
  ): Promise<RouteElementResult> {
256
239
  const segments = match.segments;
257
240
 
258
- // Load all modules along the segment chain
241
+ // ── Access pre-pass ──────────────────────────────────────────────────
242
+ // Run access checks eagerly to determine which segments are denied.
243
+ // This gates metadata() resolution: denied segments' metadata functions
244
+ // are never called, preventing unauthorized side effects (DB queries)
245
+ // and metadata content leaking into the <head> of denied responses.
246
+ //
247
+ // Verdicts are passed to AccessGate for synchronous replay (DenySignal
248
+ // renders the deny page in-tree; RedirectSignal propagates). For PASSING
249
+ // segments, no verdict is passed — AccessGate re-runs access during
250
+ // renderToReadableStream to populate React.cache for layout dedup.
251
+ //
252
+ // See TIM-1027, design/04-authorization.md, design/13-security.md.
253
+ const accessVerdicts = new Map<number, DenySignal | RedirectSignal>();
254
+ let firstDeniedIndex = Infinity;
255
+
256
+ for (let i = 0; i < segments.length; i++) {
257
+ const segment = segments[i];
258
+ if (!segment.access || i >= firstDeniedIndex) continue;
259
+
260
+ try {
261
+ const accessMod = await loadModule(segment.access);
262
+ const accessFn = accessMod.default as (() => Promise<void>) | undefined;
263
+ if (accessFn) {
264
+ await withSpan(
265
+ 'timber.access.pre',
266
+ { 'timber.segment': segment.segmentName ?? 'unknown' },
267
+ () => accessFn()
268
+ );
269
+ }
270
+ } catch (error: unknown) {
271
+ if (error instanceof DenySignal || error instanceof RedirectSignal) {
272
+ accessVerdicts.set(i, error);
273
+ firstDeniedIndex = i;
274
+ } else {
275
+ throw error;
276
+ }
277
+ }
278
+ }
279
+
280
+ // ── Module loading + metadata resolution ─────────────────────────────
259
281
  const metadataEntries: Array<{ metadata: Metadata; isPage: boolean }> = [];
260
282
  const layoutComponents: LayoutComponentEntry[] = [];
261
283
  let PageComponent: ((...args: unknown[]) => unknown) | null = null;
@@ -264,6 +286,7 @@ export async function buildRouteElement(
264
286
  for (let i = 0; i < segments.length; i++) {
265
287
  const segment = segments[i];
266
288
  const isLeaf = i === segments.length - 1;
289
+ const isDenied = i >= firstDeniedIndex;
267
290
 
268
291
  // Load layout
269
292
  if (segment.layout) {
@@ -279,9 +302,11 @@ export async function buildRouteElement(
279
302
  // middleware and rendering. See coerceSegmentParams() in pipeline.ts.
280
303
 
281
304
  rejectLegacyGenerateMetadata(mod, segment.layout.filePath ?? segment.urlPath);
282
- const layoutMetadata = await extractMetadata(mod, segment);
283
- if (layoutMetadata) {
284
- metadataEntries.push({ metadata: layoutMetadata, isPage: false });
305
+ if (!isDenied) {
306
+ const layoutMetadata = await extractMetadata(mod, segment);
307
+ if (layoutMetadata) {
308
+ metadataEntries.push({ metadata: layoutMetadata, isPage: false });
309
+ }
285
310
  }
286
311
  deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
287
312
  }
@@ -297,9 +322,11 @@ export async function buildRouteElement(
297
322
  PageComponent = mod.default as (...args: unknown[]) => unknown;
298
323
  }
299
324
  rejectLegacyGenerateMetadata(mod, segment.page.filePath ?? segment.urlPath);
300
- const pageMetadata = await extractMetadata(mod, segment);
301
- if (pageMetadata) {
302
- metadataEntries.push({ metadata: pageMetadata, isPage: true });
325
+ if (!isDenied) {
326
+ const pageMetadata = await extractMetadata(mod, segment);
327
+ if (pageMetadata) {
328
+ metadataEntries.push({ metadata: pageMetadata, isPage: true });
329
+ }
303
330
  }
304
331
  deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
305
332
  }
@@ -317,16 +344,6 @@ export async function buildRouteElement(
317
344
  );
318
345
  }
319
346
 
320
- // Access checks are NOT run here. AccessGate components in the element
321
- // tree call accessFn directly during renderToReadableStream, sharing the
322
- // same React.cache scope as layout/page components. A requireUser() call
323
- // in access.ts populates React.cache; the same call in a layout is a hit.
324
- //
325
- // Previously (before TIM-662), access checks ran eagerly in a pre-render
326
- // loop OUTSIDE renderToReadableStream, breaking React.cache dedup.
327
- //
328
- // See design/04-authorization.md §"Pre-Render Pass and Verdict Replay"
329
-
330
347
  // Build deny page fallback chains for each segment position.
331
348
  // When AccessGate or PageDenyBoundary catches a DenySignal, they render
332
349
  // the matching deny page in-tree instead of throwing into React Flight.
@@ -359,8 +376,11 @@ export async function buildRouteElement(
359
376
  ? new URL(requestUrl.origin)
360
377
  : resolvedMetadata.metadataBase;
361
378
 
362
- for (const segment of segments) {
379
+ for (let si = 0; si < segments.length; si++) {
380
+ const segment = segments[si];
363
381
  if (!segment.metadataRoutes) continue;
382
+ // Skip auto-linking for denied segments — same gate as metadata().
383
+ if (si >= firstDeniedIndex) continue;
364
384
  for (const baseName of Object.keys(segment.metadataRoutes)) {
365
385
  const convention = METADATA_ROUTE_CONVENTIONS[baseName];
366
386
  if (!convention) continue;
@@ -461,7 +481,7 @@ export async function buildRouteElement(
461
481
  //
462
482
  // When clientStateTree is provided (from X-Timber-State-Tree header on
463
483
  // client navigation), sync layouts the client already has are skipped.
464
- // Access.ts already ran for ALL segments in the pre-render loop above.
484
+ // Access.ts was pre-checked eagerly above for metadata gating (TIM-1027).
465
485
  // See design/19-client-navigation.md §"X-Timber-State-Tree Header"
466
486
  //
467
487
  // hasRenderedLayoutBelow tracks whether a non-skipped layout has been
@@ -517,10 +537,15 @@ export async function buildRouteElement(
517
537
  const accessMod = await loadModule(segment.access);
518
538
  const accessFn = accessMod.default as (() => unknown) | undefined;
519
539
  if (accessFn) {
540
+ // Pass verdict for denied/redirected segments so AccessGate replays
541
+ // without re-execution. Passing segments omit verdict so AccessGate
542
+ // re-runs access during render for React.cache population.
543
+ const verdict = accessVerdicts.get(i);
520
544
  element = h(AccessGate, {
521
545
  accessFn,
522
546
  segmentName: segment.segmentName,
523
547
  denyPages: denyPageChains.get(i),
548
+ ...(verdict ? { verdict } : {}),
524
549
  children: element,
525
550
  });
526
551
  }
@@ -661,10 +686,15 @@ export async function buildRouteElement(
661
686
  const accessMod = await loadModule(segment.access);
662
687
  const accessFn = accessMod.default as (() => unknown) | undefined;
663
688
  if (accessFn) {
689
+ // Pass verdict for denied/redirected segments so AccessGate replays
690
+ // without re-execution. Passing segments omit verdict so AccessGate
691
+ // re-runs access during render for React.cache population.
692
+ const verdict = accessVerdicts.get(i);
664
693
  element = h(AccessGate, {
665
694
  accessFn,
666
695
  segmentName: segment.segmentName,
667
696
  denyPages: denyPageChains.get(i),
697
+ ...(verdict ? { verdict } : {}),
668
698
  children: element,
669
699
  });
670
700
  }