@timber-js/app 0.2.0-alpha.4 → 0.2.0-alpha.40

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 (336) hide show
  1. package/LICENSE +8 -0
  2. package/dist/_chunks/{als-registry-B7DbZ2hS.js → als-registry-Ba7URUIn.js} +1 -1
  3. package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -0
  4. package/dist/_chunks/chunk-DYhsFzuS.js +33 -0
  5. package/dist/_chunks/debug-ECi_61pb.js +108 -0
  6. package/dist/_chunks/debug-ECi_61pb.js.map +1 -0
  7. package/dist/_chunks/define-cookie-BmKbSyp0.js +93 -0
  8. package/dist/_chunks/define-cookie-BmKbSyp0.js.map +1 -0
  9. package/dist/_chunks/error-boundary-BAN3751q.js +211 -0
  10. package/dist/_chunks/error-boundary-BAN3751q.js.map +1 -0
  11. package/dist/_chunks/{format-CwdaB0_2.js → format-cX7wzEp2.js} +2 -2
  12. package/dist/_chunks/{format-CwdaB0_2.js.map → format-cX7wzEp2.js.map} +1 -1
  13. package/dist/_chunks/{interception-BOoWmLUA.js → interception-D2djYaIm.js} +112 -77
  14. package/dist/_chunks/interception-D2djYaIm.js.map +1 -0
  15. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
  16. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
  17. package/dist/_chunks/{request-context-CZJi4CuK.js → request-context-BxYIJM24.js} +93 -69
  18. package/dist/_chunks/request-context-BxYIJM24.js.map +1 -0
  19. package/dist/_chunks/segment-context-C6byCyZU.js +69 -0
  20. package/dist/_chunks/segment-context-C6byCyZU.js.map +1 -0
  21. package/dist/_chunks/stale-reload-C0ValzG7.js +47 -0
  22. package/dist/_chunks/stale-reload-C0ValzG7.js.map +1 -0
  23. package/dist/_chunks/{tracing-Cwn7697K.js → tracing-CuXiCP5p.js} +17 -3
  24. package/dist/_chunks/{tracing-Cwn7697K.js.map → tracing-CuXiCP5p.js.map} +1 -1
  25. package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-BvW0TKDn.js} +1 -1
  26. package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-BvW0TKDn.js.map} +1 -1
  27. package/dist/_chunks/wrappers-C6J0nNji.js +331 -0
  28. package/dist/_chunks/wrappers-C6J0nNji.js.map +1 -0
  29. package/dist/adapters/compress-module.d.ts.map +1 -1
  30. package/dist/adapters/nitro.d.ts +17 -1
  31. package/dist/adapters/nitro.d.ts.map +1 -1
  32. package/dist/adapters/nitro.js +56 -13
  33. package/dist/adapters/nitro.js.map +1 -1
  34. package/dist/cache/fast-hash.d.ts +22 -0
  35. package/dist/cache/fast-hash.d.ts.map +1 -0
  36. package/dist/cache/index.d.ts +5 -2
  37. package/dist/cache/index.d.ts.map +1 -1
  38. package/dist/cache/index.js +88 -18
  39. package/dist/cache/index.js.map +1 -1
  40. package/dist/cache/register-cached-function.d.ts.map +1 -1
  41. package/dist/cache/singleflight.d.ts +18 -1
  42. package/dist/cache/singleflight.d.ts.map +1 -1
  43. package/dist/cache/timber-cache.d.ts.map +1 -1
  44. package/dist/client/error-boundary.d.ts +10 -1
  45. package/dist/client/error-boundary.d.ts.map +1 -1
  46. package/dist/client/error-boundary.js +1 -125
  47. package/dist/client/index.d.ts +3 -2
  48. package/dist/client/index.d.ts.map +1 -1
  49. package/dist/client/index.js +213 -93
  50. package/dist/client/index.js.map +1 -1
  51. package/dist/client/link.d.ts +22 -8
  52. package/dist/client/link.d.ts.map +1 -1
  53. package/dist/client/navigation-context.d.ts +2 -2
  54. package/dist/client/router.d.ts +25 -3
  55. package/dist/client/router.d.ts.map +1 -1
  56. package/dist/client/rsc-fetch.d.ts +23 -2
  57. package/dist/client/rsc-fetch.d.ts.map +1 -1
  58. package/dist/client/segment-cache.d.ts +1 -1
  59. package/dist/client/segment-cache.d.ts.map +1 -1
  60. package/dist/client/segment-context.d.ts +1 -1
  61. package/dist/client/segment-context.d.ts.map +1 -1
  62. package/dist/client/segment-merger.d.ts.map +1 -1
  63. package/dist/client/stale-reload.d.ts +15 -0
  64. package/dist/client/stale-reload.d.ts.map +1 -1
  65. package/dist/client/top-loader.d.ts +1 -1
  66. package/dist/client/top-loader.d.ts.map +1 -1
  67. package/dist/client/transition-root.d.ts +1 -1
  68. package/dist/client/transition-root.d.ts.map +1 -1
  69. package/dist/client/use-params.d.ts +2 -2
  70. package/dist/client/use-params.d.ts.map +1 -1
  71. package/dist/client/use-query-states.d.ts +1 -1
  72. package/dist/codec.d.ts +21 -0
  73. package/dist/codec.d.ts.map +1 -0
  74. package/dist/cookies/define-cookie.d.ts +33 -12
  75. package/dist/cookies/define-cookie.d.ts.map +1 -1
  76. package/dist/cookies/index.js +1 -83
  77. package/dist/fonts/css.d.ts +1 -0
  78. package/dist/fonts/css.d.ts.map +1 -1
  79. package/dist/fonts/local.d.ts +4 -2
  80. package/dist/fonts/local.d.ts.map +1 -1
  81. package/dist/index.d.ts +112 -35
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +635 -233
  84. package/dist/index.js.map +1 -1
  85. package/dist/params/define.d.ts +76 -0
  86. package/dist/params/define.d.ts.map +1 -0
  87. package/dist/params/index.d.ts +8 -0
  88. package/dist/params/index.d.ts.map +1 -0
  89. package/dist/params/index.js +104 -0
  90. package/dist/params/index.js.map +1 -0
  91. package/dist/plugins/adapter-build.d.ts.map +1 -1
  92. package/dist/plugins/build-manifest.d.ts.map +1 -1
  93. package/dist/plugins/client-chunks.d.ts +32 -0
  94. package/dist/plugins/client-chunks.d.ts.map +1 -0
  95. package/dist/plugins/dev-error-overlay.d.ts +26 -1
  96. package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
  97. package/dist/plugins/entries.d.ts +7 -0
  98. package/dist/plugins/entries.d.ts.map +1 -1
  99. package/dist/plugins/fonts.d.ts +9 -1
  100. package/dist/plugins/fonts.d.ts.map +1 -1
  101. package/dist/plugins/mdx.d.ts +6 -0
  102. package/dist/plugins/mdx.d.ts.map +1 -1
  103. package/dist/plugins/routing.d.ts.map +1 -1
  104. package/dist/plugins/server-bundle.d.ts.map +1 -1
  105. package/dist/plugins/static-build.d.ts.map +1 -1
  106. package/dist/routing/codegen.d.ts +2 -2
  107. package/dist/routing/codegen.d.ts.map +1 -1
  108. package/dist/routing/index.js +1 -1
  109. package/dist/routing/scanner.d.ts.map +1 -1
  110. package/dist/routing/status-file-lint.d.ts +2 -1
  111. package/dist/routing/status-file-lint.d.ts.map +1 -1
  112. package/dist/routing/types.d.ts +6 -4
  113. package/dist/routing/types.d.ts.map +1 -1
  114. package/dist/rsc-runtime/rsc.d.ts +1 -1
  115. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  116. package/dist/rsc-runtime/ssr.d.ts +12 -0
  117. package/dist/rsc-runtime/ssr.d.ts.map +1 -1
  118. package/dist/search-params/codecs.d.ts +1 -1
  119. package/dist/search-params/define.d.ts +153 -0
  120. package/dist/search-params/define.d.ts.map +1 -0
  121. package/dist/search-params/index.d.ts +4 -5
  122. package/dist/search-params/index.d.ts.map +1 -1
  123. package/dist/search-params/index.js +3 -474
  124. package/dist/search-params/registry.d.ts +1 -1
  125. package/dist/search-params/wrappers.d.ts +53 -0
  126. package/dist/search-params/wrappers.d.ts.map +1 -0
  127. package/dist/server/access-gate.d.ts +4 -0
  128. package/dist/server/access-gate.d.ts.map +1 -1
  129. package/dist/server/action-client.d.ts.map +1 -1
  130. package/dist/server/action-encryption.d.ts +76 -0
  131. package/dist/server/action-encryption.d.ts.map +1 -0
  132. package/dist/server/action-handler.d.ts.map +1 -1
  133. package/dist/server/als-registry.d.ts +18 -4
  134. package/dist/server/als-registry.d.ts.map +1 -1
  135. package/dist/server/build-manifest.d.ts +2 -2
  136. package/dist/server/debug.d.ts +46 -15
  137. package/dist/server/debug.d.ts.map +1 -1
  138. package/dist/server/default-logger.d.ts +22 -0
  139. package/dist/server/default-logger.d.ts.map +1 -0
  140. package/dist/server/deny-renderer.d.ts.map +1 -1
  141. package/dist/server/early-hints.d.ts +13 -5
  142. package/dist/server/early-hints.d.ts.map +1 -1
  143. package/dist/server/error-boundary-wrapper.d.ts +4 -0
  144. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  145. package/dist/server/flight-injection-state.d.ts +78 -0
  146. package/dist/server/flight-injection-state.d.ts.map +1 -0
  147. package/dist/server/flight-scripts.d.ts +39 -0
  148. package/dist/server/flight-scripts.d.ts.map +1 -0
  149. package/dist/server/flush.d.ts.map +1 -1
  150. package/dist/server/form-data.d.ts +29 -0
  151. package/dist/server/form-data.d.ts.map +1 -1
  152. package/dist/server/html-injectors.d.ts +5 -11
  153. package/dist/server/html-injectors.d.ts.map +1 -1
  154. package/dist/server/index.d.ts +4 -2
  155. package/dist/server/index.d.ts.map +1 -1
  156. package/dist/server/index.js +1975 -1649
  157. package/dist/server/index.js.map +1 -1
  158. package/dist/server/logger.d.ts +24 -7
  159. package/dist/server/logger.d.ts.map +1 -1
  160. package/dist/server/node-stream-transforms.d.ts +77 -0
  161. package/dist/server/node-stream-transforms.d.ts.map +1 -0
  162. package/dist/server/pipeline.d.ts +7 -4
  163. package/dist/server/pipeline.d.ts.map +1 -1
  164. package/dist/server/primitives.d.ts +30 -3
  165. package/dist/server/primitives.d.ts.map +1 -1
  166. package/dist/server/render-timeout.d.ts +51 -0
  167. package/dist/server/render-timeout.d.ts.map +1 -0
  168. package/dist/server/request-context.d.ts +65 -38
  169. package/dist/server/request-context.d.ts.map +1 -1
  170. package/dist/server/route-element-builder.d.ts +7 -0
  171. package/dist/server/route-element-builder.d.ts.map +1 -1
  172. package/dist/server/route-handler.d.ts.map +1 -1
  173. package/dist/server/route-matcher.d.ts +2 -2
  174. package/dist/server/route-matcher.d.ts.map +1 -1
  175. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  176. package/dist/server/rsc-entry/helpers.d.ts +46 -3
  177. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  178. package/dist/server/rsc-entry/index.d.ts +6 -1
  179. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  180. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  181. package/dist/server/rsc-entry/rsc-stream.d.ts +9 -0
  182. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  183. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  184. package/dist/server/slot-resolver.d.ts +1 -1
  185. package/dist/server/slot-resolver.d.ts.map +1 -1
  186. package/dist/server/ssr-entry.d.ts +22 -0
  187. package/dist/server/ssr-entry.d.ts.map +1 -1
  188. package/dist/server/ssr-render.d.ts +39 -21
  189. package/dist/server/ssr-render.d.ts.map +1 -1
  190. package/dist/server/tracing.d.ts +10 -0
  191. package/dist/server/tracing.d.ts.map +1 -1
  192. package/dist/server/tree-builder.d.ts +19 -12
  193. package/dist/server/tree-builder.d.ts.map +1 -1
  194. package/dist/server/types.d.ts +1 -3
  195. package/dist/server/types.d.ts.map +1 -1
  196. package/dist/server/version-skew.d.ts +61 -0
  197. package/dist/server/version-skew.d.ts.map +1 -0
  198. package/dist/server/waituntil-bridge.d.ts.map +1 -1
  199. package/dist/shared/merge-search-params.d.ts +22 -0
  200. package/dist/shared/merge-search-params.d.ts.map +1 -0
  201. package/dist/shims/navigation-client.d.ts +1 -1
  202. package/dist/shims/navigation-client.d.ts.map +1 -1
  203. package/dist/shims/navigation.d.ts +1 -1
  204. package/dist/shims/navigation.d.ts.map +1 -1
  205. package/dist/utils/state-machine.d.ts +80 -0
  206. package/dist/utils/state-machine.d.ts.map +1 -0
  207. package/package.json +17 -14
  208. package/src/adapters/compress-module.ts +24 -4
  209. package/src/adapters/nitro.ts +58 -9
  210. package/src/cache/fast-hash.ts +34 -0
  211. package/src/cache/index.ts +5 -2
  212. package/src/cache/register-cached-function.ts +7 -3
  213. package/src/cache/singleflight.ts +62 -4
  214. package/src/cache/timber-cache.ts +34 -26
  215. package/src/cli.ts +0 -0
  216. package/src/client/browser-entry.ts +94 -90
  217. package/src/client/error-boundary.tsx +18 -1
  218. package/src/client/index.ts +10 -1
  219. package/src/client/link.tsx +78 -19
  220. package/src/client/navigation-context.ts +2 -2
  221. package/src/client/router.ts +105 -60
  222. package/src/client/rsc-fetch.ts +63 -2
  223. package/src/client/segment-cache.ts +1 -1
  224. package/src/client/segment-context.ts +6 -1
  225. package/src/client/segment-merger.ts +2 -8
  226. package/src/client/stale-reload.ts +32 -6
  227. package/src/client/top-loader.tsx +10 -9
  228. package/src/client/transition-root.tsx +7 -1
  229. package/src/client/use-params.ts +3 -3
  230. package/src/client/use-query-states.ts +1 -1
  231. package/src/codec.ts +21 -0
  232. package/src/cookies/define-cookie.ts +69 -18
  233. package/src/fonts/css.ts +2 -1
  234. package/src/fonts/local.ts +7 -3
  235. package/src/index.ts +280 -85
  236. package/src/params/define.ts +260 -0
  237. package/src/params/index.ts +28 -0
  238. package/src/plugins/adapter-build.ts +6 -0
  239. package/src/plugins/build-manifest.ts +11 -0
  240. package/src/plugins/client-chunks.ts +65 -0
  241. package/src/plugins/dev-error-overlay.ts +70 -1
  242. package/src/plugins/dev-server.ts +38 -4
  243. package/src/plugins/entries.ts +12 -11
  244. package/src/plugins/fonts.ts +171 -19
  245. package/src/plugins/mdx.ts +9 -5
  246. package/src/plugins/routing.ts +40 -14
  247. package/src/plugins/server-bundle.ts +32 -1
  248. package/src/plugins/shims.ts +1 -1
  249. package/src/plugins/static-build.ts +8 -4
  250. package/src/routing/codegen.ts +109 -88
  251. package/src/routing/scanner.ts +55 -6
  252. package/src/routing/status-file-lint.ts +2 -1
  253. package/src/routing/types.ts +7 -4
  254. package/src/rsc-runtime/rsc.ts +2 -0
  255. package/src/rsc-runtime/ssr.ts +50 -0
  256. package/src/rsc-runtime/vendor-types.d.ts +7 -0
  257. package/src/search-params/codecs.ts +1 -1
  258. package/src/search-params/define.ts +504 -0
  259. package/src/search-params/index.ts +12 -18
  260. package/src/search-params/registry.ts +1 -1
  261. package/src/search-params/wrappers.ts +85 -0
  262. package/src/server/access-gate.tsx +40 -9
  263. package/src/server/action-client.ts +14 -5
  264. package/src/server/action-encryption.ts +144 -0
  265. package/src/server/action-handler.ts +19 -2
  266. package/src/server/als-registry.ts +18 -4
  267. package/src/server/build-manifest.ts +4 -4
  268. package/src/server/compress.ts +25 -7
  269. package/src/server/debug.ts +55 -17
  270. package/src/server/default-logger.ts +98 -0
  271. package/src/server/deny-renderer.ts +2 -1
  272. package/src/server/early-hints.ts +36 -15
  273. package/src/server/error-boundary-wrapper.ts +57 -14
  274. package/src/server/flight-injection-state.ts +152 -0
  275. package/src/server/flight-scripts.ts +59 -0
  276. package/src/server/flush.ts +2 -1
  277. package/src/server/form-data.ts +76 -0
  278. package/src/server/html-injectors.ts +103 -66
  279. package/src/server/index.ts +9 -4
  280. package/src/server/logger.ts +38 -35
  281. package/src/server/node-stream-transforms.ts +381 -0
  282. package/src/server/pipeline.ts +131 -39
  283. package/src/server/primitives.ts +47 -5
  284. package/src/server/render-timeout.ts +108 -0
  285. package/src/server/request-context.ts +112 -119
  286. package/src/server/route-element-builder.ts +106 -114
  287. package/src/server/route-handler.ts +2 -1
  288. package/src/server/route-matcher.ts +2 -2
  289. package/src/server/rsc-entry/error-renderer.ts +5 -3
  290. package/src/server/rsc-entry/helpers.ts +122 -3
  291. package/src/server/rsc-entry/index.ts +125 -49
  292. package/src/server/rsc-entry/rsc-payload.ts +52 -12
  293. package/src/server/rsc-entry/rsc-stream.ts +33 -8
  294. package/src/server/rsc-entry/ssr-renderer.ts +40 -13
  295. package/src/server/slot-resolver.ts +199 -210
  296. package/src/server/ssr-entry.ts +169 -17
  297. package/src/server/ssr-render.ts +266 -67
  298. package/src/server/tracing.ts +23 -0
  299. package/src/server/tree-builder.ts +91 -57
  300. package/src/server/types.ts +1 -3
  301. package/src/server/version-skew.ts +104 -0
  302. package/src/server/waituntil-bridge.ts +4 -1
  303. package/src/shared/merge-search-params.ts +48 -0
  304. package/src/shims/navigation-client.ts +1 -1
  305. package/src/shims/navigation.ts +1 -1
  306. package/src/utils/state-machine.ts +111 -0
  307. package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
  308. package/dist/_chunks/debug-B4WUeqJ-.js +0 -75
  309. package/dist/_chunks/debug-B4WUeqJ-.js.map +0 -1
  310. package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
  311. package/dist/_chunks/request-context-CZJi4CuK.js.map +0 -1
  312. package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
  313. package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
  314. package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
  315. package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
  316. package/dist/client/error-boundary.js.map +0 -1
  317. package/dist/cookies/index.js.map +0 -1
  318. package/dist/plugins/dynamic-transform.d.ts +0 -72
  319. package/dist/plugins/dynamic-transform.d.ts.map +0 -1
  320. package/dist/search-params/analyze.d.ts +0 -54
  321. package/dist/search-params/analyze.d.ts.map +0 -1
  322. package/dist/search-params/builtin-codecs.d.ts +0 -105
  323. package/dist/search-params/builtin-codecs.d.ts.map +0 -1
  324. package/dist/search-params/create.d.ts +0 -106
  325. package/dist/search-params/create.d.ts.map +0 -1
  326. package/dist/search-params/index.js.map +0 -1
  327. package/dist/server/prerender.d.ts +0 -77
  328. package/dist/server/prerender.d.ts.map +0 -1
  329. package/dist/server/response-cache.d.ts +0 -53
  330. package/dist/server/response-cache.d.ts.map +0 -1
  331. package/src/plugins/dynamic-transform.ts +0 -161
  332. package/src/search-params/analyze.ts +0 -192
  333. package/src/search-params/builtin-codecs.ts +0 -228
  334. package/src/search-params/create.ts +0 -321
  335. package/src/server/prerender.ts +0 -139
  336. package/src/server/response-cache.ts +0 -277
@@ -17,6 +17,7 @@ import { DenySignal, RedirectSignal } from './primitives.js';
17
17
  import type { AccessGateProps, SlotAccessGateProps, ReactElement } from './tree-builder.js';
18
18
  import { withSpan, setSpanAttribute } from './tracing.js';
19
19
  import { isDebug } from './debug.js';
20
+ import { rawSegmentParams } from './request-context.js';
20
21
 
21
22
  // ─── AccessGate ─────────────────────────────────────────────────────────────
22
23
 
@@ -35,7 +36,7 @@ import { isDebug } from './debug.js';
35
36
  * gets the same data by calling the same cached functions (React.cache dedup).
36
37
  */
37
38
  export function AccessGate(props: AccessGateProps): ReactElement | Promise<ReactElement> {
38
- const { accessFn, params, searchParams, segmentName, verdict, children } = props;
39
+ const { accessFn, segmentName, verdict, children } = props;
39
40
 
40
41
  // Fast path: replay pre-computed verdict from the pre-render pass.
41
42
  // This is synchronous — Suspense boundaries cannot interfere with the
@@ -52,7 +53,7 @@ export function AccessGate(props: AccessGateProps): ReactElement | Promise<React
52
53
 
53
54
  // Fallback: call accessFn directly (used by tree-builder.ts which
54
55
  // doesn't run a pre-render pass, and for backward compat).
55
- return accessGateFallback(accessFn, params, searchParams, segmentName, children);
56
+ return accessGateFallback(accessFn, segmentName, children);
56
57
  }
57
58
 
58
59
  /**
@@ -61,14 +62,13 @@ export function AccessGate(props: AccessGateProps): ReactElement | Promise<React
61
62
  */
62
63
  async function accessGateFallback(
63
64
  accessFn: AccessGateProps['accessFn'],
64
- params: AccessGateProps['params'],
65
- searchParams: AccessGateProps['searchParams'],
66
65
  segmentName: AccessGateProps['segmentName'],
67
66
  children: ReactElement
68
67
  ): Promise<ReactElement> {
69
68
  await withSpan('timber.access', { 'timber.segment': segmentName ?? 'unknown' }, async () => {
70
69
  try {
71
- await accessFn({ params, searchParams });
70
+ const params = await rawSegmentParams();
71
+ await accessFn({ params });
72
72
  await setSpanAttribute('timber.result', 'pass');
73
73
  } catch (error: unknown) {
74
74
  if (error instanceof DenySignal) {
@@ -96,18 +96,28 @@ async function accessGateFallback(
96
96
  * The HTTP status code is unaffected — slot denial is a UI concern, not
97
97
  * a protocol concern. The parent layout and sibling slots still render.
98
98
  *
99
+ * DeniedComponent is passed instead of a pre-built element so that
100
+ * DenySignal.data can be forwarded as the dangerouslyPassData prop
101
+ * and the slot name can be passed as the slot prop. See TIM-488.
102
+ *
99
103
  * redirect() in slot access.ts is a dev-mode error — redirecting from a
100
104
  * slot doesn't make architectural sense.
101
105
  */
102
106
  export async function SlotAccessGate(props: SlotAccessGateProps): Promise<ReactElement> {
103
- const { accessFn, params, searchParams, deniedFallback, defaultFallback, children } = props;
107
+ const { accessFn, DeniedComponent, slotName, createElement, defaultFallback, children } = props;
104
108
 
105
109
  try {
106
- await accessFn({ params, searchParams });
110
+ const params = await rawSegmentParams();
111
+ await accessFn({ params });
107
112
  } catch (error: unknown) {
108
113
  // DenySignal → graceful degradation (denied.tsx → default.tsx → null)
114
+ // Build the denied element dynamically so DenySignal.data is forwarded.
109
115
  if (error instanceof DenySignal) {
110
- return deniedFallback ?? defaultFallback ?? null;
116
+ return (
117
+ buildDeniedFallback(DeniedComponent, slotName, error.data, createElement) ??
118
+ defaultFallback ??
119
+ null
120
+ );
111
121
  }
112
122
 
113
123
  // RedirectSignal in slot access → dev-mode error.
@@ -123,7 +133,11 @@ export async function SlotAccessGate(props: SlotAccessGateProps): Promise<ReactE
123
133
  );
124
134
  }
125
135
  // In production, treat as a deny — render fallback rather than crash.
126
- return deniedFallback ?? defaultFallback ?? null;
136
+ return (
137
+ buildDeniedFallback(DeniedComponent, slotName, undefined, createElement) ??
138
+ defaultFallback ??
139
+ null
140
+ );
127
141
  }
128
142
 
129
143
  // Unhandled error — re-throw so error boundaries can catch it.
@@ -141,3 +155,20 @@ export async function SlotAccessGate(props: SlotAccessGateProps): Promise<ReactE
141
155
  // Access passed — render slot content.
142
156
  return children;
143
157
  }
158
+
159
+ /**
160
+ * Build the denied fallback element dynamically with DenySignal data.
161
+ * Returns null if no DeniedComponent is available.
162
+ */
163
+ function buildDeniedFallback(
164
+ DeniedComponent: SlotAccessGateProps['DeniedComponent'],
165
+ slotName: string,
166
+ data: unknown,
167
+ createElement: SlotAccessGateProps['createElement']
168
+ ): ReactElement | null {
169
+ if (!DeniedComponent) return null;
170
+ return createElement(DeniedComponent, {
171
+ slot: slotName,
172
+ dangerouslyPassData: data,
173
+ });
174
+ }
@@ -184,7 +184,7 @@ async function runActionMiddleware<TCtx>(
184
184
  // Re-export parseFormData for use throughout the framework
185
185
  import { parseFormData } from './form-data.js';
186
186
  import { formatSize } from '#/utils/format.js';
187
- import { isDebug } from './debug.js';
187
+ import { isDebug, isDevMode } from './debug.js';
188
188
 
189
189
  /**
190
190
  * Extract validation errors from a schema error.
@@ -247,12 +247,15 @@ export function handleActionError(error: unknown): ActionResult<never> {
247
247
  };
248
248
  }
249
249
 
250
- // In dev, include the message for debugging
251
- const isDev = isDebug();
250
+ // In dev, include the message for debugging.
251
+ // Uses isDevMode() — NOT isDebug() — because this data is sent to the
252
+ // browser. TIMBER_DEBUG must never cause error messages to leak to clients.
253
+ // See design/13-security.md principle 4: "Errors don't leak."
254
+ const devMode = isDevMode();
252
255
  return {
253
256
  serverError: {
254
257
  code: 'INTERNAL_ERROR',
255
- ...(isDev && error instanceof Error ? { data: { message: error.message } } : {}),
258
+ ...(devMode && error instanceof Error ? { data: { message: error.message } } : {}),
256
259
  },
257
260
  };
258
261
  }
@@ -292,8 +295,14 @@ export function createActionClient<TCtx = Record<string, never>>(
292
295
  // Determine input — either FormData (from useActionState) or direct arg
293
296
  let rawInput: unknown;
294
297
  if (args.length === 2 && args[1] instanceof FormData) {
295
- // Called as (prevState, formData) by React useActionState
298
+ // Called as (prevState, formData) by React useActionState (with-JS path)
296
299
  rawInput = schema ? parseFormData(args[1]) : args[1];
300
+ } else if (args.length === 1 && args[0] instanceof FormData) {
301
+ // No-JS path: React's decodeAction binds FormData as the sole argument.
302
+ // The form POSTs without JavaScript, decodeAction resolves the server
303
+ // reference and binds the FormData, then executeAction calls fn() with
304
+ // no additional args — so the bound FormData arrives as args[0].
305
+ rawInput = schema ? parseFormData(args[0]) : args[0];
297
306
  } else {
298
307
  // Direct call: action(input)
299
308
  rawInput = args[0];
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Server action bound args encryption utilities.
3
+ *
4
+ * Provides key management for the RSC plugin's built-in bound args encryption.
5
+ * The RSC plugin (@vitejs/plugin-rsc) handles the actual encrypt/decrypt via
6
+ * AES-256-GCM — this module handles:
7
+ *
8
+ * 1. Key sourcing: auto-generated at build time (embedded in bundle), overridable
9
+ * via env var for cross-build key sharing (rolling/blue-green deployments)
10
+ * 2. Build-time key expression generation for the RSC plugin's `defineEncryptionKey`
11
+ *
12
+ * Encryption is always on in production. In dev mode, it's on by default
13
+ * (matching the RSC plugin's behavior) but can be disabled for debugging.
14
+ *
15
+ * ## Known Security Considerations
16
+ *
17
+ * 1. **defineEncryptionKey is a raw JS expression.** The RSC plugin inlines it
18
+ * verbatim into generated code. We only emit the hardcoded string
19
+ * `process.env.TIMBER_ACTIONS_ENCRYPTION_KEY` — never user-controlled input.
20
+ * If this function is ever extended to accept configurable env var names,
21
+ * the expression MUST be validated against a safe pattern.
22
+ *
23
+ * 2. **Key material lives in GC-visible JS strings.** `atob()` decodes the key
24
+ * into a regular JavaScript string on the V8 heap. JavaScript has no
25
+ * `SecureString` or memory-zeroing primitive — this is an inherent platform
26
+ * limitation. Acceptable for web server use; would need review for FIPS.
27
+ *
28
+ * 3. **TIMBER_ACTIONS_ENCRYPTION_KEY must be set at both build time and runtime.**
29
+ * At build time, we validate the key format and emit a runtime expression.
30
+ * If the env var is present at build time but missing at runtime, the server
31
+ * will crash on first action invocation with an opaque `atob(undefined)` error.
32
+ * If the env var is present at runtime but was absent at build time, the RSC
33
+ * plugin will have generated its own key and the env var is silently ignored.
34
+ *
35
+ * See design/08-forms-and-actions.md §"Security"
36
+ * See design/13-security.md
37
+ */
38
+
39
+ // ─── Types ────────────────────────────────────────────────────────────────
40
+
41
+ /** User-facing configuration for action bound args encryption. */
42
+ export interface ActionEncryptionConfig {
43
+ /**
44
+ * Disable encryption in dev mode for easier debugging.
45
+ * Has no effect in production — encryption is always enabled.
46
+ * Default: false (encryption is on in dev too).
47
+ */
48
+ disableInDev?: boolean;
49
+ }
50
+
51
+ // ─── Key Resolution ───────────────────────────────────────────────────────
52
+
53
+ /**
54
+ * Regex for safe `defineEncryptionKey` expressions.
55
+ *
56
+ * The RSC plugin inlines this expression verbatim into generated JavaScript.
57
+ * We restrict it to `process.env.<UPPER_SNAKE_CASE>` to prevent code injection.
58
+ * See "Known Security Considerations" at the top of this file.
59
+ */
60
+ const SAFE_KEY_EXPR = /^process\.env\.[A-Z_][A-Z0-9_]*$/;
61
+
62
+ /**
63
+ * Build the `defineEncryptionKey` expression for the RSC plugin.
64
+ *
65
+ * The RSC plugin accepts a JavaScript expression string that will be
66
+ * inlined into the encryption runtime module. At runtime, this expression
67
+ * must evaluate to the base64-encoded encryption key.
68
+ *
69
+ * Priority:
70
+ * 1. `TIMBER_ACTIONS_ENCRYPTION_KEY` env var (for cross-build key sharing
71
+ * in rolling/blue-green deployments)
72
+ * 2. Auto-generated at build time (RSC plugin default — embedded in bundle,
73
+ * consistent across all instances of the same build)
74
+ *
75
+ * For env var keys, we generate a runtime expression that reads the env var.
76
+ * For auto-generated keys, we return undefined and let the RSC plugin handle it.
77
+ */
78
+ export function resolveEncryptionKeyExpression(): string | undefined {
79
+ // Check for env var override — used for cross-build key sharing where
80
+ // multiple builds must agree on the same encryption key.
81
+ const envKey = process.env.TIMBER_ACTIONS_ENCRYPTION_KEY;
82
+ if (envKey) {
83
+ // Validate the key format (must be base64-encoded 32-byte key)
84
+ validateKeyFormat(envKey);
85
+
86
+ // Return a runtime expression that reads the env var at startup.
87
+ // This ensures the key is read at runtime, not embedded in the build.
88
+ const expr = 'process.env.TIMBER_ACTIONS_ENCRYPTION_KEY';
89
+
90
+ // Defense-in-depth: validate the expression matches our safe pattern.
91
+ // This is redundant today (hardcoded string), but protects against
92
+ // future refactors that might make the expression configurable.
93
+ if (!SAFE_KEY_EXPR.test(expr)) {
94
+ throw new Error(`Unsafe encryption key expression: ${expr}`);
95
+ }
96
+
97
+ return expr;
98
+ }
99
+
100
+ // No override — let the RSC plugin auto-generate a per-build key
101
+ return undefined;
102
+ }
103
+
104
+ /**
105
+ * Determine whether action encryption should be enabled.
106
+ *
107
+ * Encryption is always enabled in production. In dev mode, it's enabled
108
+ * by default but can be disabled via config for debugging.
109
+ */
110
+ export function shouldEnableEncryption(isDev: boolean, config?: ActionEncryptionConfig): boolean {
111
+ if (!isDev) return true; // Always on in production
112
+ if (config?.disableInDev) return false; // Opt-out in dev
113
+ return true; // On by default in dev too
114
+ }
115
+
116
+ // ─── Key Validation ───────────────────────────────────────────────────────
117
+
118
+ /**
119
+ * Validate that a key string is a valid base64-encoded 256-bit key.
120
+ * Throws a descriptive error if the key is malformed.
121
+ */
122
+ export function validateKeyFormat(key: string): void {
123
+ // Decode base64 and check length (32 bytes = 256 bits)
124
+ try {
125
+ const decoded = atob(key);
126
+ const bytes = decoded.length;
127
+ if (bytes !== 32) {
128
+ throw new Error(
129
+ `TIMBER_ACTIONS_ENCRYPTION_KEY must be a base64-encoded 256-bit (32-byte) key. ` +
130
+ `Got ${bytes} bytes. Generate one with: ` +
131
+ `node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"`
132
+ );
133
+ }
134
+ } catch (error) {
135
+ if (error instanceof Error && error.message.includes('TIMBER_ACTIONS_ENCRYPTION_KEY')) {
136
+ throw error;
137
+ }
138
+ throw new Error(
139
+ `TIMBER_ACTIONS_ENCRYPTION_KEY is not valid base64. ` +
140
+ `Generate a key with: ` +
141
+ `node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"`
142
+ );
143
+ }
144
+ }
@@ -31,6 +31,8 @@ import { handleActionError } from './action-client.js';
31
31
  import { enforceBodyLimits, enforceFieldLimit, type BodyLimitsConfig } from './body-limits.js';
32
32
  import { parseFormData } from './form-data.js';
33
33
  import type { FormFlashData } from './form-flash.js';
34
+ import { checkVersionSkew, applyReloadHeaders } from './version-skew.js';
35
+ import { logActionError } from './logger.js';
34
36
 
35
37
  // ─── Types ────────────────────────────────────────────────────────────────
36
38
 
@@ -90,6 +92,21 @@ export async function handleActionRequest(
90
92
  req: Request,
91
93
  config: ActionDispatchConfig
92
94
  ): Promise<Response | FormRerender | null> {
95
+ // Version skew detection — reject actions from stale clients (TIM-446).
96
+ // On mismatch, return a structured RSC error response that the client
97
+ // handles by showing a brief "App updated" message and reloading.
98
+ const skewCheck = checkVersionSkew(req);
99
+ if (!skewCheck.ok) {
100
+ const reloadHeaders = new Headers({
101
+ 'Content-Type': RSC_CONTENT_TYPE,
102
+ });
103
+ applyReloadHeaders(reloadHeaders);
104
+ // Return the reload signal as an RSC stream so createFromFetch can
105
+ // decode it. The client checks X-Timber-Reload before processing.
106
+ const rscStream = renderToReadableStream({ _versionSkew: true });
107
+ return new Response(rscStream, { status: 200, headers: reloadHeaders });
108
+ }
109
+
93
110
  // CSRF validation — reject cross-origin mutation requests.
94
111
  const csrfResult = validateCsrf(req, config.csrf);
95
112
  if (!csrfResult.ok) {
@@ -177,7 +194,7 @@ async function handleRscAction(
177
194
  });
178
195
  } catch (error) {
179
196
  // Log full error server-side for debugging
180
- console.error('[timber] server action error:', error);
197
+ logActionError({ method: req.method, path: new URL(req.url).pathname, error });
181
198
 
182
199
  // Return structured error response — ActionError gets its code/data,
183
200
  // unexpected errors get sanitized { code: 'INTERNAL_ERROR' }
@@ -293,7 +310,7 @@ async function handleFormAction(
293
310
  renderer: config.revalidateRenderer,
294
311
  });
295
312
  } catch (error) {
296
- console.error('[timber] server action error:', error);
313
+ logActionError({ method: req.method, path: new URL(req.url).pathname, error });
297
314
 
298
315
  // Return the error as flash data for re-render.
299
316
  // handleActionError produces { serverError } for ActionErrors
@@ -39,11 +39,25 @@ export interface RequestContextStore {
39
39
  /** Original (pre-overlay) frozen headers, kept for overlay merging. */
40
40
  originalHeaders: Headers;
41
41
  /**
42
- * Promise resolving to the route's typed search params (when search-params.ts
43
- * exists) or to the raw URLSearchParams. Stored as a Promise so the framework
44
- * can later support partial pre-rendering where param resolution is deferred.
42
+ * Promise resolving to the raw URLSearchParams for the current request.
43
+ * To get typed parsed params, import a search params definition and
44
+ * call `.parse(searchParams())`.
45
45
  */
46
- searchParamsPromise: Promise<URLSearchParams | Record<string, unknown>>;
46
+ searchParamsPromise: Promise<URLSearchParams>;
47
+ /**
48
+ * Raw search string from the request URL (e.g. "?foo=bar&baz=1").
49
+ * Available synchronously for use in `redirect()` with `preserveSearchParams`.
50
+ */
51
+ searchString: string;
52
+ /**
53
+ * Promise resolving to the coerced segment params for the current request.
54
+ * Set by the pipeline after route matching and param coercion, before
55
+ * middleware and rendering. Pages and layouts read params via
56
+ * `rawSegmentParams()` instead of receiving them as a prop.
57
+ *
58
+ * See design/07-routing.md §"params.ts — Convention File for Typed Params"
59
+ */
60
+ segmentParamsPromise?: Promise<Record<string, string | string[]>>;
47
61
  /** Outgoing Set-Cookie entries (name → serialized value + options). Last write wins. */
48
62
  cookieJar: Map<string, CookieEntry>;
49
63
  /** Whether the response has flushed (headers committed). */
@@ -95,10 +95,10 @@ export function buildCssLinkTags(cssUrls: string[]): string {
95
95
  * into 103 Early Hints responses. This avoids platform-specific 103
96
96
  * sending code.
97
97
  *
98
- * Example output: `</assets/root.css>; rel=preload; as=style, </assets/page.css>; rel=preload; as=style`
98
+ * Example output: `</assets/root.css>; as=style; rel=preload, </assets/page.css>; as=style; rel=preload`
99
99
  */
100
100
  export function buildLinkHeaders(cssUrls: string[]): string {
101
- return cssUrls.map((url) => `<${url}>; rel=preload; as=style`).join(', ');
101
+ return cssUrls.map((url) => `<${url}>; as=style; rel=preload`).join(', ');
102
102
  }
103
103
 
104
104
  // ─── Font utilities ──────────────────────────────────────────────────────
@@ -153,10 +153,10 @@ export function buildFontPreloadTags(fonts: ManifestFontEntry[]): string {
153
153
  *
154
154
  * Cloudflare CDN converts Link headers with rel=preload into 103 Early Hints.
155
155
  *
156
- * Example: `</fonts/inter.woff2>; rel=preload; as=font; crossorigin`
156
+ * Example: `</fonts/inter.woff2>; as=font; rel=preload; crossorigin`
157
157
  */
158
158
  export function buildFontLinkHeaders(fonts: ManifestFontEntry[]): string {
159
- return fonts.map((f) => `<${f.href}>; rel=preload; as=font; crossorigin`).join(', ');
159
+ return fonts.map((f) => `<${f.href}>; as=font; rel=preload; crossorigin`).join(', ');
160
160
  }
161
161
 
162
162
  // ─── JS chunk utilities ──────────────────────────────────────────────────
@@ -160,15 +160,33 @@ export function compressResponse(request: Request, response: Response): Response
160
160
  });
161
161
  }
162
162
 
163
- // ─── Gzip (CompressionStream API) ────────────────────────────────────────
163
+ // ─── Gzip (node:zlib with Z_SYNC_FLUSH) ──────────────────────────────────
164
+ //
165
+ // Uses node:zlib's createGzip with Z_SYNC_FLUSH so each chunk is flushed
166
+ // to the output immediately. The Web Platform CompressionStream API buffers
167
+ // internally and does NOT flush per-chunk — this kills streaming because
168
+ // the browser doesn't receive the HTML shell until the gzip stream closes
169
+ // (i.e. after all Suspense boundaries resolve).
170
+ //
171
+ // Z_SYNC_FLUSH adds ~2–5% size overhead vs Z_NO_FLUSH but preserves
172
+ // correct streaming behavior: the shell renders instantly, Suspense
173
+ // fallbacks are visible immediately, and streamed content appears
174
+ // progressively.
175
+
176
+ import { createGzip, constants } from 'node:zlib';
177
+ import { Readable } from 'node:stream';
164
178
 
165
179
  /**
166
- * Compress a ReadableStream with gzip using the Web Platform CompressionStream API.
167
- * Available in Node 18+, Bun, and Deno — no npm dependency needed.
180
+ * Compress a ReadableStream with gzip, flushing each chunk immediately.
181
+ *
182
+ * Uses node:zlib's createGzip with Z_SYNC_FLUSH to ensure each HTML chunk
183
+ * (shell, Suspense resolution, RSC payload) is delivered to the browser
184
+ * as soon as it's available — preserving streaming semantics.
168
185
  */
169
186
  function compressWithGzip(body: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {
170
- const compressionStream = new CompressionStream('gzip');
171
- // Cast needed: CompressionStream's WritableStream<BufferSource> type is wider
172
- // than ReadableStream's Uint8Array, but Uint8Array is a valid BufferSource.
173
- return body.pipeThrough(compressionStream as unknown as TransformStream<Uint8Array, Uint8Array>);
187
+ const gzip = createGzip({ flush: constants.Z_SYNC_FLUSH });
188
+ const nodeInput = Readable.fromWeb(body as import('stream/web').ReadableStream);
189
+ nodeInput.pipe(gzip);
190
+
191
+ return Readable.toWeb(gzip) as ReadableStream<Uint8Array>;
174
192
  }
@@ -1,18 +1,30 @@
1
1
  /**
2
2
  * Runtime debug flag for timber.js.
3
3
  *
4
- * Provides `isDebug()` a runtime check that returns true when timber's
5
- * debug/warning logging should be active. This is true in two cases:
4
+ * Two distinct functions for two distinct security levels:
6
5
  *
7
- * 1. Development mode: `process.env.NODE_ENV !== 'production'`
8
- * (statically replaced and tree-shaken in production builds — zero cost)
6
+ * ## `isDebug()` server-side logging only
9
7
  *
10
- * 2. TIMBER_DEBUG flag: A runtime environment variable that survives
11
- * production builds. When set to any truthy value ("1", "true", etc.),
12
- * timber's own diagnostics are re-enabled without affecting React's mode.
8
+ * Returns true when timber's debug/warning messages should be written to
9
+ * stderr / the server console. This NEVER affects what is sent to the
10
+ * client (no error details, no timing headers, no stack traces).
13
11
  *
14
- * The TIMBER_DEBUG check uses a dynamic property access pattern that
15
- * prevents the bundler from statically replacing or eliminating it.
12
+ * Active when any of:
13
+ * - `NODE_ENV !== 'production'` (standard dev mode)
14
+ * - `TIMBER_DEBUG` env var is set to a truthy value at runtime
15
+ * - `timber.config.ts` has `debug: true`
16
+ *
17
+ * ## `isDevMode()` — client-visible dev behavior
18
+ *
19
+ * Returns true ONLY when `NODE_ENV !== 'production'`. This gates anything
20
+ * that changes what clients can observe:
21
+ * - Dev error pages with stack traces (fallback-error.ts)
22
+ * - Detailed Server-Timing headers (pipeline.ts)
23
+ * - Error messages in action INTERNAL_ERROR payloads (action-client.ts)
24
+ * - Pipeline error handler wiring (Vite overlay)
25
+ *
26
+ * `isDevMode()` is statically replaced in production builds → the guarded
27
+ * code is tree-shaken to zero bytes. TIMBER_DEBUG cannot enable it.
16
28
  *
17
29
  * Usage:
18
30
  * In Cloudflare Workers wrangler.toml:
@@ -25,10 +37,33 @@
25
37
  * In timber.config.ts:
26
38
  * export default { debug: true }
27
39
  *
40
+ * See design/13-security.md for the security taxonomy.
28
41
  * See design/18-build-system.md for build pipeline details.
29
42
  */
30
43
 
31
- // ─── Debug Flag ─────────────────────────────────────────────────────────────
44
+ // ─── Dev Mode (client-visible) ──────────────────────────────────────────────
45
+
46
+ /**
47
+ * Check if the application is running in development mode.
48
+ *
49
+ * This is the ONLY function that should gate client-visible dev behavior:
50
+ * - Dev error pages with stack traces
51
+ * - Server-Timing mode default (`'detailed'` in dev, `'total'` in prod)
52
+ * - Error messages in action `INTERNAL_ERROR` payloads
53
+ * - Pipeline error handler wiring (Vite overlay)
54
+ *
55
+ * Returns `process.env.NODE_ENV !== 'production'`, which is statically
56
+ * replaced by the bundler in production builds. Code guarded by this
57
+ * function is tree-shaken to zero bytes in production.
58
+ *
59
+ * TIMBER_DEBUG does NOT enable this — that would leak server internals
60
+ * to clients. Use `isDebug()` for server-side-only logging.
61
+ */
62
+ export function isDevMode(): boolean {
63
+ return process.env.NODE_ENV !== 'production';
64
+ }
65
+
66
+ // ─── Debug Flag (server-side logging only) ──────────────────────────────────
32
67
 
33
68
  /**
34
69
  * Config-level debug override. Set via `setDebugFromConfig()` during
@@ -45,19 +80,20 @@ export function setDebugFromConfig(debug: boolean): void {
45
80
  }
46
81
 
47
82
  /**
48
- * Check if timber debug logging is active.
83
+ * Check if timber debug logging is active (server-side only).
49
84
  *
50
85
  * Returns true if ANY of these conditions hold:
51
86
  * - NODE_ENV is not 'production' (standard dev mode)
52
87
  * - TIMBER_DEBUG environment variable is set to a truthy value at runtime
53
88
  * - timber.config.ts has `debug: true`
54
89
  *
55
- * The TIMBER_DEBUG check is deliberately written as a dynamic property
56
- * access so bundlers cannot statically replace it. The `_envKey` variable
57
- * prevents the bundler from seeing `process.env.TIMBER_DEBUG` as a
58
- * compile-time constant.
90
+ * This function controls ONLY server-side logging messages written to
91
+ * stderr or the server console. It NEVER affects client-visible behavior
92
+ * (error pages, response headers, action payloads). For client-visible
93
+ * behavior, use `isDevMode()`.
59
94
  *
60
- * This function is intentionally NOT inlineable it reads runtime state.
95
+ * The TIMBER_DEBUG check is deliberately written as a dynamic property
96
+ * access so bundlers cannot statically replace it.
61
97
  */
62
98
  export function isDebug(): boolean {
63
99
  // Fast path: dev mode (statically replaced to `true` in dev, `false` in prod)
@@ -89,7 +125,9 @@ function _readTimberDebugEnv(): boolean {
89
125
  try {
90
126
  const key = 'TIMBER_DEBUG';
91
127
  const val =
92
- typeof process !== 'undefined' && process.env ? (process.env as Record<string, string | undefined>)[key] : undefined;
128
+ typeof process !== 'undefined' && process.env
129
+ ? (process.env as Record<string, string | undefined>)[key]
130
+ : undefined;
93
131
  if (val && val !== '0' && val !== 'false') return true;
94
132
  } catch {
95
133
  // process may not exist or env may throw — safe to ignore
@@ -0,0 +1,98 @@
1
+ /**
2
+ * DefaultLogger — human-readable stderr logging when no custom logger is configured.
3
+ *
4
+ * Ships as the fallback so production deployments always have error visibility,
5
+ * even without an `instrumentation.ts` logger export. Output is one line per
6
+ * event, designed for `fly logs`, `kubectl logs`, Cloudflare dashboard tails, etc.
7
+ *
8
+ * Format:
9
+ * [timber] ERROR message key=value key=value trace_id=4bf92f35
10
+ * [timber] WARN message key=value key=value trace_id=4bf92f35
11
+ * [timber] INFO message method=GET path=/dashboard status=200 durationMs=43 trace_id=4bf92f35
12
+ *
13
+ * Behavior:
14
+ * - Suppressed entirely in dev mode (dev logging handles all output)
15
+ * - `debug` suppressed unless TIMBER_DEBUG is set
16
+ * - Replaced entirely when a custom logger is set via `setLogger()`
17
+ *
18
+ * See design/17-logging.md §"DefaultLogger"
19
+ */
20
+
21
+ import { isDevMode, isDebug } from './debug.js';
22
+ import { formatSsrError } from './error-formatter.js';
23
+ import type { TimberLogger } from './logger.js';
24
+
25
+ /**
26
+ * Format data fields as `key=value` pairs for human-readable output.
27
+ * - `error` key is serialized via formatSsrError for stack trace cleanup
28
+ * - `trace_id` is truncated to 8 chars for readability (full ID in OTEL)
29
+ * - Other values are stringified inline
30
+ */
31
+ function formatDataFields(data?: Record<string, unknown>): string {
32
+ if (!data) return '';
33
+
34
+ const parts: string[] = [];
35
+ let traceId: string | undefined;
36
+
37
+ for (const [key, value] of Object.entries(data)) {
38
+ if (key === 'trace_id') {
39
+ // Defer trace_id to the end
40
+ traceId = typeof value === 'string' ? value : String(value);
41
+ continue;
42
+ }
43
+ if (key === 'error') {
44
+ // Serialize errors with formatSsrError for clean output
45
+ parts.push(`error=${formatSsrError(value)}`);
46
+ continue;
47
+ }
48
+ if (value === undefined || value === null) continue;
49
+ parts.push(`${key}=${value}`);
50
+ }
51
+
52
+ // trace_id always last, truncated to 8 chars for readability
53
+ if (traceId) {
54
+ parts.push(`trace_id=${traceId.slice(0, 8)}`);
55
+ }
56
+
57
+ return parts.length > 0 ? ' ' + parts.join(' ') : '';
58
+ }
59
+
60
+ /** Pad level string to fixed width for alignment. */
61
+ function padLevel(level: string): string {
62
+ return level.padEnd(5);
63
+ }
64
+
65
+ export function createDefaultLogger(): TimberLogger {
66
+ return {
67
+ error(msg: string, data?: Record<string, unknown>): void {
68
+ if (isDevMode()) return;
69
+ const fields = formatDataFields(data);
70
+ // Use process.stderr.write for consistent output (no extra newline handling)
71
+ process.stderr.write(`[timber] ${padLevel('ERROR')} ${msg}${fields}\n`);
72
+ },
73
+
74
+ warn(msg: string, data?: Record<string, unknown>): void {
75
+ if (isDevMode()) return;
76
+ const fields = formatDataFields(data);
77
+ process.stderr.write(`[timber] ${padLevel('WARN')} ${msg}${fields}\n`);
78
+ },
79
+
80
+ info(msg: string, data?: Record<string, unknown>): void {
81
+ // info is suppressed by default — per-request lines are too noisy
82
+ // without a custom logger. Enable with TIMBER_DEBUG.
83
+ if (isDevMode()) return;
84
+ if (!isDebug()) return;
85
+ const fields = formatDataFields(data);
86
+ process.stderr.write(`[timber] ${padLevel('INFO')} ${msg}${fields}\n`);
87
+ },
88
+
89
+ debug(msg: string, data?: Record<string, unknown>): void {
90
+ // debug is suppressed in dev (dev logger handles it) and in
91
+ // production unless TIMBER_DEBUG is explicitly set.
92
+ if (isDevMode()) return;
93
+ if (!isDebug()) return;
94
+ const fields = formatDataFields(data);
95
+ process.stderr.write(`[timber] ${padLevel('DEBUG')} ${msg}${fields}\n`);
96
+ },
97
+ };
98
+ }