@octanejs/app-core 0.0.9 → 0.0.13

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/app-core",
3
- "version": "0.0.9",
3
+ "version": "0.0.13",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -75,14 +75,14 @@
75
75
  }
76
76
  },
77
77
  "dependencies": {
78
- "@ripple-ts/adapter": "^0.3.108",
78
+ "@ripple-ts/adapter": "^0.3.112",
79
79
  "esbuild": "^0.28.1"
80
80
  },
81
81
  "peerDependencies": {
82
- "octane": "0.1.13"
82
+ "octane": "0.1.17"
83
83
  },
84
84
  "devDependencies": {
85
85
  "@types/node": "^24.13.3",
86
- "octane": "0.1.13"
86
+ "octane": "0.1.17"
87
87
  }
88
88
  }
package/src/constants.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // @ts-check
2
2
  export const DEFAULT_OUTDIR = 'dist';
3
3
  export const ENTRY_FILENAME = 'entry.js';
4
+ /** Default upper bound for an encoded server-function request body. */
5
+ export const DEFAULT_RPC_MAX_BODY_BYTES = 1_048_576;
4
6
  /** Context.state key a middleware sets to apply a per-request CSP nonce. */
5
7
  export const OCTANE_NONCE_STATE_KEY = 'octane.nonce';
package/src/index.js CHANGED
@@ -16,3 +16,4 @@ export {
16
16
  is_rpc_request,
17
17
  runMiddlewareChain,
18
18
  } from './middleware.js';
19
+ export { handleRpcRequest } from './server/rpc.js';
@@ -15,7 +15,34 @@
15
15
 
16
16
  import { normalizeRendererConfig } from 'octane/compiler/renderers';
17
17
 
18
- import { DEFAULT_OUTDIR } from './constants.js';
18
+ import { DEFAULT_OUTDIR, DEFAULT_RPC_MAX_BODY_BYTES } from './constants.js';
19
+
20
+ /**
21
+ * @param {unknown} value
22
+ * @returns {string}
23
+ */
24
+ function normalize_rpc_origin(value) {
25
+ if (typeof value !== 'string') {
26
+ throw new Error('[octane] server.rpc.allowedOrigins must contain only HTTP or HTTPS origins.');
27
+ }
28
+
29
+ try {
30
+ const origin = new URL(value);
31
+ if (
32
+ (origin.protocol !== 'http:' && origin.protocol !== 'https:') ||
33
+ origin.username !== '' ||
34
+ origin.password !== '' ||
35
+ origin.pathname !== '/' ||
36
+ origin.search !== '' ||
37
+ origin.hash !== ''
38
+ ) {
39
+ throw new Error('Not an HTTP or HTTPS origin');
40
+ }
41
+ return origin.origin;
42
+ } catch {
43
+ throw new Error('[octane] server.rpc.allowedOrigins must contain only HTTP or HTTPS origins.');
44
+ }
45
+ }
19
46
 
20
47
  /**
21
48
  * @param {unknown} route
@@ -176,6 +203,24 @@ export function resolveOctaneConfig(raw, options = {}) {
176
203
  throw new Error("[octane] server.render must be 'streaming' or 'buffered'.");
177
204
  }
178
205
 
206
+ const rawRpc = raw.server?.rpc;
207
+ if (
208
+ rawRpc !== undefined &&
209
+ (rawRpc === null || typeof rawRpc !== 'object' || Array.isArray(rawRpc))
210
+ ) {
211
+ throw new Error('[octane] server.rpc must be an object when provided.');
212
+ }
213
+ if (
214
+ rawRpc?.maxBodyBytes !== undefined &&
215
+ (!Number.isSafeInteger(rawRpc.maxBodyBytes) || rawRpc.maxBodyBytes <= 0)
216
+ ) {
217
+ throw new Error('[octane] server.rpc.maxBodyBytes must be a positive safe integer.');
218
+ }
219
+ if (rawRpc?.allowedOrigins !== undefined && !Array.isArray(rawRpc.allowedOrigins)) {
220
+ throw new Error('[octane] server.rpc.allowedOrigins must be an array.');
221
+ }
222
+ const allowedRpcOrigins = [...new Set((rawRpc?.allowedOrigins ?? []).map(normalize_rpc_origin))];
223
+
179
224
  // ------------------------------------------------------------------
180
225
  // Apply defaults
181
226
  // ------------------------------------------------------------------
@@ -201,6 +246,10 @@ export function resolveOctaneConfig(raw, options = {}) {
201
246
  server: {
202
247
  trustProxy: raw.server?.trustProxy ?? false,
203
248
  render: raw.server?.render ?? 'streaming',
249
+ rpc: {
250
+ allowedOrigins: allowedRpcOrigins,
251
+ maxBodyBytes: rawRpc?.maxBodyBytes ?? DEFAULT_RPC_MAX_BODY_BYTES,
252
+ },
204
253
  },
205
254
  };
206
255
  }
@@ -24,6 +24,7 @@
24
24
 
25
25
  import { createRouter } from './router.js';
26
26
  import { createContext, runMiddlewareChain } from './middleware.js';
27
+ import { handleRpcRequest } from './rpc.js';
27
28
  import { handleServerRoute } from './server-route.js';
28
29
  import { composeHtmlStream } from './html-stream.js';
29
30
  import {
@@ -48,12 +49,7 @@ import {
48
49
  get_route_entry_export_name,
49
50
  get_route_entry_path,
50
51
  } from '../routes.js';
51
- import {
52
- patch_global_fetch,
53
- build_rpc_lookup,
54
- is_rpc_request,
55
- handle_rpc_request,
56
- } from '@ripple-ts/adapter/rpc';
52
+ import { patch_global_fetch, build_rpc_lookup, is_rpc_request } from '@ripple-ts/adapter/rpc';
57
53
 
58
54
  export { resolveOctaneConfig } from '../resolve-config.js';
59
55
 
@@ -127,6 +123,7 @@ export function createHandler(manifest, deps) {
127
123
  const router = createRouter(manifest.routes);
128
124
  const globalMiddlewares = manifest.middlewares ?? [];
129
125
  const trustProxy = manifest.trustProxy ?? false;
126
+ const rpcPolicy = manifest.rpc;
130
127
  const runtime = manifest.runtime;
131
128
  validateSsrTemplate(htmlTemplate);
132
129
  // Also pin the built-template contract up front. The marker is emitted by
@@ -158,17 +155,7 @@ export function createHandler(manifest, deps) {
158
155
  headers: { 'Content-Type': 'application/json' },
159
156
  });
160
157
  }
161
- /** @type {import('@ripple-ts/adapter/rpc').AsyncContext<{ origin?: string, platform?: unknown }>} */
162
- const requestAsyncContext =
163
- platform === undefined
164
- ? asyncContext
165
- : {
166
- run(store, fn) {
167
- return asyncContext.run({ ...store, platform }, fn);
168
- },
169
- getStore: () => asyncContext.getStore(),
170
- };
171
- return handle_rpc_request(request, {
158
+ return handleRpcRequest(request, {
172
159
  resolveFunction(/** @type {string} */ hash) {
173
160
  const entry = rpcLookup.get(hash);
174
161
  if (!entry) return null;
@@ -176,8 +163,12 @@ export function createHandler(manifest, deps) {
176
163
  return typeof fn === 'function' ? fn : null;
177
164
  },
178
165
  executeServerFunction,
179
- asyncContext: requestAsyncContext,
166
+ asyncContext,
180
167
  trustProxy,
168
+ middlewares: globalMiddlewares,
169
+ allowedOrigins: rpcPolicy?.allowedOrigins,
170
+ maxBodyBytes: rpcPolicy?.maxBodyBytes,
171
+ platform,
181
172
  });
182
173
  }
183
174
 
@@ -288,39 +279,67 @@ export function createHandler(manifest, deps) {
288
279
  }
289
280
 
290
281
  const headContent = [...preloadTags, dataScript].join('\n');
291
- const html = applyHydrationNonce(htmlTemplate, nonce).replace('<!--ssr-head-->', headContent);
282
+ const noncedTemplate = applyHydrationNonce(htmlTemplate, nonce);
292
283
 
293
284
  const status = route.status ?? 200;
294
285
  const headers = { 'Content-Type': 'text/html; charset=utf-8' };
295
286
 
296
- const [prefix, suffix] = splitSsrTemplate(html);
287
+ // `headChannel: 'separate'` below: this handler renders the ROUTE into the
288
+ // template's `<div id="root">`, not a document, so core's default fold has
289
+ // no `</head>` to target and would prepend hoisted `<title>`/`<meta>`/
290
+ // `<link>` into the body, where a title loses to the template's and a
291
+ // canonical or description is ignored. Splicing at `<!--ssr-head-->`
292
+ // instead is also what makes hydration's `document.head` adoption find the
293
+ // ownership markers rather than creating duplicates.
294
+ //
295
+ // The replacement is a FUNCTION, not a string: a string replacement makes
296
+ // `$&`, `` $` ``, `$'` and `$1` in the inserted text expand against the
297
+ // match, and this text now carries author-controlled metadata as well as
298
+ // the serialized route data.
299
+ /** @param {string} hoistedHead */
300
+ const splitAroundBody = (hoistedHead) =>
301
+ splitSsrTemplate(noncedTemplate.replace('<!--ssr-head-->', () => headContent + hoistedHead));
297
302
 
298
303
  if (manifest.render === 'buffered') {
299
304
  // Await-everything fallback (`prerender` from octane/static): no
300
305
  // streaming, one document. The deduped scoped-style tags lead the body
301
306
  // markup inside #root — the same position they hold in the streamed
302
307
  // shell — so hydrateRoot's leading-style skip applies unchanged.
303
- const { html: body, css } = await prerender(RootComponent, undefined, {
308
+ const {
309
+ html: body,
310
+ css,
311
+ head,
312
+ } = await prerender(RootComponent, undefined, {
304
313
  nonce: nonce ?? undefined,
314
+ headChannel: 'separate',
305
315
  signal: context.request.signal,
306
316
  onError(/** @type {unknown} */ error) {
307
317
  console.error('[octane] SSR render error:', error);
308
318
  },
309
319
  });
320
+ const [prefix, suffix] = splitAroundBody(head ?? '');
310
321
  return new Response(prefix + css + body + suffix, { status, headers });
311
322
  }
312
323
 
313
324
  // Streaming (default): shell flushes at first await, suspense segments
314
325
  // stream out-of-order behind it — identical to dev.
326
+ let hoistedHead = '';
315
327
  /** @type {ReadableStream<Uint8Array>} */
316
328
  const renderStream = await renderToReadableStream(RootComponent, undefined, {
317
329
  nonce: nonce ?? undefined,
330
+ headChannel: 'separate',
331
+ // Fires before the shell is written, so the metadata is in hand before
332
+ // the template prefix (which carries `<head>`) is composed below.
333
+ onHeadReady(/** @type {string} */ head) {
334
+ hoistedHead = head;
335
+ },
318
336
  signal: context.request.signal,
319
337
  onError(/** @type {unknown} */ error) {
320
338
  console.error('[octane] SSR render error:', error);
321
339
  },
322
340
  });
323
341
 
342
+ const [prefix, suffix] = splitAroundBody(hoistedHead);
324
343
  const body = composeHtmlStream(prefix, renderStream, suffix);
325
344
 
326
345
  return new Response(body, { status, headers });
@@ -0,0 +1,254 @@
1
+ // @ts-check
2
+ /**
3
+ * Bundler-neutral, security-owning server-function request boundary.
4
+ *
5
+ * The Ripple adapter owns the existing wire path, hash lookup, proxy-origin
6
+ * derivation, and request-scoped fetch primitives. Octane owns which requests
7
+ * may cross its server-function boundary, the bounded body reader, application
8
+ * authorization middleware, and production-safe error disclosure.
9
+ */
10
+
11
+ import { derive_origin } from '@ripple-ts/adapter/rpc';
12
+
13
+ import { DEFAULT_RPC_MAX_BODY_BYTES } from '../constants.js';
14
+ import { createContext, runMiddlewareChain } from './middleware.js';
15
+
16
+ const RPC_PATH_PREFIX = '/_$_ripple_rpc_$_/';
17
+
18
+ /**
19
+ * @param {number} status
20
+ * @param {string} message
21
+ * @param {HeadersInit} [headers]
22
+ * @returns {Response}
23
+ */
24
+ function rpcError(status, message, headers) {
25
+ return new Response(JSON.stringify({ error: message }), {
26
+ status,
27
+ headers: { 'Content-Type': 'application/json; charset=utf-8', ...headers },
28
+ });
29
+ }
30
+
31
+ /**
32
+ * @param {Request} request
33
+ * @param {boolean} trustProxy
34
+ * @param {readonly string[]} allowedOrigins
35
+ * @returns {string | null}
36
+ */
37
+ function allowedRequestOrigin(request, trustProxy, allowedOrigins) {
38
+ let requestOrigin;
39
+ try {
40
+ requestOrigin = new URL(derive_origin(request, trustProxy)).origin;
41
+ } catch {
42
+ return null;
43
+ }
44
+
45
+ const browserOrigin = request.headers.get('origin');
46
+ if (browserOrigin === null) {
47
+ return request.headers.get('sec-fetch-site')?.toLowerCase() === 'cross-site'
48
+ ? null
49
+ : requestOrigin;
50
+ }
51
+
52
+ let normalizedBrowserOrigin;
53
+ try {
54
+ normalizedBrowserOrigin = new URL(browserOrigin).origin;
55
+ } catch {
56
+ return null;
57
+ }
58
+ if (normalizedBrowserOrigin !== browserOrigin) return null;
59
+
60
+ return normalizedBrowserOrigin === requestOrigin ||
61
+ allowedOrigins.includes(normalizedBrowserOrigin)
62
+ ? requestOrigin
63
+ : null;
64
+ }
65
+
66
+ /**
67
+ * @param {Response} response
68
+ * @param {string | null} origin
69
+ * @returns {Response}
70
+ */
71
+ function withRpcCors(response, origin) {
72
+ if (origin === null) return response;
73
+
74
+ const headers = new Headers(response.headers);
75
+ headers.set('Access-Control-Allow-Origin', origin);
76
+ headers.append('Vary', 'Origin');
77
+ return new Response(response.body, {
78
+ status: response.status,
79
+ statusText: response.statusText,
80
+ headers,
81
+ });
82
+ }
83
+
84
+ /**
85
+ * @param {Request} request
86
+ * @param {number} maxBodyBytes
87
+ * @returns {Promise<{ body: string } | { error: Response }>}
88
+ */
89
+ async function readBoundedRpcBody(request, maxBodyBytes) {
90
+ const contentLength = request.headers.get('content-length');
91
+ if (contentLength !== null) {
92
+ if (!/^\d+$/.test(contentLength)) {
93
+ return { error: rpcError(400, 'Invalid RPC request') };
94
+ }
95
+ const declaredLength = Number(contentLength);
96
+ if (!Number.isSafeInteger(declaredLength) || declaredLength > maxBodyBytes) {
97
+ return { error: rpcError(413, 'RPC request exceeds the maximum body size') };
98
+ }
99
+ }
100
+
101
+ if (request.body === null || request.signal.aborted) {
102
+ return { error: rpcError(400, 'Invalid RPC request') };
103
+ }
104
+
105
+ const reader = request.body.getReader();
106
+ const decoder = new TextDecoder('utf-8', { fatal: true });
107
+ let bytesRead = 0;
108
+ let body = '';
109
+
110
+ try {
111
+ while (true) {
112
+ const { done, value } = await reader.read();
113
+ if (done) break;
114
+ bytesRead += value.byteLength;
115
+ if (bytesRead > maxBodyBytes) {
116
+ try {
117
+ await reader.cancel();
118
+ } catch {
119
+ // A hostile stream cannot downgrade an oversized request into another status.
120
+ }
121
+ return { error: rpcError(413, 'RPC request exceeds the maximum body size') };
122
+ }
123
+ body += decoder.decode(value, { stream: true });
124
+ }
125
+ body += decoder.decode();
126
+ } catch {
127
+ return { error: rpcError(400, 'Invalid RPC request') };
128
+ } finally {
129
+ reader.releaseLock();
130
+ }
131
+
132
+ try {
133
+ if (!Array.isArray(JSON.parse(body))) {
134
+ return { error: rpcError(400, 'Invalid server function arguments') };
135
+ }
136
+ } catch {
137
+ return { error: rpcError(400, 'Invalid server function arguments') };
138
+ }
139
+
140
+ return { body };
141
+ }
142
+
143
+ /**
144
+ * @param {unknown} error
145
+ * @returns {boolean}
146
+ */
147
+ function isInvalidRpcPayload(error) {
148
+ return (
149
+ typeof error === 'object' &&
150
+ error !== null &&
151
+ 'code' in error &&
152
+ error.code === 'OCTANE_INVALID_RPC_PAYLOAD'
153
+ );
154
+ }
155
+
156
+ /**
157
+ * Validate and execute an Octane server function behind application middleware.
158
+ *
159
+ * @param {Request} request
160
+ * @param {import('@octanejs/app-core').RpcRequestOptions} options
161
+ * @returns {Promise<Response>}
162
+ */
163
+ export async function handleRpcRequest(request, options) {
164
+ if (request.method === 'OPTIONS') {
165
+ const origin = allowedRequestOrigin(
166
+ request,
167
+ options.trustProxy ?? false,
168
+ options.allowedOrigins ?? [],
169
+ );
170
+ const browserOrigin = request.headers.get('origin');
171
+ if (origin === null || browserOrigin === null || browserOrigin === origin) {
172
+ return rpcError(403, 'Cross-origin RPC requests are not allowed');
173
+ }
174
+ if (request.headers.get('access-control-request-method') !== 'POST') {
175
+ return rpcError(405, 'RPC requests require POST', { Allow: 'POST' });
176
+ }
177
+
178
+ return new Response(null, {
179
+ status: 204,
180
+ headers: {
181
+ 'Access-Control-Allow-Origin': browserOrigin,
182
+ 'Access-Control-Allow-Methods': 'POST',
183
+ 'Access-Control-Allow-Headers':
184
+ request.headers.get('access-control-request-headers') ?? 'content-type',
185
+ Vary: 'Origin, Access-Control-Request-Headers',
186
+ },
187
+ });
188
+ }
189
+
190
+ if (request.method !== 'POST') {
191
+ return rpcError(405, 'RPC requests require POST', { Allow: 'POST' });
192
+ }
193
+
194
+ const contentType = request.headers.get('content-type');
195
+ if (contentType === null || !/^application\/json(?:\s*;|\s*$)/i.test(contentType)) {
196
+ return rpcError(415, 'RPC requests require application/json');
197
+ }
198
+
199
+ const hash = new URL(request.url).pathname.slice(RPC_PATH_PREFIX.length);
200
+ if (!/^[a-f0-9]{8}$/.test(hash)) {
201
+ return rpcError(400, 'Invalid RPC request');
202
+ }
203
+
204
+ const origin = allowedRequestOrigin(
205
+ request,
206
+ options.trustProxy ?? false,
207
+ options.allowedOrigins ?? [],
208
+ );
209
+ if (origin === null) {
210
+ return rpcError(403, 'Cross-origin RPC requests are not allowed');
211
+ }
212
+ const browserOrigin = request.headers.get('origin');
213
+ const corsOrigin = browserOrigin === origin ? null : browserOrigin;
214
+
215
+ const context = createContext(request, {}, options.platform);
216
+ const store =
217
+ options.platform === undefined ? { origin } : { origin, platform: options.platform };
218
+
219
+ try {
220
+ const response = await options.asyncContext.run(store, async () =>
221
+ runMiddlewareChain(
222
+ context,
223
+ options.middlewares ?? [],
224
+ [],
225
+ async () => {
226
+ const fn = await options.resolveFunction(hash);
227
+ if (fn === null) return rpcError(404, 'RPC function not found');
228
+ const payload = await readBoundedRpcBody(
229
+ request,
230
+ options.maxBodyBytes ?? DEFAULT_RPC_MAX_BODY_BYTES,
231
+ );
232
+ if ('error' in payload) return payload.error;
233
+ try {
234
+ const result = await options.executeServerFunction(fn, payload.body);
235
+ return new Response(result, {
236
+ status: 200,
237
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
238
+ });
239
+ } catch (error) {
240
+ if (isInvalidRpcPayload(error)) {
241
+ return rpcError(400, 'Invalid server function arguments');
242
+ }
243
+ throw error;
244
+ }
245
+ },
246
+ [],
247
+ ),
248
+ );
249
+ return withRpcCors(response, corsOrigin);
250
+ } catch (error) {
251
+ console.error('[octane] RPC request error:', error);
252
+ return withRpcCors(rpcError(500, 'Internal Server Error'), corsOrigin);
253
+ }
254
+ }
@@ -277,6 +277,7 @@ export const manifest = {
277
277
  layouts,
278
278
  middlewares: octaneConfig.middlewares,
279
279
  trustProxy: octaneConfig.server.trustProxy,
280
+ rpc: octaneConfig.server.rpc,
280
281
  render: octaneConfig.server.render,
281
282
  rootBoundary,
282
283
  rootBoundaryEntries,
@@ -387,6 +388,7 @@ export const handler = createHandler(
387
388
  layouts,
388
389
  middlewares: octaneConfig.middlewares,
389
390
  trustProxy: octaneConfig.server.trustProxy,
391
+ rpc: octaneConfig.server.rpc,
390
392
  render: octaneConfig.server.render,
391
393
  rootBoundary,
392
394
  rootBoundaryEntries,
package/types/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { RuntimePrimitives } from '@ripple-ts/adapter';
2
+ import type { AsyncContext } from '@ripple-ts/adapter/rpc';
2
3
 
3
4
  // ============================================================================
4
5
  // Shared app/config exports
@@ -138,6 +139,21 @@ export function handleServerRoute(
138
139
  ): Promise<Response>;
139
140
  export function is_rpc_request(pathname: string): boolean;
140
141
 
142
+ /** Security policy and execution dependencies for a server-function request. */
143
+ export interface RpcRequestOptions {
144
+ resolveFunction: (hash: string) => Function | null | Promise<Function | null>;
145
+ executeServerFunction: (fn: Function, body: string) => Promise<string>;
146
+ asyncContext: AsyncContext<{ origin?: string; platform?: unknown }>;
147
+ trustProxy?: boolean;
148
+ middlewares?: Middleware[];
149
+ allowedOrigins?: readonly string[];
150
+ maxBodyBytes?: number;
151
+ platform?: unknown;
152
+ }
153
+
154
+ /** Apply Octane's security policy and global middleware to a server function. */
155
+ export function handleRpcRequest(request: Request, options: RpcRequestOptions): Promise<Response>;
156
+
141
157
  // ============================================================================
142
158
  // Configuration
143
159
  // ============================================================================
@@ -339,6 +355,13 @@ export interface OctaneConfigOptions {
339
355
  * @default false
340
356
  */
341
357
  trustProxy?: boolean;
358
+ /** Security policy for compiler-generated `module server` requests. */
359
+ rpc?: {
360
+ /** Additional exact HTTP(S) origins permitted to call server functions. */
361
+ allowedOrigins?: string[];
362
+ /** Maximum encoded request size in bytes. @default 1048576 */
363
+ maxBodyBytes?: number;
364
+ };
342
365
  /**
343
366
  * Production SSR mode: 'streaming' (default) flushes the shell at
344
367
  * first await and streams suspense segments out-of-order (same engine
@@ -378,6 +401,12 @@ export interface ResolvedOctaneConfig {
378
401
  server: {
379
402
  /** @default false */
380
403
  trustProxy: boolean;
404
+ rpc: {
405
+ /** Additional normalized HTTP(S) origins. @default [] */
406
+ allowedOrigins: string[];
407
+ /** Maximum encoded request size in bytes. @default 1048576 */
408
+ maxBodyBytes: number;
409
+ };
381
410
  /** @default 'streaming' */
382
411
  render: 'streaming' | 'buffered';
383
412
  };
@@ -29,6 +29,8 @@ export interface ServerManifest {
29
29
  middlewares: Middleware[];
30
30
  /** Trust X-Forwarded-* headers when deriving origin for RPC fetch */
31
31
  trustProxy?: boolean;
32
+ /** Validated `module server` origin and body-size policy. */
33
+ rpc?: Partial<ResolvedOctaneConfig['server']['rpc']>;
32
34
  /** 'streaming' (default) renders via renderToReadableStream; 'buffered' awaits everything via prerender */
33
35
  render?: 'streaming' | 'buffered';
34
36
  /** Resolved server-compiled global boundary components. */