@timber-js/app 0.2.0-alpha.129 → 0.2.0-alpha.131

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.
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,OAAO,uCAAuC,CAAC;AA4B/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAsDxD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,QAAQ,CAAC,CAmMnB;AAED,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"ssr-entry.d.ts","sourceRoot":"","sources":["../../src/server/ssr-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,OAAO,uCAAuC,CAAC;AA4B/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAsDxD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,QAAQ,CAAC,CAuMnB;AAED,eAAe,SAAS,CAAC"}
@@ -39,6 +39,11 @@ export declare function renderSsrStream(element: ReactNode, options?: {
39
39
  deferSuspenseFor?: number;
40
40
  signal?: AbortSignal;
41
41
  renderTimeoutMs?: number;
42
+ /** Request info threaded into render-error logs (method/path). */
43
+ requestInfo?: {
44
+ method: string;
45
+ path: string;
46
+ };
42
47
  /**
43
48
  * Fired synchronously when the render function is about to resolve
44
49
  * (i.e. at hold-end / shell-ready). `resolved` reports whether Fizz's
@@ -62,6 +67,11 @@ export declare function renderSsrNodeStream(element: ReactNode, options?: {
62
67
  deferSuspenseFor?: number;
63
68
  signal?: AbortSignal;
64
69
  renderTimeoutMs?: number;
70
+ /** Request info threaded into render-error logs (method/path). */
71
+ requestInfo?: {
72
+ method: string;
73
+ path: string;
74
+ };
65
75
  /**
66
76
  * Fired synchronously at the moment `renderSsrNodeStream`'s returned
67
77
  * Promise is about to resolve. `resolved` reports whether Fizz's
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-render.d.ts","sourceRoot":"","sources":["../../src/server/ssr-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAuFvC;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE;IACR,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;CACtD,GACA,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAErC;AAED,wEAAwE;AACxE,eAAO,MAAM,cAAc,SAAkB,CAAC;AAU9C;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE;IACR,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;CACtD,GACA,OAAO,CAAC,OAAO,aAAa,EAAE,QAAQ,CAAC,CAgGzC;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,OAAO,aAAa,EAAE,QAAQ,GACvC,cAAc,CAAC,UAAU,CAAC,CAE5B;AAqGD;;;;;;;;;;;;GAYG;AACH,2CAA2C;AAC3C,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,MAAM,CAAC,EAAE,WAAW,GACnB,cAAc,CAAC,UAAU,CAAC,CA2B5B;AAWD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,cAAc,CAAC,UAAU,CAAC,EACtC,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,OAAO,GACvB,QAAQ,CASV"}
1
+ {"version":3,"file":"ssr-render.d.ts","sourceRoot":"","sources":["../../src/server/ssr-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAuFvC;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE;IACR,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,WAAW,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C;;;;;;OAMG;IACH,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;CACtD,GACA,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAErC;AAED,wEAAwE;AACxE,eAAO,MAAM,cAAc,SAAkB,CAAC;AAU9C;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE;IACR,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,WAAW,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;CACtD,GACA,OAAO,CAAC,OAAO,aAAa,EAAE,QAAQ,CAAC,CAqGzC;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,OAAO,aAAa,EAAE,QAAQ,GACvC,cAAc,CAAC,UAAU,CAAC,CAE5B;AA2GD;;;;;;;;;;;;GAYG;AACH,2CAA2C;AAC3C,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,MAAM,CAAC,EAAE,WAAW,GACnB,cAAc,CAAC,UAAU,CAAC,CA2B5B;AAWD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,cAAc,CAAC,UAAU,CAAC,EACtC,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,OAAO,GACvB,QAAQ,CASV"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.129",
3
+ "version": "0.2.0-alpha.131",
4
4
  "description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -13,6 +13,31 @@
13
13
 
14
14
  import type { Plugin } from 'vite';
15
15
 
16
+ // Build-time-only packages that must never be bundled into the runtime
17
+ // server environments. With `noExternal: true` rolldown otherwise follows
18
+ // the transitive graph through the user's `timber.config.ts` (loaded by the
19
+ // `virtual:timber-cache-handler` dynamic import) into adapter code, dragging
20
+ // build tooling into the prod server bundle. The most visible symptom is
21
+ // rolldown trying to parse `fsevents.node` as JS and failing with
22
+ // `[UNLOADABLE_DEPENDENCY] … stream did not contain valid UTF-8`.
23
+ //
24
+ // - `nitro`, `@mdx-js/rollup` — build-time tooling reachable
25
+ // transitively through adapter modules and timber's own MDX activation.
26
+ // - `rollup` — pulled in by nitro and
27
+ // @mdx-js/rollup; declares `fsevents` as an optional native dep.
28
+ // - `fsevents`, `chokidar` — native/optional deps of
29
+ // rollup/vite. They are macOS-only file watchers with no runtime use
30
+ // in a server bundle.
31
+ //
32
+ // Mirrored on both rsc and ssr environments. See TIM-988.
33
+ const BUILD_ONLY_EXTERNALS: string[] = [
34
+ 'nitro',
35
+ 'rollup',
36
+ '@mdx-js/rollup',
37
+ 'fsevents',
38
+ 'chokidar',
39
+ ];
40
+
16
41
  export function timberServerBundle(): Plugin[] {
17
42
  const bundlePlugin: Plugin = {
18
43
  name: 'timber-server-bundle',
@@ -80,11 +105,11 @@ export function timberServerBundle(): Plugin[] {
80
105
  ssr: { target: 'webworker' },
81
106
  environments: {
82
107
  rsc: {
83
- resolve: { noExternal: true },
108
+ resolve: { noExternal: true, external: BUILD_ONLY_EXTERNALS },
84
109
  define: serverDefine,
85
110
  },
86
111
  ssr: {
87
- resolve: { noExternal: true },
112
+ resolve: { noExternal: true, external: BUILD_ONLY_EXTERNALS },
88
113
  define: serverDefine,
89
114
  },
90
115
  },
@@ -29,6 +29,12 @@ export function parseFormData(formData: FormData): Record<string, unknown> {
29
29
  // Skip React internal fields
30
30
  if (key.startsWith('$ACTION_')) continue;
31
31
 
32
+ // Skip prototype-pollution keys at the top level — assigning to
33
+ // `flat['__proto__']` invokes the prototype setter and can splice
34
+ // attacker-controlled data into `flat`'s prototype chain.
35
+ // The dot-path case is filtered separately in `expandDotPaths`.
36
+ if (DANGEROUS_KEYS.has(key)) continue;
37
+
32
38
  const values = formData.getAll(key);
33
39
  const processed = values.map(normalizeValue);
34
40
 
@@ -82,18 +88,23 @@ function expandDotPaths(flat: Record<string, unknown>): Record<string, unknown>
82
88
  }
83
89
  }
84
90
 
85
- // Fast path: no dot-notation keys, return as-is
91
+ // Fast path: no dot-notation keys, return as-is.
92
+ // Top-level dangerous keys are already filtered in `parseFormData` above.
86
93
  if (!hasDotPaths) return flat;
87
94
 
88
95
  for (const [key, value] of Object.entries(flat)) {
89
- if (!key.includes('.')) {
90
- result[key] = value;
96
+ // Reject any path that contains __proto__ / constructor / prototype.
97
+ // Without this, `__proto__.x=1` writes to Object.prototype because
98
+ // `result['__proto__']` resolves to the prototype object itself.
99
+ const parts = key.split('.');
100
+ if (parts.some((p) => DANGEROUS_KEYS.has(p))) continue;
101
+
102
+ if (parts.length === 1) {
103
+ result[parts[0]] = value;
91
104
  continue;
92
105
  }
93
106
 
94
- const parts = key.split('.');
95
107
  let current: Record<string, unknown> = result;
96
-
97
108
  for (let i = 0; i < parts.length - 1; i++) {
98
109
  const part = parts[i];
99
110
  if (current[part] === undefined || current[part] === null) {
@@ -113,6 +124,15 @@ function expandDotPaths(flat: Record<string, unknown>): Record<string, unknown>
113
124
  return result;
114
125
  }
115
126
 
127
+ // `__proto__` is the only key in this parser that escapes the dot-path
128
+ // walk's `typeof !== 'object'` reset: accessing `obj['__proto__']` returns
129
+ // `Object.prototype` (itself an object), so the walk steps into the global
130
+ // prototype and the leaf write mutates it. `constructor` resolves to a
131
+ // function (reset fires) and `prototype` is undefined on fresh objects
132
+ // (reset fires), so neither escapes the walk — they parse as legit nested
133
+ // keys and do not mutate anything global.
134
+ const DANGEROUS_KEYS = new Set(['__proto__']);
135
+
116
136
  // ─── Coercion Helpers ────────────────────────────────────────────────────
117
137
 
118
138
  /**
@@ -55,24 +55,20 @@ export function createRenderTimeout(timeoutMs: number, parentSignal?: AbortSigna
55
55
  const controller = new AbortController();
56
56
  const reason = new RenderTimeoutError(timeoutMs, 'RSC stream read timed out');
57
57
 
58
- const timer = setTimeout(() => {
59
- controller.abort(reason);
60
- }, timeoutMs);
58
+ const timer = setTimeout(() => controller.abort(reason), timeoutMs);
59
+
60
+ let onParentAbort: (() => void) | null = null;
61
61
 
62
- // If there's a parent signal (e.g. request abort), chain it
63
62
  if (parentSignal) {
64
63
  if (parentSignal.aborted) {
65
64
  clearTimeout(timer);
66
65
  controller.abort(parentSignal.reason);
67
66
  } else {
68
- parentSignal.addEventListener(
69
- 'abort',
70
- () => {
71
- clearTimeout(timer);
72
- controller.abort(parentSignal.reason);
73
- },
74
- { once: true }
75
- );
67
+ onParentAbort = () => {
68
+ clearTimeout(timer);
69
+ controller.abort(parentSignal.reason);
70
+ };
71
+ parentSignal.addEventListener('abort', onParentAbort, { once: true });
76
72
  }
77
73
  }
78
74
 
@@ -80,6 +76,10 @@ export function createRenderTimeout(timeoutMs: number, parentSignal?: AbortSigna
80
76
  signal: controller.signal,
81
77
  cancel: () => {
82
78
  clearTimeout(timer);
79
+ if (onParentAbort && parentSignal) {
80
+ parentSignal.removeEventListener('abort', onParentAbort);
81
+ onParentAbort = null;
82
+ }
83
83
  },
84
84
  };
85
85
  }
@@ -198,6 +198,7 @@ export async function handleSsr(
198
198
  } = _nodeStreamImports;
199
199
 
200
200
  const renderTimeoutMs = _runtimeConfig.renderTimeoutMs ?? undefined;
201
+ const requestInfo = { method: 'GET', path: navContext.pathname };
201
202
 
202
203
  let nodeHtmlStream: import('node:stream').Readable;
203
204
  try {
@@ -206,12 +207,13 @@ export async function handleSsr(
206
207
  deferSuspenseFor: navContext.deferSuspenseFor,
207
208
  signal: navContext.signal,
208
209
  renderTimeoutMs,
210
+ requestInfo,
209
211
  onFizzAllReadyResolved: (resolved) => {
210
212
  navContext._fizzAllReadyResolved = resolved;
211
213
  },
212
214
  });
213
215
  } catch (renderError) {
214
- logRenderError({ method: '', path: '', error: renderError });
216
+ logRenderError({ ...requestInfo, error: renderError });
215
217
  throw new SsrStreamError(
216
218
  'SSR renderToReadableStream failed due to RSC stream error',
217
219
  renderError
@@ -268,6 +270,7 @@ export async function handleSsr(
268
270
 
269
271
  // Web Streams path (CF Workers / fallback)
270
272
  const renderTimeoutMs = _runtimeConfig.renderTimeoutMs ?? undefined;
273
+ const requestInfo = { method: 'GET', path: navContext.pathname };
271
274
  let htmlStream: ReadableStream<Uint8Array>;
272
275
  try {
273
276
  htmlStream = await renderSsrStream(wrappedElement, {
@@ -275,12 +278,13 @@ export async function handleSsr(
275
278
  deferSuspenseFor: navContext.deferSuspenseFor,
276
279
  signal: navContext.signal,
277
280
  renderTimeoutMs,
281
+ requestInfo,
278
282
  onFizzAllReadyResolved: (resolved) => {
279
283
  navContext._fizzAllReadyResolved = resolved;
280
284
  },
281
285
  });
282
286
  } catch (renderError) {
283
- logRenderError({ method: '', path: '', error: renderError });
287
+ logRenderError({ ...requestInfo, error: renderError });
284
288
  throw new SsrStreamError(
285
289
  'SSR renderToReadableStream failed due to RSC stream error',
286
290
  renderError
@@ -128,6 +128,8 @@ export async function renderSsrStream(
128
128
  deferSuspenseFor?: number;
129
129
  signal?: AbortSignal;
130
130
  renderTimeoutMs?: number;
131
+ /** Request info threaded into render-error logs (method/path). */
132
+ requestInfo?: { method: string; path: string };
131
133
  /**
132
134
  * Fired synchronously when the render function is about to resolve
133
135
  * (i.e. at hold-end / shell-ready). `resolved` reports whether Fizz's
@@ -166,6 +168,8 @@ export async function renderSsrNodeStream(
166
168
  deferSuspenseFor?: number;
167
169
  signal?: AbortSignal;
168
170
  renderTimeoutMs?: number;
171
+ /** Request info threaded into render-error logs (method/path). */
172
+ requestInfo?: { method: string; path: string };
169
173
  /**
170
174
  * Fired synchronously at the moment `renderSsrNodeStream`'s returned
171
175
  * Promise is about to resolve. `resolved` reports whether Fizz's
@@ -178,6 +182,7 @@ export async function renderSsrNodeStream(
178
182
  const signal = options?.signal;
179
183
  const deferMs = options?.deferSuspenseFor;
180
184
  const timeoutMs = options?.renderTimeoutMs;
185
+ const requestInfo = options?.requestInfo;
181
186
  const onFizzAllReadyResolved = options?.onFizzAllReadyResolved;
182
187
 
183
188
  return new Promise<import('node:stream').Readable>((resolve, reject) => {
@@ -229,7 +234,11 @@ export async function renderSsrNodeStream(
229
234
  // Skip React's sanitized production errors — the original was
230
235
  // already logged by the RSC onError with full details.
231
236
  if (isReactSanitizedError(error)) return;
232
- logRenderError({ method: '', path: '', error });
237
+ logRenderError({
238
+ method: requestInfo?.method ?? '',
239
+ path: requestInfo?.path ?? '',
240
+ error,
241
+ });
233
242
  },
234
243
  });
235
244
 
@@ -250,8 +259,8 @@ export async function renderSsrNodeStream(
250
259
  'abort',
251
260
  () => {
252
261
  logRenderError({
253
- method: '',
254
- path: '',
262
+ method: requestInfo?.method ?? '',
263
+ path: requestInfo?.path ?? '',
255
264
  error: new Error(
256
265
  `SSR render timed out after ${timeoutMs}ms — aborting. ` +
257
266
  'A Suspense component or downstream fetch may be hanging.'
@@ -293,11 +302,13 @@ async function renderViaReadableStream(
293
302
  deferSuspenseFor?: number;
294
303
  signal?: AbortSignal;
295
304
  renderTimeoutMs?: number;
305
+ requestInfo?: { method: string; path: string };
296
306
  onFizzAllReadyResolved?: (resolved: boolean) => void;
297
307
  }
298
308
  ): Promise<ReadableStream<Uint8Array>> {
299
309
  const signal = options?.signal;
300
310
  const timeoutMs = options?.renderTimeoutMs;
311
+ const requestInfo = options?.requestInfo;
301
312
  const onFizzAllReadyResolved = options?.onFizzAllReadyResolved;
302
313
 
303
314
  // If a render timeout is configured, create a combined signal that
@@ -318,8 +329,8 @@ async function renderViaReadableStream(
318
329
  if (isAbortError(error) || effectiveSignal?.aborted) return;
319
330
  if (error instanceof RenderTimeoutError) {
320
331
  logRenderError({
321
- method: '',
322
- path: '',
332
+ method: requestInfo?.method ?? '',
333
+ path: requestInfo?.path ?? '',
323
334
  error: new Error(
324
335
  `SSR render timed out after ${timeoutMs}ms — aborting. ` +
325
336
  'A Suspense component or downstream fetch may be hanging.'
@@ -330,7 +341,11 @@ async function renderViaReadableStream(
330
341
  // Skip React's sanitized production errors — the original was
331
342
  // already logged by the RSC onError with full details.
332
343
  if (isReactSanitizedError(error)) return;
333
- logRenderError({ method: '', path: '', error });
344
+ logRenderError({
345
+ method: requestInfo?.method ?? '',
346
+ path: requestInfo?.path ?? '',
347
+ error,
348
+ });
334
349
  },
335
350
  });
336
351
  } catch (error) {