@timber-js/app 0.1.20 → 0.1.22

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 (137) hide show
  1. package/dist/_chunks/als-registry-c0AGnbqS.js +39 -0
  2. package/dist/_chunks/als-registry-c0AGnbqS.js.map +1 -0
  3. package/dist/_chunks/{interception-c-a3uODY.js → interception-DGDIjDbR.js} +10 -3
  4. package/dist/_chunks/interception-DGDIjDbR.js.map +1 -0
  5. package/dist/_chunks/{metadata-routes-BDnswgRO.js → metadata-routes-CQCnF4VK.js} +14 -2
  6. package/dist/_chunks/metadata-routes-CQCnF4VK.js.map +1 -0
  7. package/dist/_chunks/{request-context-BzES06i1.js → request-context-C69VW4xS.js} +2 -4
  8. package/dist/_chunks/request-context-C69VW4xS.js.map +1 -0
  9. package/dist/_chunks/ssr-data-B2yikEEB.js +90 -0
  10. package/dist/_chunks/ssr-data-B2yikEEB.js.map +1 -0
  11. package/dist/_chunks/{tracing-BtOwb8O6.js → tracing-tIvqStk8.js} +2 -3
  12. package/dist/_chunks/tracing-tIvqStk8.js.map +1 -0
  13. package/dist/_chunks/{use-cookie-D2cZu0jK.js → use-cookie-D5aS4slY.js} +2 -2
  14. package/dist/_chunks/{use-cookie-D2cZu0jK.js.map → use-cookie-D5aS4slY.js.map} +1 -1
  15. package/dist/_chunks/{use-query-states-wEXY2JQB.js → use-query-states-DAhgj8Gx.js} +1 -1
  16. package/dist/_chunks/{use-query-states-wEXY2JQB.js.map → use-query-states-DAhgj8Gx.js.map} +1 -1
  17. package/dist/cache/index.js +2 -1
  18. package/dist/cache/index.js.map +1 -1
  19. package/dist/client/error-boundary.js +1 -1
  20. package/dist/client/index.d.ts +1 -1
  21. package/dist/client/index.d.ts.map +1 -1
  22. package/dist/client/index.js +40 -26
  23. package/dist/client/index.js.map +1 -1
  24. package/dist/client/router-ref.d.ts.map +1 -1
  25. package/dist/client/router.d.ts.map +1 -1
  26. package/dist/client/ssr-data.d.ts +3 -0
  27. package/dist/client/ssr-data.d.ts.map +1 -1
  28. package/dist/client/state.d.ts +47 -0
  29. package/dist/client/state.d.ts.map +1 -0
  30. package/dist/client/types.d.ts +10 -1
  31. package/dist/client/types.d.ts.map +1 -1
  32. package/dist/client/unload-guard.d.ts +3 -0
  33. package/dist/client/unload-guard.d.ts.map +1 -1
  34. package/dist/client/use-params.d.ts +19 -6
  35. package/dist/client/use-params.d.ts.map +1 -1
  36. package/dist/client/use-search-params.d.ts +3 -0
  37. package/dist/client/use-search-params.d.ts.map +1 -1
  38. package/dist/cookies/index.js +4 -2
  39. package/dist/cookies/index.js.map +1 -1
  40. package/dist/index.js +4 -1
  41. package/dist/index.js.map +1 -1
  42. package/dist/plugins/shims.d.ts.map +1 -1
  43. package/dist/routing/index.js +1 -1
  44. package/dist/routing/scanner.d.ts.map +1 -1
  45. package/dist/rsc-runtime/browser.d.ts +13 -0
  46. package/dist/rsc-runtime/browser.d.ts.map +1 -0
  47. package/dist/rsc-runtime/rsc.d.ts +14 -0
  48. package/dist/rsc-runtime/rsc.d.ts.map +1 -0
  49. package/dist/rsc-runtime/ssr.d.ts +13 -0
  50. package/dist/rsc-runtime/ssr.d.ts.map +1 -0
  51. package/dist/search-params/builtin-codecs.d.ts +105 -0
  52. package/dist/search-params/builtin-codecs.d.ts.map +1 -0
  53. package/dist/search-params/index.d.ts +1 -0
  54. package/dist/search-params/index.d.ts.map +1 -1
  55. package/dist/search-params/index.js +167 -2
  56. package/dist/search-params/index.js.map +1 -1
  57. package/dist/server/actions.d.ts +2 -7
  58. package/dist/server/actions.d.ts.map +1 -1
  59. package/dist/server/als-registry.d.ts +80 -0
  60. package/dist/server/als-registry.d.ts.map +1 -0
  61. package/dist/server/early-hints-sender.d.ts.map +1 -1
  62. package/dist/server/form-flash.d.ts.map +1 -1
  63. package/dist/server/index.d.ts +1 -0
  64. package/dist/server/index.d.ts.map +1 -1
  65. package/dist/server/index.js +242 -76
  66. package/dist/server/index.js.map +1 -1
  67. package/dist/server/metadata-routes.d.ts +27 -0
  68. package/dist/server/metadata-routes.d.ts.map +1 -1
  69. package/dist/server/pipeline.d.ts +7 -0
  70. package/dist/server/pipeline.d.ts.map +1 -1
  71. package/dist/server/primitives.d.ts +14 -6
  72. package/dist/server/primitives.d.ts.map +1 -1
  73. package/dist/server/request-context.d.ts +2 -32
  74. package/dist/server/request-context.d.ts.map +1 -1
  75. package/dist/server/route-matcher.d.ts +5 -0
  76. package/dist/server/route-matcher.d.ts.map +1 -1
  77. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  78. package/dist/server/rsc-entry/rsc-payload.d.ts +25 -0
  79. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -0
  80. package/dist/server/rsc-entry/rsc-stream.d.ts +43 -0
  81. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -0
  82. package/dist/server/rsc-entry/ssr-renderer.d.ts +52 -0
  83. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -0
  84. package/dist/server/rsc-prop-warnings.d.ts +53 -0
  85. package/dist/server/rsc-prop-warnings.d.ts.map +1 -0
  86. package/dist/server/server-timing.d.ts +49 -0
  87. package/dist/server/server-timing.d.ts.map +1 -0
  88. package/dist/server/tracing.d.ts +2 -6
  89. package/dist/server/tracing.d.ts.map +1 -1
  90. package/dist/server/types.d.ts +11 -0
  91. package/dist/server/types.d.ts.map +1 -1
  92. package/package.json +1 -1
  93. package/src/client/browser-entry.ts +1 -1
  94. package/src/client/index.ts +1 -1
  95. package/src/client/router-ref.ts +6 -12
  96. package/src/client/router.ts +14 -4
  97. package/src/client/ssr-data.ts +25 -9
  98. package/src/client/state.ts +83 -0
  99. package/src/client/types.ts +18 -1
  100. package/src/client/unload-guard.ts +6 -3
  101. package/src/client/use-params.ts +42 -32
  102. package/src/client/use-search-params.ts +9 -5
  103. package/src/plugins/shims.ts +26 -2
  104. package/src/routing/scanner.ts +18 -2
  105. package/src/rsc-runtime/browser.ts +18 -0
  106. package/src/rsc-runtime/rsc.ts +19 -0
  107. package/src/rsc-runtime/ssr.ts +13 -0
  108. package/src/search-params/builtin-codecs.ts +228 -0
  109. package/src/search-params/index.ts +11 -0
  110. package/src/server/action-handler.ts +1 -1
  111. package/src/server/actions.ts +4 -10
  112. package/src/server/als-registry.ts +116 -0
  113. package/src/server/deny-renderer.ts +1 -1
  114. package/src/server/early-hints-sender.ts +1 -3
  115. package/src/server/form-flash.ts +1 -5
  116. package/src/server/index.ts +1 -0
  117. package/src/server/metadata-routes.ts +61 -0
  118. package/src/server/pipeline.ts +164 -38
  119. package/src/server/primitives.ts +110 -6
  120. package/src/server/request-context.ts +8 -36
  121. package/src/server/route-matcher.ts +25 -2
  122. package/src/server/rsc-entry/error-renderer.ts +1 -1
  123. package/src/server/rsc-entry/index.ts +42 -380
  124. package/src/server/rsc-entry/rsc-payload.ts +126 -0
  125. package/src/server/rsc-entry/rsc-stream.ts +162 -0
  126. package/src/server/rsc-entry/ssr-renderer.ts +228 -0
  127. package/src/server/rsc-prop-warnings.ts +187 -0
  128. package/src/server/server-timing.ts +132 -0
  129. package/src/server/ssr-entry.ts +1 -1
  130. package/src/server/tracing.ts +3 -11
  131. package/src/server/types.ts +16 -0
  132. package/dist/_chunks/interception-c-a3uODY.js.map +0 -1
  133. package/dist/_chunks/metadata-routes-BDnswgRO.js.map +0 -1
  134. package/dist/_chunks/request-context-BzES06i1.js.map +0 -1
  135. package/dist/_chunks/ssr-data-BgSwMbN9.js +0 -38
  136. package/dist/_chunks/ssr-data-BgSwMbN9.js.map +0 -1
  137. package/dist/_chunks/tracing-BtOwb8O6.js.map +0 -1
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Server-Timing header — dev-mode timing breakdowns for Chrome DevTools.
3
+ *
4
+ * Collects timing entries per request using ALS. Each pipeline phase
5
+ * (proxy, middleware, render, SSR, access, fetch) records an entry.
6
+ * Before response flush, entries are formatted into a Server-Timing header.
7
+ *
8
+ * Only active in dev mode — zero overhead in production.
9
+ *
10
+ * See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing
11
+ * Task: LOCAL-290
12
+ */
13
+
14
+ import { timingAls } from './als-registry.js';
15
+
16
+ // ─── Types ────────────────────────────────────────────────────────────────
17
+
18
+ export interface TimingEntry {
19
+ /** Metric name (alphanumeric + hyphens, no spaces). */
20
+ name: string;
21
+ /** Duration in milliseconds. */
22
+ dur: number;
23
+ /** Human-readable description (shown in DevTools). */
24
+ desc?: string;
25
+ }
26
+
27
+ // ─── Public API ───────────────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Run a callback with a per-request timing collector.
31
+ * Must be called at the top of the request pipeline (wraps the full request).
32
+ */
33
+ export function runWithTimingCollector<T>(fn: () => T): T {
34
+ return timingAls.run({ entries: [] }, fn);
35
+ }
36
+
37
+ /**
38
+ * Record a timing entry for the current request.
39
+ * No-ops if called outside a timing collector (e.g. in production).
40
+ */
41
+ export function recordTiming(entry: TimingEntry): void {
42
+ const store = timingAls.getStore();
43
+ if (!store) return;
44
+ store.entries.push(entry);
45
+ }
46
+
47
+ /**
48
+ * Run a function and automatically record its duration as a timing entry.
49
+ * Returns the function's result. No-ops the recording if outside a collector.
50
+ */
51
+ export async function withTiming<T>(
52
+ name: string,
53
+ desc: string | undefined,
54
+ fn: () => T | Promise<T>
55
+ ): Promise<T> {
56
+ const store = timingAls.getStore();
57
+ if (!store) return fn();
58
+
59
+ const start = performance.now();
60
+ try {
61
+ return await fn();
62
+ } finally {
63
+ const dur = Math.round(performance.now() - start);
64
+ store.entries.push({ name, dur, desc });
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Get the Server-Timing header value for the current request.
70
+ * Returns null if no entries exist or outside a collector.
71
+ *
72
+ * Format: `name;dur=123;desc="description", name2;dur=456`
73
+ * See RFC 6797 / Server-Timing spec for format details.
74
+ */
75
+ export function getServerTimingHeader(): string | null {
76
+ const store = timingAls.getStore();
77
+ if (!store || store.entries.length === 0) return null;
78
+
79
+ // Deduplicate names — if a name appears multiple times, suffix with index
80
+ const nameCounts = new Map<string, number>();
81
+ const entries = store.entries.map((entry) => {
82
+ const count = nameCounts.get(entry.name) ?? 0;
83
+ nameCounts.set(entry.name, count + 1);
84
+ const uniqueName = count > 0 ? `${entry.name}-${count}` : entry.name;
85
+ return { ...entry, name: uniqueName };
86
+ });
87
+
88
+ const parts = entries.map((entry) => {
89
+ let part = `${entry.name};dur=${entry.dur}`;
90
+ if (entry.desc) {
91
+ // Escape quotes in desc per Server-Timing spec
92
+ const safeDesc = entry.desc.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
93
+ part += `;desc="${safeDesc}"`;
94
+ }
95
+ return part;
96
+ });
97
+
98
+ // Respect header size limits — browsers typically handle up to 8KB headers.
99
+ // Truncate if the header exceeds 4KB to leave room for other headers.
100
+ const MAX_HEADER_SIZE = 4096;
101
+ let result = '';
102
+ for (let i = 0; i < parts.length; i++) {
103
+ const candidate = result ? `${result}, ${parts[i]}` : parts[i]!;
104
+ if (candidate.length > MAX_HEADER_SIZE) break;
105
+ result = candidate;
106
+ }
107
+
108
+ return result || null;
109
+ }
110
+
111
+ /**
112
+ * Sanitize a URL for use in Server-Timing desc.
113
+ * Strips query params and truncates long paths to avoid information leakage.
114
+ */
115
+ export function sanitizeUrlForTiming(url: string): string {
116
+ try {
117
+ const parsed = new URL(url);
118
+ const origin = parsed.host;
119
+ let path = parsed.pathname;
120
+ // Truncate long paths
121
+ if (path.length > 50) {
122
+ path = path.slice(0, 47) + '...';
123
+ }
124
+ return `${origin}${path}`;
125
+ } catch {
126
+ // Not a valid URL — truncate raw string
127
+ if (url.length > 60) {
128
+ return url.slice(0, 57) + '...';
129
+ }
130
+ return url;
131
+ }
132
+ }
@@ -14,7 +14,7 @@
14
14
 
15
15
  // @ts-expect-error — virtual module provided by timber-entries plugin
16
16
  import config from 'virtual:timber-config';
17
- import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr';
17
+ import { createFromReadableStream } from '#/rsc-runtime/ssr.js';
18
18
  import { AsyncLocalStorage } from 'node:async_hooks';
19
19
 
20
20
  import { renderSsrStream, buildSsrResponse } from './ssr-render.js';
@@ -8,19 +8,11 @@
8
8
  * See design/17-logging.md §"trace_id is Always Set"
9
9
  */
10
10
 
11
- import { AsyncLocalStorage } from 'node:async_hooks';
12
11
  import { randomUUID } from 'node:crypto';
12
+ import { traceAls, type TraceStore } from './als-registry.js';
13
13
 
14
- // ─── ALS Store ────────────────────────────────────────────────────────────
15
-
16
- export interface TraceStore {
17
- /** 32-char lowercase hex trace ID (OTEL or UUID fallback). */
18
- traceId: string;
19
- /** OTEL span ID if available, undefined otherwise. */
20
- spanId?: string;
21
- }
22
-
23
- const traceAls = new AsyncLocalStorage<TraceStore>();
14
+ // Re-export the TraceStore type for public API consumers.
15
+ export type { TraceStore } from './als-registry.js';
24
16
 
25
17
  // ─── Public API ───────────────────────────────────────────────────────────
26
18
 
@@ -2,6 +2,22 @@
2
2
 
3
3
  import type { EarlyHint } from './early-hints.js';
4
4
 
5
+ /**
6
+ * A value that is safe to pass through `JSON.stringify` without data loss.
7
+ *
8
+ * Used as the constraint for `deny(status, data)` and `RenderError` digest data
9
+ * because the post-flush path serializes via JSON.stringify, not React Flight.
10
+ * Types that survive Flight (Date, Map, Set, BigInt) would silently coerce or
11
+ * throw when serialized as JSON. This type prevents that at compile time.
12
+ */
13
+ export type JsonSerializable =
14
+ | string
15
+ | number
16
+ | boolean
17
+ | null
18
+ | JsonSerializable[]
19
+ | { [key: string]: JsonSerializable };
20
+
5
21
  export interface MiddlewareContext {
6
22
  req: Request;
7
23
  requestHeaders: Headers;
@@ -1 +0,0 @@
1
- {"version":3,"file":"interception-c-a3uODY.js","names":[],"sources":["../../src/routing/types.ts","../../src/routing/scanner.ts","../../src/routing/codegen.ts","../../src/routing/interception.ts"],"sourcesContent":["/**\n * Route tree types for timber.js file-system routing.\n *\n * The route tree is built by scanning the app/ directory and recognizing\n * file conventions (page.*, layout.*, middleware.ts, access.ts, route.ts, etc.).\n */\n\n/** Segment type classification */\nexport type SegmentType =\n | 'static' // e.g. \"dashboard\"\n | 'dynamic' // e.g. \"[id]\"\n | 'catch-all' // e.g. \"[...slug]\"\n | 'optional-catch-all' // e.g. \"[[...slug]]\"\n | 'group' // e.g. \"(marketing)\"\n | 'slot' // e.g. \"@sidebar\"\n | 'intercepting' // e.g. \"(.)photo\", \"(..)photo\", \"(...)photo\"\n | 'private'; // e.g. \"_components\", \"_lib\" — excluded from routing\n\n/**\n * Intercepting route marker — indicates how many levels up to resolve the\n * intercepted route from the intercepting route's location.\n *\n * See design/07-routing.md §\"Intercepting Routes\"\n */\nexport type InterceptionMarker = '(.)' | '(..)' | '(...)' | '(..)(..)';\n\n/** All recognized interception markers, ordered longest-first for parsing. */\nexport const INTERCEPTION_MARKERS: InterceptionMarker[] = ['(..)(..)', '(.)', '(..)', '(...)'];\n\n/** A single file discovered in a route segment */\nexport interface RouteFile {\n /** Absolute path to the file */\n filePath: string;\n /** File extension without leading dot (e.g. \"tsx\", \"ts\", \"mdx\") */\n extension: string;\n}\n\n/** A node in the segment tree */\nexport interface SegmentNode {\n /** The raw directory name (e.g. \"dashboard\", \"[id]\", \"(auth)\", \"@sidebar\") */\n segmentName: string;\n /** Classified segment type */\n segmentType: SegmentType;\n /** The dynamic param name, if dynamic (e.g. \"id\" for \"[id]\", \"slug\" for \"[...slug]\") */\n paramName?: string;\n /** The URL path prefix at this segment level (e.g. \"/dashboard\") */\n urlPath: string;\n /** For intercepting segments: the marker used, e.g. \"(.)\". */\n interceptionMarker?: InterceptionMarker;\n /**\n * For intercepting segments: the segment name after stripping the marker.\n * E.g., for \"(.)photo\" this is \"photo\".\n */\n interceptedSegmentName?: string;\n\n // --- File conventions ---\n page?: RouteFile;\n layout?: RouteFile;\n middleware?: RouteFile;\n access?: RouteFile;\n route?: RouteFile;\n error?: RouteFile;\n default?: RouteFile;\n /** Status-code files: 4xx.tsx, 5xx.tsx, {status}.tsx (component format) */\n statusFiles?: Map<string, RouteFile>;\n /** JSON status-code files: 4xx.json, 5xx.json, {status}.json */\n jsonStatusFiles?: Map<string, RouteFile>;\n /** denied.tsx — slot-only denial rendering */\n denied?: RouteFile;\n /** Legacy compat: not-found.tsx (maps to 404), forbidden.tsx (403), unauthorized.tsx (401) */\n legacyStatusFiles?: Map<string, RouteFile>;\n /** prerender.ts — signals build-time pre-rendering for this segment's shell */\n prerender?: RouteFile;\n /** search-params.ts — typed search params definition for this route */\n searchParams?: RouteFile;\n /** Metadata route files (sitemap.ts, robots.ts, icon.tsx, etc.) keyed by base name */\n metadataRoutes?: Map<string, RouteFile>;\n\n // --- Children ---\n children: SegmentNode[];\n /** Parallel route slots (keyed by slot name without @) */\n slots: Map<string, SegmentNode>;\n}\n\n/** The full route tree output from the scanner */\nexport interface RouteTree {\n /** The root segment node (representing app/) */\n root: SegmentNode;\n /** All discovered proxy.ts files (should be at most one, in app/) */\n proxy?: RouteFile;\n}\n\n/** Configuration passed to the scanner */\nexport interface ScannerConfig {\n /** Recognized page/layout extensions (without dots). Default: ['tsx', 'ts', 'jsx', 'js'] */\n pageExtensions?: string[];\n}\n\n/** Default page extensions */\nexport const DEFAULT_PAGE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js'];\n","/**\n * Route discovery scanner.\n *\n * Pure function: (appDir, config) → RouteTree\n *\n * Scans the app/ directory and builds a segment tree recognizing all\n * timber.js file conventions. Does NOT handle request matching — this\n * is discovery only.\n */\n\nimport { readdirSync, statSync } from 'node:fs';\nimport { join, extname, basename } from 'node:path';\nimport type {\n RouteTree,\n SegmentNode,\n SegmentType,\n RouteFile,\n ScannerConfig,\n InterceptionMarker,\n} from './types.js';\nimport { DEFAULT_PAGE_EXTENSIONS, INTERCEPTION_MARKERS } from './types.js';\nimport { classifyMetadataRoute } from '#/server/metadata-routes.js';\n\n/**\n * Pattern matching encoded path delimiters that must be rejected during route discovery.\n * %2F / %2f (forward slash) and %5C / %5c (backslash) can cause route collisions\n * when decoded. See design/13-security.md §\"Encoded separators rejected\".\n */\nconst ENCODED_SEPARATOR_PATTERN = /%(?:2[fF]|5[cC])/;\n\n/**\n * Pattern matching encoded null bytes (%00) that must be rejected.\n * See design/13-security.md §\"Null bytes rejected\".\n */\nconst ENCODED_NULL_PATTERN = /%00/;\n\n/**\n * File convention names that use pageExtensions (can be .tsx, .ts, .jsx, .js, .mdx, etc.)\n */\nconst PAGE_EXT_CONVENTIONS = new Set(['page', 'layout', 'error', 'default', 'denied']);\n\n/**\n * Legacy compat status-code files.\n * Maps legacy file name → HTTP status code for the fallback chain.\n * See design/10-error-handling.md §\"Fallback Chain\".\n */\nconst LEGACY_STATUS_FILES: Record<string, number> = {\n 'not-found': 404,\n 'forbidden': 403,\n 'unauthorized': 401,\n};\n\n/**\n * File convention names that are always .ts/.tsx (never .mdx etc.)\n */\nconst FIXED_CONVENTIONS = new Set(['middleware', 'access', 'route', 'prerender', 'search-params']);\n\n/**\n * Status-code file patterns:\n * - Exact 3-digit codes: 401.tsx, 429.tsx, 503.tsx\n * - Category catch-alls: 4xx.tsx, 5xx.tsx\n */\nconst STATUS_CODE_PATTERN = /^(\\d{3}|[45]xx)$/;\n\n/**\n * Scan the app/ directory and build the route tree.\n *\n * @param appDir - Absolute path to the app/ directory\n * @param config - Scanner configuration\n * @returns The complete route tree\n */\nexport function scanRoutes(appDir: string, config: ScannerConfig = {}): RouteTree {\n const pageExtensions = config.pageExtensions ?? DEFAULT_PAGE_EXTENSIONS;\n const extSet = new Set(pageExtensions);\n\n const tree: RouteTree = {\n root: createSegmentNode('', 'static', '/'),\n };\n\n // Check for proxy.ts at app root\n const proxyFile = findFixedFile(appDir, 'proxy');\n if (proxyFile) {\n tree.proxy = proxyFile;\n }\n\n // Scan the root directory's files\n scanSegmentFiles(appDir, tree.root, extSet);\n\n // Scan children recursively\n scanChildren(appDir, tree.root, extSet);\n\n // Validate: detect route group collisions (different groups producing pages at the same URL)\n validateRouteGroupCollisions(tree.root);\n\n return tree;\n}\n\n/**\n * Create an empty segment node.\n */\nfunction createSegmentNode(\n segmentName: string,\n segmentType: SegmentType,\n urlPath: string,\n paramName?: string,\n interceptionMarker?: InterceptionMarker,\n interceptedSegmentName?: string\n): SegmentNode {\n return {\n segmentName,\n segmentType,\n urlPath,\n paramName,\n interceptionMarker,\n interceptedSegmentName,\n children: [],\n slots: new Map(),\n };\n}\n\n/**\n * Classify a directory name into its segment type.\n */\nexport function classifySegment(dirName: string): {\n type: SegmentType;\n paramName?: string;\n interceptionMarker?: InterceptionMarker;\n interceptedSegmentName?: string;\n} {\n // Private folder: _name (excluded from routing)\n if (dirName.startsWith('_')) {\n return { type: 'private' };\n }\n\n // Parallel route slot: @name\n if (dirName.startsWith('@')) {\n return { type: 'slot' };\n }\n\n // Intercepting routes: (.)name, (..)name, (...)name, (..)(..)name\n // Check before route groups since intercepting markers also start with (\n const interception = parseInterceptionMarker(dirName);\n if (interception) {\n return {\n type: 'intercepting',\n interceptionMarker: interception.marker,\n interceptedSegmentName: interception.segmentName,\n };\n }\n\n // Route group: (name)\n if (dirName.startsWith('(') && dirName.endsWith(')')) {\n return { type: 'group' };\n }\n\n // Optional catch-all: [[...name]]\n if (dirName.startsWith('[[...') && dirName.endsWith(']]')) {\n const paramName = dirName.slice(5, -2);\n return { type: 'optional-catch-all', paramName };\n }\n\n // Catch-all: [...name]\n if (dirName.startsWith('[...') && dirName.endsWith(']')) {\n const paramName = dirName.slice(4, -1);\n return { type: 'catch-all', paramName };\n }\n\n // Dynamic: [name]\n if (dirName.startsWith('[') && dirName.endsWith(']')) {\n const paramName = dirName.slice(1, -1);\n return { type: 'dynamic', paramName };\n }\n\n return { type: 'static' };\n}\n\n/**\n * Parse an interception marker from a directory name.\n *\n * Returns the marker and the remaining segment name, or null if not an\n * intercepting route. Markers are checked longest-first to avoid (..)\n * matching before (..)(..).\n *\n * Examples:\n * \"(.)photo\" → { marker: \"(.)\", segmentName: \"photo\" }\n * \"(..)feed\" → { marker: \"(..)\", segmentName: \"feed\" }\n * \"(...)photos\" → { marker: \"(...)\", segmentName: \"photos\" }\n * \"(..)(..)admin\" → { marker: \"(..)(..)\", segmentName: \"admin\" }\n * \"(marketing)\" → null (route group, not interception)\n */\nfunction parseInterceptionMarker(\n dirName: string\n): { marker: InterceptionMarker; segmentName: string } | null {\n for (const marker of INTERCEPTION_MARKERS) {\n if (dirName.startsWith(marker)) {\n const rest = dirName.slice(marker.length);\n // Must have a segment name after the marker, and the rest must not\n // be empty or end with ) (which would be a route group like \"(auth)\")\n if (rest.length > 0 && !rest.endsWith(')')) {\n return { marker, segmentName: rest };\n }\n }\n }\n return null;\n}\n\n/**\n * Compute the URL path for a child segment given its parent's URL path.\n * Route groups, slots, and intercepting routes do NOT add URL depth.\n */\nfunction computeUrlPath(parentUrlPath: string, dirName: string, segmentType: SegmentType): string {\n // Groups, slots, and intercepting routes don't add to URL path\n if (segmentType === 'group' || segmentType === 'slot' || segmentType === 'intercepting') {\n return parentUrlPath;\n }\n\n const parentPath = parentUrlPath === '/' ? '' : parentUrlPath;\n return `${parentPath}/${dirName}`;\n}\n\n/**\n * Scan a directory for file conventions and populate the segment node.\n */\nfunction scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string>): void {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch {\n return;\n }\n\n for (const entry of entries) {\n const fullPath = join(dirPath, entry);\n\n // Skip directories — handled by scanChildren\n try {\n if (statSync(fullPath).isDirectory()) continue;\n } catch {\n continue;\n }\n\n const ext = extname(entry).slice(1); // remove leading dot\n const name = basename(entry, `.${ext}`);\n\n // Page-extension conventions (page, layout, error, default, denied)\n if (PAGE_EXT_CONVENTIONS.has(name) && extSet.has(ext)) {\n const file: RouteFile = { filePath: fullPath, extension: ext };\n switch (name) {\n case 'page':\n node.page = file;\n break;\n case 'layout':\n node.layout = file;\n break;\n case 'error':\n node.error = file;\n break;\n case 'default':\n node.default = file;\n break;\n case 'denied':\n node.denied = file;\n break;\n }\n continue;\n }\n\n // Fixed conventions (middleware, access, route) — always .ts or .tsx\n if (FIXED_CONVENTIONS.has(name) && /\\.?[jt]sx?$/.test(ext)) {\n const file: RouteFile = { filePath: fullPath, extension: ext };\n switch (name) {\n case 'middleware':\n node.middleware = file;\n break;\n case 'access':\n node.access = file;\n break;\n case 'route':\n node.route = file;\n break;\n case 'prerender':\n node.prerender = file;\n break;\n case 'search-params':\n node.searchParams = file;\n break;\n }\n continue;\n }\n\n // JSON status-code files (401.json, 4xx.json, 503.json, 5xx.json)\n // Recognized regardless of pageExtensions — .json is a data format, not a page extension.\n if (STATUS_CODE_PATTERN.test(name) && ext === 'json') {\n if (!node.jsonStatusFiles) {\n node.jsonStatusFiles = new Map();\n }\n node.jsonStatusFiles.set(name, { filePath: fullPath, extension: ext });\n continue;\n }\n\n // Status-code files (401.tsx, 4xx.tsx, 503.tsx, 5xx.tsx)\n if (STATUS_CODE_PATTERN.test(name) && extSet.has(ext)) {\n if (!node.statusFiles) {\n node.statusFiles = new Map();\n }\n node.statusFiles.set(name, { filePath: fullPath, extension: ext });\n continue;\n }\n\n // Legacy compat files (not-found.tsx, forbidden.tsx, unauthorized.tsx)\n if (name in LEGACY_STATUS_FILES && extSet.has(ext)) {\n if (!node.legacyStatusFiles) {\n node.legacyStatusFiles = new Map();\n }\n node.legacyStatusFiles.set(name, { filePath: fullPath, extension: ext });\n continue;\n }\n\n // Metadata route files (sitemap.ts, robots.ts, icon.tsx, opengraph-image.tsx, etc.)\n // See design/16-metadata.md §\"Metadata Routes\"\n const metaInfo = classifyMetadataRoute(entry);\n if (metaInfo) {\n if (!node.metadataRoutes) {\n node.metadataRoutes = new Map();\n }\n node.metadataRoutes.set(name, { filePath: fullPath, extension: ext });\n }\n }\n\n // Validate: route.ts + page.* is a hard build error\n if (node.route && node.page) {\n throw new Error(\n `Build error: route.ts and page.* cannot coexist in the same segment.\\n` +\n ` route.ts: ${node.route.filePath}\\n` +\n ` page: ${node.page.filePath}\\n` +\n `A URL is either an API endpoint or a rendered page, not both.`\n );\n }\n}\n\n/**\n * Recursively scan child directories and build the segment tree.\n */\nfunction scanChildren(dirPath: string, parentNode: SegmentNode, extSet: Set<string>): void {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch {\n return;\n }\n\n for (const entry of entries) {\n const fullPath = join(dirPath, entry);\n\n try {\n if (!statSync(fullPath).isDirectory()) continue;\n } catch {\n continue;\n }\n\n // Reject directories with encoded path delimiters or null bytes.\n // These can cause route collisions when decoded at the URL boundary.\n // See design/13-security.md §\"Encoded separators rejected\" and §\"Null bytes rejected\".\n if (ENCODED_SEPARATOR_PATTERN.test(entry)) {\n throw new Error(\n `Build error: directory name contains an encoded path delimiter (%%2F or %%5C).\\n` +\n ` Directory: ${fullPath}\\n` +\n `Encoded separators in directory names cause route collisions when decoded. ` +\n `Rename the directory to remove the encoded delimiter.`\n );\n }\n if (ENCODED_NULL_PATTERN.test(entry)) {\n throw new Error(\n `Build error: directory name contains an encoded null byte (%%00).\\n` +\n ` Directory: ${fullPath}\\n` +\n `Encoded null bytes in directory names are not allowed. ` +\n `Rename the directory to remove the null byte encoding.`\n );\n }\n\n const { type, paramName, interceptionMarker, interceptedSegmentName } = classifySegment(entry);\n\n // Skip private folders — underscore-prefixed dirs are excluded from routing\n if (type === 'private') continue;\n\n const urlPath = computeUrlPath(parentNode.urlPath, entry, type);\n const childNode = createSegmentNode(\n entry,\n type,\n urlPath,\n paramName,\n interceptionMarker,\n interceptedSegmentName\n );\n\n // Scan this segment's files\n scanSegmentFiles(fullPath, childNode, extSet);\n\n // Recurse into subdirectories\n scanChildren(fullPath, childNode, extSet);\n\n // Attach to parent: slots go into slots map, everything else is a child\n if (type === 'slot') {\n const slotName = entry.slice(1); // remove @\n parentNode.slots.set(slotName, childNode);\n } else {\n parentNode.children.push(childNode);\n }\n }\n}\n\n/**\n * Validate that route groups don't produce conflicting pages/routes at the same URL path.\n *\n * Two route groups like (auth)/login/page.tsx and (marketing)/login/page.tsx both claim\n * /login — the scanner must detect and reject this at build time.\n *\n * Parallel slots are excluded from collision detection — they intentionally coexist at\n * the same URL path as their parent (that's the whole point of parallel routes).\n */\nfunction validateRouteGroupCollisions(root: SegmentNode): void {\n // Map from urlPath → { filePath, source } for the first page/route seen at that path\n const seen = new Map<string, { filePath: string; segmentPath: string }>();\n collectRoutableLeaves(root, seen, '', false);\n}\n\n/**\n * Walk the segment tree and collect all routable leaves (page or route files),\n * throwing on collision. Slots are tracked in their own collision space since\n * they are parallel routes that intentionally share URL paths with their parent.\n */\nfunction collectRoutableLeaves(\n node: SegmentNode,\n seen: Map<string, { filePath: string; segmentPath: string }>,\n segmentPath: string,\n insideSlot: boolean\n): void {\n const currentPath = segmentPath\n ? `${segmentPath}/${node.segmentName}`\n : node.segmentName || '(root)';\n\n // Only check collisions for non-slot pages — slots intentionally share URL paths\n if (!insideSlot) {\n const routableFile = node.page ?? node.route;\n if (routableFile) {\n const existing = seen.get(node.urlPath);\n if (existing) {\n throw new Error(\n `Build error: route collision — multiple route groups produce a page/route at the same URL path.\\n` +\n ` URL path: ${node.urlPath}\\n` +\n ` File 1: ${existing.filePath} (via ${existing.segmentPath})\\n` +\n ` File 2: ${routableFile.filePath} (via ${currentPath})\\n` +\n `Each URL path must map to exactly one page or route handler. ` +\n `Rename or move one of the conflicting files.`\n );\n }\n seen.set(node.urlPath, { filePath: routableFile.filePath, segmentPath: currentPath });\n }\n }\n\n // Recurse into children\n for (const child of node.children) {\n collectRoutableLeaves(child, seen, currentPath, insideSlot);\n }\n\n // Recurse into slots — each slot is its own parallel route space\n for (const [, slotNode] of node.slots) {\n collectRoutableLeaves(slotNode, seen, currentPath, true);\n }\n}\n\n/**\n * Find a fixed-extension file (proxy.ts) in a directory.\n */\nfunction findFixedFile(dirPath: string, name: string): RouteFile | undefined {\n for (const ext of ['ts', 'tsx']) {\n const fullPath = join(dirPath, `${name}.${ext}`);\n try {\n if (statSync(fullPath).isFile()) {\n return { filePath: fullPath, extension: ext };\n }\n } catch {\n // File doesn't exist\n }\n }\n return undefined;\n}\n","/**\n * Route map codegen.\n *\n * Walks the scanned RouteTree and generates a TypeScript declaration file\n * mapping every route to its params and searchParams shapes.\n *\n * This runs at build time and in dev (regenerated on file changes).\n * No runtime overhead — purely static type generation.\n */\n\nimport { existsSync } from 'node:fs';\nimport { join, relative, posix } from 'node:path';\nimport type { RouteTree, SegmentNode } from './types.js';\n\n/** A single route entry extracted from the segment tree. */\ninterface RouteEntry {\n /** URL path pattern (e.g. \"/products/[id]\") */\n urlPath: string;\n /** Accumulated params from all ancestor dynamic segments */\n params: ParamEntry[];\n /** Whether this route has a co-located search-params.ts */\n hasSearchParams: boolean;\n /** Absolute path to search-params.ts (for computing relative import paths) */\n searchParamsAbsPath?: string;\n /** Whether this is an API route (route.ts) vs page route */\n isApiRoute: boolean;\n}\n\ninterface ParamEntry {\n name: string;\n type: 'string' | 'string[]' | 'string[] | undefined';\n}\n\n/** Options for route map generation. */\nexport interface CodegenOptions {\n /** Absolute path to the app/ directory. Required for search-params.ts detection. */\n appDir?: string;\n /**\n * Absolute path to the directory where the .d.ts file will be written.\n * Used to compute correct relative import paths for search-params.ts files.\n * Defaults to appDir when not provided (preserves backward compat for tests).\n */\n outputDir?: string;\n}\n\n/**\n * Generate a TypeScript declaration file string from a scanned route tree.\n *\n * The output is a `declare module '@timber-js/app'` block containing the Routes\n * interface that maps every route path to its params and searchParams shape.\n */\nexport function generateRouteMap(tree: RouteTree, options: CodegenOptions = {}): string {\n const routes: RouteEntry[] = [];\n collectRoutes(tree.root, [], options.appDir, routes);\n\n // Sort routes alphabetically for deterministic output\n routes.sort((a, b) => a.urlPath.localeCompare(b.urlPath));\n\n // When outputDir differs from appDir, import paths must be relative to outputDir\n const importBase = options.outputDir ?? options.appDir;\n\n return formatDeclarationFile(routes, importBase);\n}\n\n/**\n * Recursively walk the segment tree and collect route entries.\n *\n * A route entry is created for any segment that has a `page` or `route` file.\n * Params accumulate from ancestor dynamic segments.\n */\nfunction collectRoutes(\n node: SegmentNode,\n ancestorParams: ParamEntry[],\n appDir: string | undefined,\n routes: RouteEntry[]\n): void {\n // Accumulate params from this segment\n const params = [...ancestorParams];\n if (node.paramName) {\n params.push({\n name: node.paramName,\n type: paramTypeForSegment(node.segmentType),\n });\n }\n\n // Check if this segment is a leaf route (has page or route file)\n const isPage = !!node.page;\n const isApiRoute = !!node.route;\n\n if (isPage || isApiRoute) {\n const entry: RouteEntry = {\n urlPath: node.urlPath,\n params: [...params],\n hasSearchParams: false,\n isApiRoute,\n };\n\n // Detect co-located search-params.ts\n if (appDir && isPage) {\n const segmentDir = resolveSegmentDir(appDir, node);\n const searchParamsFile = findSearchParamsFile(segmentDir);\n if (searchParamsFile) {\n entry.hasSearchParams = true;\n entry.searchParamsAbsPath = searchParamsFile;\n }\n }\n\n routes.push(entry);\n }\n\n // Recurse into children\n for (const child of node.children) {\n collectRoutes(child, params, appDir, routes);\n }\n\n // Recurse into slots (they share the parent's URL path, but may have their own pages)\n for (const [, slot] of node.slots) {\n collectRoutes(slot, params, appDir, routes);\n }\n}\n\n/**\n * Determine the TypeScript type for a segment's param.\n */\nfunction paramTypeForSegment(segmentType: string): ParamEntry['type'] {\n switch (segmentType) {\n case 'catch-all':\n return 'string[]';\n case 'optional-catch-all':\n return 'string[] | undefined';\n default:\n return 'string';\n }\n}\n\n/**\n * Resolve the absolute directory path for a segment node.\n *\n * Reconstructs the filesystem path by walking from appDir through\n * the segment names encoded in the urlPath, accounting for groups and slots.\n */\nfunction resolveSegmentDir(appDir: string, node: SegmentNode): string {\n // The node's page/route file path gives us the actual directory\n const file = node.page ?? node.route;\n if (file) {\n // The file is in the segment directory — go up one level\n const parts = file.filePath.split('/');\n parts.pop(); // remove filename\n return parts.join('/');\n }\n // Fallback: construct from urlPath (imprecise for groups, but acceptable)\n return appDir;\n}\n\n/**\n * Find a search-params.ts file in a directory.\n */\nfunction findSearchParamsFile(dirPath: string): string | undefined {\n for (const ext of ['ts', 'tsx']) {\n const candidate = join(dirPath, `search-params.${ext}`);\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n return undefined;\n}\n\n/**\n * Format the collected routes into a TypeScript declaration file.\n */\nfunction formatDeclarationFile(routes: RouteEntry[], importBase?: string): string {\n const lines: string[] = [];\n\n lines.push('// This file is auto-generated by timber.js route map codegen.');\n lines.push('// Do not edit manually. Regenerated on build and in dev mode.');\n lines.push('');\n // export {} makes this file a module, so all declare module blocks are\n // augmentations rather than ambient replacements. Without this, the\n // declare module blocks would replace the original module types entirely\n // (removing exports like bindUseQueryStates that aren't listed here).\n lines.push('export {};');\n lines.push('');\n lines.push(\"declare module '@timber-js/app' {\");\n lines.push(' interface Routes {');\n\n for (const route of routes) {\n const paramsType = formatParamsType(route.params);\n const searchParamsType = formatSearchParamsType(route, importBase);\n\n lines.push(` '${route.urlPath}': {`);\n lines.push(` params: ${paramsType}`);\n lines.push(` searchParams: ${searchParamsType}`);\n lines.push(` }`);\n }\n\n lines.push(' }');\n lines.push('}');\n lines.push('');\n\n // Generate @timber-js/app/server augmentation — typed searchParams() generic\n const pageRoutes = routes.filter((r) => !r.isApiRoute);\n\n if (pageRoutes.length > 0) {\n lines.push(\"declare module '@timber-js/app/server' {\");\n lines.push(\" import type { Routes } from '@timber-js/app'\");\n lines.push(\n \" export function searchParams<R extends keyof Routes>(): Promise<Routes[R]['searchParams']>\"\n );\n lines.push('}');\n lines.push('');\n }\n\n // Generate overloads for @timber-js/app/client\n const dynamicRoutes = routes.filter((r) => r.params.length > 0);\n\n if (dynamicRoutes.length > 0 || pageRoutes.length > 0) {\n lines.push(\"declare module '@timber-js/app/client' {\");\n lines.push(\n \" import type { SearchParamsDefinition, SetParams, QueryStatesOptions, SearchParamCodec } from '@timber-js/app/search-params'\"\n );\n lines.push('');\n\n // useParams overloads\n if (dynamicRoutes.length > 0) {\n for (const route of dynamicRoutes) {\n const paramsType = formatParamsType(route.params);\n lines.push(` export function useParams(route: '${route.urlPath}'): ${paramsType}`);\n }\n lines.push(' export function useParams(): Record<string, string | string[]>');\n lines.push('');\n }\n\n // useQueryStates overloads\n if (pageRoutes.length > 0) {\n lines.push(...formatUseQueryStatesOverloads(pageRoutes, importBase));\n lines.push('');\n }\n\n // Typed Link overloads\n if (pageRoutes.length > 0) {\n lines.push(' // Typed Link props per route');\n lines.push(...formatTypedLinkOverloads(pageRoutes, importBase));\n }\n\n lines.push('}');\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Format the params type for a route entry.\n */\nfunction formatParamsType(params: ParamEntry[]): string {\n if (params.length === 0) {\n return '{}';\n }\n\n const fields = params.map((p) => `${p.name}: ${p.type}`);\n return `{ ${fields.join('; ')} }`;\n}\n\n/**\n * Format the params type for Link props.\n *\n * Link params accept `string | number` for single dynamic segments\n * (convenience — values are stringified at runtime). Catch-all and\n * optional catch-all remain `string[]` / `string[] | undefined`.\n *\n * See design/07-routing.md §\"Typed params and searchParams on <Link>\"\n */\nfunction formatLinkParamsType(params: ParamEntry[]): string {\n if (params.length === 0) {\n return '{}';\n }\n\n const fields = params.map((p) => {\n // Single dynamic segments accept string | number for convenience\n const type = p.type === 'string' ? 'string | number' : p.type;\n return `${p.name}: ${type}`;\n });\n return `{ ${fields.join('; ')} }`;\n}\n\n/**\n * Format the searchParams type for a route entry.\n *\n * When a search-params.ts exists, we reference its inferred type via an import type.\n * The import path is relative to `importBase` (the directory where the .d.ts will be\n * written). When importBase is undefined, falls back to a bare relative path.\n */\nfunction formatSearchParamsType(route: RouteEntry, importBase?: string): string {\n if (route.hasSearchParams && route.searchParamsAbsPath) {\n const absPath = route.searchParamsAbsPath.replace(/\\.(ts|tsx)$/, '');\n let importPath: string;\n if (importBase) {\n // Make the path relative to the output directory, converted to posix separators\n importPath = './' + relative(importBase, absPath).replace(/\\\\/g, '/');\n } else {\n importPath = './' + posix.basename(absPath);\n }\n // Use (typeof import('...'))[' default'] instead of import('...').default\n // because with moduleResolution:\"bundler\", import('...').default is treated as\n // a namespace member access which doesn't work for default exports.\n return `(typeof import('${importPath}'))['default'] extends import('@timber-js/app/search-params').SearchParamsDefinition<infer T> ? T : never`;\n }\n return '{}';\n}\n\n/**\n * Generate useQueryStates overloads.\n *\n * For each page route:\n * - Routes with search-params.ts get a typed overload returning the inferred T\n * - Routes without search-params.ts get an overload returning [{}, SetParams<{}>]\n *\n * A fallback overload for standalone codecs (existing API) is emitted last.\n */\nfunction formatUseQueryStatesOverloads(routes: RouteEntry[], importBase?: string): string[] {\n const lines: string[] = [];\n\n for (const route of routes) {\n const searchParamsType = route.hasSearchParams\n ? formatSearchParamsType(route, importBase)\n : '{}';\n lines.push(\n ` export function useQueryStates<R extends '${route.urlPath}'>(route: R, options?: QueryStatesOptions): [${searchParamsType}, SetParams<${searchParamsType}>]`\n );\n }\n\n // Fallback: standalone codecs (existing API)\n lines.push(\n ' export function useQueryStates<T extends Record<string, unknown>>(codecs: { [K in keyof T]: SearchParamCodec<T[K]> }, options?: QueryStatesOptions): [T, SetParams<T>]'\n );\n\n return lines;\n}\n\n/**\n * Generate typed Link overloads.\n *\n * For each page route, we generate a Link function overload that:\n * - Constrains href to the route pattern\n * - Types the params prop based on dynamic segments\n * - Types the searchParams prop based on search-params.ts (if present)\n *\n * Routes without dynamic segments accept href as a literal string with no params.\n * Routes with dynamic segments require a params prop.\n */\nfunction formatTypedLinkOverloads(routes: RouteEntry[], importBase?: string): string[] {\n const lines: string[] = [];\n\n for (const route of routes) {\n const hasDynamicParams = route.params.length > 0;\n const paramsType = formatLinkParamsType(route.params);\n const searchParamsType = route.hasSearchParams\n ? formatSearchParamsType(route, importBase)\n : null;\n\n if (hasDynamicParams) {\n // Route with dynamic segments — params prop required\n const spProp = searchParamsType\n ? `searchParams?: { definition: SearchParamsDefinition<${searchParamsType}>; values: Partial<${searchParamsType}> }`\n : `searchParams?: never`;\n lines.push(\n ` export function Link(props: Omit<import('react').AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> & {`\n );\n lines.push(` href: '${route.urlPath}'`);\n lines.push(` params: ${paramsType}`);\n lines.push(` ${spProp}`);\n lines.push(` prefetch?: boolean; scroll?: boolean; children?: import('react').ReactNode`);\n lines.push(` }): import('react').JSX.Element`);\n } else {\n // Static route — no params needed\n const spProp = searchParamsType\n ? `searchParams?: { definition: SearchParamsDefinition<${searchParamsType}>; values: Partial<${searchParamsType}> }`\n : `searchParams?: never`;\n lines.push(\n ` export function Link(props: Omit<import('react').AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> & {`\n );\n lines.push(` href: '${route.urlPath}'`);\n lines.push(` params?: never`);\n lines.push(` ${spProp}`);\n lines.push(` prefetch?: boolean; scroll?: boolean; children?: import('react').ReactNode`);\n lines.push(` }): import('react').JSX.Element`);\n }\n }\n\n // Fallback overload for arbitrary string hrefs (escape hatch)\n lines.push(\n ` export function Link(props: import('./client/link.js').LinkProps): import('react').JSX.Element`\n );\n\n return lines;\n}\n","/**\n * Intercepting route utilities.\n *\n * Computes rewrite rules from the route tree that enable intercepting routes\n * to conditionally render when navigating via client-side (soft) navigation.\n *\n * The mechanism: at build time, each intercepting route directory generates a\n * conditional rewrite. On soft navigation, the client sends an `X-Timber-URL`\n * header with the current pathname. The server checks if any rewrite's source\n * (the intercepted URL) matches the target pathname AND the header matches\n * the intercepting route's parent URL. If both match, the intercepting route\n * renders instead of the normal route.\n *\n * On hard navigation (no header), no rewrite matches, and the normal route\n * renders.\n *\n * See design/07-routing.md §\"Intercepting Routes\"\n */\n\nimport type { SegmentNode, InterceptionMarker } from './types.js';\n\n/** A conditional rewrite rule generated from an intercepting route. */\nexport interface InterceptionRewrite {\n /**\n * The URL pattern that this rewrite intercepts (the target of navigation).\n * E.g., \"/photo/[id]\" for a (.)photo/[id] interception.\n */\n interceptedPattern: string;\n /**\n * The URL prefix that the client must be navigating FROM for this rewrite\n * to apply. Matched against the X-Timber-URL header.\n * E.g., \"/feed\" for a (.)photo/[id] inside /feed/@modal/.\n */\n interceptingPrefix: string;\n /**\n * Segments chain from root → intercepting leaf. Used to build the element\n * tree when the interception is active.\n */\n segmentPath: SegmentNode[];\n}\n\n/**\n * Collect all interception rewrite rules from the route tree.\n *\n * Walks the tree recursively. For each intercepting segment, computes the\n * intercepted URL based on the marker and the segment's position.\n */\nexport function collectInterceptionRewrites(root: SegmentNode): InterceptionRewrite[] {\n const rewrites: InterceptionRewrite[] = [];\n walkForInterceptions(root, [root], rewrites);\n return rewrites;\n}\n\n/**\n * Recursively walk the segment tree to find intercepting routes.\n */\nfunction walkForInterceptions(\n node: SegmentNode,\n ancestors: SegmentNode[],\n rewrites: InterceptionRewrite[]\n): void {\n // Check children\n for (const child of node.children) {\n if (child.segmentType === 'intercepting' && child.interceptionMarker) {\n // Found an intercepting route — collect rewrites from its sub-tree\n collectFromInterceptingNode(child, ancestors, rewrites);\n } else {\n walkForInterceptions(child, [...ancestors, child], rewrites);\n }\n }\n\n // Check slots (intercepting routes are typically inside slots like @modal)\n for (const [, slot] of node.slots) {\n walkForInterceptions(slot, ancestors, rewrites);\n }\n}\n\n/**\n * For an intercepting segment, find all leaf pages in its sub-tree and\n * generate rewrite rules for each.\n */\nfunction collectFromInterceptingNode(\n interceptingNode: SegmentNode,\n ancestors: SegmentNode[],\n rewrites: InterceptionRewrite[]\n): void {\n const marker = interceptingNode.interceptionMarker!;\n const segmentName = interceptingNode.interceptedSegmentName!;\n\n // Compute the intercepted URL base based on the marker\n const parentUrlPath = ancestors[ancestors.length - 1].urlPath;\n const interceptedBase = computeInterceptedBase(parentUrlPath, marker);\n const interceptedUrlBase =\n interceptedBase === '/' ? `/${segmentName}` : `${interceptedBase}/${segmentName}`;\n\n // Find all leaf pages in the intercepting sub-tree\n collectLeavesWithRewrites(\n interceptingNode,\n interceptedUrlBase,\n parentUrlPath,\n [...ancestors, interceptingNode],\n rewrites\n );\n}\n\n/**\n * Recursively find leaf pages in an intercepting sub-tree and generate\n * rewrite rules for each.\n */\nfunction collectLeavesWithRewrites(\n node: SegmentNode,\n interceptedUrlPath: string,\n interceptingPrefix: string,\n segmentPath: SegmentNode[],\n rewrites: InterceptionRewrite[]\n): void {\n if (node.page) {\n rewrites.push({\n interceptedPattern: interceptedUrlPath,\n interceptingPrefix,\n segmentPath: [...segmentPath],\n });\n }\n\n for (const child of node.children) {\n const childUrl =\n child.segmentType === 'group'\n ? interceptedUrlPath\n : `${interceptedUrlPath}/${child.segmentName}`;\n collectLeavesWithRewrites(\n child,\n childUrl,\n interceptingPrefix,\n [...segmentPath, child],\n rewrites\n );\n }\n}\n\n/**\n * Compute the base URL that an intercepting route intercepts, given the\n * parent's URL path and the interception marker.\n *\n * - (.) — same level: parent's URL path\n * - (..) — one level up: parent's parent URL path\n * - (...) — root level: /\n * - (..)(..) — two levels up: parent's grandparent URL path\n *\n * Level counting operates on URL path segments, NOT filesystem directories.\n * Route groups and parallel slots are already excluded from urlPath (they\n * don't add URL depth), so (..) correctly climbs visible segments. This\n * avoids the Vinext bug where path.dirname() on filesystem paths would\n * waste climbs on invisible route groups.\n */\nfunction computeInterceptedBase(parentUrlPath: string, marker: InterceptionMarker): string {\n switch (marker) {\n case '(.)':\n return parentUrlPath;\n case '(..)': {\n const parts = parentUrlPath.split('/').filter(Boolean);\n parts.pop();\n return parts.length === 0 ? '/' : `/${parts.join('/')}`;\n }\n case '(...)':\n return '/';\n case '(..)(..)': {\n const parts = parentUrlPath.split('/').filter(Boolean);\n parts.pop();\n parts.pop();\n return parts.length === 0 ? '/' : `/${parts.join('/')}`;\n }\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"metadata-routes-BDnswgRO.js","names":[],"sources":["../../src/server/metadata-routes.ts"],"sourcesContent":["/**\n * Metadata route classification for timber.js.\n *\n * Metadata routes are file-based endpoints that generate well-known URLs for\n * crawlers and browsers (sitemap.xml, robots.txt, OG images, etc.).\n *\n * These routes run through proxy.ts but NOT through middleware.ts or access.ts —\n * they are public endpoints by nature.\n *\n * See design/16-metadata.md §\"Metadata Routes\"\n */\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Classification of a metadata route file. */\nexport interface MetadataRouteInfo {\n /** The metadata route type. */\n type: MetadataRouteType;\n /** The content type to serve this route with. */\n contentType: string;\n /** Whether this route can appear in nested segments (not just app root). */\n nestable: boolean;\n}\n\nexport type MetadataRouteType =\n | 'sitemap'\n | 'robots'\n | 'manifest'\n | 'favicon'\n | 'icon'\n | 'opengraph-image'\n | 'twitter-image'\n | 'apple-icon';\n\n// ─── Convention Table ────────────────────────────────────────────────────────\n\n/**\n * All recognized metadata route file conventions.\n *\n * Each entry maps a base file name (without extension) to its route info.\n * The extensions determine whether the file is static or dynamic.\n *\n * Static extensions: .xml, .txt, .json, .png, .jpg, .ico, .svg\n * Dynamic extensions: .ts, .tsx\n */\nexport const METADATA_ROUTE_CONVENTIONS: Record<\n string,\n {\n type: MetadataRouteType;\n contentType: string;\n nestable: boolean;\n staticExtensions: string[];\n dynamicExtensions: string[];\n /** The URL path this file serves at (relative to segment). */\n servePath: string;\n }\n> = {\n 'sitemap': {\n type: 'sitemap',\n contentType: 'application/xml',\n nestable: true,\n staticExtensions: ['xml'],\n dynamicExtensions: ['ts'],\n servePath: 'sitemap.xml',\n },\n 'robots': {\n type: 'robots',\n contentType: 'text/plain',\n nestable: false,\n staticExtensions: ['txt'],\n dynamicExtensions: ['ts'],\n servePath: 'robots.txt',\n },\n 'manifest': {\n type: 'manifest',\n contentType: 'application/manifest+json',\n nestable: false,\n staticExtensions: ['json'],\n dynamicExtensions: ['ts'],\n servePath: 'manifest.webmanifest',\n },\n 'favicon': {\n type: 'favicon',\n contentType: 'image/x-icon',\n nestable: false,\n staticExtensions: ['ico'],\n dynamicExtensions: [],\n servePath: 'favicon.ico',\n },\n 'icon': {\n type: 'icon',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg', 'svg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'icon',\n },\n 'opengraph-image': {\n type: 'opengraph-image',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'opengraph-image',\n },\n 'twitter-image': {\n type: 'twitter-image',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'twitter-image',\n },\n 'apple-icon': {\n type: 'apple-icon',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'apple-icon',\n },\n};\n\n// ─── Classification ──────────────────────────────────────────────────────────\n\n/**\n * Classify a file name as a metadata route, or return null if it's not one.\n *\n * @param fileName - The full file name including extension (e.g. \"sitemap.xml\", \"icon.tsx\")\n * @returns Classification info, or null if not a metadata route\n */\nexport function classifyMetadataRoute(fileName: string): MetadataRouteInfo | null {\n const dotIndex = fileName.lastIndexOf('.');\n if (dotIndex === -1) return null;\n\n const baseName = fileName.slice(0, dotIndex);\n const ext = fileName.slice(dotIndex + 1);\n\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return null;\n\n const isStatic = convention.staticExtensions.includes(ext);\n const isDynamic = convention.dynamicExtensions.includes(ext);\n\n if (!isStatic && !isDynamic) return null;\n\n return {\n type: convention.type,\n contentType: convention.contentType,\n nestable: convention.nestable,\n };\n}\n\n/**\n * Get the serve path for a metadata route type.\n *\n * @param type - The metadata route type\n * @returns The URL path fragment this route serves at\n */\nexport function getMetadataRouteServePath(type: MetadataRouteType): string {\n for (const convention of Object.values(METADATA_ROUTE_CONVENTIONS)) {\n if (convention.type === type) return convention.servePath;\n }\n throw new Error(`[timber] Unknown metadata route type: ${type}`);\n}\n\n/**\n * Get the auto-link tags to inject into <head> for metadata route files\n * discovered in a segment.\n *\n * @param type - The metadata route type\n * @param href - The resolved URL path to the metadata route\n * @returns An object with tag/attrs for the <head>, or null if no auto-link\n */\nexport function getMetadataRouteAutoLink(\n type: MetadataRouteType,\n href: string\n): { rel: string; href: string; type?: string } | null {\n switch (type) {\n case 'icon':\n return { rel: 'icon', href };\n case 'apple-icon':\n return { rel: 'apple-touch-icon', href };\n case 'manifest':\n return { rel: 'manifest', href };\n default:\n return null;\n }\n}\n"],"mappings":";;;;;;;;;;AA6CA,IAAa,6BAWT;CACF,WAAW;EACT,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,CAAC,KAAK;EACzB,WAAW;EACZ;CACD,UAAU;EACR,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,CAAC,KAAK;EACzB,WAAW;EACZ;CACD,YAAY;EACV,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,OAAO;EAC1B,mBAAmB,CAAC,KAAK;EACzB,WAAW;EACZ;CACD,WAAW;EACT,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,EAAE;EACrB,WAAW;EACZ;CACD,QAAQ;EACN,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB;GAAC;GAAO;GAAO;GAAM;EACvC,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACD,mBAAmB;EACjB,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,OAAO,MAAM;EAChC,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACD,iBAAiB;EACf,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,OAAO,MAAM;EAChC,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACD,cAAc;EACZ,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACF;;;;;;;AAUD,SAAgB,sBAAsB,UAA4C;CAChF,MAAM,WAAW,SAAS,YAAY,IAAI;AAC1C,KAAI,aAAa,GAAI,QAAO;CAE5B,MAAM,WAAW,SAAS,MAAM,GAAG,SAAS;CAC5C,MAAM,MAAM,SAAS,MAAM,WAAW,EAAE;CAExC,MAAM,aAAa,2BAA2B;AAC9C,KAAI,CAAC,WAAY,QAAO;CAExB,MAAM,WAAW,WAAW,iBAAiB,SAAS,IAAI;CAC1D,MAAM,YAAY,WAAW,kBAAkB,SAAS,IAAI;AAE5D,KAAI,CAAC,YAAY,CAAC,UAAW,QAAO;AAEpC,QAAO;EACL,MAAM,WAAW;EACjB,aAAa,WAAW;EACxB,UAAU,WAAW;EACtB;;;;;;;;AASH,SAAgB,0BAA0B,MAAiC;AACzE,MAAK,MAAM,cAAc,OAAO,OAAO,2BAA2B,CAChE,KAAI,WAAW,SAAS,KAAM,QAAO,WAAW;AAElD,OAAM,IAAI,MAAM,yCAAyC,OAAO;;;;;;;;;;AAWlE,SAAgB,yBACd,MACA,MACqD;AACrD,SAAQ,MAAR;EACE,KAAK,OACH,QAAO;GAAE,KAAK;GAAQ;GAAM;EAC9B,KAAK,aACH,QAAO;GAAE,KAAK;GAAoB;GAAM;EAC1C,KAAK,WACH,QAAO;GAAE,KAAK;GAAY;GAAM;EAClC,QACE,QAAO"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"request-context-BzES06i1.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 { AsyncLocalStorage } from 'node:async_hooks';\nimport { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { Routes } from '#/index.js';\n\n// ─── ALS Store ────────────────────────────────────────────────────────────\n\ninterface RequestContextStore {\n /** Incoming request headers (read-only view). */\n headers: Headers;\n /** Raw cookie header string, parsed lazily into a Map on first access. */\n cookieHeader: string;\n /** Lazily-parsed cookie map (mutable — reflects write-overlay from set()). */\n parsedCookies?: Map<string, string>;\n /** Original (pre-overlay) frozen headers, kept for overlay merging. */\n originalHeaders: Headers;\n /**\n * Promise resolving to the route's typed search params (when search-params.ts\n * exists) or to the raw URLSearchParams. Stored as a Promise so the framework\n * can later support partial pre-rendering where param resolution is deferred.\n */\n searchParamsPromise: Promise<URLSearchParams | Record<string, unknown>>;\n /** Outgoing Set-Cookie entries (name → serialized value + options). Last write wins. */\n cookieJar: Map<string, CookieEntry>;\n /** Whether the response has flushed (headers committed). */\n flushed: boolean;\n /** Whether the current context allows cookie mutation. */\n mutableContext: boolean;\n}\n\n/** A single outgoing cookie entry in the cookie jar. */\ninterface CookieEntry {\n name: string;\n value: string;\n options: CookieOptions;\n}\n\n/** @internal */\nexport const requestContextAls = new AsyncLocalStorage<RequestContextStore>();\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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"}
@@ -1,38 +0,0 @@
1
- //#region src/client/ssr-data.ts
2
- var _ssrDataProvider;
3
- var currentSsrData;
4
- /**
5
- * Set the SSR data for the current request via module-level state.
6
- *
7
- * In production, ssr-entry.ts uses ALS (runWithSsrData) instead.
8
- * This function is retained for tests and as a fallback.
9
- */
10
- function setSsrData(data) {
11
- currentSsrData = data;
12
- }
13
- /**
14
- * Clear the SSR data after rendering completes.
15
- *
16
- * In production, ALS scope handles cleanup automatically.
17
- * This function is retained for tests and as a fallback.
18
- */
19
- function clearSsrData() {
20
- currentSsrData = void 0;
21
- }
22
- /**
23
- * Read the current request's SSR data. Returns undefined when called
24
- * outside an SSR render (i.e. on the client after hydration).
25
- *
26
- * Prefers the ALS-backed provider when registered (server-side),
27
- * falling back to module-level state (tests, legacy).
28
- *
29
- * Used by client hooks' server snapshot functions.
30
- */
31
- function getSsrData() {
32
- if (_ssrDataProvider) return _ssrDataProvider();
33
- return currentSsrData;
34
- }
35
- //#endregion
36
- export { getSsrData as n, setSsrData as r, clearSsrData as t };
37
-
38
- //# sourceMappingURL=ssr-data-BgSwMbN9.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ssr-data-BgSwMbN9.js","names":[],"sources":["../../src/client/ssr-data.ts"],"sourcesContent":["/**\n * SSR Data — per-request state for client hooks during server-side rendering.\n *\n * RSC and SSR are separate Vite module graphs (see design/18-build-system.md),\n * so the RSC environment's request-context ALS is not visible to SSR modules.\n * This module provides getter/setter functions that ssr-entry.ts uses to\n * populate per-request data for React's render.\n *\n * Request isolation: On the server, ssr-entry.ts registers an ALS-backed\n * provider via registerSsrDataProvider(). getSsrData() reads from the ALS\n * store, ensuring correct per-request data even when Suspense boundaries\n * resolve asynchronously across concurrent requests. The module-level\n * setSsrData/clearSsrData functions are kept as a fallback for tests\n * and environments without ALS.\n *\n * IMPORTANT: This module must NOT import node:async_hooks or any Node.js-only\n * APIs, as it's imported by 'use client' hooks that are bundled for the browser.\n * The ALS instance lives in ssr-entry.ts (server-only); this module only holds\n * a reference to the provider function.\n */\n\n// ─── Types ────────────────────────────────────────────────────────\n\nexport interface SsrData {\n /** The request's URL pathname (e.g. '/dashboard/settings') */\n pathname: string;\n /** The request's search params as a plain record */\n searchParams: Record<string, string>;\n /** The request's cookies as name→value pairs */\n cookies: Map<string, string>;\n /** The request's route params (e.g. { id: '123' }) */\n params: Record<string, string | string[]>;\n /**\n * Mutable reference to NavContext for error boundary → RSC communication.\n * When TimberErrorBoundary catches a DenySignal, it sets\n * `_navContext._denyHandledByBoundary = true` to prevent the RSC entry\n * from promoting the denial to page-level. See LOCAL-298.\n */\n _navContext?: { _denyHandledByBoundary?: boolean };\n}\n\n// ─── ALS-Backed Provider ─────────────────────────────────────────\n//\n// Server-side code (ssr-entry.ts) registers a provider that reads\n// from AsyncLocalStorage. This avoids importing node:async_hooks\n// in this browser-bundled module.\n\nlet _ssrDataProvider: (() => SsrData | undefined) | undefined;\n\n/**\n * Register an ALS-backed SSR data provider. Called once at module load\n * by ssr-entry.ts to wire up per-request data via AsyncLocalStorage.\n *\n * When registered, getSsrData() reads from the provider (ALS store)\n * instead of module-level state, ensuring correct isolation for\n * concurrent requests with streaming Suspense.\n */\nexport function registerSsrDataProvider(provider: () => SsrData | undefined): void {\n _ssrDataProvider = provider;\n}\n\n// ─── Module-Level Fallback ────────────────────────────────────────\n//\n// Used by tests and as a fallback when no ALS provider is registered.\n\nlet currentSsrData: SsrData | undefined;\n\n/**\n * Set the SSR data for the current request via module-level state.\n *\n * In production, ssr-entry.ts uses ALS (runWithSsrData) instead.\n * This function is retained for tests and as a fallback.\n */\nexport function setSsrData(data: SsrData): void {\n currentSsrData = data;\n}\n\n/**\n * Clear the SSR data after rendering completes.\n *\n * In production, ALS scope handles cleanup automatically.\n * This function is retained for tests and as a fallback.\n */\nexport function clearSsrData(): void {\n currentSsrData = undefined;\n}\n\n/**\n * Read the current request's SSR data. Returns undefined when called\n * outside an SSR render (i.e. on the client after hydration).\n *\n * Prefers the ALS-backed provider when registered (server-side),\n * falling back to module-level state (tests, legacy).\n *\n * Used by client hooks' server snapshot functions.\n */\nexport function getSsrData(): SsrData | undefined {\n if (_ssrDataProvider) {\n return _ssrDataProvider();\n }\n return currentSsrData;\n}\n"],"mappings":";AA+CA,IAAI;AAkBJ,IAAI;;;;;;;AAQJ,SAAgB,WAAW,MAAqB;AAC9C,kBAAiB;;;;;;;;AASnB,SAAgB,eAAqB;AACnC,kBAAiB,KAAA;;;;;;;;;;;AAYnB,SAAgB,aAAkC;AAChD,KAAI,iBACF,QAAO,kBAAkB;AAE3B,QAAO"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"tracing-BtOwb8O6.js","names":[],"sources":["../../src/server/tracing.ts"],"sourcesContent":["/**\n * Tracing — per-request trace ID via AsyncLocalStorage, OTEL span helpers.\n *\n * traceId() is always available in server code (middleware, access, components, actions).\n * Returns a 32-char lowercase hex string — the OTEL trace ID when an SDK is active,\n * or a crypto.randomUUID()-derived fallback otherwise.\n *\n * See design/17-logging.md §\"trace_id is Always Set\"\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport { randomUUID } from 'node:crypto';\n\n// ─── ALS Store ────────────────────────────────────────────────────────────\n\nexport interface TraceStore {\n /** 32-char lowercase hex trace ID (OTEL or UUID fallback). */\n traceId: string;\n /** OTEL span ID if available, undefined otherwise. */\n spanId?: string;\n}\n\nconst traceAls = new AsyncLocalStorage<TraceStore>();\n\n// ─── Public API ───────────────────────────────────────────────────────────\n\n/**\n * Returns the current request's trace ID — always a 32-char lowercase hex string.\n *\n * With OTEL: the real OTEL trace ID (matches Jaeger/Honeycomb/Datadog).\n * Without OTEL: crypto.randomUUID() with hyphens stripped.\n *\n * Throws if called outside a request context (no ALS store).\n */\nexport function traceId(): string {\n const store = traceAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] traceId() 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.traceId;\n}\n\n/**\n * Returns the current OTEL span ID if available, undefined otherwise.\n */\nexport function spanId(): string | undefined {\n return traceAls.getStore()?.spanId;\n}\n\n// ─── Framework-Internal Helpers ───────────────────────────────────────────\n\n/**\n * Generate a 32-char lowercase hex ID from crypto.randomUUID().\n * Same format as OTEL trace IDs — zero-friction upgrade path.\n */\nexport function generateTraceId(): string {\n return randomUUID().replace(/-/g, '');\n}\n\n/**\n * Run a callback within a trace context. Used by the pipeline to establish\n * per-request ALS scope.\n */\nexport function runWithTraceId<T>(id: string, fn: () => T): T {\n return traceAls.run({ traceId: id }, fn);\n}\n\n/**\n * Replace the trace ID in the current ALS store. Used when OTEL creates\n * a root span and we want to switch from the UUID fallback to the real\n * OTEL trace ID.\n */\nexport function replaceTraceId(newTraceId: string, newSpanId?: string): void {\n const store = traceAls.getStore();\n if (store) {\n store.traceId = newTraceId;\n store.spanId = newSpanId;\n }\n}\n\n/**\n * Update the span ID in the current ALS store. Used when entering a new\n * OTEL span to keep log–trace correlation accurate.\n */\nexport function updateSpanId(newSpanId: string | undefined): void {\n const store = traceAls.getStore();\n if (store) {\n store.spanId = newSpanId;\n }\n}\n\n/**\n * Get the current trace store, or undefined if outside a request context.\n * Framework-internal — use traceId()/spanId() in user code.\n */\nexport function getTraceStore(): TraceStore | undefined {\n return traceAls.getStore();\n}\n\n// ─── Dev-Mode OTEL Auto-Init ─────────────────────────────────────────────\n\n/**\n * Initialize a minimal OTEL SDK in dev mode so spans are recorded and\n * fed to the DevSpanProcessor for dev log output.\n *\n * If the user already configured an OTEL SDK in register(), we add\n * our DevSpanProcessor alongside theirs. If no SDK is configured,\n * we create a BasicTracerProvider with our processor.\n *\n * Only called in dev mode — zero overhead in production.\n */\nexport async function initDevTracing(\n config: import('./dev-logger.js').DevLoggerConfig\n): Promise<void> {\n const api = await getOtelApi();\n if (!api) return;\n\n const { DevSpanProcessor } = await import('./dev-span-processor.js');\n const { BasicTracerProvider } = await import('@opentelemetry/sdk-trace-base');\n const { AsyncLocalStorageContextManager } = await import('@opentelemetry/context-async-hooks');\n const processor = new DevSpanProcessor(config);\n\n // Register a context manager so OTEL can propagate the active span\n // across async boundaries. Without this, startActiveSpan can't make\n // spans \"active\" — child spans get random trace IDs and getActiveSpan()\n // returns undefined.\n const contextManager = new AsyncLocalStorageContextManager();\n contextManager.enable();\n api.context.setGlobalContextManager(contextManager);\n\n // Create a minimal TracerProvider with our DevSpanProcessor.\n // If the user also configures an SDK in register(), their provider\n // will coexist — the global provider set last wins for new tracers,\n // but our processor captures all spans from the timber.js tracer.\n const provider = new BasicTracerProvider({\n spanProcessors: [processor],\n });\n api.trace.setGlobalTracerProvider(provider);\n\n // Reset cached tracer so next getTracer() picks up the new provider\n _tracer = undefined;\n}\n\n// ─── OTEL Span Helpers ───────────────────────────────────────────────────\n\n/**\n * Attempt to get the @opentelemetry/api tracer. Returns undefined if the\n * package is not installed or no SDK is registered.\n *\n * timber.js depends on @opentelemetry/api as the vendor-neutral interface.\n * The API is a no-op by default — spans are only emitted when the developer\n * initializes an SDK in register().\n */\nlet _otelApi: typeof import('@opentelemetry/api') | null | undefined;\n\nasync function getOtelApi(): Promise<typeof import('@opentelemetry/api') | null> {\n if (_otelApi === undefined) {\n try {\n _otelApi = await import('@opentelemetry/api');\n } catch {\n _otelApi = null;\n }\n }\n return _otelApi;\n}\n\n/** OTEL tracer instance, lazily created. */\nlet _tracer: import('@opentelemetry/api').Tracer | null | undefined;\n\n/**\n * Get the timber.js OTEL tracer. Returns null if @opentelemetry/api is not available.\n */\nexport async function getTracer(): Promise<import('@opentelemetry/api').Tracer | null> {\n if (_tracer === undefined) {\n const api = await getOtelApi();\n if (api) {\n _tracer = api.trace.getTracer('timber.js');\n } else {\n _tracer = null;\n }\n }\n return _tracer;\n}\n\n/**\n * Run a function within an OTEL span. If OTEL is not available, runs the function\n * directly without any span overhead.\n *\n * Automatically:\n * - Creates the span as a child of the current context\n * - Updates the ALS span ID for log–trace correlation\n * - Ends the span when the function completes\n * - Records exceptions on error\n */\nexport async function withSpan<T>(\n name: string,\n attributes: Record<string, string | number | boolean>,\n fn: () => T | Promise<T>\n): Promise<T> {\n const tracer = await getTracer();\n if (!tracer) {\n return fn();\n }\n\n const api = (await getOtelApi())!;\n return tracer.startActiveSpan(name, { attributes }, async (span) => {\n const prevSpanId = spanId();\n updateSpanId(span.spanContext().spanId);\n try {\n const result = await fn();\n span.setStatus({ code: api.SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.setStatus({ code: api.SpanStatusCode.ERROR });\n if (error instanceof Error) {\n span.recordException(error);\n }\n throw error;\n } finally {\n span.end();\n updateSpanId(prevSpanId);\n }\n });\n}\n\n/**\n * Set an attribute on the current active span (if any).\n * Used for setting span attributes after span creation (e.g. timber.result on access spans).\n */\nexport async function setSpanAttribute(\n key: string,\n value: string | number | boolean\n): Promise<void> {\n const api = await getOtelApi();\n if (!api) return;\n\n const activeSpan = api.trace.getActiveSpan();\n if (activeSpan) {\n activeSpan.setAttribute(key, value);\n }\n}\n\n/**\n * Add a span event to the current active span (if any).\n * Used for timber.cache HIT/MISS events — recorded as span events, not child spans.\n */\nexport async function addSpanEvent(\n name: string,\n attributes?: Record<string, string | number | boolean>\n): Promise<void> {\n const api = await getOtelApi();\n if (!api) return;\n\n const activeSpan = api.trace.getActiveSpan();\n if (activeSpan) {\n activeSpan.addEvent(name, attributes);\n }\n}\n\n/**\n * Try to extract the OTEL trace ID from the current active span context.\n * Returns undefined if OTEL is not active or no span exists.\n */\nexport async function getOtelTraceId(): Promise<{ traceId: string; spanId: string } | undefined> {\n const api = await getOtelApi();\n if (!api) return undefined;\n\n const activeSpan = api.trace.getActiveSpan();\n if (!activeSpan) return undefined;\n\n const ctx = activeSpan.spanContext();\n // OTEL uses \"0000000000000000\" as invalid trace IDs\n if (!ctx.traceId || ctx.traceId === '00000000000000000000000000000000') {\n return undefined;\n }\n\n return { traceId: ctx.traceId, spanId: ctx.spanId };\n}\n"],"mappings":";;;;;;;;;;;;AAsBA,IAAM,WAAW,IAAI,mBAA+B;;;;;;;;;AAYpD,SAAgB,UAAkB;CAChC,MAAM,QAAQ,SAAS,UAAU;AACjC,KAAI,CAAC,MACH,OAAM,IAAI,MACR,mJAED;AAEH,QAAO,MAAM;;;;;AAMf,SAAgB,SAA6B;AAC3C,QAAO,SAAS,UAAU,EAAE;;;;;;AAS9B,SAAgB,kBAA0B;AACxC,QAAO,YAAY,CAAC,QAAQ,MAAM,GAAG;;;;;;AAOvC,SAAgB,eAAkB,IAAY,IAAgB;AAC5D,QAAO,SAAS,IAAI,EAAE,SAAS,IAAI,EAAE,GAAG;;;;;;;AAQ1C,SAAgB,eAAe,YAAoB,WAA0B;CAC3E,MAAM,QAAQ,SAAS,UAAU;AACjC,KAAI,OAAO;AACT,QAAM,UAAU;AAChB,QAAM,SAAS;;;;;;;AAQnB,SAAgB,aAAa,WAAqC;CAChE,MAAM,QAAQ,SAAS,UAAU;AACjC,KAAI,MACF,OAAM,SAAS;;;;;;AAQnB,SAAgB,gBAAwC;AACtD,QAAO,SAAS,UAAU;;;;;;;;;;AAyD5B,IAAI;AAEJ,eAAe,aAAkE;AAC/E,KAAI,aAAa,KAAA,EACf,KAAI;AACF,aAAW,MAAM,OAAO;SAClB;AACN,aAAW;;AAGf,QAAO;;;AAIT,IAAI;;;;AAKJ,eAAsB,YAAiE;AACrF,KAAI,YAAY,KAAA,GAAW;EACzB,MAAM,MAAM,MAAM,YAAY;AAC9B,MAAI,IACF,WAAU,IAAI,MAAM,UAAU,YAAY;MAE1C,WAAU;;AAGd,QAAO;;;;;;;;;;;;AAaT,eAAsB,SACpB,MACA,YACA,IACY;CACZ,MAAM,SAAS,MAAM,WAAW;AAChC,KAAI,CAAC,OACH,QAAO,IAAI;CAGb,MAAM,MAAO,MAAM,YAAY;AAC/B,QAAO,OAAO,gBAAgB,MAAM,EAAE,YAAY,EAAE,OAAO,SAAS;EAClE,MAAM,aAAa,QAAQ;AAC3B,eAAa,KAAK,aAAa,CAAC,OAAO;AACvC,MAAI;GACF,MAAM,SAAS,MAAM,IAAI;AACzB,QAAK,UAAU,EAAE,MAAM,IAAI,eAAe,IAAI,CAAC;AAC/C,UAAO;WACA,OAAO;AACd,QAAK,UAAU,EAAE,MAAM,IAAI,eAAe,OAAO,CAAC;AAClD,OAAI,iBAAiB,MACnB,MAAK,gBAAgB,MAAM;AAE7B,SAAM;YACE;AACR,QAAK,KAAK;AACV,gBAAa,WAAW;;GAE1B;;;;;;AAOJ,eAAsB,iBACpB,KACA,OACe;CACf,MAAM,MAAM,MAAM,YAAY;AAC9B,KAAI,CAAC,IAAK;CAEV,MAAM,aAAa,IAAI,MAAM,eAAe;AAC5C,KAAI,WACF,YAAW,aAAa,KAAK,MAAM;;;;;;AAQvC,eAAsB,aACpB,MACA,YACe;CACf,MAAM,MAAM,MAAM,YAAY;AAC9B,KAAI,CAAC,IAAK;CAEV,MAAM,aAAa,IAAI,MAAM,eAAe;AAC5C,KAAI,WACF,YAAW,SAAS,MAAM,WAAW;;;;;;AAQzC,eAAsB,iBAA2E;CAC/F,MAAM,MAAM,MAAM,YAAY;AAC9B,KAAI,CAAC,IAAK,QAAO,KAAA;CAEjB,MAAM,aAAa,IAAI,MAAM,eAAe;AAC5C,KAAI,CAAC,WAAY,QAAO,KAAA;CAExB,MAAM,MAAM,WAAW,aAAa;AAEpC,KAAI,CAAC,IAAI,WAAW,IAAI,YAAY,mCAClC;AAGF,QAAO;EAAE,SAAS,IAAI;EAAS,QAAQ,IAAI;EAAQ"}