@timber-js/app 0.1.48 → 0.1.50

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 (70) hide show
  1. package/dist/_chunks/{als-registry-c0AGnbqS.js → als-registry-k-AtAQ9R.js} +4 -2
  2. package/dist/_chunks/{als-registry-c0AGnbqS.js.map → als-registry-k-AtAQ9R.js.map} +1 -1
  3. package/dist/_chunks/interception-DGDIjDbR.js.map +1 -1
  4. package/dist/_chunks/{request-context-C69VW4xS.js → request-context-CRj2Zh1E.js} +2 -2
  5. package/dist/_chunks/request-context-CRj2Zh1E.js.map +1 -0
  6. package/dist/_chunks/{tracing-tIvqStk8.js → tracing-DF0G3FB7.js} +2 -2
  7. package/dist/_chunks/{tracing-tIvqStk8.js.map → tracing-DF0G3FB7.js.map} +1 -1
  8. package/dist/_chunks/use-query-states-DAhgj8Gx.js.map +1 -1
  9. package/dist/adapters/nitro.d.ts.map +1 -1
  10. package/dist/adapters/nitro.js +33 -6
  11. package/dist/adapters/nitro.js.map +1 -1
  12. package/dist/cache/index.js +2 -2
  13. package/dist/client/index.js.map +1 -1
  14. package/dist/client/navigation-context.d.ts +1 -1
  15. package/dist/client/navigation-context.d.ts.map +1 -1
  16. package/dist/client/router.d.ts.map +1 -1
  17. package/dist/client/transition-root.d.ts.map +1 -1
  18. package/dist/client/use-query-states.d.ts.map +1 -1
  19. package/dist/client/use-router.d.ts.map +1 -1
  20. package/dist/cookies/index.js +2 -2
  21. package/dist/index.d.ts +8 -12
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +53 -10
  24. package/dist/index.js.map +1 -1
  25. package/dist/plugins/chunks.d.ts.map +1 -1
  26. package/dist/plugins/entries.d.ts +10 -0
  27. package/dist/plugins/entries.d.ts.map +1 -1
  28. package/dist/routing/scanner.d.ts.map +1 -1
  29. package/dist/server/als-registry.d.ts +4 -0
  30. package/dist/server/als-registry.d.ts.map +1 -1
  31. package/dist/server/index.js +39 -7
  32. package/dist/server/index.js.map +1 -1
  33. package/dist/server/metadata-platform.d.ts.map +1 -1
  34. package/dist/server/metadata-social.d.ts.map +1 -1
  35. package/dist/server/pipeline.d.ts.map +1 -1
  36. package/dist/server/primitives.d.ts +8 -4
  37. package/dist/server/primitives.d.ts.map +1 -1
  38. package/dist/server/request-context.d.ts.map +1 -1
  39. package/dist/server/rsc-entry/index.d.ts +1 -0
  40. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  41. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  42. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  43. package/dist/server/rsc-prop-warnings.d.ts.map +1 -1
  44. package/dist/server/waituntil-bridge.d.ts +27 -0
  45. package/dist/server/waituntil-bridge.d.ts.map +1 -0
  46. package/package.json +2 -2
  47. package/src/adapters/nitro.ts +45 -9
  48. package/src/client/browser-entry.ts +80 -12
  49. package/src/client/navigation-context.ts +4 -1
  50. package/src/client/router.ts +5 -7
  51. package/src/client/transition-root.tsx +5 -11
  52. package/src/client/use-query-states.ts +4 -1
  53. package/src/client/use-router.ts +3 -1
  54. package/src/index.ts +8 -25
  55. package/src/plugins/chunks.ts +2 -1
  56. package/src/plugins/entries.ts +66 -2
  57. package/src/routing/scanner.ts +1 -4
  58. package/src/server/als-registry.ts +10 -0
  59. package/src/server/compress.ts +0 -1
  60. package/src/server/metadata-platform.ts +4 -1
  61. package/src/server/metadata-social.ts +4 -1
  62. package/src/server/pipeline.ts +6 -23
  63. package/src/server/primitives.ts +19 -9
  64. package/src/server/request-context.ts +1 -5
  65. package/src/server/rsc-entry/index.ts +16 -0
  66. package/src/server/rsc-entry/rsc-stream.ts +1 -4
  67. package/src/server/rsc-entry/ssr-renderer.ts +1 -3
  68. package/src/server/rsc-prop-warnings.ts +7 -17
  69. package/src/server/waituntil-bridge.ts +34 -0
  70. package/dist/_chunks/request-context-C69VW4xS.js.map +0 -1
@@ -33,6 +33,7 @@ const VIRTUAL_IDS = {
33
33
  ssrEntry: 'virtual:timber-ssr-entry',
34
34
  browserEntry: 'virtual:timber-browser-entry',
35
35
  config: 'virtual:timber-config',
36
+ instrumentation: 'virtual:timber-instrumentation',
36
37
  } as const;
37
38
 
38
39
  /**
@@ -53,6 +54,9 @@ const ENTRY_FILE_MAP: Record<string, string> = {
53
54
  /** The \0-prefixed resolved ID for virtual:timber-config */
54
55
  const RESOLVED_CONFIG_ID = `\0${VIRTUAL_IDS.config}`;
55
56
 
57
+ /** The \0-prefixed resolved ID for virtual:timber-instrumentation */
58
+ const RESOLVED_INSTRUMENTATION_ID = `\0${VIRTUAL_IDS.instrumentation}`;
59
+
56
60
  /**
57
61
  * Strip the \0 prefix from a module ID.
58
62
  *
@@ -103,6 +107,7 @@ function generateConfigModule(ctx: PluginContext): string {
103
107
  clientJavascript: ctx.clientJavascript,
104
108
  dev: ctx.dev ?? false,
105
109
  slowPhaseMs: ctx.config.dev?.slowPhaseMs ?? 200,
110
+ slowRequestMs: ctx.config.slowRequestMs ?? 3000,
106
111
  cookieSecrets,
107
112
  };
108
113
 
@@ -116,6 +121,56 @@ function generateConfigModule(ctx: PluginContext): string {
116
121
  ].join('\n');
117
122
  }
118
123
 
124
+ /**
125
+ * Detect the user's instrumentation.ts file at the project root.
126
+ *
127
+ * Checks for instrumentation.ts, .js, and .mjs — matching the same
128
+ * extensions as timber.config.ts detection.
129
+ */
130
+ function detectInstrumentationFile(root: string): string | null {
131
+ const extensions = ['.ts', '.js', '.mjs'];
132
+ for (const ext of extensions) {
133
+ const candidate = resolve(root, `instrumentation${ext}`);
134
+ if (existsSync(candidate)) return candidate;
135
+ }
136
+ return null;
137
+ }
138
+
139
+ /**
140
+ * Generate the virtual:timber-instrumentation module source.
141
+ *
142
+ * When the user's instrumentation.ts exists, generates a module that
143
+ * dynamically imports it. When it doesn't exist, generates a module
144
+ * that returns null. The RSC entry calls this loader at startup time.
145
+ *
146
+ * See design/17-logging.md §"instrumentation.ts — The Entry Point"
147
+ */
148
+ export function generateInstrumentationModule(instrumentationPath: string | null): string {
149
+ if (instrumentationPath) {
150
+ // Use the absolute path so the bundler can resolve and include it.
151
+ // The dynamic import ensures the file is loaded lazily and errors
152
+ // in the user's code don't prevent the module from being parsed.
153
+ return [
154
+ '// Auto-generated instrumentation loader — do not edit.',
155
+ '// Generated by timber-entries plugin.',
156
+ '',
157
+ `export default async function loadUserInstrumentation() {`,
158
+ ` return import(${JSON.stringify(instrumentationPath)});`,
159
+ `}`,
160
+ ].join('\n');
161
+ }
162
+
163
+ return [
164
+ '// Auto-generated instrumentation loader — do not edit.',
165
+ '// Generated by timber-entries plugin.',
166
+ '// No instrumentation.ts found at project root.',
167
+ '',
168
+ `export default async function loadUserInstrumentation() {`,
169
+ ` return null;`,
170
+ `}`,
171
+ ].join('\n');
172
+ }
173
+
119
174
  /**
120
175
  * Create the timber-entries Vite plugin.
121
176
  *
@@ -153,19 +208,28 @@ export function timberEntries(ctx: PluginContext): Plugin {
153
208
  return RESOLVED_CONFIG_ID;
154
209
  }
155
210
 
211
+ // Check instrumentation virtual module
212
+ if (cleanId === VIRTUAL_IDS.instrumentation) {
213
+ return RESOLVED_INSTRUMENTATION_ID;
214
+ }
215
+
156
216
  return null;
157
217
  },
158
218
 
159
219
  /**
160
- * Load the virtual:timber-config module.
220
+ * Load virtual modules: virtual:timber-config and virtual:timber-instrumentation.
161
221
  *
162
222
  * Entry files (rsc/ssr/browser) are real TypeScript files that Vite
163
- * processes normally. Only virtual:timber-config needs generated code.
223
+ * processes normally. Only config and instrumentation need generated code.
164
224
  */
165
225
  load(id: string) {
166
226
  if (id === RESOLVED_CONFIG_ID) {
167
227
  return generateConfigModule(ctx);
168
228
  }
229
+ if (id === RESOLVED_INSTRUMENTATION_ID) {
230
+ const instrumentationPath = detectInstrumentationFile(ctx.root);
231
+ return generateInstrumentationModule(instrumentationPath);
232
+ }
169
233
  return null;
170
234
  },
171
235
 
@@ -19,10 +19,7 @@ import type {
19
19
  InterceptionMarker,
20
20
  } from './types.js';
21
21
  import { DEFAULT_PAGE_EXTENSIONS, INTERCEPTION_MARKERS } from './types.js';
22
- import {
23
- classifyMetadataRoute,
24
- isDynamicMetadataExtension,
25
- } from '#/server/metadata-routes.js';
22
+ import { classifyMetadataRoute, isDynamicMetadataExtension } from '#/server/metadata-routes.js';
26
23
 
27
24
  /**
28
25
  * Pattern matching encoded path delimiters that must be rejected during route discovery.
@@ -114,3 +114,13 @@ export type EarlyHintsSenderFn = (links: string[]) => void;
114
114
 
115
115
  /** @internal — import via early-hints-sender.ts public API */
116
116
  export const earlyHintsSenderAls = new AsyncLocalStorage<EarlyHintsSenderFn>();
117
+
118
+ // ─── waitUntil Bridge ────────────────────────────────────────────────────
119
+ // Used by: waituntil-bridge.ts (waitUntil())
120
+ // Design doc: design/11-platform.md §"waitUntil()"
121
+
122
+ /** Function that extends the request lifecycle with a background promise. */
123
+ export type WaitUntilFn = (promise: Promise<unknown>) => void;
124
+
125
+ /** @internal — import via waituntil-bridge.ts public API */
126
+ export const waitUntilAls = new AsyncLocalStorage<WaitUntilFn>();
@@ -153,4 +153,3 @@ function compressWithGzip(body: ReadableStream<Uint8Array>): ReadableStream<Uint
153
153
  // than ReadableStream's Uint8Array, but Uint8Array is a valid BufferSource.
154
154
  return body.pipeThrough(compressionStream as unknown as TransformStream<Uint8Array, Uint8Array>);
155
155
  }
156
-
@@ -218,7 +218,10 @@ export function renderAppLinks(
218
218
  /**
219
219
  * Render Apple iTunes smart banner meta tag.
220
220
  */
221
- export function renderItunes(itunes: NonNullable<Metadata['itunes']>, elements: HeadElement[]): void {
221
+ export function renderItunes(
222
+ itunes: NonNullable<Metadata['itunes']>,
223
+ elements: HeadElement[]
224
+ ): void {
222
225
  const parts = [`app-id=${itunes.appId}`];
223
226
  if (itunes.affiliateData) parts.push(`affiliate-data=${itunes.affiliateData}`);
224
227
  if (itunes.appArgument) parts.push(`app-argument=${itunes.appArgument}`);
@@ -15,7 +15,10 @@ import type { HeadElement } from './metadata.js';
15
15
  * Handles og:title, og:description, og:image (with dimensions/alt),
16
16
  * og:video, og:audio, og:article:author, and other OG properties.
17
17
  */
18
- export function renderOpenGraph(og: NonNullable<Metadata['openGraph']>, elements: HeadElement[]): void {
18
+ export function renderOpenGraph(
19
+ og: NonNullable<Metadata['openGraph']>,
20
+ elements: HeadElement[]
21
+ ): void {
19
22
  const simpleProps: Array<[string, string | undefined]> = [
20
23
  ['og:title', og.title],
21
24
  ['og:description', og.description],
@@ -14,11 +14,7 @@
14
14
  import { canonicalize } from './canonicalize.js';
15
15
  import { runProxy, type ProxyExport } from './proxy.js';
16
16
  import { runMiddleware, type MiddlewareFn } from './middleware-runner.js';
17
- import {
18
- runWithTimingCollector,
19
- withTiming,
20
- getServerTimingHeader,
21
- } from './server-timing.js';
17
+ import { runWithTimingCollector, withTiming, getServerTimingHeader } from './server-timing.js';
22
18
  import {
23
19
  runWithRequestContext,
24
20
  applyRequestHeaderOverlay,
@@ -258,9 +254,7 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
258
254
  return response;
259
255
  };
260
256
 
261
- return enableServerTiming
262
- ? runWithTimingCollector(runRequest)
263
- : runRequest();
257
+ return enableServerTiming ? runWithTimingCollector(runRequest) : runRequest();
264
258
  });
265
259
  });
266
260
  };
@@ -276,12 +270,9 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
276
270
  } else {
277
271
  proxyExport = config.proxy!;
278
272
  }
279
- const proxyFn = () =>
280
- runProxy(proxyExport, req, () => handleRequest(req, method, path));
273
+ const proxyFn = () => runProxy(proxyExport, req, () => handleRequest(req, method, path));
281
274
  return await withSpan('timber.proxy', {}, () =>
282
- enableServerTiming
283
- ? withTiming('proxy', 'proxy.ts', proxyFn)
284
- : proxyFn()
275
+ enableServerTiming ? withTiming('proxy', 'proxy.ts', proxyFn) : proxyFn()
285
276
  );
286
277
  } catch (error) {
287
278
  // Uncaught proxy.ts error → bare HTTP 500
@@ -430,9 +421,7 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
430
421
  setMutableCookieContext(true);
431
422
  const middlewareFn = () => runMiddleware(match.middleware!, ctx);
432
423
  const middlewareResponse = await withSpan('timber.middleware', {}, () =>
433
- enableServerTiming
434
- ? withTiming('mw', 'middleware.ts', middlewareFn)
435
- : middlewareFn()
424
+ enableServerTiming ? withTiming('mw', 'middleware.ts', middlewareFn) : middlewareFn()
436
425
  );
437
426
  setMutableCookieContext(false);
438
427
  if (middlewareResponse) {
@@ -487,9 +476,7 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
487
476
  const renderFn = () =>
488
477
  render(req, match, responseHeaders, requestHeaderOverlay, interception);
489
478
  const response = await withSpan('timber.render', { 'http.route': canonicalPathname }, () =>
490
- enableServerTiming
491
- ? withTiming('render', 'RSC + SSR render', renderFn)
492
- : renderFn()
479
+ enableServerTiming ? withTiming('render', 'RSC + SSR render', renderFn) : renderFn()
493
480
  );
494
481
  markResponseFlushed();
495
482
  return response;
@@ -542,8 +529,6 @@ async function fireOnRequestError(
542
529
  );
543
530
  }
544
531
 
545
-
546
-
547
532
  // ─── Cookie Helpers ──────────────────────────────────────────────────────
548
533
 
549
534
  /**
@@ -585,5 +570,3 @@ function ensureMutableResponse(response: Response): Response {
585
570
  });
586
571
  }
587
572
  }
588
-
589
-
@@ -4,6 +4,7 @@
4
4
  // use to control request flow. See design/10-error-handling.md.
5
5
 
6
6
  import type { JsonSerializable } from './types.js';
7
+ import { getWaitUntil as _getWaitUntil } from './waituntil-bridge.js';
7
8
 
8
9
  // ─── Dev-mode validation ────────────────────────────────────────────────────
9
10
 
@@ -71,10 +72,7 @@ export function findNonSerializable(value: unknown, path = 'data'): string | nul
71
72
  }
72
73
 
73
74
  for (const key of Object.keys(value as Record<string, unknown>)) {
74
- const result = findNonSerializable(
75
- (value as Record<string, unknown>)[key],
76
- `${path}.${key}`
77
- );
75
+ const result = findNonSerializable((value as Record<string, unknown>)[key], `${path}.${key}`);
78
76
  if (result) return result;
79
77
  }
80
78
  return null;
@@ -338,14 +336,26 @@ let _waitUntilWarned = false;
338
336
  * Register a promise to be kept alive after the response is sent.
339
337
  * Maps to `ctx.waitUntil()` on Cloudflare Workers and similar platforms.
340
338
  *
341
- * If the adapter does not support `waitUntil`, a warning is logged once
342
- * and the promise is left to resolve (or reject) without being tracked.
339
+ * In production, the platform adapter installs a per-request waitUntil
340
+ * function via ALS (see waituntil-bridge.ts). This function checks the
341
+ * ALS bridge first, then falls back to the legacy adapter argument.
342
+ *
343
+ * If neither is available, a warning is logged once and the promise is
344
+ * left to resolve (or reject) without being tracked.
343
345
  *
344
346
  * @param promise - The background work to keep alive.
345
- * @param adapter - The platform adapter (injected by the framework at runtime).
347
+ * @param adapter - Optional legacy adapter (prefer ALS bridge in production).
346
348
  */
347
- export function waitUntil(promise: Promise<unknown>, adapter: WaitUntilAdapter): void {
348
- if (typeof adapter.waitUntil === 'function') {
349
+ export function waitUntil(promise: Promise<unknown>, adapter?: WaitUntilAdapter): void {
350
+ // Check ALS bridge first (installed by generated entry points)
351
+ const alsFn = _getWaitUntil();
352
+ if (alsFn) {
353
+ alsFn(promise);
354
+ return;
355
+ }
356
+
357
+ // Fall back to legacy adapter argument
358
+ if (adapter && typeof adapter.waitUntil === 'function') {
349
359
  adapter.waitUntil(promise);
350
360
  return;
351
361
  }
@@ -12,11 +12,7 @@
12
12
 
13
13
  import { createHmac, timingSafeEqual } from 'node:crypto';
14
14
  import type { Routes } from '#/index.js';
15
- import {
16
- requestContextAls,
17
- type RequestContextStore,
18
- type CookieEntry,
19
- } from './als-registry.js';
15
+ import { requestContextAls, type RequestContextStore, type CookieEntry } from './als-registry.js';
20
16
 
21
17
  // Re-export the ALS for framework-internal consumers that need direct access.
22
18
  export { requestContextAls };
@@ -21,6 +21,8 @@ import routeManifest from 'virtual:timber-route-manifest';
21
21
  import config from 'virtual:timber-config';
22
22
  // @ts-expect-error — virtual module provided by timber-build-manifest plugin
23
23
  import buildManifest from 'virtual:timber-build-manifest';
24
+ // @ts-expect-error — virtual module provided by timber-entries plugin
25
+ import loadUserInstrumentation from 'virtual:timber-instrumentation';
24
26
 
25
27
  import type { FormRerender } from '#/server/action-handler.js';
26
28
  import { handleActionRequest, isActionRequest } from '#/server/action-handler.js';
@@ -51,6 +53,7 @@ import { createMetadataRouteMatcher, createRouteMatcher } from '#/server/route-m
51
53
  import { initDevTracing } from '#/server/tracing.js';
52
54
 
53
55
  import { renderFallbackError as renderFallback } from '#/server/fallback-error.js';
56
+ import { loadInstrumentation } from '#/server/instrumentation.js';
54
57
  import { handleApiRoute } from './api-handler.js';
55
58
  import { renderErrorPage, renderNoMatchPage } from './error-renderer.js';
56
59
  import {
@@ -86,6 +89,12 @@ export function setDevPipelineErrorHandler(handler: (error: Error, phase: string
86
89
  * 103 Early Hints → middleware.ts → render (RSC → SSR → HTML).
87
90
  */
88
91
  async function createRequestHandler(manifest: typeof routeManifest, runtimeConfig: typeof config) {
92
+ // Load the user's instrumentation.ts — register() is awaited before the
93
+ // server accepts any requests. The logger and onRequestError hooks are
94
+ // wired into the framework. This runs once at startup.
95
+ // See design/17-logging.md §"register() — Server Startup"
96
+ await loadInstrumentation(loadUserInstrumentation);
97
+
89
98
  // Initialize cookie signing secrets from config (design/29-cookies.md §"Signed Cookies")
90
99
  const cookieSecrets = (runtimeConfig as Record<string, unknown>).cookieSecrets as
91
100
  | string[]
@@ -174,6 +183,9 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
174
183
  return renderNoMatchPage(req, manifest.root, responseHeaders, clientBootstrap);
175
184
  },
176
185
  interceptionRewrites: manifest.interceptionRewrites,
186
+ // Slow request threshold from timber.config.ts. Default 3000ms, 0 to disable.
187
+ // See design/17-logging.md §"slowRequestMs"
188
+ slowRequestMs: (runtimeConfig as Record<string, unknown>).slowRequestMs as number | undefined,
177
189
  enableServerTiming: isDev,
178
190
  onPipelineError: isDev
179
191
  ? (error: Error, phase: string) => {
@@ -446,4 +458,8 @@ async function renderRoute(
446
458
  // the handler with per-request 103 Early Hints sender via ALS.
447
459
  export { runWithEarlyHintsSender } from '#/server/early-hints-sender.js';
448
460
 
461
+ // Re-export for generated entry points to wrap the handler with per-request
462
+ // waitUntil support via ALS. See design/11-platform.md §"waitUntil()".
463
+ export { runWithWaitUntil } from '#/server/waituntil-bridge.js';
464
+
449
465
  export default await createRequestHandler(routeManifest, config);
@@ -46,10 +46,7 @@ export interface RscStreamResult {
46
46
  * fires onError during stream consumption. Signals are captured in the
47
47
  * returned `signals` object for the caller to handle.
48
48
  */
49
- export function renderRscStream(
50
- element: React.ReactElement,
51
- req: Request
52
- ): RscStreamResult {
49
+ export function renderRscStream(element: React.ReactElement, req: Request): RscStreamResult {
53
50
  const signals: RenderSignals = {
54
51
  denySignal: null,
55
52
  redirectSignal: null,
@@ -121,9 +121,7 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
121
121
  // promotion if the denial was already handled by a TimberErrorBoundary
122
122
  // (e.g., slot error boundary). The boundary sets navContext._denyHandledByBoundary
123
123
  // during SSR rendering. See LOCAL-298.
124
- function checkCapturedSignals(
125
- skipHandledDeny = false
126
- ): Response | Promise<Response> | null {
124
+ function checkCapturedSignals(skipHandledDeny = false): Response | Promise<Response> | null {
127
125
  if (signals.redirectSignal) {
128
126
  return buildRedirectResponse(req, signals.redirectSignal, responseHeaders);
129
127
  }
@@ -42,8 +42,7 @@ const DETECTION_RULES: Array<{
42
42
  pattern: /RegExp/i,
43
43
  info: {
44
44
  type: 'RegExp',
45
- suggestion:
46
- 'Use .toString() to serialize, and new RegExp() to reconstruct on the client.',
45
+ suggestion: 'Use .toString() to serialize, and new RegExp() to reconstruct on the client.',
47
46
  },
48
47
  },
49
48
  {
@@ -58,24 +57,21 @@ const DETECTION_RULES: Array<{
58
57
  pattern: /URLSearchParams/,
59
58
  info: {
60
59
  type: 'URLSearchParams',
61
- suggestion:
62
- 'Pass .toString() to serialize, or spread entries: Object.fromEntries(params).',
60
+ suggestion: 'Pass .toString() to serialize, or spread entries: Object.fromEntries(params).',
63
61
  },
64
62
  },
65
63
  {
66
64
  pattern: /Headers/,
67
65
  info: {
68
66
  type: 'Headers',
69
- suggestion:
70
- 'Convert to a plain object: Object.fromEntries(headers.entries()).',
67
+ suggestion: 'Convert to a plain object: Object.fromEntries(headers.entries()).',
71
68
  },
72
69
  },
73
70
  {
74
71
  pattern: /Symbol/i,
75
72
  info: {
76
73
  type: 'Symbol',
77
- suggestion:
78
- 'Symbols cannot be serialized. Use a string identifier instead.',
74
+ suggestion: 'Symbols cannot be serialized. Use a string identifier instead.',
79
75
  },
80
76
  },
81
77
  {
@@ -91,8 +87,7 @@ const DETECTION_RULES: Array<{
91
87
  pattern: /Classes or null prototypes/i,
92
88
  info: {
93
89
  type: 'class instance',
94
- suggestion:
95
- 'Spread to a plain object: { ...instance } or extract the needed properties.',
90
+ suggestion: 'Spread to a plain object: { ...instance } or extract the needed properties.',
96
91
  },
97
92
  },
98
93
  {
@@ -116,9 +111,7 @@ const DETECTION_RULES: Array<{
116
111
  * Returns type info with an actionable fix, or null if the error
117
112
  * is not related to RSC prop serialization.
118
113
  */
119
- export function detectNonSerializableType(
120
- errorMessage: string
121
- ): NonSerializableTypeInfo | null {
114
+ export function detectNonSerializableType(errorMessage: string): NonSerializableTypeInfo | null {
122
115
  if (!errorMessage) return null;
123
116
 
124
117
  for (const rule of DETECTION_RULES) {
@@ -171,10 +164,7 @@ export function formatRscPropWarning(
171
164
  * @param requestPath - The request pathname for context
172
165
  * @returns true if a warning was emitted
173
166
  */
174
- export function checkAndWarnRscPropError(
175
- error: unknown,
176
- requestPath: string
177
- ): boolean {
167
+ export function checkAndWarnRscPropError(error: unknown, requestPath: string): boolean {
178
168
  if (process.env.NODE_ENV === 'production') return false;
179
169
  if (!(error instanceof Error)) return false;
180
170
 
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Per-request waitUntil bridge — ALS bridge for platform adapters.
3
+ *
4
+ * The generated entry point (Nitro, Cloudflare) wraps the handler with
5
+ * `runWithWaitUntil`, binding the platform's lifecycle extension function
6
+ * (e.g., h3's `event.waitUntil()` or CF's `ctx.waitUntil()`) for the
7
+ * request duration. The `waitUntil()` primitive reads from this ALS to
8
+ * dispatch background work to the correct platform API.
9
+ *
10
+ * Design doc: design/11-platform.md §"waitUntil()"
11
+ */
12
+
13
+ import { waitUntilAls } from './als-registry.js';
14
+
15
+ /**
16
+ * Run a function with a per-request waitUntil handler installed.
17
+ *
18
+ * Called by generated entry points (Nitro node-server/bun, Cloudflare)
19
+ * to bind the platform's lifecycle extension for the request duration.
20
+ */
21
+ export function runWithWaitUntil<T>(waitUntilFn: (promise: Promise<unknown>) => void, fn: () => T): T {
22
+ return waitUntilAls.run(waitUntilFn, fn);
23
+ }
24
+
25
+ /**
26
+ * Get the current request's waitUntil function, if available.
27
+ *
28
+ * Returns undefined when no platform adapter has installed a waitUntil
29
+ * handler for the current request (e.g., on platforms that don't support
30
+ * lifecycle extension, or outside a request context).
31
+ */
32
+ export function getWaitUntil(): ((promise: Promise<unknown>) => void) | undefined {
33
+ return waitUntilAls.getStore();
34
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"request-context-C69VW4xS.js","names":[],"sources":["../../src/server/request-context.ts"],"sourcesContent":["/**\n * Request Context — per-request ALS store for headers() and cookies().\n *\n * Follows the same pattern as tracing.ts: a module-level AsyncLocalStorage\n * instance, public accessor functions that throw outside request scope,\n * and a framework-internal `runWithRequestContext()` to establish scope.\n *\n * See design/04-authorization.md §\"AccessContext does not include cookies or headers\"\n * and design/11-platform.md §\"AsyncLocalStorage\".\n * See design/29-cookies.md for cookie mutation semantics.\n */\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { Routes } from '#/index.js';\nimport {\n requestContextAls,\n type RequestContextStore,\n type CookieEntry,\n} from './als-registry.js';\n\n// Re-export the ALS for framework-internal consumers that need direct access.\nexport { requestContextAls };\n\n// No fallback needed — we use enterWith() instead of run() to ensure\n// the ALS context persists for the entire request lifecycle including\n// async stream consumption by React's renderToReadableStream.\n\n// ─── Cookie Signing Secrets ──────────────────────────────────────────────\n\n/**\n * Module-level cookie signing secrets. Index 0 is the newest (used for signing).\n * All entries are tried for verification (key rotation support).\n *\n * Set by the framework at startup via `setCookieSecrets()`.\n * See design/29-cookies.md §\"Signed Cookies\"\n */\nlet _cookieSecrets: string[] = [];\n\n/**\n * Configure the cookie signing secrets.\n *\n * Called by the framework during server initialization with values from\n * `cookies.secret` or `cookies.secrets` in timber.config.ts.\n *\n * The first secret (index 0) is used for signing new cookies.\n * All secrets are tried for verification (supports key rotation).\n */\nexport function setCookieSecrets(secrets: string[]): void {\n _cookieSecrets = secrets.filter(Boolean);\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────\n\n/**\n * Returns a read-only view of the current request's headers.\n *\n * Available in middleware, access checks, server components, and server actions.\n * Throws if called outside a request context (security principle #2: no global fallback).\n */\nexport function headers(): ReadonlyHeaders {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] headers() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n return store.headers;\n}\n\n/**\n * Returns a cookie accessor for the current request.\n *\n * Available in middleware, access checks, server components, and server actions.\n * Throws if called outside a request context (security principle #2: no global fallback).\n *\n * Read methods (.get, .has, .getAll) are always available and reflect\n * read-your-own-writes from .set() calls in the same request.\n *\n * Mutation methods (.set, .delete, .clear) are only available in mutable\n * contexts (middleware.ts, server actions, route.ts handlers). Calling them\n * in read-only contexts (access.ts, server components) throws.\n *\n * See design/29-cookies.md\n */\nexport function cookies(): RequestCookies {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] cookies() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n\n // Parse cookies lazily on first access\n if (!store.parsedCookies) {\n store.parsedCookies = parseCookieHeader(store.cookieHeader);\n }\n\n const map = store.parsedCookies;\n return {\n get(name: string): string | undefined {\n return map.get(name);\n },\n has(name: string): boolean {\n return map.has(name);\n },\n getAll(): Array<{ name: string; value: string }> {\n return Array.from(map.entries()).map(([name, value]) => ({ name, value }));\n },\n get size(): number {\n return map.size;\n },\n\n getSigned(name: string): string | undefined {\n const raw = map.get(name);\n if (!raw || _cookieSecrets.length === 0) return undefined;\n return verifySignedCookie(raw, _cookieSecrets);\n },\n\n set(name: string, value: string, options?: CookieOptions): void {\n assertMutable(store, 'set');\n if (store.flushed) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n `[timber] warn: cookies().set('${name}') called after response headers were committed.\\n` +\n ` The cookie will NOT be sent. Move cookie mutations to middleware.ts, a server action,\\n` +\n ` or a route.ts handler.`\n );\n }\n return;\n }\n let storedValue = value;\n if (options?.signed) {\n if (_cookieSecrets.length === 0) {\n throw new Error(\n `[timber] cookies().set('${name}', ..., { signed: true }) requires ` +\n `cookies.secret or cookies.secrets in timber.config.ts.`\n );\n }\n storedValue = signCookieValue(value, _cookieSecrets[0]);\n }\n const opts = { ...DEFAULT_COOKIE_OPTIONS, ...options };\n store.cookieJar.set(name, { name, value: storedValue, options: opts });\n // Read-your-own-writes: update the parsed cookies map with the signed value\n // so getSigned() can verify it in the same request\n map.set(name, storedValue);\n },\n\n delete(name: string, options?: Pick<CookieOptions, 'path' | 'domain'>): void {\n assertMutable(store, 'delete');\n if (store.flushed) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n `[timber] warn: cookies().delete('${name}') called after response headers were committed.\\n` +\n ` The cookie will NOT be deleted. Move cookie mutations to middleware.ts, a server action,\\n` +\n ` or a route.ts handler.`\n );\n }\n return;\n }\n const opts: CookieOptions = {\n ...DEFAULT_COOKIE_OPTIONS,\n ...options,\n maxAge: 0,\n expires: new Date(0),\n };\n store.cookieJar.set(name, { name, value: '', options: opts });\n // Remove from read view\n map.delete(name);\n },\n\n clear(): void {\n assertMutable(store, 'clear');\n if (store.flushed) return;\n // Delete every incoming cookie\n for (const name of Array.from(map.keys())) {\n store.cookieJar.set(name, {\n name,\n value: '',\n options: { ...DEFAULT_COOKIE_OPTIONS, maxAge: 0, expires: new Date(0) },\n });\n }\n map.clear();\n },\n\n toString(): string {\n return Array.from(map.entries())\n .map(([name, value]) => `${name}=${value}`)\n .join('; ');\n },\n };\n}\n\n/**\n * Returns a Promise resolving to the current request's search params.\n *\n * In `page.tsx`, `middleware.ts`, and `access.ts` the framework pre-parses the\n * route's `search-params.ts` definition and the Promise resolves to the typed\n * object. In all other server component contexts it resolves to raw\n * `URLSearchParams`.\n *\n * Returned as a Promise to match the `params` prop convention and to allow\n * future partial pre-rendering support where param resolution may be deferred.\n *\n * Throws if called outside a request context.\n */\nexport function searchParams<R extends keyof Routes>(): Promise<Routes[R]['searchParams']>;\nexport function searchParams(): Promise<URLSearchParams | Record<string, unknown>>;\nexport function searchParams(): Promise<URLSearchParams | Record<string, unknown>> {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] searchParams() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n return store.searchParamsPromise;\n}\n\n/**\n * Replace the search params Promise for the current request with one that\n * resolves to the typed parsed result from the route's search-params.ts.\n * Called by the framework before rendering the page — not for app code.\n */\nexport function setParsedSearchParams(parsed: Record<string, unknown>): void {\n const store = requestContextAls.getStore();\n if (store) {\n store.searchParamsPromise = Promise.resolve(parsed);\n }\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\n/**\n * Read-only Headers interface. The standard Headers class is mutable;\n * this type narrows it to read-only methods. The underlying object is\n * still a Headers instance, but user code should not mutate it.\n */\nexport type ReadonlyHeaders = Pick<\n Headers,\n 'get' | 'has' | 'entries' | 'keys' | 'values' | 'forEach' | typeof Symbol.iterator\n>;\n\n/** Options for setting a cookie. See design/29-cookies.md. */\nexport interface CookieOptions {\n /** Domain scope. Default: omitted (current domain only). */\n domain?: string;\n /** URL path scope. Default: '/'. */\n path?: string;\n /** Expiration date. Mutually exclusive with maxAge. */\n expires?: Date;\n /** Max age in seconds. Mutually exclusive with expires. */\n maxAge?: number;\n /** Prevent client-side JS access. Default: true. */\n httpOnly?: boolean;\n /** Only send over HTTPS. Default: true. */\n secure?: boolean;\n /** Cross-site request policy. Default: 'lax'. */\n sameSite?: 'strict' | 'lax' | 'none';\n /** Partitioned (CHIPS) — isolate cookie per top-level site. Default: false. */\n partitioned?: boolean;\n /**\n * Sign the cookie value with HMAC-SHA256 for integrity verification.\n * Requires `cookies.secret` or `cookies.secrets` in timber.config.ts.\n * See design/29-cookies.md §\"Signed Cookies\".\n */\n signed?: boolean;\n}\n\nconst DEFAULT_COOKIE_OPTIONS: CookieOptions = {\n path: '/',\n httpOnly: true,\n secure: true,\n sameSite: 'lax',\n};\n\n/**\n * Cookie accessor returned by `cookies()`.\n *\n * Read methods are always available. Mutation methods throw in read-only\n * contexts (access.ts, server components).\n */\nexport interface RequestCookies {\n /** Get a cookie value by name. Returns undefined if not present. */\n get(name: string): string | undefined;\n /** Check if a cookie exists. */\n has(name: string): boolean;\n /** Get all cookies as an array of { name, value } pairs. */\n getAll(): Array<{ name: string; value: string }>;\n /** Number of cookies. */\n readonly size: number;\n /**\n * Get a signed cookie value, verifying its HMAC-SHA256 signature.\n * Returns undefined if the cookie is missing, the signature is invalid,\n * or no secrets are configured. Never throws.\n *\n * See design/29-cookies.md §\"Signed Cookies\"\n */\n getSigned(name: string): string | undefined;\n /** Set a cookie. Only available in mutable contexts (middleware, actions, route handlers). */\n set(name: string, value: string, options?: CookieOptions): void;\n /** Delete a cookie. Only available in mutable contexts. */\n delete(name: string, options?: Pick<CookieOptions, 'path' | 'domain'>): void;\n /** Delete all cookies. Only available in mutable contexts. */\n clear(): void;\n /** Serialize cookies as a Cookie header string. */\n toString(): string;\n}\n\n// ─── Framework-Internal Helpers ───────────────────────────────────────────\n\n/**\n * Run a callback within a request context. Used by the pipeline to establish\n * per-request ALS scope so that `headers()` and `cookies()` work.\n *\n * @param req - The incoming Request object.\n * @param fn - The function to run within the request context.\n */\nexport function runWithRequestContext<T>(req: Request, fn: () => T): T {\n const originalCopy = new Headers(req.headers);\n const store: RequestContextStore = {\n headers: freezeHeaders(req.headers),\n originalHeaders: originalCopy,\n cookieHeader: req.headers.get('cookie') ?? '',\n searchParamsPromise: Promise.resolve(new URL(req.url).searchParams),\n cookieJar: new Map(),\n flushed: false,\n mutableContext: false,\n };\n return requestContextAls.run(store, fn);\n}\n\n/**\n * Enable cookie mutation for the current context. Called by the framework\n * when entering middleware.ts, server actions, or route.ts handlers.\n *\n * See design/29-cookies.md §\"Context Tracking\"\n */\nexport function setMutableCookieContext(mutable: boolean): void {\n const store = requestContextAls.getStore();\n if (store) {\n store.mutableContext = mutable;\n }\n}\n\n/**\n * Mark the response as flushed (headers committed). After this point,\n * cookie mutations log a warning instead of throwing.\n *\n * See design/29-cookies.md §\"Streaming Constraint: Post-Flush Cookie Warning\"\n */\nexport function markResponseFlushed(): void {\n const store = requestContextAls.getStore();\n if (store) {\n store.flushed = true;\n }\n}\n\n/**\n * Collect all Set-Cookie headers from the cookie jar.\n * Called by the framework at flush time to apply cookies to the response.\n *\n * Returns an array of serialized Set-Cookie header values.\n */\nexport function getSetCookieHeaders(): string[] {\n const store = requestContextAls.getStore();\n if (!store) return [];\n return Array.from(store.cookieJar.values()).map(serializeCookieEntry);\n}\n\n/**\n * Apply middleware-injected request headers to the current request context.\n *\n * Called by the pipeline after middleware.ts runs. Merges overlay headers\n * on top of the original request headers so downstream code (access.ts,\n * server components, server actions) sees them via `headers()`.\n *\n * The original request headers are never mutated — a new frozen Headers\n * object is created with the overlay applied on top.\n *\n * See design/07-routing.md §\"Request Header Injection\"\n */\nexport function applyRequestHeaderOverlay(overlay: Headers): void {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error('[timber] applyRequestHeaderOverlay() called outside of a request context.');\n }\n\n // Check if the overlay has any headers — skip if empty\n let hasOverlay = false;\n overlay.forEach(() => {\n hasOverlay = true;\n });\n if (!hasOverlay) return;\n\n // Merge: start with original headers, overlay on top\n const merged = new Headers(store.originalHeaders);\n overlay.forEach((value, key) => {\n merged.set(key, value);\n });\n store.headers = freezeHeaders(merged);\n}\n\n// ─── Read-Only Headers ────────────────────────────────────────────────────\n\nconst MUTATING_METHODS = new Set(['set', 'append', 'delete']);\n\n/**\n * Wrap a Headers object in a Proxy that throws on mutating methods.\n * Object.freeze doesn't work on Headers (native internal slots), so we\n * intercept property access and reject set/append/delete at runtime.\n *\n * Read methods (get, has, entries, etc.) must be bound to the underlying\n * Headers instance because they access private #headersList slots.\n */\nfunction freezeHeaders(source: Headers): Headers {\n const copy = new Headers(source);\n return new Proxy(copy, {\n get(target, prop) {\n if (typeof prop === 'string' && MUTATING_METHODS.has(prop)) {\n return () => {\n throw new Error(\n `[timber] headers() returns a read-only Headers object. ` +\n `Calling .${prop}() is not allowed. ` +\n `Use ctx.requestHeaders in middleware to inject headers for downstream components.`\n );\n };\n }\n const value = Reflect.get(target, prop);\n // Bind methods to the real Headers instance so private slot access works\n if (typeof value === 'function') {\n return value.bind(target);\n }\n return value;\n },\n });\n}\n\n// ─── Cookie Helpers ───────────────────────────────────────────────────────\n\n/** Throw if cookie mutation is attempted in a read-only context. */\nfunction assertMutable(store: RequestContextStore, method: string): void {\n if (!store.mutableContext) {\n throw new Error(\n `[timber] cookies().${method}() cannot be called in this context.\\n` +\n ` Set cookies in middleware.ts, server actions, or route.ts handlers.`\n );\n }\n}\n\n/**\n * Parse a Cookie header string into a Map of name → value pairs.\n * Follows RFC 6265 §4.2.1: cookies are semicolon-separated key=value pairs.\n */\nfunction parseCookieHeader(header: string): Map<string, string> {\n const map = new Map<string, string>();\n if (!header) return map;\n\n for (const pair of header.split(';')) {\n const eqIndex = pair.indexOf('=');\n if (eqIndex === -1) continue;\n const name = pair.slice(0, eqIndex).trim();\n const value = pair.slice(eqIndex + 1).trim();\n if (name) {\n map.set(name, value);\n }\n }\n\n return map;\n}\n\n// ─── Cookie Signing ──────────────────────────────────────────────────────\n\n/**\n * Sign a cookie value with HMAC-SHA256.\n * Returns `value.hex_signature`.\n */\nfunction signCookieValue(value: string, secret: string): string {\n const signature = createHmac('sha256', secret).update(value).digest('hex');\n return `${value}.${signature}`;\n}\n\n/**\n * Verify a signed cookie value against an array of secrets.\n * Returns the original value if any secret produces a matching signature,\n * or undefined if none match. Uses timing-safe comparison.\n *\n * The signed format is `value.hex_signature` — split at the last `.`.\n */\nfunction verifySignedCookie(raw: string, secrets: string[]): string | undefined {\n const lastDot = raw.lastIndexOf('.');\n if (lastDot <= 0 || lastDot === raw.length - 1) return undefined;\n\n const value = raw.slice(0, lastDot);\n const signature = raw.slice(lastDot + 1);\n\n // Hex-encoded SHA-256 is always 64 chars\n if (signature.length !== 64) return undefined;\n\n const signatureBuffer = Buffer.from(signature, 'hex');\n // If the hex decode produced fewer bytes, the signature was not valid hex\n if (signatureBuffer.length !== 32) return undefined;\n\n for (const secret of secrets) {\n const expected = createHmac('sha256', secret).update(value).digest();\n if (timingSafeEqual(expected, signatureBuffer)) {\n return value;\n }\n }\n return undefined;\n}\n\n/** Serialize a CookieEntry into a Set-Cookie header value. */\nfunction serializeCookieEntry(entry: CookieEntry): string {\n const parts = [`${entry.name}=${entry.value}`];\n const opts = entry.options;\n\n if (opts.domain) parts.push(`Domain=${opts.domain}`);\n if (opts.path) parts.push(`Path=${opts.path}`);\n if (opts.expires) parts.push(`Expires=${opts.expires.toUTCString()}`);\n if (opts.maxAge !== undefined) parts.push(`Max-Age=${opts.maxAge}`);\n if (opts.httpOnly) parts.push('HttpOnly');\n if (opts.secure) parts.push('Secure');\n if (opts.sameSite) {\n parts.push(`SameSite=${opts.sameSite.charAt(0).toUpperCase()}${opts.sameSite.slice(1)}`);\n }\n if (opts.partitioned) parts.push('Partitioned');\n\n return parts.join('; ');\n}\n"],"mappings":"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"}