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

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 (183) hide show
  1. package/dist/_chunks/{actions-DzFMnUAR.js → actions-OIZ8siQu.js} +28 -43
  2. package/dist/_chunks/actions-OIZ8siQu.js.map +1 -0
  3. package/dist/_chunks/{cli-schema-sync-DH-M8Poy.js → cli-schema-sync-CBiMEtqa.js} +1 -1
  4. package/dist/_chunks/{cli-schema-sync-D2IpXicg.js → cli-schema-sync-CQaxupHv.js} +47 -54
  5. package/dist/_chunks/cli-schema-sync-CQaxupHv.js.map +1 -0
  6. package/dist/_chunks/{handler-store-DjISKYEB.js → handler-store-BVDgyTh7.js} +26 -2
  7. package/dist/_chunks/handler-store-BVDgyTh7.js.map +1 -0
  8. package/dist/_chunks/{href-validation-EZktELp8.js → href-validation-DyZIzfMJ.js} +18 -2
  9. package/dist/_chunks/{href-validation-EZktELp8.js.map → href-validation-DyZIzfMJ.js.map} +1 -1
  10. package/dist/_chunks/{navigation-root-DLc4s4ZC.js → navigation-root-bUDZLw74.js} +23 -2
  11. package/dist/_chunks/navigation-root-bUDZLw74.js.map +1 -0
  12. package/dist/_chunks/tracing-M_zKubhk.js.map +1 -1
  13. package/dist/_chunks/{walkers-7FBPM-hX.js → walkers-Ck4Y45XN.js} +49 -8
  14. package/dist/_chunks/walkers-Ck4Y45XN.js.map +1 -0
  15. package/dist/adapters/cloudflare.d.ts +1 -15
  16. package/dist/adapters/cloudflare.d.ts.map +1 -1
  17. package/dist/adapters/cloudflare.js +10 -28
  18. package/dist/adapters/cloudflare.js.map +1 -1
  19. package/dist/adapters/compress-module.d.ts.map +1 -1
  20. package/dist/adapters/nitro.d.ts.map +1 -1
  21. package/dist/adapters/nitro.js +18 -5
  22. package/dist/adapters/nitro.js.map +1 -1
  23. package/dist/cache/index.js +39 -12
  24. package/dist/cache/index.js.map +1 -1
  25. package/dist/cache/redis-handler.d.ts +72 -8
  26. package/dist/cache/redis-handler.d.ts.map +1 -1
  27. package/dist/cache/stable-stringify.d.ts +9 -0
  28. package/dist/cache/stable-stringify.d.ts.map +1 -1
  29. package/dist/cache/timber-cache.d.ts +3 -1
  30. package/dist/cache/timber-cache.d.ts.map +1 -1
  31. package/dist/cli.js +1 -1
  32. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  33. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  34. package/dist/client/browser-entry/scroll.d.ts +9 -0
  35. package/dist/client/browser-entry/scroll.d.ts.map +1 -1
  36. package/dist/client/error-boundary.d.ts.map +1 -1
  37. package/dist/client/error-boundary.js +24 -10
  38. package/dist/client/error-boundary.js.map +1 -1
  39. package/dist/client/error-reconstituter.d.ts +8 -1
  40. package/dist/client/error-reconstituter.d.ts.map +1 -1
  41. package/dist/client/index.js +3 -3
  42. package/dist/client/index.js.map +1 -1
  43. package/dist/client/internal.js +99 -24
  44. package/dist/client/internal.js.map +1 -1
  45. package/dist/client/link.d.ts.map +1 -1
  46. package/dist/client/navigation-api.d.ts.map +1 -1
  47. package/dist/client/navigation-root.d.ts +13 -0
  48. package/dist/client/navigation-root.d.ts.map +1 -1
  49. package/dist/client/router.d.ts +7 -0
  50. package/dist/client/router.d.ts.map +1 -1
  51. package/dist/client/rsc-fetch.d.ts.map +1 -1
  52. package/dist/dev-tools/404-page.d.ts +6 -2
  53. package/dist/dev-tools/404-page.d.ts.map +1 -1
  54. package/dist/dev-tools/dev-page-shell.d.ts +77 -0
  55. package/dist/dev-tools/dev-page-shell.d.ts.map +1 -0
  56. package/dist/dev-tools/error-page.d.ts +4 -30
  57. package/dist/dev-tools/error-page.d.ts.map +1 -1
  58. package/dist/dev-tools/index.d.ts +2 -1
  59. package/dist/dev-tools/index.d.ts.map +1 -1
  60. package/dist/dev-tools/instrumentation.d.ts +11 -1
  61. package/dist/dev-tools/instrumentation.d.ts.map +1 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +432 -255
  64. package/dist/index.js.map +1 -1
  65. package/dist/plugins/cache.d.ts +43 -9
  66. package/dist/plugins/cache.d.ts.map +1 -1
  67. package/dist/plugins/cloudflare-wasm.d.ts +4 -1
  68. package/dist/plugins/cloudflare-wasm.d.ts.map +1 -1
  69. package/dist/plugins/dev-server.d.ts +16 -0
  70. package/dist/plugins/dev-server.d.ts.map +1 -1
  71. package/dist/plugins/server-bundle.d.ts.map +1 -1
  72. package/dist/plugins/shims.d.ts +23 -0
  73. package/dist/plugins/shims.d.ts.map +1 -1
  74. package/dist/routing/codegen-types.d.ts +4 -0
  75. package/dist/routing/codegen-types.d.ts.map +1 -1
  76. package/dist/routing/index.js +2 -2
  77. package/dist/server/action-handler.d.ts.map +1 -1
  78. package/dist/server/actions.d.ts +3 -2
  79. package/dist/server/actions.d.ts.map +1 -1
  80. package/dist/server/cookie-context.d.ts.map +1 -1
  81. package/dist/server/cookie-parsing.d.ts.map +1 -1
  82. package/dist/server/deny-boundary.d.ts +27 -2
  83. package/dist/server/deny-boundary.d.ts.map +1 -1
  84. package/dist/server/deny-renderer.d.ts.map +1 -1
  85. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  86. package/dist/server/fallback-error.d.ts +2 -2
  87. package/dist/server/fallback-error.d.ts.map +1 -1
  88. package/dist/server/index.js +1 -1
  89. package/dist/server/internal.js +227 -179
  90. package/dist/server/internal.js.map +1 -1
  91. package/dist/server/node-stream-transforms.d.ts +24 -6
  92. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  93. package/dist/server/pipeline.d.ts +11 -0
  94. package/dist/server/pipeline.d.ts.map +1 -1
  95. package/dist/server/primitives.d.ts +1 -11
  96. package/dist/server/primitives.d.ts.map +1 -1
  97. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  98. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  99. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  100. package/dist/server/slot-resolver.d.ts +6 -4
  101. package/dist/server/slot-resolver.d.ts.map +1 -1
  102. package/dist/server/ssr-entry.d.ts.map +1 -1
  103. package/dist/server/status-code-resolver.d.ts +6 -0
  104. package/dist/server/status-code-resolver.d.ts.map +1 -1
  105. package/dist/server/tracing.d.ts +2 -1
  106. package/dist/server/tracing.d.ts.map +1 -1
  107. package/dist/server/utils/mdx-file.d.ts +15 -0
  108. package/dist/server/utils/mdx-file.d.ts.map +1 -0
  109. package/dist/shared/head-header.d.ts +54 -0
  110. package/dist/shared/head-header.d.ts.map +1 -0
  111. package/dist/shared/href-validation.d.ts +11 -0
  112. package/dist/shared/href-validation.d.ts.map +1 -1
  113. package/dist/shared/redirect-type.d.ts +17 -0
  114. package/dist/shared/redirect-type.d.ts.map +1 -0
  115. package/dist/shims/headers.d.ts +52 -1
  116. package/dist/shims/headers.d.ts.map +1 -1
  117. package/dist/shims/navigation-client.d.ts +33 -11
  118. package/dist/shims/navigation-client.d.ts.map +1 -1
  119. package/package.json +11 -10
  120. package/src/adapters/cloudflare.ts +13 -47
  121. package/src/adapters/compress-module.ts +9 -3
  122. package/src/adapters/nitro.ts +9 -2
  123. package/src/cache/redis-handler.ts +77 -13
  124. package/src/cache/stable-stringify.ts +65 -2
  125. package/src/cache/timber-cache.ts +25 -13
  126. package/src/cli.ts +0 -0
  127. package/src/client/browser-entry/action-dispatch.ts +7 -2
  128. package/src/client/browser-entry/router-init.ts +2 -1
  129. package/src/client/browser-entry/scroll.ts +23 -0
  130. package/src/client/error-boundary.tsx +33 -5
  131. package/src/client/error-reconstituter.tsx +15 -1
  132. package/src/client/link.tsx +4 -1
  133. package/src/client/navigation-api.ts +5 -1
  134. package/src/client/navigation-root.tsx +16 -0
  135. package/src/client/router.ts +107 -29
  136. package/src/client/rsc-fetch.ts +6 -6
  137. package/src/dev-tools/404-page.ts +22 -123
  138. package/src/dev-tools/dev-page-shell.ts +256 -0
  139. package/src/dev-tools/error-page.ts +27 -203
  140. package/src/dev-tools/index.ts +7 -1
  141. package/src/dev-tools/instrumentation.ts +39 -2
  142. package/src/index.ts +1 -0
  143. package/src/plugins/cache.ts +79 -21
  144. package/src/plugins/cloudflare-wasm.ts +48 -10
  145. package/src/plugins/dev-server.ts +32 -8
  146. package/src/plugins/server-bundle.ts +25 -9
  147. package/src/plugins/shims.ts +94 -52
  148. package/src/routing/codegen-types.ts +4 -0
  149. package/src/routing/codegen.ts +63 -6
  150. package/src/routing/scanner.ts +80 -63
  151. package/src/server/action-handler.ts +10 -2
  152. package/src/server/actions.ts +3 -2
  153. package/src/server/compress.ts +11 -2
  154. package/src/server/cookie-context.ts +11 -0
  155. package/src/server/cookie-parsing.ts +16 -4
  156. package/src/server/deny-boundary.ts +106 -13
  157. package/src/server/deny-renderer.ts +40 -10
  158. package/src/server/error-boundary-wrapper.ts +1 -14
  159. package/src/server/fallback-error.ts +9 -4
  160. package/src/server/node-stream-transforms.ts +61 -24
  161. package/src/server/pipeline.ts +12 -0
  162. package/src/server/primitives.ts +10 -31
  163. package/src/server/route-matcher.ts +22 -6
  164. package/src/server/rsc-entry/error-renderer.ts +1 -12
  165. package/src/server/rsc-entry/index.ts +7 -2
  166. package/src/server/rsc-entry/rsc-payload.ts +4 -3
  167. package/src/server/slot-resolver.ts +27 -14
  168. package/src/server/ssr-entry.ts +17 -28
  169. package/src/server/status-code-resolver.ts +3 -2
  170. package/src/server/tracing.ts +17 -1
  171. package/src/server/tree-match.ts +15 -5
  172. package/src/server/utils/mdx-file.ts +22 -0
  173. package/src/shared/head-header.ts +69 -0
  174. package/src/shared/href-validation.ts +24 -0
  175. package/src/shared/redirect-type.ts +17 -0
  176. package/src/shims/headers.ts +119 -2
  177. package/src/shims/navigation-client.ts +48 -27
  178. package/LICENSE +0 -8
  179. package/dist/_chunks/actions-DzFMnUAR.js.map +0 -1
  180. package/dist/_chunks/cli-schema-sync-D2IpXicg.js.map +0 -1
  181. package/dist/_chunks/handler-store-DjISKYEB.js.map +0 -1
  182. package/dist/_chunks/navigation-root-DLc4s4ZC.js.map +0 -1
  183. package/dist/_chunks/walkers-7FBPM-hX.js.map +0 -1
@@ -3,7 +3,7 @@ import { a as warnRedirectInSuspense, c as warnSuspenseWrappingChildren, i as wa
3
3
  import { i as getMetadataRouteServePath, n as classifyMetadataRoute, r as getMetadataRouteAutoLink, t as METADATA_ROUTE_CONVENTIONS } from "../_chunks/metadata-routes-BjepjefT.js";
4
4
  import { n as classifyUrlSegment } from "../_chunks/segment-classify-CDDRVKs7.js";
5
5
  import { a as logProxyError, c as logRequestReceived, d as logSwrRefetchFailed, f as logWaitUntilRejected, h as swallow, i as logMiddlewareShortCircuit, l as logRouteError, m as setLogger, n as logCacheMiss, o as logRenderError, p as logWaitUntilUnsupported, r as logMiddlewareError, s as logRequestCompleted, t as getLogger, u as logSlowRequest } from "../_chunks/logger-Dt-jQaBO.js";
6
- import { C as setMatchedSegmentPath, E as getCookie, O as getSetCookieHeaders, S as runWithRequestContext, T as setSegmentParams, _ as getHeader, b as getSegmentParams, c as DenySignal, d as RenderError, g as applyRequestHeaderOverlay, l as RedirectSignal, n as executeAction, o as coerce, r as isRscActionRequest, t as buildNoJsResponse, w as setMutableCookieContext, x as markResponseFlushed, y as getSearchParams } from "../_chunks/actions-DzFMnUAR.js";
6
+ import { C as setMatchedSegmentPath, E as getCookie, O as getSetCookieHeaders, S as runWithRequestContext, T as setSegmentParams, _ as getHeader, b as getSegmentParams, c as DenySignal, g as applyRequestHeaderOverlay, l as RedirectSignal, n as executeAction, o as coerce, r as isRscActionRequest, t as buildNoJsResponse, u as RenderError, w as setMutableCookieContext, x as markResponseFlushed, y as getSearchParams } from "../_chunks/actions-OIZ8siQu.js";
7
7
  import "../client/error-boundary.js";
8
8
  import { n as toBracketKey } from "../_chunks/resolve-schema-Dj8tQloi.js";
9
9
  import "../_chunks/segment-context-ZDnXDkbz.js";
@@ -1119,6 +1119,39 @@ function resolveMetadataUrls(metadata) {
1119
1119
  return result;
1120
1120
  }
1121
1121
  //#endregion
1122
+ //#region src/client/error-reconstituter.tsx
1123
+ /**
1124
+ * Reconstitutes a SerializableError into a real Error instance before
1125
+ * passing to the user's error component.
1126
+ *
1127
+ * TSX error pages are 'use client' components that receive { error: Error, digest, reset }.
1128
+ * Error objects are not RSC-serializable (React Flight throws "Only plain objects
1129
+ * can be passed to Client Components"). This wrapper receives the error as a plain
1130
+ * SerializableError object, reconstitutes a real Error instance, and passes it
1131
+ * to the user's error component — ensuring error instanceof Error works correctly.
1132
+ *
1133
+ * See design/spike-TIM-565-unify-error-pages.md §"Edge Case B"
1134
+ * See design/10-error-handling.md §"RSC → SSR for Error Pages via SerializableError"
1135
+ */
1136
+ /**
1137
+ * Reconstitute a SerializableError into a real Error instance and render
1138
+ * the user's error component with the proper props.
1139
+ */
1140
+ function ErrorReconstituter({ error: serialized, digest, reset, component, status, dangerouslyPassData }) {
1141
+ return createElement(component, {
1142
+ error: Object.assign(new Error(serialized.message), {
1143
+ name: serialized.name,
1144
+ ...serialized.stack != null ? { stack: serialized.stack } : {}
1145
+ }),
1146
+ digest,
1147
+ reset,
1148
+ ...status != null ? {
1149
+ status,
1150
+ dangerouslyPassData
1151
+ } : {}
1152
+ });
1153
+ }
1154
+ //#endregion
1122
1155
  //#region src/server/safe-load.ts
1123
1156
  /**
1124
1157
  * Custom error class for module load failures.
@@ -1160,6 +1193,165 @@ async function loadModule(loader) {
1160
1193
  throw new ModuleLoadError(loader.filePath, error);
1161
1194
  }
1162
1195
  }
1196
+ /** Reverse index: status code → legacy file name. Built once at module load. */
1197
+ var STATUS_TO_LEGACY_FILE = Object.fromEntries(Object.entries({
1198
+ "not-found": 404,
1199
+ "forbidden": 403,
1200
+ "unauthorized": 401
1201
+ }).map(([name, status]) => [status, name]));
1202
+ /**
1203
+ * Look up `{statusStr}` then `{categoryKey}` (e.g. "4xx" / "5xx") in a
1204
+ * status-file group on a single segment. Shared by all three fallback
1205
+ * chains — the only structural difference between component 4xx,
1206
+ * component 5xx, and JSON resolution is *which* group is searched and
1207
+ * how the per-segment loop is layered around it.
1208
+ */
1209
+ function lookupInGroup(group, statusStr, categoryKey, segmentIndex, status) {
1210
+ if (!group) return null;
1211
+ const exact = group[statusStr];
1212
+ if (exact) return {
1213
+ file: exact,
1214
+ status,
1215
+ kind: "exact",
1216
+ segmentIndex
1217
+ };
1218
+ const category = group[categoryKey];
1219
+ if (category) return {
1220
+ file: category,
1221
+ status,
1222
+ kind: "category",
1223
+ segmentIndex
1224
+ };
1225
+ return null;
1226
+ }
1227
+ /**
1228
+ * Look up the legacy convention file (`not-found.tsx` / `forbidden.tsx` /
1229
+ * `unauthorized.tsx`) for `status` on a single segment. Returns null if
1230
+ * `status` has no legacy mapping or the file isn't present.
1231
+ */
1232
+ function lookupLegacy(group, status, segmentIndex) {
1233
+ if (!group) return null;
1234
+ const name = STATUS_TO_LEGACY_FILE[status];
1235
+ if (!name) return null;
1236
+ const file = group[name];
1237
+ return file ? {
1238
+ file,
1239
+ status,
1240
+ kind: "legacy",
1241
+ segmentIndex
1242
+ } : null;
1243
+ }
1244
+ /**
1245
+ * Resolve the status-code file to render for a given HTTP status code.
1246
+ *
1247
+ * Walks the segment chain from leaf to root following the fallback chain
1248
+ * defined in design/10-error-handling.md. Returns null if no file is found
1249
+ * (caller should render the framework default).
1250
+ *
1251
+ * @param status - The HTTP status code (4xx or 5xx).
1252
+ * @param segments - The matched segment chain from root (index 0) to leaf (last).
1253
+ * @param format - The response format family ('component' or 'json'). Defaults to 'component'.
1254
+ */
1255
+ function resolveStatusFile(status, segments, format = "component") {
1256
+ if (status < 400 || status > 599) return null;
1257
+ if (format === "json") return resolveJson(status, segments);
1258
+ if (status <= 499) return resolve4xx(status, segments);
1259
+ return resolve5xx(status, segments);
1260
+ }
1261
+ /**
1262
+ * 4xx component fallback chain — three separate full passes leaf→root.
1263
+ *
1264
+ * The passes must be separate (not interleaved per-segment) so that a
1265
+ * root-level `404.tsx` beats a leaf-level `error.tsx`. The 5xx chain
1266
+ * inverts this and is per-segment: a leaf's `error.tsx` beats a root's
1267
+ * `5xx.tsx`. This asymmetry is the only reason these two functions exist
1268
+ * separately.
1269
+ *
1270
+ * Pass 1 — {status}.tsx → 4xx.tsx (statusFiles)
1271
+ * Pass 2 — not-found / forbidden / unauthorized (legacyStatusFiles)
1272
+ * Pass 3 — error.tsx (error)
1273
+ */
1274
+ function resolve4xx(status, segments) {
1275
+ const statusStr = String(status);
1276
+ for (let i = segments.length - 1; i >= 0; i--) {
1277
+ const r = lookupInGroup(segments[i].statusFiles, statusStr, "4xx", i, status);
1278
+ if (r) return r;
1279
+ }
1280
+ for (let i = segments.length - 1; i >= 0; i--) {
1281
+ const r = lookupLegacy(segments[i].legacyStatusFiles, status, i);
1282
+ if (r) return r;
1283
+ }
1284
+ for (let i = segments.length - 1; i >= 0; i--) {
1285
+ const errorFile = segments[i].error;
1286
+ if (errorFile) return {
1287
+ file: errorFile,
1288
+ status,
1289
+ kind: "error",
1290
+ segmentIndex: i
1291
+ };
1292
+ }
1293
+ return null;
1294
+ }
1295
+ /**
1296
+ * 5xx component fallback chain — single pass, per-segment leaf→root.
1297
+ *
1298
+ * At each segment: {status}.tsx → 5xx.tsx → error.tsx. A leaf's
1299
+ * `error.tsx` therefore beats a root's `5xx.tsx`, which is the
1300
+ * intentional inverse of the 4xx chain.
1301
+ */
1302
+ function resolve5xx(status, segments) {
1303
+ const statusStr = String(status);
1304
+ for (let i = segments.length - 1; i >= 0; i--) {
1305
+ const segment = segments[i];
1306
+ const r = lookupInGroup(segment.statusFiles, statusStr, "5xx", i, status);
1307
+ if (r) return r;
1308
+ if (segment.error) return {
1309
+ file: segment.error,
1310
+ status,
1311
+ kind: "error",
1312
+ segmentIndex: i
1313
+ };
1314
+ }
1315
+ return null;
1316
+ }
1317
+ /**
1318
+ * JSON fallback chain (for both 4xx and 5xx) — single pass leaf→root.
1319
+ *
1320
+ * At each segment: {status}.json → {category}.json. No legacy compat,
1321
+ * no error.tsx — the JSON chain terminates at the category catch-all
1322
+ * and the caller falls back to a bare-JSON framework default.
1323
+ */
1324
+ function resolveJson(status, segments) {
1325
+ const statusStr = String(status);
1326
+ const categoryKey = status >= 500 ? "5xx" : "4xx";
1327
+ for (let i = segments.length - 1; i >= 0; i--) {
1328
+ const r = lookupInGroup(segments[i].jsonStatusFiles, statusStr, categoryKey, i, status);
1329
+ if (r) return r;
1330
+ }
1331
+ return null;
1332
+ }
1333
+ /**
1334
+ * Resolve the denial file for a parallel route slot.
1335
+ *
1336
+ * Slot denial is graceful degradation — no HTTP status on the wire.
1337
+ * Fallback chain: denied.tsx → default.tsx → null.
1338
+ *
1339
+ * @param slotNode - The segment node for the slot (segmentType === 'slot').
1340
+ */
1341
+ function resolveSlotDenied(slotNode) {
1342
+ const slotName = slotNode.segmentName.replace(/^@/, "");
1343
+ if (slotNode.denied) return {
1344
+ file: slotNode.denied,
1345
+ slotName,
1346
+ kind: "denied"
1347
+ };
1348
+ if (slotNode.default) return {
1349
+ file: slotNode.default,
1350
+ slotName,
1351
+ kind: "default"
1352
+ };
1353
+ return null;
1354
+ }
1163
1355
  //#endregion
1164
1356
  //#region src/server/deny-boundary.ts
1165
1357
  /**
@@ -1197,28 +1389,45 @@ async function loadModule(loader) {
1197
1389
  * Returns a React element for the matching component, or null if no match.
1198
1390
  */
1199
1391
  function renderMatchingDenyPage(chain, status, data) {
1200
- const h = createElement;
1201
1392
  for (const entry of chain) {
1202
- if (entry.status === status) return h(entry.component, {
1203
- status,
1204
- dangerouslyPassData: data
1205
- });
1206
- if (entry.status === 400 && status >= 400 && status <= 499) return h(entry.component, {
1207
- status,
1208
- dangerouslyPassData: data
1209
- });
1210
- if (entry.status === 500 && status >= 500 && status <= 599) return h(entry.component, {
1211
- status,
1212
- dangerouslyPassData: data
1213
- });
1214
- if (entry.status === null) return h(entry.component, {
1215
- status,
1216
- dangerouslyPassData: data
1217
- });
1393
+ if (entry.status === status) return renderDenyEntry(entry, status, data);
1394
+ if (entry.status === 400 && status >= 400 && status <= 499) return renderDenyEntry(entry, status, data);
1395
+ if (entry.status === 500 && status >= 500 && status <= 599) return renderDenyEntry(entry, status, data);
1396
+ if (entry.status === null) return renderDenyEntry(entry, status, data);
1218
1397
  }
1219
1398
  return null;
1220
1399
  }
1221
1400
  /**
1401
+ * Build the element for a matched deny chain entry with the props contract
1402
+ * the file's convention documents:
1403
+ *
1404
+ * - Status-code and legacy files: { status, dangerouslyPassData }
1405
+ * - error.tsx (TSX): { error, digest, reset } via ErrorReconstituter, plus
1406
+ * { status, dangerouslyPassData } for dual-shape implementations. Without
1407
+ * the Error prop, any error.tsx written per the docs (reading
1408
+ * error.message) crashes — turning a clean deny(404) into a 500. TIM-1081.
1409
+ * - error.mdx: plain { status } — server component, static content.
1410
+ */
1411
+ function renderDenyEntry(entry, status, data) {
1412
+ const h = createElement;
1413
+ if (entry.kind !== "error") return h(entry.component, {
1414
+ status,
1415
+ dangerouslyPassData: data
1416
+ });
1417
+ if (entry.isMdx) return h(entry.component, { status });
1418
+ return h(ErrorReconstituter, {
1419
+ error: {
1420
+ message: `Access denied with status ${status}`,
1421
+ name: "DenySignal"
1422
+ },
1423
+ digest: null,
1424
+ reset: void 0,
1425
+ component: entry.component,
1426
+ status,
1427
+ dangerouslyPassData: data
1428
+ });
1429
+ }
1430
+ /**
1222
1431
  * Set the deny status in the request context ALS.
1223
1432
  * Called from AccessGate / PageDenyBoundary when a DenySignal is caught.
1224
1433
  * The pipeline reads this after render to set the HTTP status code.
@@ -2589,167 +2798,6 @@ async function wrapWithErrorBoundaries(segment, element, loadModule, createEleme
2589
2798
  return element;
2590
2799
  }
2591
2800
  //#endregion
2592
- //#region src/server/status-code-resolver.ts
2593
- /** Reverse index: status code → legacy file name. Built once at module load. */
2594
- var STATUS_TO_LEGACY_FILE = Object.fromEntries(Object.entries({
2595
- "not-found": 404,
2596
- "forbidden": 403,
2597
- "unauthorized": 401
2598
- }).map(([name, status]) => [status, name]));
2599
- /**
2600
- * Look up `{statusStr}` then `{categoryKey}` (e.g. "4xx" / "5xx") in a
2601
- * status-file group on a single segment. Shared by all three fallback
2602
- * chains — the only structural difference between component 4xx,
2603
- * component 5xx, and JSON resolution is *which* group is searched and
2604
- * how the per-segment loop is layered around it.
2605
- */
2606
- function lookupInGroup(group, statusStr, categoryKey, segmentIndex, status) {
2607
- if (!group) return null;
2608
- const exact = group[statusStr];
2609
- if (exact) return {
2610
- file: exact,
2611
- status,
2612
- kind: "exact",
2613
- segmentIndex
2614
- };
2615
- const category = group[categoryKey];
2616
- if (category) return {
2617
- file: category,
2618
- status,
2619
- kind: "category",
2620
- segmentIndex
2621
- };
2622
- return null;
2623
- }
2624
- /**
2625
- * Look up the legacy convention file (`not-found.tsx` / `forbidden.tsx` /
2626
- * `unauthorized.tsx`) for `status` on a single segment. Returns null if
2627
- * `status` has no legacy mapping or the file isn't present.
2628
- */
2629
- function lookupLegacy(group, status, segmentIndex) {
2630
- if (!group) return null;
2631
- const name = STATUS_TO_LEGACY_FILE[status];
2632
- if (!name) return null;
2633
- const file = group[name];
2634
- return file ? {
2635
- file,
2636
- status,
2637
- kind: "legacy",
2638
- segmentIndex
2639
- } : null;
2640
- }
2641
- /**
2642
- * Resolve the status-code file to render for a given HTTP status code.
2643
- *
2644
- * Walks the segment chain from leaf to root following the fallback chain
2645
- * defined in design/10-error-handling.md. Returns null if no file is found
2646
- * (caller should render the framework default).
2647
- *
2648
- * @param status - The HTTP status code (4xx or 5xx).
2649
- * @param segments - The matched segment chain from root (index 0) to leaf (last).
2650
- * @param format - The response format family ('component' or 'json'). Defaults to 'component'.
2651
- */
2652
- function resolveStatusFile(status, segments, format = "component") {
2653
- if (status < 400 || status > 599) return null;
2654
- if (format === "json") return resolveJson(status, segments);
2655
- if (status <= 499) return resolve4xx(status, segments);
2656
- return resolve5xx(status, segments);
2657
- }
2658
- /**
2659
- * 4xx component fallback chain — three separate full passes leaf→root.
2660
- *
2661
- * The passes must be separate (not interleaved per-segment) so that a
2662
- * root-level `404.tsx` beats a leaf-level `error.tsx`. The 5xx chain
2663
- * inverts this and is per-segment: a leaf's `error.tsx` beats a root's
2664
- * `5xx.tsx`. This asymmetry is the only reason these two functions exist
2665
- * separately.
2666
- *
2667
- * Pass 1 — {status}.tsx → 4xx.tsx (statusFiles)
2668
- * Pass 2 — not-found / forbidden / unauthorized (legacyStatusFiles)
2669
- * Pass 3 — error.tsx (error)
2670
- */
2671
- function resolve4xx(status, segments) {
2672
- const statusStr = String(status);
2673
- for (let i = segments.length - 1; i >= 0; i--) {
2674
- const r = lookupInGroup(segments[i].statusFiles, statusStr, "4xx", i, status);
2675
- if (r) return r;
2676
- }
2677
- for (let i = segments.length - 1; i >= 0; i--) {
2678
- const r = lookupLegacy(segments[i].legacyStatusFiles, status, i);
2679
- if (r) return r;
2680
- }
2681
- for (let i = segments.length - 1; i >= 0; i--) {
2682
- const errorFile = segments[i].error;
2683
- if (errorFile) return {
2684
- file: errorFile,
2685
- status,
2686
- kind: "error",
2687
- segmentIndex: i
2688
- };
2689
- }
2690
- return null;
2691
- }
2692
- /**
2693
- * 5xx component fallback chain — single pass, per-segment leaf→root.
2694
- *
2695
- * At each segment: {status}.tsx → 5xx.tsx → error.tsx. A leaf's
2696
- * `error.tsx` therefore beats a root's `5xx.tsx`, which is the
2697
- * intentional inverse of the 4xx chain.
2698
- */
2699
- function resolve5xx(status, segments) {
2700
- const statusStr = String(status);
2701
- for (let i = segments.length - 1; i >= 0; i--) {
2702
- const segment = segments[i];
2703
- const r = lookupInGroup(segment.statusFiles, statusStr, "5xx", i, status);
2704
- if (r) return r;
2705
- if (segment.error) return {
2706
- file: segment.error,
2707
- status,
2708
- kind: "error",
2709
- segmentIndex: i
2710
- };
2711
- }
2712
- return null;
2713
- }
2714
- /**
2715
- * JSON fallback chain (for both 4xx and 5xx) — single pass leaf→root.
2716
- *
2717
- * At each segment: {status}.json → {category}.json. No legacy compat,
2718
- * no error.tsx — the JSON chain terminates at the category catch-all
2719
- * and the caller falls back to a bare-JSON framework default.
2720
- */
2721
- function resolveJson(status, segments) {
2722
- const statusStr = String(status);
2723
- const categoryKey = status >= 500 ? "5xx" : "4xx";
2724
- for (let i = segments.length - 1; i >= 0; i--) {
2725
- const r = lookupInGroup(segments[i].jsonStatusFiles, statusStr, categoryKey, i, status);
2726
- if (r) return r;
2727
- }
2728
- return null;
2729
- }
2730
- /**
2731
- * Resolve the denial file for a parallel route slot.
2732
- *
2733
- * Slot denial is graceful degradation — no HTTP status on the wire.
2734
- * Fallback chain: denied.tsx → default.tsx → null.
2735
- *
2736
- * @param slotNode - The segment node for the slot (segmentType === 'slot').
2737
- */
2738
- function resolveSlotDenied(slotNode) {
2739
- const slotName = slotNode.segmentName.replace(/^@/, "");
2740
- if (slotNode.denied) return {
2741
- file: slotNode.denied,
2742
- slotName,
2743
- kind: "denied"
2744
- };
2745
- if (slotNode.default) return {
2746
- file: slotNode.default,
2747
- slotName,
2748
- kind: "default"
2749
- };
2750
- return null;
2751
- }
2752
- //#endregion
2753
2801
  //#region src/server/flush.ts
2754
2802
  /**
2755
2803
  * Flush controller for timber.js rendering.