@reckona/mreact-server 0.0.1

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/dist/flight.js ADDED
@@ -0,0 +1,1284 @@
1
+ import { getNativeFlight } from "./native-flight.js";
2
+ export const CLIENT_REFERENCE_TYPE = Symbol.for("modular.react.client_reference");
3
+ export const SERVER_REFERENCE_TYPE = Symbol.for("modular.react.server_reference");
4
+ const CACHE_SCOPE_SYMBOL = Symbol.for("modular.react.cache_scope");
5
+ const REACT_COMPAT_ELEMENT_TYPE = Symbol.for("modular.react.element");
6
+ const REACT_COMPAT_FRAGMENT_TYPE = Symbol.for("modular.react.fragment");
7
+ const DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
8
+ const reactFlightBinaryRowTags = ["A", "O", "o", "U", "S", "s", "L", "l", "G", "g", "M", "m", "V"];
9
+ const reactFlightRowTags = ["C", "D", "E", "F", "H", "I", "J", "N", "P", "R", "T", "W", "X", "x", "r"];
10
+ const reactFlightModelTokens = [
11
+ "$",
12
+ "$$",
13
+ "$@",
14
+ "$D",
15
+ "$E",
16
+ "$F",
17
+ "$I",
18
+ "$K",
19
+ "$L",
20
+ "$N",
21
+ "$Q",
22
+ "$S",
23
+ "$W",
24
+ "$Y",
25
+ "$Z",
26
+ "$i",
27
+ "$n",
28
+ "$u",
29
+ "$undefined",
30
+ ];
31
+ export function getReactFlightProtocolCoverage() {
32
+ return {
33
+ binaryRowTags: [...reactFlightBinaryRowTags],
34
+ modelTokens: [...reactFlightModelTokens],
35
+ rowTags: [...reactFlightRowTags],
36
+ };
37
+ }
38
+ export function createClientReference(moduleId, exportName = "default", chunks) {
39
+ return {
40
+ $$typeof: CLIENT_REFERENCE_TYPE,
41
+ moduleId,
42
+ exportName,
43
+ ...(chunks === undefined ? {} : { chunks }),
44
+ };
45
+ }
46
+ export function createServerReference(moduleId, exportName = "default", bound) {
47
+ return {
48
+ $$typeof: SERVER_REFERENCE_TYPE,
49
+ moduleId,
50
+ exportName,
51
+ ...(bound === undefined ? {} : { bound }),
52
+ };
53
+ }
54
+ export function isClientReference(value) {
55
+ return (typeof value === "object" &&
56
+ value !== null &&
57
+ value.$$typeof === CLIENT_REFERENCE_TYPE);
58
+ }
59
+ export function isServerReference(value) {
60
+ return (typeof value === "object" &&
61
+ value !== null &&
62
+ value.$$typeof === SERVER_REFERENCE_TYPE);
63
+ }
64
+ export async function renderToFlightResponse(renderable, props = {}) {
65
+ return runWithFlightCacheScope(async () => {
66
+ const state = {
67
+ clientReferences: [],
68
+ clientReferenceIndexes: new Map(),
69
+ serverReferences: [],
70
+ serverReferenceIndexes: new Map(),
71
+ };
72
+ const rootValue = typeof renderable === "function"
73
+ ? await renderable(props)
74
+ : renderable;
75
+ return {
76
+ version: 1,
77
+ root: await serializeFlightValue(rootValue, state),
78
+ clientReferences: state.clientReferences,
79
+ serverReferences: state.serverReferences,
80
+ };
81
+ });
82
+ }
83
+ export function stringifyFlightResponse(response) {
84
+ return JSON.stringify(response);
85
+ }
86
+ export function renderFlightResponseScript(response, options = {}) {
87
+ const idAttribute = options.id === undefined ? "" : ` id="${escapeAttribute(options.id)}"`;
88
+ const nonceAttribute = options.nonce === undefined ? "" : ` nonce="${escapeAttribute(options.nonce)}"`;
89
+ return `<script type="application/json" data-mreact-flight${idAttribute}${nonceAttribute}>${serializeJsonForHtml(response)}</script>`;
90
+ }
91
+ export function createServerActionHandler(actions, options = {}) {
92
+ return async (request) => {
93
+ if (request.method !== "POST") {
94
+ return jsonResponse({ ok: false, error: "Method not allowed." }, 405);
95
+ }
96
+ const originResponse = validateRequestOrigin(request, options.allowedOrigins);
97
+ if (originResponse !== undefined) {
98
+ return originResponse;
99
+ }
100
+ const csrfResponse = validateCsrfToken(request, options.csrf);
101
+ if (csrfResponse !== undefined) {
102
+ return csrfResponse;
103
+ }
104
+ // Issue 076: mark the nonce as used only after the action runs.
105
+ // The validator now just reads + checks; the commit happens in a
106
+ // try/finally below.
107
+ const nonceCheck = validateServerActionNonce(request, options.replayProtection);
108
+ if (nonceCheck.response !== undefined) {
109
+ return nonceCheck.response;
110
+ }
111
+ const payload = await readServerActionPayload(request, options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES);
112
+ if (payload instanceof Response) {
113
+ return payload;
114
+ }
115
+ if (typeof payload.moduleId !== "string" || typeof payload.exportName !== "string") {
116
+ return jsonResponse({ ok: false, error: "Invalid server action reference." }, 400);
117
+ }
118
+ const actionEntry = actions[serverActionKey(payload.moduleId, payload.exportName)];
119
+ if (actionEntry === undefined) {
120
+ return jsonResponse({ ok: false, error: "Unknown server action." }, 404);
121
+ }
122
+ const action = getServerAction(actionEntry);
123
+ const validateArgs = getServerActionArgsValidator(actionEntry);
124
+ const boundArgs = Array.isArray(payload.bound) ? payload.bound : [];
125
+ const args = [...boundArgs, ...(Array.isArray(payload.args) ? payload.args : [])];
126
+ const validationResult = validateArgs?.(args);
127
+ if (validationResult !== undefined && validationResult !== true) {
128
+ return jsonResponse({
129
+ ok: false,
130
+ error: typeof validationResult === "string"
131
+ ? validationResult
132
+ : "Invalid server action arguments.",
133
+ }, 400);
134
+ }
135
+ const authorizationResult = await options.authorize?.(request, {
136
+ moduleId: payload.moduleId,
137
+ exportName: payload.exportName,
138
+ }, args);
139
+ if (authorizationResult !== undefined && authorizationResult !== true) {
140
+ return jsonResponse({
141
+ ok: false,
142
+ error: typeof authorizationResult === "string"
143
+ ? authorizationResult
144
+ : "Server action not authorized.",
145
+ }, 403);
146
+ }
147
+ try {
148
+ const value = await action(...args);
149
+ // Only commit the nonce on a successful run -- a flaky network
150
+ // retry can otherwise lose the request permanently (Issue 076).
151
+ if (nonceCheck.commit)
152
+ nonceCheck.commit();
153
+ return jsonResponse({ ok: true, value }, 200);
154
+ }
155
+ catch (error) {
156
+ return jsonResponse({
157
+ ok: false,
158
+ error: error instanceof Error ? error.message : String(error),
159
+ }, 500);
160
+ }
161
+ };
162
+ }
163
+ export function toReactFlightRows(response) {
164
+ // Issue 081 note: a native encoder exists in
165
+ // `packages/router-native/src/flight.rs::encode_flight_response`
166
+ // but is intentionally *not* wired here. Microbenchmarking on
167
+ // 2026-05-13 showed the JS-stringify -> napi -> Rust-parse ->
168
+ // Rust-stringify round-trip dominates and produces a 7-9x
169
+ // regression vs. the pure JS path. Re-wiring it requires
170
+ // accepting a `napi::JsObject` directly to avoid the double JSON
171
+ // pass; tracked as a follow-up.
172
+ const rows = [];
173
+ const clientWireIds = new Map();
174
+ const serverWireIds = new Map();
175
+ let nextWireId = 1;
176
+ for (const reference of response.clientReferences) {
177
+ const wireId = nextWireId;
178
+ nextWireId += 1;
179
+ clientWireIds.set(reference.id, wireId);
180
+ rows.push(`${formatReactFlightId(wireId)}:I${JSON.stringify([
181
+ reference.moduleId,
182
+ reference.chunks ?? [],
183
+ reference.exportName,
184
+ ])}`);
185
+ }
186
+ const state = {
187
+ clientWireIds,
188
+ serverWireIds,
189
+ outlineRows: rows,
190
+ nextWireId,
191
+ };
192
+ for (const reference of response.serverReferences) {
193
+ const wireId = state.nextWireId;
194
+ state.nextWireId += 1;
195
+ serverWireIds.set(reference.id, wireId);
196
+ rows.push(`${formatReactFlightId(wireId)}:F${JSON.stringify({
197
+ id: serverActionKey(reference.moduleId, reference.exportName),
198
+ bound: reference.bound === undefined
199
+ ? null
200
+ : reference.bound.map((value) => encodeReactFlightModel(value, state)),
201
+ name: reference.exportName,
202
+ })}`);
203
+ }
204
+ if (isFlightErrorModel(response.root)) {
205
+ rows.push(`0:E${JSON.stringify(encodeReactFlightError(response.root))}`);
206
+ }
207
+ else {
208
+ rows.push(`0:${JSON.stringify(encodeReactFlightModel(response.root, state))}`);
209
+ }
210
+ return rows.join("\n");
211
+ }
212
+ export function fromReactFlightRows(rows) {
213
+ // Issue 081 note: a native decoder exists in
214
+ // `packages/router-native/src/flight.rs::decode_flight_rows`
215
+ // but is intentionally *not* wired here. Benchmarking on
216
+ // 2026-05-13 showed 4-14x regression vs. the pure JS walker —
217
+ // V8's JSON.parse is already extremely optimized and the
218
+ // napi -> serde_json::parse -> walk -> serde_json::serialize ->
219
+ // JS.parse round-trip multiplies the work. Wiring it requires
220
+ // returning a `napi::JsObject` directly (avoiding the double
221
+ // JSON pass); see `docs/benchmarks/2026-05-13-flight-rust-port.md`.
222
+ const lines = rows.split(/\r?\n/).filter(Boolean);
223
+ const metadataLine = lines.find((line) => line.startsWith("M0:"));
224
+ const rootLine = lines.find((line) => line.startsWith("J0:"));
225
+ if (metadataLine !== undefined && rootLine !== undefined) {
226
+ const metadata = JSON.parse(metadataLine.slice(3));
227
+ return {
228
+ version: metadata.version,
229
+ clientReferences: metadata.clientReferences,
230
+ serverReferences: metadata.serverReferences,
231
+ root: JSON.parse(rootLine.slice(3)),
232
+ };
233
+ }
234
+ const clientReferences = [];
235
+ const serverReferences = [];
236
+ const modelChunks = new Map();
237
+ const errorChunks = new Map();
238
+ let root;
239
+ for (const line of lines) {
240
+ const row = parseReactFlightRow(line);
241
+ if (row.tag === "I") {
242
+ clientReferences.push(parseReactFlightClientReference(row.id, row.payload));
243
+ continue;
244
+ }
245
+ if (row.tag === "F") {
246
+ serverReferences.push(parseReactFlightServerReference(row.id, row.payload, modelChunks, errorChunks));
247
+ continue;
248
+ }
249
+ if (row.tag === "E") {
250
+ const error = parseReactFlightError(row.payload);
251
+ errorChunks.set(row.id, error);
252
+ if (row.id === 0) {
253
+ root = error;
254
+ }
255
+ continue;
256
+ }
257
+ if (row.tag === "T") {
258
+ modelChunks.set(row.id, parseReactFlightTextChunk(row.payload));
259
+ continue;
260
+ }
261
+ if (isReactFlightBinaryRowTag(row.tag)) {
262
+ modelChunks.set(row.id, parseReactFlightBinaryChunk(row.tag, row.payload));
263
+ continue;
264
+ }
265
+ if (isReactFlightMetadataTag(row.tag)) {
266
+ continue;
267
+ }
268
+ if (row.tag === undefined && row.payload !== "") {
269
+ modelChunks.set(row.id, JSON.parse(row.payload));
270
+ }
271
+ }
272
+ if (root === undefined && modelChunks.has(0)) {
273
+ root = decodeReactFlightModel(modelChunks.get(0), modelChunks, errorChunks);
274
+ }
275
+ if (root === undefined) {
276
+ throw new Error("Invalid React Flight rows.");
277
+ }
278
+ return {
279
+ version: 1,
280
+ root,
281
+ clientReferences,
282
+ serverReferences,
283
+ };
284
+ }
285
+ export function mergeReactFlightRows(response, rows) {
286
+ // Issue 081 note: same finding as `fromReactFlightRows` — the native
287
+ // merge path is slower than the JS one given the double-JSON tax.
288
+ const modelChunks = new Map();
289
+ const errorChunks = new Map();
290
+ const clientReferences = [...response.clientReferences];
291
+ const serverReferences = [...response.serverReferences];
292
+ for (const line of rows.split(/\r?\n/).filter(Boolean)) {
293
+ const row = parseReactFlightRow(line);
294
+ if (row.tag === "I") {
295
+ clientReferences.push(parseReactFlightClientReference(row.id, row.payload));
296
+ continue;
297
+ }
298
+ if (row.tag === "F") {
299
+ serverReferences.push(parseReactFlightServerReference(row.id, row.payload, modelChunks, errorChunks));
300
+ continue;
301
+ }
302
+ if (row.tag === "E") {
303
+ errorChunks.set(row.id, parseReactFlightError(row.payload));
304
+ continue;
305
+ }
306
+ if (row.tag === "T") {
307
+ modelChunks.set(row.id, parseReactFlightTextChunk(row.payload));
308
+ continue;
309
+ }
310
+ if (isReactFlightBinaryRowTag(row.tag)) {
311
+ modelChunks.set(row.id, parseReactFlightBinaryChunk(row.tag, row.payload));
312
+ continue;
313
+ }
314
+ if (isReactFlightMetadataTag(row.tag)) {
315
+ continue;
316
+ }
317
+ if (row.tag === undefined && row.payload !== "") {
318
+ modelChunks.set(row.id, JSON.parse(row.payload));
319
+ }
320
+ }
321
+ return {
322
+ ...response,
323
+ clientReferences,
324
+ serverReferences,
325
+ root: resolveFlightPromiseChunks(response.root, modelChunks, errorChunks),
326
+ };
327
+ }
328
+ export function createFlightClientManifest(references, resolveChunks) {
329
+ return references.map((reference) => ({
330
+ ...reference,
331
+ chunks: resolveChunks(reference),
332
+ }));
333
+ }
334
+ export function renderFlightPreloadLinks(response, options = {}) {
335
+ const seen = new Set();
336
+ const nonceAttribute = options.nonce === undefined ? "" : ` nonce="${escapeAttribute(options.nonce)}"`;
337
+ return response.clientReferences
338
+ .flatMap((reference) => reference.chunks ?? [])
339
+ .filter((chunk) => {
340
+ if (seen.has(chunk)) {
341
+ return false;
342
+ }
343
+ seen.add(chunk);
344
+ return true;
345
+ })
346
+ .map((chunk) => `<link rel="modulepreload" href="${escapeAttribute(chunk)}"${nonceAttribute}>`)
347
+ .join("");
348
+ }
349
+ function resolveFlightPromiseChunks(model, modelChunks, errorChunks) {
350
+ if (model === null ||
351
+ typeof model === "string" ||
352
+ typeof model === "number" ||
353
+ typeof model === "boolean") {
354
+ return model;
355
+ }
356
+ if (Array.isArray(model)) {
357
+ return model.map((item) => resolveFlightPromiseChunks(item, modelChunks, errorChunks));
358
+ }
359
+ if (model.kind === "promise") {
360
+ const error = errorChunks.get(model.id);
361
+ if (error !== undefined) {
362
+ return error;
363
+ }
364
+ if (modelChunks.has(model.id)) {
365
+ return decodeReactFlightModel(modelChunks.get(model.id), modelChunks, errorChunks);
366
+ }
367
+ return model;
368
+ }
369
+ if (model.kind === "element") {
370
+ return {
371
+ ...model,
372
+ props: Object.fromEntries(Object.entries(model.props).map(([key, value]) => [
373
+ key,
374
+ resolveFlightPromiseChunks(value, modelChunks, errorChunks),
375
+ ])),
376
+ };
377
+ }
378
+ if (model.kind === "map") {
379
+ return {
380
+ ...model,
381
+ entries: model.entries.map(([key, value]) => [
382
+ resolveFlightPromiseChunks(key, modelChunks, errorChunks),
383
+ resolveFlightPromiseChunks(value, modelChunks, errorChunks),
384
+ ]),
385
+ };
386
+ }
387
+ if (model.kind === "set") {
388
+ return {
389
+ ...model,
390
+ values: model.values.map((value) => resolveFlightPromiseChunks(value, modelChunks, errorChunks)),
391
+ };
392
+ }
393
+ if ("kind" in model) {
394
+ return model;
395
+ }
396
+ return Object.fromEntries(Object.entries(model).map(([key, value]) => [
397
+ key,
398
+ value === undefined ? undefined : resolveFlightPromiseChunks(value, modelChunks, errorChunks),
399
+ ]));
400
+ }
401
+ function encodeReactFlightModel(model, state) {
402
+ if (model === null ||
403
+ typeof model === "number" ||
404
+ typeof model === "boolean") {
405
+ return model;
406
+ }
407
+ if (typeof model === "string") {
408
+ return model.startsWith("$") ? `$${model}` : model;
409
+ }
410
+ if (Array.isArray(model)) {
411
+ return model.map((item) => encodeReactFlightModel(item, state));
412
+ }
413
+ if (model.kind === "undefined") {
414
+ return "$u";
415
+ }
416
+ if (model.kind === "date") {
417
+ return `$D${model.value}`;
418
+ }
419
+ if (model.kind === "bigint") {
420
+ return `$n${model.value}`;
421
+ }
422
+ if (model.kind === "number") {
423
+ if (model.value === "Infinity") {
424
+ return "$I";
425
+ }
426
+ if (model.value === "NaN") {
427
+ return "$N";
428
+ }
429
+ return `$${model.value}`;
430
+ }
431
+ if (model.kind === "symbol") {
432
+ return `$S${model.name}`;
433
+ }
434
+ if (model.kind === "map") {
435
+ const id = allocateReactFlightOutlineRow(state, model.entries.map(([key, value]) => [
436
+ encodeReactFlightModel(key, state),
437
+ encodeReactFlightModel(value, state),
438
+ ]));
439
+ return `$Q${formatReactFlightId(id)}`;
440
+ }
441
+ if (model.kind === "set") {
442
+ const id = allocateReactFlightOutlineRow(state, model.values.map((value) => encodeReactFlightModel(value, state)));
443
+ return `$W${formatReactFlightId(id)}`;
444
+ }
445
+ if (model.kind === "form-data") {
446
+ const id = allocateReactFlightOutlineRow(state, model.entries.map(([key, value]) => [key, encodeReactFlightModel(value, state)]));
447
+ return `$K${formatReactFlightId(id)}`;
448
+ }
449
+ if (model.kind === "iterable") {
450
+ const id = allocateReactFlightOutlineRow(state, model.values.map((value) => encodeReactFlightModel(value, state)));
451
+ return `$i${formatReactFlightId(id)}`;
452
+ }
453
+ if (model.kind === "error") {
454
+ const id = state.nextWireId;
455
+ state.nextWireId += 1;
456
+ state.outlineRows.push(`${formatReactFlightId(id)}:E${JSON.stringify(encodeReactFlightError(model))}`);
457
+ return `$Z${formatReactFlightId(id)}`;
458
+ }
459
+ if (model.kind === "promise") {
460
+ return `$@${formatReactFlightId(model.id)}`;
461
+ }
462
+ if (model.kind === "server-reference") {
463
+ return `$F${state.serverWireIds.get(model.id) ?? model.id}`;
464
+ }
465
+ if (model.kind === "client-reference") {
466
+ return `$L${state.clientWireIds.get(model.id) ?? model.id}`;
467
+ }
468
+ if (model.kind === "element") {
469
+ return [
470
+ "$",
471
+ encodeReactFlightElementType(model.type, state.clientWireIds),
472
+ model.key,
473
+ encodeReactFlightProps(model.props, state),
474
+ ];
475
+ }
476
+ if (isReactFlightBinaryModel(model)) {
477
+ return model;
478
+ }
479
+ return encodeReactFlightProps(model, state);
480
+ }
481
+ function allocateReactFlightOutlineRow(state, payload) {
482
+ const id = state.nextWireId;
483
+ state.nextWireId += 1;
484
+ state.outlineRows.push(`${formatReactFlightId(id)}:${JSON.stringify(payload)}`);
485
+ return id;
486
+ }
487
+ function encodeReactFlightElementType(type, clientWireIds) {
488
+ if (typeof type === "string") {
489
+ return type;
490
+ }
491
+ if (type.kind === "fragment") {
492
+ return "$Sreact.fragment";
493
+ }
494
+ return `$L${clientWireIds.get(type.id) ?? type.id}`;
495
+ }
496
+ function encodeReactFlightProps(props, state) {
497
+ return Object.fromEntries(Object.entries(props)
498
+ .filter((entry) => entry[1] !== undefined)
499
+ .map(([key, value]) => [
500
+ key,
501
+ encodeReactFlightModel(value, state),
502
+ ]));
503
+ }
504
+ function parseReactFlightRow(line) {
505
+ const separator = line.indexOf(":");
506
+ if (separator < 0) {
507
+ throw new Error("Invalid React Flight row.");
508
+ }
509
+ const id = separator === 0 ? 0 : parseReactFlightId(line.slice(0, separator));
510
+ const body = line.slice(separator + 1);
511
+ const first = body[0];
512
+ const hasTag = first !== undefined && isReactFlightRowTag(first, body);
513
+ if (first !== undefined && !hasTag && looksLikeUnsupportedReactFlightTag(first, body)) {
514
+ throw new Error(`Unsupported React Flight row tag: ${first}`);
515
+ }
516
+ return {
517
+ id,
518
+ ...(hasTag ? { tag: first } : {}),
519
+ payload: hasTag ? body.slice(1) : body,
520
+ };
521
+ }
522
+ function looksLikeUnsupportedReactFlightTag(tag, body) {
523
+ return /^[A-Z]$/.test(tag) && (body[1] === "{" || body[1] === "[" || body[1] === "\"");
524
+ }
525
+ function parseReactFlightTextChunk(payload) {
526
+ const separator = payload.indexOf(",");
527
+ if (separator < 0) {
528
+ throw new Error("Invalid React Flight text row.");
529
+ }
530
+ return payload.slice(separator + 1);
531
+ }
532
+ function parseReactFlightBinaryChunk(tag, payload) {
533
+ const separator = payload.indexOf(",");
534
+ if (separator < 0) {
535
+ throw new Error("Invalid React Flight binary row.");
536
+ }
537
+ const byteLength = parseReactFlightId(payload.slice(0, separator));
538
+ const bytes = decodeBase64Bytes(payload.slice(separator + 1));
539
+ if (bytes.length !== byteLength) {
540
+ throw new Error("React Flight binary row length did not match declared payload length.");
541
+ }
542
+ return createReactFlightBinaryModel(tag, bytes);
543
+ }
544
+ function isReactFlightMetadataTag(tag) {
545
+ return (tag === "H" ||
546
+ tag === "N" ||
547
+ tag === "P" ||
548
+ tag === "D" ||
549
+ tag === "J" ||
550
+ tag === "W" ||
551
+ tag === "R" ||
552
+ tag === "r" ||
553
+ tag === "X" ||
554
+ tag === "x" ||
555
+ tag === "C");
556
+ }
557
+ function isReactFlightRowTag(tag, body) {
558
+ if (tag === "I" ||
559
+ tag === "F" ||
560
+ tag === "E" ||
561
+ tag === "T" ||
562
+ tag === "H" ||
563
+ tag === "N" ||
564
+ tag === "P" ||
565
+ tag === "D" ||
566
+ tag === "J" ||
567
+ tag === "W" ||
568
+ tag === "R" ||
569
+ tag === "r" ||
570
+ tag === "X" ||
571
+ tag === "x" ||
572
+ tag === "C") {
573
+ return true;
574
+ }
575
+ return isReactFlightBinaryRowTag(tag) && /^[AOoUSsLlGgMmV][0-9a-f]+,/i.test(body);
576
+ }
577
+ function isReactFlightBinaryRowTag(tag) {
578
+ return (tag === "A" ||
579
+ tag === "O" ||
580
+ tag === "o" ||
581
+ tag === "U" ||
582
+ tag === "S" ||
583
+ tag === "s" ||
584
+ tag === "L" ||
585
+ tag === "l" ||
586
+ tag === "G" ||
587
+ tag === "g" ||
588
+ tag === "M" ||
589
+ tag === "m" ||
590
+ tag === "V");
591
+ }
592
+ function createReactFlightBinaryModel(tag, bytes) {
593
+ const byteValues = Array.from(bytes);
594
+ if (tag === "A") {
595
+ return {
596
+ kind: "array-buffer",
597
+ bytes: byteValues,
598
+ };
599
+ }
600
+ if (tag === "V") {
601
+ return {
602
+ kind: "data-view",
603
+ bytes: byteValues,
604
+ };
605
+ }
606
+ return {
607
+ kind: "typed-array",
608
+ arrayType: getReactFlightTypedArrayName(tag),
609
+ bytes: byteValues,
610
+ };
611
+ }
612
+ function getReactFlightTypedArrayName(tag) {
613
+ switch (tag) {
614
+ case "O":
615
+ return "Int8Array";
616
+ case "o":
617
+ return "Uint8Array";
618
+ case "U":
619
+ return "Uint8ClampedArray";
620
+ case "S":
621
+ return "Int16Array";
622
+ case "s":
623
+ return "Uint16Array";
624
+ case "L":
625
+ return "Int32Array";
626
+ case "l":
627
+ return "Uint32Array";
628
+ case "G":
629
+ return "Float32Array";
630
+ case "g":
631
+ return "Float64Array";
632
+ case "M":
633
+ return "BigInt64Array";
634
+ case "m":
635
+ return "BigUint64Array";
636
+ default:
637
+ throw new Error(`Unsupported React Flight typed array row tag: ${tag}`);
638
+ }
639
+ }
640
+ function decodeBase64Bytes(value) {
641
+ // Issue 081: route through the native binding when available. The
642
+ // native implementation accepts both URL-safe and standard alphabets
643
+ // and tolerates missing padding, matching the JS fallback below.
644
+ const native = getNativeFlight()?.decodeFlightBase64;
645
+ if (native !== undefined) {
646
+ const result = native(value);
647
+ // napi-rs returns a Buffer which is a Uint8Array subclass; the
648
+ // callsite types it as Uint8Array so this is structurally fine.
649
+ return result instanceof Uint8Array ? result : new Uint8Array(result);
650
+ }
651
+ const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
652
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
653
+ const binary = globalThis.atob(padded);
654
+ const bytes = new Uint8Array(binary.length);
655
+ for (let index = 0; index < binary.length; index += 1) {
656
+ bytes[index] = binary.charCodeAt(index);
657
+ }
658
+ return bytes;
659
+ }
660
+ function parseReactFlightClientReference(id, payload) {
661
+ const value = JSON.parse(payload);
662
+ if (Array.isArray(value)) {
663
+ return {
664
+ id,
665
+ moduleId: String(value[0]),
666
+ chunks: Array.isArray(value[1]) ? value[1].map(String) : [],
667
+ exportName: String(value[2] ?? "default"),
668
+ };
669
+ }
670
+ const object = valueIsObject(value) ? value : {};
671
+ return {
672
+ id,
673
+ moduleId: String(object.id ?? object.moduleId ?? ""),
674
+ chunks: Array.isArray(object.chunks) ? object.chunks.map(String) : [],
675
+ exportName: String(object.name ?? object.exportName ?? "default"),
676
+ };
677
+ }
678
+ function parseReactFlightServerReference(id, payload, modelChunks = new Map(), errorChunks = new Map()) {
679
+ const value = JSON.parse(payload);
680
+ const object = valueIsObject(value) ? value : {};
681
+ const actionId = String(object.id ?? "");
682
+ const separator = actionId.lastIndexOf("#");
683
+ const bound = Array.isArray(object.bound)
684
+ ? object.bound.map((entry) => decodeReactFlightModel(entry, modelChunks, errorChunks))
685
+ : undefined;
686
+ return {
687
+ id,
688
+ moduleId: separator < 0 ? actionId : actionId.slice(0, separator),
689
+ exportName: typeof object.name === "string"
690
+ ? object.name
691
+ : separator < 0
692
+ ? "default"
693
+ : actionId.slice(separator + 1),
694
+ ...(bound === undefined ? {} : { bound }),
695
+ };
696
+ }
697
+ // Issue 079: hard cap on Flight tree depth to prevent stack-exhaustion
698
+ // DoS from a deeply-nested payload. The cap is far higher than any
699
+ // legitimate component tree.
700
+ const MAX_FLIGHT_DECODE_DEPTH = 256;
701
+ class FlightDecodeError extends Error {
702
+ constructor(message) {
703
+ super(message);
704
+ this.name = "FlightDecodeError";
705
+ }
706
+ }
707
+ function flightTooDeep() {
708
+ throw new FlightDecodeError(`MR_FLIGHT_TOO_DEEP: nested deeper than ${MAX_FLIGHT_DECODE_DEPTH} levels`);
709
+ }
710
+ function decodeReactFlightModel(value, modelChunks = new Map(), errorChunks = new Map(), depth = 0) {
711
+ if (depth > MAX_FLIGHT_DECODE_DEPTH)
712
+ flightTooDeep();
713
+ if (value === null ||
714
+ typeof value === "number" ||
715
+ typeof value === "boolean") {
716
+ return value;
717
+ }
718
+ if (typeof value === "string") {
719
+ return decodeReactFlightString(value, modelChunks, errorChunks);
720
+ }
721
+ if (Array.isArray(value)) {
722
+ if (value[0] === "$") {
723
+ return {
724
+ kind: "element",
725
+ type: decodeReactFlightElementType(value[1]),
726
+ key: typeof value[2] === "string" ? value[2] : null,
727
+ props: decodeReactFlightProps(valueIsObject(value[3]) ? value[3] : {}, modelChunks, errorChunks, depth + 1),
728
+ };
729
+ }
730
+ return value.map((item) => decodeReactFlightModel(item, modelChunks, errorChunks, depth + 1));
731
+ }
732
+ if (isReactFlightBinaryModel(value)) {
733
+ return value;
734
+ }
735
+ if (valueIsObject(value)) {
736
+ return decodeReactFlightProps(value, modelChunks, errorChunks, depth + 1);
737
+ }
738
+ return { kind: "undefined" };
739
+ }
740
+ function decodeReactFlightString(value, modelChunks, errorChunks) {
741
+ if (value === "$undefined" || value === "$u") {
742
+ return { kind: "undefined" };
743
+ }
744
+ if (value.startsWith("$$")) {
745
+ return value.slice(1);
746
+ }
747
+ if (value === "$I") {
748
+ return { kind: "number", value: "Infinity" };
749
+ }
750
+ if (value === "$-Infinity") {
751
+ return { kind: "number", value: "-Infinity" };
752
+ }
753
+ if (value === "$-0") {
754
+ return { kind: "number", value: "-0" };
755
+ }
756
+ if (value === "$N") {
757
+ return { kind: "number", value: "NaN" };
758
+ }
759
+ if (value.startsWith("$D")) {
760
+ return { kind: "date", value: value.slice(2) };
761
+ }
762
+ if (value.startsWith("$n")) {
763
+ return { kind: "bigint", value: value.slice(2) };
764
+ }
765
+ if (/^\$[AOoUSsLlGgMmV][0-9a-f]+$/.test(value)) {
766
+ return decodeReactFlightChunk(value.slice(2), modelChunks, errorChunks);
767
+ }
768
+ if (value.startsWith("$S")) {
769
+ return { kind: "symbol", name: value.slice(2) };
770
+ }
771
+ if (/^\$F[0-9a-f]+$/i.test(value)) {
772
+ return {
773
+ kind: "server-reference",
774
+ id: parseReactFlightId(value.slice(2)),
775
+ };
776
+ }
777
+ if (/^\$L[0-9a-f]+$/i.test(value)) {
778
+ return {
779
+ kind: "client-reference",
780
+ id: parseReactFlightId(value.slice(2)),
781
+ };
782
+ }
783
+ if (/^\$@[0-9a-f]*$/i.test(value)) {
784
+ return {
785
+ kind: "promise",
786
+ id: value.length === 2 ? 0 : parseReactFlightId(value.slice(2)),
787
+ };
788
+ }
789
+ if (/^\$Q[0-9a-f]+$/i.test(value)) {
790
+ const decoded = decodeReactFlightChunk(value.slice(2), modelChunks, errorChunks);
791
+ const entries = Array.isArray(decoded)
792
+ ? decoded.map((entry) => Array.isArray(entry) ? [entry[0] ?? { kind: "undefined" }, entry[1] ?? { kind: "undefined" }] : [entry, { kind: "undefined" }])
793
+ : [];
794
+ return {
795
+ kind: "map",
796
+ entries,
797
+ };
798
+ }
799
+ if (/^\$W[0-9a-f]+$/i.test(value)) {
800
+ const decoded = decodeReactFlightChunk(value.slice(2), modelChunks, errorChunks);
801
+ return {
802
+ kind: "set",
803
+ values: Array.isArray(decoded) ? decoded : [],
804
+ };
805
+ }
806
+ if (/^\$K[0-9a-f]+$/i.test(value)) {
807
+ const decoded = decodeReactFlightChunk(value.slice(2), modelChunks, errorChunks);
808
+ const entries = Array.isArray(decoded)
809
+ ? decoded.flatMap((entry) => Array.isArray(entry) && typeof entry[0] === "string"
810
+ ? [[entry[0], entry[1] ?? { kind: "undefined" }]]
811
+ : [])
812
+ : [];
813
+ return {
814
+ kind: "form-data",
815
+ entries,
816
+ };
817
+ }
818
+ if (/^\$i[0-9a-f]+$/i.test(value)) {
819
+ const decoded = decodeReactFlightChunk(value.slice(2), modelChunks, errorChunks);
820
+ return {
821
+ kind: "iterable",
822
+ values: Array.isArray(decoded) ? decoded : [],
823
+ };
824
+ }
825
+ if (/^\$Z[0-9a-f]+$/i.test(value)) {
826
+ return errorChunks.get(parseReactFlightId(value.slice(2))) ?? {
827
+ kind: "error",
828
+ name: "Error",
829
+ message: "Unknown React Flight error.",
830
+ };
831
+ }
832
+ if (value === "$Y" || value.startsWith("$E")) {
833
+ return { kind: "undefined" };
834
+ }
835
+ if (/^\$[0-9a-f]+$/i.test(value)) {
836
+ return decodeReactFlightChunk(value.slice(1), modelChunks, errorChunks);
837
+ }
838
+ return value;
839
+ }
840
+ function decodeReactFlightChunk(id, modelChunks, errorChunks) {
841
+ const numericId = parseReactFlightId(id);
842
+ const error = errorChunks.get(numericId);
843
+ if (error !== undefined) {
844
+ return error;
845
+ }
846
+ if (!modelChunks.has(numericId)) {
847
+ return {
848
+ kind: "promise",
849
+ id: numericId,
850
+ };
851
+ }
852
+ return decodeReactFlightModel(modelChunks.get(numericId), modelChunks, errorChunks);
853
+ }
854
+ function decodeReactFlightElementType(value) {
855
+ if (value === "$Sreact.fragment") {
856
+ return { kind: "fragment" };
857
+ }
858
+ if (typeof value === "string" && /^\$L[0-9a-f]+$/i.test(value)) {
859
+ return {
860
+ kind: "client-reference",
861
+ id: parseReactFlightId(value.slice(2)),
862
+ };
863
+ }
864
+ return typeof value === "string" ? value : String(value);
865
+ }
866
+ function decodeReactFlightProps(value, modelChunks, errorChunks, depth = 0) {
867
+ if (depth > MAX_FLIGHT_DECODE_DEPTH)
868
+ flightTooDeep();
869
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [
870
+ key,
871
+ decodeReactFlightModel(child, modelChunks, errorChunks, depth + 1),
872
+ ]));
873
+ }
874
+ function isReactFlightBinaryModel(value) {
875
+ return (valueIsObject(value) &&
876
+ (value.kind === "array-buffer" || value.kind === "typed-array" || value.kind === "data-view"));
877
+ }
878
+ function parseReactFlightError(payload) {
879
+ const value = JSON.parse(payload);
880
+ const object = valueIsObject(value) ? value : {};
881
+ const digest = typeof object.digest === "string" ? object.digest : undefined;
882
+ return {
883
+ kind: "error",
884
+ name: typeof object.name === "string" ? object.name : "Error",
885
+ message: typeof object.message === "string" ? object.message : "React Flight error.",
886
+ ...(digest === undefined ? {} : { digest }),
887
+ };
888
+ }
889
+ function encodeReactFlightError(model) {
890
+ return {
891
+ digest: model.digest ?? "",
892
+ name: model.name,
893
+ message: model.message,
894
+ stack: [],
895
+ env: "Server",
896
+ };
897
+ }
898
+ function isFlightErrorModel(model) {
899
+ return (typeof model === "object" &&
900
+ model !== null &&
901
+ !Array.isArray(model) &&
902
+ model.kind === "error");
903
+ }
904
+ function valueIsObject(value) {
905
+ return typeof value === "object" && value !== null && !Array.isArray(value);
906
+ }
907
+ function formatReactFlightId(id) {
908
+ return id.toString(16);
909
+ }
910
+ function parseReactFlightId(value) {
911
+ return Number.parseInt(value, 16);
912
+ }
913
+ async function serializeFlightValue(value, state) {
914
+ const awaited = await value;
915
+ if (awaited === null) {
916
+ return null;
917
+ }
918
+ if (awaited === undefined) {
919
+ return { kind: "undefined" };
920
+ }
921
+ if (typeof awaited === "string" ||
922
+ typeof awaited === "boolean") {
923
+ return awaited;
924
+ }
925
+ if (typeof awaited === "number") {
926
+ if (Number.isNaN(awaited)) {
927
+ return { kind: "number", value: "NaN" };
928
+ }
929
+ if (awaited === Infinity) {
930
+ return { kind: "number", value: "Infinity" };
931
+ }
932
+ if (awaited === -Infinity) {
933
+ return { kind: "number", value: "-Infinity" };
934
+ }
935
+ if (Object.is(awaited, -0)) {
936
+ return { kind: "number", value: "-0" };
937
+ }
938
+ return awaited;
939
+ }
940
+ if (typeof awaited === "bigint") {
941
+ return { kind: "bigint", value: awaited.toString() };
942
+ }
943
+ if (typeof awaited === "symbol") {
944
+ return { kind: "symbol", name: Symbol.keyFor(awaited) ?? awaited.description ?? "" };
945
+ }
946
+ if (Array.isArray(awaited)) {
947
+ return await Promise.all(awaited.map((item) => serializeFlightValue(item, state)));
948
+ }
949
+ if (isServerReference(awaited)) {
950
+ return {
951
+ kind: "server-reference",
952
+ id: await getServerReferenceId(awaited, state),
953
+ };
954
+ }
955
+ if (isReactCompatElement(awaited)) {
956
+ return await serializeElement(awaited, state);
957
+ }
958
+ if (awaited instanceof Date) {
959
+ return { kind: "date", value: awaited.toJSON() };
960
+ }
961
+ if (awaited instanceof Map) {
962
+ return {
963
+ kind: "map",
964
+ entries: await Promise.all(Array.from(awaited.entries()).map(async ([key, value]) => [
965
+ await serializeFlightValue(key, state),
966
+ await serializeFlightValue(value, state),
967
+ ])),
968
+ };
969
+ }
970
+ if (awaited instanceof Set) {
971
+ return {
972
+ kind: "set",
973
+ values: await Promise.all(Array.from(awaited.values()).map((value) => serializeFlightValue(value, state))),
974
+ };
975
+ }
976
+ if (isFormDataLike(awaited)) {
977
+ return {
978
+ kind: "form-data",
979
+ entries: await Promise.all(Array.from(awaited.entries()).map(async ([key, value]) => [
980
+ key,
981
+ await serializeFlightValue(value, state),
982
+ ])),
983
+ };
984
+ }
985
+ if (isIterableObject(awaited)) {
986
+ return {
987
+ kind: "iterable",
988
+ values: await Promise.all(Array.from(awaited).map((value) => serializeFlightValue(value, state))),
989
+ };
990
+ }
991
+ if (awaited instanceof Error) {
992
+ return {
993
+ kind: "error",
994
+ name: awaited.name,
995
+ message: awaited.message,
996
+ };
997
+ }
998
+ if (typeof awaited === "object") {
999
+ return await serializeObject(awaited, state);
1000
+ }
1001
+ throw new TypeError(`Unsupported Flight value: ${typeof awaited}`);
1002
+ }
1003
+ async function serializeElement(element, state) {
1004
+ if (typeof element.type === "function") {
1005
+ return await serializeFlightValue(element.type(element.props), state);
1006
+ }
1007
+ if (isClientReference(element.type)) {
1008
+ return {
1009
+ kind: "element",
1010
+ type: {
1011
+ kind: "client-reference",
1012
+ id: getClientReferenceId(element.type, state),
1013
+ },
1014
+ key: element.key,
1015
+ props: await serializeProps(element.props, state),
1016
+ };
1017
+ }
1018
+ if (typeof element.type === "string") {
1019
+ return {
1020
+ kind: "element",
1021
+ type: element.type,
1022
+ key: element.key,
1023
+ props: await serializeProps(element.props, state),
1024
+ };
1025
+ }
1026
+ if (element.type === REACT_COMPAT_FRAGMENT_TYPE) {
1027
+ return {
1028
+ kind: "element",
1029
+ type: { kind: "fragment" },
1030
+ key: element.key,
1031
+ props: await serializeProps(element.props, state),
1032
+ };
1033
+ }
1034
+ throw new TypeError("Unsupported Flight element type.");
1035
+ }
1036
+ async function serializeProps(props, state) {
1037
+ const entries = await Promise.all(Object.entries(props).map(async ([key, value]) => [
1038
+ key,
1039
+ await serializeFlightValue(value, state),
1040
+ ]));
1041
+ return Object.fromEntries(entries);
1042
+ }
1043
+ async function serializeObject(object, state) {
1044
+ return Object.fromEntries(await Promise.all(Object.entries(object).map(async ([key, value]) => [
1045
+ key,
1046
+ await serializeFlightValue(value, state),
1047
+ ])));
1048
+ }
1049
+ function getClientReferenceId(reference, state) {
1050
+ const key = `${reference.moduleId}:${reference.exportName}`;
1051
+ const existing = state.clientReferenceIndexes.get(key);
1052
+ if (existing !== undefined) {
1053
+ return existing;
1054
+ }
1055
+ const id = state.clientReferences.length;
1056
+ state.clientReferences.push({
1057
+ id,
1058
+ moduleId: reference.moduleId,
1059
+ exportName: reference.exportName,
1060
+ ...(reference.chunks === undefined ? {} : { chunks: reference.chunks }),
1061
+ });
1062
+ state.clientReferenceIndexes.set(key, id);
1063
+ return id;
1064
+ }
1065
+ async function getServerReferenceId(reference, state) {
1066
+ const serializedBound = reference.bound === undefined
1067
+ ? undefined
1068
+ : await Promise.all(reference.bound.map((value) => serializeFlightValue(value, state)));
1069
+ const key = `${reference.moduleId}:${reference.exportName}:${JSON.stringify(serializedBound ?? null)}`;
1070
+ const existing = state.serverReferenceIndexes.get(key);
1071
+ if (existing !== undefined) {
1072
+ return existing;
1073
+ }
1074
+ const id = state.serverReferences.length;
1075
+ state.serverReferences.push({
1076
+ id,
1077
+ moduleId: reference.moduleId,
1078
+ exportName: reference.exportName,
1079
+ ...(serializedBound === undefined ? {} : { bound: serializedBound }),
1080
+ });
1081
+ state.serverReferenceIndexes.set(key, id);
1082
+ return id;
1083
+ }
1084
+ function isReactCompatElement(value) {
1085
+ return (typeof value === "object" &&
1086
+ value !== null &&
1087
+ value.$$typeof === REACT_COMPAT_ELEMENT_TYPE);
1088
+ }
1089
+ function isFormDataLike(value) {
1090
+ return typeof FormData !== "undefined" && value instanceof FormData;
1091
+ }
1092
+ function isIterableObject(value) {
1093
+ return typeof value === "object" && value !== null && Symbol.iterator in value;
1094
+ }
1095
+ function serverActionKey(moduleId, exportName) {
1096
+ return `${moduleId}#${exportName}`;
1097
+ }
1098
+ async function readServerActionPayload(request, maxBodyBytes) {
1099
+ // Issue 076: bound the body before JSON.parse. The Content-Length
1100
+ // header (when present) is the cheap pre-check; the streaming reader
1101
+ // catches chunked / missing-length bodies too.
1102
+ const declaredLength = Number(request.headers.get("content-length") ?? "NaN");
1103
+ if (Number.isFinite(declaredLength) && declaredLength > maxBodyBytes) {
1104
+ return jsonResponse({ ok: false, error: "Payload too large." }, 413);
1105
+ }
1106
+ const body = request.body;
1107
+ let text = "";
1108
+ if (body !== null) {
1109
+ const reader = body.getReader();
1110
+ const decoder = new TextDecoder();
1111
+ let total = 0;
1112
+ try {
1113
+ while (true) {
1114
+ const { done, value } = await reader.read();
1115
+ if (done)
1116
+ break;
1117
+ total += value.byteLength;
1118
+ if (total > maxBodyBytes) {
1119
+ await reader.cancel();
1120
+ return jsonResponse({ ok: false, error: "Payload too large." }, 413);
1121
+ }
1122
+ text += decoder.decode(value, { stream: true });
1123
+ }
1124
+ text += decoder.decode();
1125
+ }
1126
+ finally {
1127
+ reader.releaseLock?.();
1128
+ }
1129
+ }
1130
+ else {
1131
+ text = await request.text();
1132
+ }
1133
+ try {
1134
+ return JSON.parse(text);
1135
+ }
1136
+ catch {
1137
+ return jsonResponse({ ok: false, error: "Invalid JSON payload." }, 400);
1138
+ }
1139
+ }
1140
+ function getServerAction(entry) {
1141
+ return typeof entry === "function" ? entry : entry.action;
1142
+ }
1143
+ function getServerActionArgsValidator(entry) {
1144
+ return typeof entry === "function" ? undefined : entry.validateArgs;
1145
+ }
1146
+ function runWithFlightCacheScope(callback) {
1147
+ const previousScope = getGlobalCacheScope();
1148
+ setGlobalCacheScope({
1149
+ functionCaches: new WeakMap(),
1150
+ controller: new AbortController(),
1151
+ ownerStack: ["renderToFlightResponse"],
1152
+ });
1153
+ try {
1154
+ const result = callback();
1155
+ if (isPromiseLike(result)) {
1156
+ return Promise.resolve(result).finally(() => {
1157
+ setGlobalCacheScope(previousScope);
1158
+ });
1159
+ }
1160
+ setGlobalCacheScope(previousScope);
1161
+ return result;
1162
+ }
1163
+ catch (error) {
1164
+ setGlobalCacheScope(previousScope);
1165
+ throw error;
1166
+ }
1167
+ }
1168
+ function isPromiseLike(value) {
1169
+ return ((typeof value === "object" || typeof value === "function") &&
1170
+ value !== null &&
1171
+ typeof value.then === "function");
1172
+ }
1173
+ function getGlobalCacheScope() {
1174
+ return globalThis[CACHE_SCOPE_SYMBOL];
1175
+ }
1176
+ function setGlobalCacheScope(scope) {
1177
+ if (scope === undefined) {
1178
+ delete globalThis[CACHE_SCOPE_SYMBOL];
1179
+ return;
1180
+ }
1181
+ globalThis[CACHE_SCOPE_SYMBOL] = scope;
1182
+ }
1183
+ function validateRequestOrigin(request, allowedOrigins) {
1184
+ // Issue 076: secure default. `"any"` disables the check (explicit
1185
+ // opt-out for embedders behind their own auth boundary). Otherwise we
1186
+ // enforce same-origin by default and extend with the array if given.
1187
+ if (allowedOrigins === "any")
1188
+ return undefined;
1189
+ const origin = request.headers.get("origin");
1190
+ // Browsers omit `Origin` on same-origin GETs but include it on
1191
+ // cross-site POSTs. Missing Origin therefore signals same-origin
1192
+ // (or non-browser traffic) and is allowed.
1193
+ if (origin === null)
1194
+ return undefined;
1195
+ let expected;
1196
+ try {
1197
+ expected = new URL(request.url).origin;
1198
+ }
1199
+ catch {
1200
+ expected = undefined;
1201
+ }
1202
+ if (expected !== undefined && origin === expected)
1203
+ return undefined;
1204
+ if (allowedOrigins !== undefined && allowedOrigins.includes(origin)) {
1205
+ return undefined;
1206
+ }
1207
+ return jsonResponse({ ok: false, error: "Origin not allowed." }, 403);
1208
+ }
1209
+ function validateCsrfToken(request, csrf) {
1210
+ // Issue 076: CSRF check is on by default. `false` disables it
1211
+ // (documented opt-out); `true` / object override the names.
1212
+ if (csrf === false)
1213
+ return undefined;
1214
+ const config = typeof csrf === "object" ? csrf : {};
1215
+ const headerName = config.headerName ?? "x-mreact-csrf";
1216
+ const cookieName = config.cookieName ?? "mreact.csrf";
1217
+ const headerToken = request.headers.get(headerName);
1218
+ const cookieToken = readCookie(request.headers.get("cookie"), cookieName);
1219
+ return headerToken !== null && cookieToken !== undefined && headerToken === cookieToken
1220
+ ? undefined
1221
+ : jsonResponse({ ok: false, error: "Invalid CSRF token." }, 403);
1222
+ }
1223
+ function validateServerActionNonce(request, replayProtection) {
1224
+ if (replayProtection === undefined) {
1225
+ return {};
1226
+ }
1227
+ const headerName = replayProtection.headerName ?? "x-mreact-action-nonce";
1228
+ const nonce = request.headers.get(headerName);
1229
+ if (nonce === null || nonce.length === 0) {
1230
+ return {
1231
+ response: jsonResponse({ ok: false, error: "Missing server action nonce." }, 400),
1232
+ };
1233
+ }
1234
+ if (replayProtection.seen.has(nonce)) {
1235
+ return {
1236
+ response: jsonResponse({ ok: false, error: "Server action nonce was already used." }, 409),
1237
+ };
1238
+ }
1239
+ // Issue 076: defer the .add() until the action succeeds so a failed
1240
+ // run does not consume a replay slot. The caller invokes `commit()`
1241
+ // on the success path only.
1242
+ return {
1243
+ commit: () => replayProtection.seen.add(nonce),
1244
+ };
1245
+ }
1246
+ function readCookie(cookieHeader, name) {
1247
+ if (cookieHeader === null) {
1248
+ return undefined;
1249
+ }
1250
+ for (const part of cookieHeader.split(";")) {
1251
+ const [rawKey, ...rawValue] = part.trim().split("=");
1252
+ if (rawKey === name) {
1253
+ // Issue 076 / 072: malformed `%`-escapes raise URIError; treat
1254
+ // the cookie as absent so a bogus cookie cannot abort the handler.
1255
+ try {
1256
+ return decodeURIComponent(rawValue.join("="));
1257
+ }
1258
+ catch {
1259
+ return undefined;
1260
+ }
1261
+ }
1262
+ }
1263
+ return undefined;
1264
+ }
1265
+ function jsonResponse(value, status) {
1266
+ return new Response(JSON.stringify(value), {
1267
+ status,
1268
+ headers: { "content-type": "application/json" },
1269
+ });
1270
+ }
1271
+ function serializeJsonForHtml(value) {
1272
+ return JSON.stringify(value)
1273
+ .replaceAll("<", "\\u003c")
1274
+ .replaceAll("\u2028", "\\u2028")
1275
+ .replaceAll("\u2029", "\\u2029");
1276
+ }
1277
+ function escapeAttribute(value) {
1278
+ return value
1279
+ .replaceAll("&", "&amp;")
1280
+ .replaceAll('"', "&quot;")
1281
+ .replaceAll("<", "&lt;")
1282
+ .replaceAll(">", "&gt;");
1283
+ }
1284
+ //# sourceMappingURL=flight.js.map