@reckona/mreact-server 0.0.65 → 0.0.67

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/src/flight.ts ADDED
@@ -0,0 +1,2057 @@
1
+ import { getNativeFlight } from "./native-flight.js";
2
+
3
+ export const CLIENT_REFERENCE_TYPE = Symbol.for("modular.react.client_reference");
4
+ export const SERVER_REFERENCE_TYPE = Symbol.for("modular.react.server_reference");
5
+ const CACHE_SCOPE_SYMBOL = Symbol.for("modular.react.cache_scope");
6
+
7
+ const REACT_COMPAT_ELEMENT_TYPE = Symbol.for("modular.react.element");
8
+ const REACT_COMPAT_FRAGMENT_TYPE = Symbol.for("modular.react.fragment");
9
+
10
+ export interface FlightClientReference {
11
+ id: number;
12
+ moduleId: string;
13
+ exportName: string;
14
+ chunks?: string[];
15
+ }
16
+
17
+ export interface FlightClientReferenceInput {
18
+ name: string;
19
+ moduleId: string;
20
+ exportName: string;
21
+ }
22
+
23
+ export interface FlightClientManifestEntry extends FlightClientReferenceInput {
24
+ chunks: string[];
25
+ }
26
+
27
+ export type ServerAction = (...args: unknown[]) => unknown | Promise<unknown>;
28
+
29
+ export type ServerActionValidationResult = boolean | string;
30
+
31
+ export interface ServerActionDescriptor {
32
+ action: ServerAction;
33
+ validateArgs?: (args: unknown[]) => ServerActionValidationResult;
34
+ }
35
+
36
+ export type ServerActionRegistry = Record<string, ServerAction | ServerActionDescriptor>;
37
+
38
+ export interface ServerActionReplayStore {
39
+ has(value: string): boolean;
40
+ add(value: string): void;
41
+ }
42
+
43
+ export interface ServerActionRequestReference {
44
+ moduleId: string;
45
+ exportName: string;
46
+ }
47
+
48
+ export interface ServerActionHandlerOptions {
49
+ // Issue 076: secure defaults. When undefined, the handler enforces the
50
+ // same-origin policy by comparing `Origin` to the request URL. Pass an
51
+ // explicit array to extend the trust set, or `"any"` to disable the
52
+ // check entirely (documented opt-out).
53
+ allowedOrigins?: readonly string[] | "any";
54
+ authorize?: (
55
+ request: Request,
56
+ reference: ServerActionRequestReference,
57
+ args: unknown[],
58
+ ) => ServerActionValidationResult | Promise<ServerActionValidationResult>;
59
+ allowedActions?: readonly ServerActionRequestReference[];
60
+ // Issue 076: CSRF is enabled by default. Pass `false` to disable
61
+ // (documented opt-out for embedders that have their own scheme).
62
+ csrf?:
63
+ | boolean
64
+ | {
65
+ cookieName?: string;
66
+ headerName?: string;
67
+ };
68
+ replayProtection?: {
69
+ headerName?: string;
70
+ seen: ServerActionReplayStore;
71
+ };
72
+ // Issue 076: bound by default so a hostile client cannot drive RSS via
73
+ // an unbounded request body. The default is 1 MiB; pass a larger value
74
+ // to allow bigger payloads.
75
+ maxBodyBytes?: number;
76
+ }
77
+
78
+ const DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
79
+
80
+ export interface FlightScriptOptions {
81
+ id?: string;
82
+ nonce?: string;
83
+ }
84
+
85
+ export interface FlightServerReference {
86
+ id: number;
87
+ moduleId: string;
88
+ exportName: string;
89
+ bound?: FlightModel[];
90
+ }
91
+
92
+ export interface ClientReference {
93
+ $$typeof: typeof CLIENT_REFERENCE_TYPE;
94
+ moduleId: string;
95
+ exportName: string;
96
+ chunks?: string[];
97
+ }
98
+
99
+ export interface ServerReference {
100
+ $$typeof: typeof SERVER_REFERENCE_TYPE;
101
+ moduleId: string;
102
+ exportName: string;
103
+ bound?: unknown[];
104
+ }
105
+
106
+ export interface FlightResponse {
107
+ version: 1;
108
+ root: FlightModel;
109
+ clientReferences: FlightClientReference[];
110
+ serverReferences: FlightServerReference[];
111
+ }
112
+
113
+ export type FlightModel =
114
+ | null
115
+ | string
116
+ | number
117
+ | boolean
118
+ | FlightModel[]
119
+ | FlightObjectModel
120
+ | FlightElementModel
121
+ | FlightClientReferenceModel
122
+ | FlightServerReferenceModel
123
+ | FlightDateModel
124
+ | FlightBigIntModel
125
+ | FlightNumberModel
126
+ | FlightSymbolModel
127
+ | FlightMapModel
128
+ | FlightSetModel
129
+ | FlightFormDataModel
130
+ | FlightIterableModel
131
+ | FlightErrorModel
132
+ | FlightPromiseModel
133
+ | FlightArrayBufferModel
134
+ | FlightTypedArrayModel
135
+ | FlightDataViewModel
136
+ | { kind: "undefined" };
137
+
138
+ export interface FlightObjectModel {
139
+ kind?: never;
140
+ [key: string]: FlightModel | undefined;
141
+ }
142
+
143
+ export interface FlightElementModel {
144
+ kind: "element";
145
+ type: string | FlightClientReferenceModel | { kind: "fragment" };
146
+ key: string | null;
147
+ props: Record<string, FlightModel>;
148
+ }
149
+
150
+ export interface FlightClientReferenceModel {
151
+ kind: "client-reference";
152
+ id: number;
153
+ }
154
+
155
+ export interface FlightServerReferenceModel {
156
+ kind: "server-reference";
157
+ id: number;
158
+ }
159
+
160
+ export interface FlightDateModel {
161
+ kind: "date";
162
+ value: string;
163
+ }
164
+
165
+ export interface FlightBigIntModel {
166
+ kind: "bigint";
167
+ value: string;
168
+ }
169
+
170
+ export interface FlightNumberModel {
171
+ kind: "number";
172
+ value: "Infinity" | "-Infinity" | "NaN" | "-0";
173
+ }
174
+
175
+ export interface FlightSymbolModel {
176
+ kind: "symbol";
177
+ name: string;
178
+ }
179
+
180
+ export interface FlightMapModel {
181
+ kind: "map";
182
+ entries: [FlightModel, FlightModel][];
183
+ }
184
+
185
+ export interface FlightSetModel {
186
+ kind: "set";
187
+ values: FlightModel[];
188
+ }
189
+
190
+ export interface FlightFormDataModel {
191
+ kind: "form-data";
192
+ entries: [string, FlightModel][];
193
+ }
194
+
195
+ export interface FlightIterableModel {
196
+ kind: "iterable";
197
+ values: FlightModel[];
198
+ }
199
+
200
+ export interface FlightErrorModel {
201
+ kind: "error";
202
+ name: string;
203
+ message: string;
204
+ digest?: string;
205
+ }
206
+
207
+ export interface FlightPromiseModel {
208
+ kind: "promise";
209
+ id: number;
210
+ }
211
+
212
+ export interface FlightArrayBufferModel {
213
+ kind: "array-buffer";
214
+ bytes: number[];
215
+ }
216
+
217
+ export interface FlightTypedArrayModel {
218
+ kind: "typed-array";
219
+ arrayType: FlightTypedArrayName;
220
+ bytes: number[];
221
+ }
222
+
223
+ export interface FlightDataViewModel {
224
+ kind: "data-view";
225
+ bytes: number[];
226
+ }
227
+
228
+ export type FlightTypedArrayName =
229
+ | "Int8Array"
230
+ | "Uint8Array"
231
+ | "Uint8ClampedArray"
232
+ | "Int16Array"
233
+ | "Uint16Array"
234
+ | "Int32Array"
235
+ | "Uint32Array"
236
+ | "Float32Array"
237
+ | "Float64Array"
238
+ | "BigInt64Array"
239
+ | "BigUint64Array";
240
+
241
+ const reactFlightBinaryRowTags = ["A", "O", "o", "U", "S", "s", "L", "l", "G", "g", "M", "m", "V"] as const;
242
+ const reactFlightRowTags = ["C", "D", "E", "F", "H", "I", "J", "N", "P", "R", "T", "W", "X", "x", "r"] as const;
243
+ const reactFlightModelTokens = [
244
+ "$",
245
+ "$$",
246
+ "$@",
247
+ "$D",
248
+ "$E",
249
+ "$F",
250
+ "$I",
251
+ "$K",
252
+ "$L",
253
+ "$N",
254
+ "$Q",
255
+ "$S",
256
+ "$W",
257
+ "$Y",
258
+ "$Z",
259
+ "$i",
260
+ "$n",
261
+ "$u",
262
+ "$undefined",
263
+ ] as const;
264
+
265
+ export interface ReactFlightProtocolCoverage {
266
+ binaryRowTags: string[];
267
+ modelTokens: string[];
268
+ rowTags: string[];
269
+ }
270
+
271
+ export function getReactFlightProtocolCoverage(): ReactFlightProtocolCoverage {
272
+ return {
273
+ binaryRowTags: [...reactFlightBinaryRowTags],
274
+ modelTokens: [...reactFlightModelTokens],
275
+ rowTags: [...reactFlightRowTags],
276
+ };
277
+ }
278
+
279
+ interface ReactCompatElementLike {
280
+ $$typeof: symbol;
281
+ type: unknown;
282
+ key: string | null;
283
+ props: Record<string, unknown>;
284
+ }
285
+
286
+ interface FlightSerializationState {
287
+ clientReferences: FlightClientReference[];
288
+ clientReferenceIndexes: Map<string, number>;
289
+ serverReferences: FlightServerReference[];
290
+ serverReferenceIndexes: Map<string, number>;
291
+ }
292
+
293
+ interface ServerCacheScope {
294
+ functionCaches: WeakMap<(...args: never[]) => unknown, unknown>;
295
+ controller: AbortController;
296
+ ownerStack: string[];
297
+ }
298
+
299
+ export function createClientReference(
300
+ moduleId: string,
301
+ exportName = "default",
302
+ chunks?: string[],
303
+ ): ClientReference {
304
+ return {
305
+ $$typeof: CLIENT_REFERENCE_TYPE,
306
+ moduleId,
307
+ exportName,
308
+ ...(chunks === undefined ? {} : { chunks }),
309
+ };
310
+ }
311
+
312
+ export function createServerReference(
313
+ moduleId: string,
314
+ exportName = "default",
315
+ bound?: unknown[],
316
+ ): ServerReference {
317
+ return {
318
+ $$typeof: SERVER_REFERENCE_TYPE,
319
+ moduleId,
320
+ exportName,
321
+ ...(bound === undefined ? {} : { bound }),
322
+ };
323
+ }
324
+
325
+ export function isClientReference(value: unknown): value is ClientReference {
326
+ return (
327
+ typeof value === "object" &&
328
+ value !== null &&
329
+ (value as { $$typeof?: unknown }).$$typeof === CLIENT_REFERENCE_TYPE
330
+ );
331
+ }
332
+
333
+ export function isServerReference(value: unknown): value is ServerReference {
334
+ return (
335
+ typeof value === "object" &&
336
+ value !== null &&
337
+ (value as { $$typeof?: unknown }).$$typeof === SERVER_REFERENCE_TYPE
338
+ );
339
+ }
340
+
341
+ export async function renderToFlightResponse<P extends Record<string, unknown>>(
342
+ renderable: ((props: P) => unknown) | unknown,
343
+ props = {} as P,
344
+ ): Promise<FlightResponse> {
345
+ return runWithFlightCacheScope(async () => {
346
+ const state: FlightSerializationState = {
347
+ clientReferences: [],
348
+ clientReferenceIndexes: new Map(),
349
+ serverReferences: [],
350
+ serverReferenceIndexes: new Map(),
351
+ };
352
+ const rootValue =
353
+ typeof renderable === "function"
354
+ ? await (renderable as (props: P) => unknown)(props)
355
+ : renderable;
356
+
357
+ return {
358
+ version: 1,
359
+ root: await serializeFlightValue(rootValue, state),
360
+ clientReferences: state.clientReferences,
361
+ serverReferences: state.serverReferences,
362
+ };
363
+ });
364
+ }
365
+
366
+ export function stringifyFlightResponse(response: FlightResponse): string {
367
+ return JSON.stringify(response);
368
+ }
369
+
370
+ export function renderFlightResponseScript(
371
+ response: FlightResponse,
372
+ options: FlightScriptOptions = {},
373
+ ): string {
374
+ const idAttribute = options.id === undefined ? "" : ` id="${escapeAttribute(options.id)}"`;
375
+ const nonceAttribute =
376
+ options.nonce === undefined ? "" : ` nonce="${escapeAttribute(options.nonce)}"`;
377
+
378
+ return `<script type="application/json" data-mreact-flight${idAttribute}${nonceAttribute}>${serializeJsonForHtml(response)}</script>`;
379
+ }
380
+
381
+ export function createServerActionHandler(
382
+ actions: ServerActionRegistry,
383
+ options: ServerActionHandlerOptions = {},
384
+ ) {
385
+ const allowedActionKeys = options.allowedActions?.map((reference) =>
386
+ serverActionKey(reference.moduleId, reference.exportName),
387
+ );
388
+ const allowedActionSet =
389
+ allowedActionKeys === undefined ? undefined : new Set(allowedActionKeys);
390
+
391
+ return async (request: Request): Promise<Response> => {
392
+ if (request.method !== "POST") {
393
+ return jsonResponse({ ok: false, error: "Method not allowed." }, 405);
394
+ }
395
+
396
+ const originResponse = validateRequestOrigin(request, options.allowedOrigins);
397
+
398
+ if (originResponse !== undefined) {
399
+ return originResponse;
400
+ }
401
+
402
+ const csrfResponse = validateCsrfToken(request, options.csrf);
403
+
404
+ if (csrfResponse !== undefined) {
405
+ return csrfResponse;
406
+ }
407
+
408
+ // Issue 076: mark the nonce as used only after the action runs.
409
+ // The validator now just reads + checks; the commit happens in a
410
+ // try/finally below.
411
+ const nonceCheck = validateServerActionNonce(request, options.replayProtection);
412
+
413
+ if (nonceCheck.response !== undefined) {
414
+ return nonceCheck.response;
415
+ }
416
+
417
+ const payload = await readServerActionPayload(
418
+ request,
419
+ options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES,
420
+ );
421
+
422
+ if (payload instanceof Response) {
423
+ return payload;
424
+ }
425
+
426
+ if (typeof payload.moduleId !== "string" || typeof payload.exportName !== "string") {
427
+ return jsonResponse({ ok: false, error: "Invalid server action reference." }, 400);
428
+ }
429
+
430
+ const reference = {
431
+ moduleId: payload.moduleId,
432
+ exportName: payload.exportName,
433
+ };
434
+
435
+ if (!isAllowedServerAction(reference, allowedActionSet)) {
436
+ return jsonResponse({ ok: false, error: "Unknown server action." }, 404);
437
+ }
438
+
439
+ const actionEntry = actions[serverActionKey(payload.moduleId, payload.exportName)];
440
+
441
+ if (actionEntry === undefined) {
442
+ return jsonResponse({ ok: false, error: "Unknown server action." }, 404);
443
+ }
444
+
445
+ const action = getServerAction(actionEntry);
446
+ const validateArgs = getServerActionArgsValidator(actionEntry);
447
+ const boundArgs = Array.isArray(payload.bound) ? payload.bound : [];
448
+ const args = [...boundArgs, ...(Array.isArray(payload.args) ? payload.args : [])];
449
+ const validationResult = validateArgs?.(args);
450
+
451
+ if (validationResult !== undefined && validationResult !== true) {
452
+ return jsonResponse(
453
+ {
454
+ ok: false,
455
+ error:
456
+ typeof validationResult === "string"
457
+ ? validationResult
458
+ : "Invalid server action arguments.",
459
+ },
460
+ 400,
461
+ );
462
+ }
463
+
464
+ const authorizationResult = await options.authorize?.(
465
+ request,
466
+ reference,
467
+ args,
468
+ );
469
+
470
+ if (authorizationResult !== undefined && authorizationResult !== true) {
471
+ return jsonResponse(
472
+ {
473
+ ok: false,
474
+ error:
475
+ typeof authorizationResult === "string"
476
+ ? authorizationResult
477
+ : "Server action not authorized.",
478
+ },
479
+ 403,
480
+ );
481
+ }
482
+
483
+ try {
484
+ const value = await action(...args);
485
+ // Only commit the nonce on a successful run -- a flaky network
486
+ // retry can otherwise lose the request permanently (Issue 076).
487
+ if (nonceCheck.commit) nonceCheck.commit();
488
+ return jsonResponse({ ok: true, value }, 200);
489
+ } catch (error) {
490
+ return jsonResponse(
491
+ {
492
+ ok: false,
493
+ error: error instanceof Error ? error.message : String(error),
494
+ },
495
+ 500,
496
+ );
497
+ }
498
+ };
499
+ }
500
+
501
+ export function toReactFlightRows(response: FlightResponse): string {
502
+ // Issue 081 note: a native encoder exists in
503
+ // `packages/router-native/src/flight.rs::encode_flight_response`
504
+ // but is intentionally *not* wired here. Microbenchmarking on
505
+ // 2026-05-13 showed the JS-stringify -> napi -> Rust-parse ->
506
+ // Rust-stringify round-trip dominates and produces a 7-9x
507
+ // regression vs. the pure JS path. Re-wiring it requires
508
+ // accepting a `napi::JsObject` directly to avoid the double JSON
509
+ // pass; tracked as a follow-up.
510
+ const rows: string[] = [];
511
+ const clientWireIds = new Map<number, number>();
512
+ const serverWireIds = new Map<number, number>();
513
+ let nextWireId = 1;
514
+
515
+ for (const reference of response.clientReferences) {
516
+ const wireId = nextWireId;
517
+ nextWireId += 1;
518
+ clientWireIds.set(reference.id, wireId);
519
+ rows.push(
520
+ `${formatReactFlightId(wireId)}:I${JSON.stringify([
521
+ reference.moduleId,
522
+ reference.chunks ?? [],
523
+ reference.exportName,
524
+ ])}`,
525
+ );
526
+ }
527
+
528
+ const state: ReactFlightEncodingState = {
529
+ clientWireIds,
530
+ serverWireIds,
531
+ outlineRows: rows,
532
+ nextWireId,
533
+ };
534
+
535
+ for (const reference of response.serverReferences) {
536
+ const wireId = state.nextWireId;
537
+ state.nextWireId += 1;
538
+ serverWireIds.set(reference.id, wireId);
539
+ rows.push(
540
+ `${formatReactFlightId(wireId)}:F${JSON.stringify({
541
+ id: serverActionKey(reference.moduleId, reference.exportName),
542
+ bound: reference.bound === undefined
543
+ ? null
544
+ : reference.bound.map((value) => encodeReactFlightModel(value, state)),
545
+ name: reference.exportName,
546
+ })}`,
547
+ );
548
+ }
549
+
550
+ if (isFlightErrorModel(response.root)) {
551
+ rows.push(`0:E${JSON.stringify(encodeReactFlightError(response.root))}`);
552
+ } else {
553
+ rows.push(`0:${JSON.stringify(encodeReactFlightModel(response.root, state))}`);
554
+ }
555
+ return rows.join("\n");
556
+ }
557
+
558
+ export function fromReactFlightRows(rows: string): FlightResponse {
559
+ // Issue 081 note: a native decoder exists in
560
+ // `packages/router-native/src/flight.rs::decode_flight_rows`
561
+ // but is intentionally *not* wired here. Benchmarking on
562
+ // 2026-05-13 showed 4-14x regression vs. the pure JS walker —
563
+ // V8's JSON.parse is already extremely optimized and the
564
+ // napi -> serde_json::parse -> walk -> serde_json::serialize ->
565
+ // JS.parse round-trip multiplies the work. Wiring it requires
566
+ // returning a `napi::JsObject` directly (avoiding the double
567
+ // JSON pass); see `docs/benchmarks/2026-05-13-flight-rust-port.md`.
568
+ const lines = rows.split(/\r?\n/).filter(Boolean);
569
+ const metadataLine = lines.find((line) => line.startsWith("M0:"));
570
+ const rootLine = lines.find((line) => line.startsWith("J0:"));
571
+
572
+ if (metadataLine !== undefined && rootLine !== undefined) {
573
+ const metadata = JSON.parse(metadataLine.slice(3)) as Omit<FlightResponse, "root">;
574
+
575
+ return {
576
+ version: metadata.version,
577
+ clientReferences: metadata.clientReferences,
578
+ serverReferences: metadata.serverReferences,
579
+ root: JSON.parse(rootLine.slice(3)) as FlightModel,
580
+ };
581
+ }
582
+
583
+ const clientReferences: FlightClientReference[] = [];
584
+ const serverReferences: FlightServerReference[] = [];
585
+ const modelChunks = new Map<number, unknown>();
586
+ const errorChunks = new Map<number, FlightErrorModel>();
587
+ let root: FlightModel | undefined;
588
+
589
+ for (const line of lines) {
590
+ const row = parseReactFlightRow(line);
591
+
592
+ if (row.tag === "I") {
593
+ clientReferences.push(parseReactFlightClientReference(row.id, row.payload));
594
+ continue;
595
+ }
596
+
597
+ if (row.tag === "F") {
598
+ serverReferences.push(parseReactFlightServerReference(row.id, row.payload, modelChunks, errorChunks));
599
+ continue;
600
+ }
601
+
602
+ if (row.tag === "E") {
603
+ const error = parseReactFlightError(row.payload);
604
+ errorChunks.set(row.id, error);
605
+
606
+ if (row.id === 0) {
607
+ root = error;
608
+ }
609
+ continue;
610
+ }
611
+
612
+ if (row.tag === "T") {
613
+ modelChunks.set(row.id, parseReactFlightTextChunk(row.payload));
614
+ continue;
615
+ }
616
+
617
+ if (isReactFlightBinaryRowTag(row.tag)) {
618
+ modelChunks.set(row.id, parseReactFlightBinaryChunk(row.tag, row.payload));
619
+ continue;
620
+ }
621
+
622
+ if (isReactFlightMetadataTag(row.tag)) {
623
+ continue;
624
+ }
625
+
626
+ if (row.tag === undefined && row.payload !== "") {
627
+ modelChunks.set(row.id, JSON.parse(row.payload));
628
+ }
629
+ }
630
+
631
+ if (root === undefined && modelChunks.has(0)) {
632
+ root = decodeReactFlightModel(modelChunks.get(0), modelChunks, errorChunks);
633
+ }
634
+
635
+ if (root === undefined) {
636
+ throw new Error("Invalid React Flight rows.");
637
+ }
638
+
639
+ return {
640
+ version: 1,
641
+ root,
642
+ clientReferences,
643
+ serverReferences,
644
+ };
645
+ }
646
+
647
+ export function mergeReactFlightRows(
648
+ response: FlightResponse,
649
+ rows: string,
650
+ ): FlightResponse {
651
+ // Issue 081 note: same finding as `fromReactFlightRows` — the native
652
+ // merge path is slower than the JS one given the double-JSON tax.
653
+ const modelChunks = new Map<number, unknown>();
654
+ const errorChunks = new Map<number, FlightErrorModel>();
655
+ const clientReferences = [...response.clientReferences];
656
+ const serverReferences = [...response.serverReferences];
657
+
658
+ for (const line of rows.split(/\r?\n/).filter(Boolean)) {
659
+ const row = parseReactFlightRow(line);
660
+
661
+ if (row.tag === "I") {
662
+ clientReferences.push(parseReactFlightClientReference(row.id, row.payload));
663
+ continue;
664
+ }
665
+
666
+ if (row.tag === "F") {
667
+ serverReferences.push(parseReactFlightServerReference(row.id, row.payload, modelChunks, errorChunks));
668
+ continue;
669
+ }
670
+
671
+ if (row.tag === "E") {
672
+ errorChunks.set(row.id, parseReactFlightError(row.payload));
673
+ continue;
674
+ }
675
+
676
+ if (row.tag === "T") {
677
+ modelChunks.set(row.id, parseReactFlightTextChunk(row.payload));
678
+ continue;
679
+ }
680
+
681
+ if (isReactFlightBinaryRowTag(row.tag)) {
682
+ modelChunks.set(row.id, parseReactFlightBinaryChunk(row.tag, row.payload));
683
+ continue;
684
+ }
685
+
686
+ if (isReactFlightMetadataTag(row.tag)) {
687
+ continue;
688
+ }
689
+
690
+ if (row.tag === undefined && row.payload !== "") {
691
+ modelChunks.set(row.id, JSON.parse(row.payload));
692
+ }
693
+ }
694
+
695
+ return {
696
+ ...response,
697
+ clientReferences,
698
+ serverReferences,
699
+ root: resolveFlightPromiseChunks(response.root, modelChunks, errorChunks),
700
+ };
701
+ }
702
+
703
+ export function createFlightClientManifest(
704
+ references: readonly FlightClientReferenceInput[],
705
+ resolveChunks: (reference: FlightClientReferenceInput) => string[],
706
+ ): FlightClientManifestEntry[] {
707
+ return references.map((reference) => ({
708
+ ...reference,
709
+ chunks: resolveChunks(reference),
710
+ }));
711
+ }
712
+
713
+ export function renderFlightPreloadLinks(
714
+ response: FlightResponse,
715
+ options: { nonce?: string } = {},
716
+ ): string {
717
+ const seen = new Set<string>();
718
+ const nonceAttribute =
719
+ options.nonce === undefined ? "" : ` nonce="${escapeAttribute(options.nonce)}"`;
720
+
721
+ return response.clientReferences
722
+ .flatMap((reference) => reference.chunks ?? [])
723
+ .filter((chunk) => {
724
+ if (seen.has(chunk)) {
725
+ return false;
726
+ }
727
+
728
+ seen.add(chunk);
729
+ return true;
730
+ })
731
+ .map((chunk) =>
732
+ `<link rel="modulepreload" href="${escapeAttribute(chunk)}"${nonceAttribute}>`,
733
+ )
734
+ .join("");
735
+ }
736
+
737
+ function resolveFlightPromiseChunks(
738
+ model: FlightModel,
739
+ modelChunks: ReadonlyMap<number, unknown>,
740
+ errorChunks: ReadonlyMap<number, FlightErrorModel>,
741
+ ): FlightModel {
742
+ if (
743
+ model === null ||
744
+ typeof model === "string" ||
745
+ typeof model === "number" ||
746
+ typeof model === "boolean"
747
+ ) {
748
+ return model;
749
+ }
750
+
751
+ if (Array.isArray(model)) {
752
+ return model.map((item) => resolveFlightPromiseChunks(item, modelChunks, errorChunks));
753
+ }
754
+
755
+ if (model.kind === "promise") {
756
+ const error = errorChunks.get(model.id);
757
+
758
+ if (error !== undefined) {
759
+ return error;
760
+ }
761
+
762
+ if (modelChunks.has(model.id)) {
763
+ return decodeReactFlightModel(modelChunks.get(model.id), modelChunks, errorChunks);
764
+ }
765
+
766
+ return model;
767
+ }
768
+
769
+ if (model.kind === "element") {
770
+ return {
771
+ ...model,
772
+ props: Object.fromEntries(
773
+ Object.entries(model.props).map(([key, value]) => [
774
+ key,
775
+ resolveFlightPromiseChunks(value, modelChunks, errorChunks),
776
+ ]),
777
+ ),
778
+ };
779
+ }
780
+
781
+ if (model.kind === "map") {
782
+ return {
783
+ ...model,
784
+ entries: model.entries.map(([key, value]): [FlightModel, FlightModel] => [
785
+ resolveFlightPromiseChunks(key, modelChunks, errorChunks),
786
+ resolveFlightPromiseChunks(value, modelChunks, errorChunks),
787
+ ]),
788
+ };
789
+ }
790
+
791
+ if (model.kind === "set") {
792
+ return {
793
+ ...model,
794
+ values: model.values.map((value) => resolveFlightPromiseChunks(value, modelChunks, errorChunks)),
795
+ };
796
+ }
797
+
798
+ if ("kind" in model) {
799
+ return model;
800
+ }
801
+
802
+ return Object.fromEntries(
803
+ Object.entries(model).map(([key, value]) => [
804
+ key,
805
+ value === undefined ? undefined : resolveFlightPromiseChunks(value, modelChunks, errorChunks),
806
+ ]),
807
+ );
808
+ }
809
+
810
+ interface ReactFlightEncodingState {
811
+ clientWireIds: ReadonlyMap<number, number>;
812
+ serverWireIds: ReadonlyMap<number, number>;
813
+ outlineRows: string[];
814
+ nextWireId: number;
815
+ }
816
+
817
+ function encodeReactFlightModel(model: FlightModel, state: ReactFlightEncodingState): unknown {
818
+ if (
819
+ model === null ||
820
+ typeof model === "number" ||
821
+ typeof model === "boolean"
822
+ ) {
823
+ return model;
824
+ }
825
+
826
+ if (typeof model === "string") {
827
+ return model.startsWith("$") ? `$${model}` : model;
828
+ }
829
+
830
+ if (Array.isArray(model)) {
831
+ return model.map((item) => encodeReactFlightModel(item, state));
832
+ }
833
+
834
+ if (model.kind === "undefined") {
835
+ return "$u";
836
+ }
837
+
838
+ if (model.kind === "date") {
839
+ return `$D${model.value}`;
840
+ }
841
+
842
+ if (model.kind === "bigint") {
843
+ return `$n${model.value}`;
844
+ }
845
+
846
+ if (model.kind === "number") {
847
+ if (model.value === "Infinity") {
848
+ return "$I";
849
+ }
850
+
851
+ if (model.value === "NaN") {
852
+ return "$N";
853
+ }
854
+
855
+ return `$${model.value}`;
856
+ }
857
+
858
+ if (model.kind === "symbol") {
859
+ return `$S${model.name}`;
860
+ }
861
+
862
+ if (model.kind === "map") {
863
+ const id = allocateReactFlightOutlineRow(
864
+ state,
865
+ model.entries.map(([key, value]) => [
866
+ encodeReactFlightModel(key, state),
867
+ encodeReactFlightModel(value, state),
868
+ ]),
869
+ );
870
+ return `$Q${formatReactFlightId(id)}`;
871
+ }
872
+
873
+ if (model.kind === "set") {
874
+ const id = allocateReactFlightOutlineRow(
875
+ state,
876
+ model.values.map((value) => encodeReactFlightModel(value, state)),
877
+ );
878
+ return `$W${formatReactFlightId(id)}`;
879
+ }
880
+
881
+ if (model.kind === "form-data") {
882
+ const id = allocateReactFlightOutlineRow(
883
+ state,
884
+ model.entries.map(([key, value]) => [key, encodeReactFlightModel(value, state)]),
885
+ );
886
+ return `$K${formatReactFlightId(id)}`;
887
+ }
888
+
889
+ if (model.kind === "iterable") {
890
+ const id = allocateReactFlightOutlineRow(
891
+ state,
892
+ model.values.map((value) => encodeReactFlightModel(value, state)),
893
+ );
894
+ return `$i${formatReactFlightId(id)}`;
895
+ }
896
+
897
+ if (model.kind === "error") {
898
+ const id = state.nextWireId;
899
+ state.nextWireId += 1;
900
+ state.outlineRows.push(`${formatReactFlightId(id)}:E${JSON.stringify(encodeReactFlightError(model))}`);
901
+ return `$Z${formatReactFlightId(id)}`;
902
+ }
903
+
904
+ if (model.kind === "promise") {
905
+ return `$@${formatReactFlightId(model.id)}`;
906
+ }
907
+
908
+ if (model.kind === "server-reference") {
909
+ return `$F${state.serverWireIds.get(model.id) ?? model.id}`;
910
+ }
911
+
912
+ if (model.kind === "client-reference") {
913
+ return `$L${state.clientWireIds.get(model.id) ?? model.id}`;
914
+ }
915
+
916
+ if (model.kind === "element") {
917
+ return [
918
+ "$",
919
+ encodeReactFlightElementType(model.type, state.clientWireIds),
920
+ model.key,
921
+ encodeReactFlightProps(model.props, state),
922
+ ];
923
+ }
924
+
925
+ if (isReactFlightBinaryModel(model)) {
926
+ return model;
927
+ }
928
+
929
+ return encodeReactFlightProps(model, state);
930
+ }
931
+
932
+ function allocateReactFlightOutlineRow(
933
+ state: ReactFlightEncodingState,
934
+ payload: unknown,
935
+ ): number {
936
+ const id = state.nextWireId;
937
+ state.nextWireId += 1;
938
+ state.outlineRows.push(`${formatReactFlightId(id)}:${JSON.stringify(payload)}`);
939
+ return id;
940
+ }
941
+
942
+ function encodeReactFlightElementType(
943
+ type: FlightElementModel["type"],
944
+ clientWireIds: ReadonlyMap<number, number>,
945
+ ): string {
946
+ if (typeof type === "string") {
947
+ return type;
948
+ }
949
+
950
+ if (type.kind === "fragment") {
951
+ return "$Sreact.fragment";
952
+ }
953
+
954
+ return `$L${clientWireIds.get(type.id) ?? type.id}`;
955
+ }
956
+
957
+ function encodeReactFlightProps(
958
+ props: Record<string, FlightModel | undefined>,
959
+ state: ReactFlightEncodingState,
960
+ ): Record<string, unknown> {
961
+ return Object.fromEntries(
962
+ Object.entries(props)
963
+ .filter((entry): entry is [string, FlightModel] => entry[1] !== undefined)
964
+ .map(([key, value]) => [
965
+ key,
966
+ encodeReactFlightModel(value, state),
967
+ ]),
968
+ );
969
+ }
970
+
971
+ interface ReactFlightRow {
972
+ id: number;
973
+ tag?: string;
974
+ payload: string;
975
+ }
976
+
977
+ type ReactFlightBinaryRowTag =
978
+ | "A"
979
+ | "O"
980
+ | "o"
981
+ | "U"
982
+ | "S"
983
+ | "s"
984
+ | "L"
985
+ | "l"
986
+ | "G"
987
+ | "g"
988
+ | "M"
989
+ | "m"
990
+ | "V";
991
+
992
+ function parseReactFlightRow(line: string): ReactFlightRow {
993
+ const separator = line.indexOf(":");
994
+
995
+ if (separator < 0) {
996
+ throw new Error("Invalid React Flight row.");
997
+ }
998
+
999
+ const id = separator === 0 ? 0 : parseReactFlightId(line.slice(0, separator));
1000
+ const body = line.slice(separator + 1);
1001
+ const first = body[0];
1002
+ const hasTag = first !== undefined && isReactFlightRowTag(first, body);
1003
+
1004
+ if (first !== undefined && !hasTag && looksLikeUnsupportedReactFlightTag(first, body)) {
1005
+ throw new Error(`Unsupported React Flight row tag: ${first}`);
1006
+ }
1007
+
1008
+ return {
1009
+ id,
1010
+ ...(hasTag ? { tag: first } : {}),
1011
+ payload: hasTag ? body.slice(1) : body,
1012
+ };
1013
+ }
1014
+
1015
+ function looksLikeUnsupportedReactFlightTag(tag: string, body: string): boolean {
1016
+ return /^[A-Z]$/.test(tag) && (body[1] === "{" || body[1] === "[" || body[1] === "\"");
1017
+ }
1018
+
1019
+ function parseReactFlightTextChunk(payload: string): string {
1020
+ const separator = payload.indexOf(",");
1021
+
1022
+ if (separator < 0) {
1023
+ throw new Error("Invalid React Flight text row.");
1024
+ }
1025
+
1026
+ return payload.slice(separator + 1);
1027
+ }
1028
+
1029
+ function parseReactFlightBinaryChunk(
1030
+ tag: ReactFlightBinaryRowTag,
1031
+ payload: string,
1032
+ ): FlightArrayBufferModel | FlightTypedArrayModel | FlightDataViewModel {
1033
+ const separator = payload.indexOf(",");
1034
+
1035
+ if (separator < 0) {
1036
+ throw new Error("Invalid React Flight binary row.");
1037
+ }
1038
+
1039
+ const byteLength = parseReactFlightId(payload.slice(0, separator));
1040
+ const bytes = decodeBase64Bytes(payload.slice(separator + 1));
1041
+
1042
+ if (bytes.length !== byteLength) {
1043
+ throw new Error("React Flight binary row length did not match declared payload length.");
1044
+ }
1045
+
1046
+ return createReactFlightBinaryModel(tag, bytes);
1047
+ }
1048
+
1049
+ function isReactFlightMetadataTag(tag: string | undefined): boolean {
1050
+ return (
1051
+ tag === "H" ||
1052
+ tag === "N" ||
1053
+ tag === "P" ||
1054
+ tag === "D" ||
1055
+ tag === "J" ||
1056
+ tag === "W" ||
1057
+ tag === "R" ||
1058
+ tag === "r" ||
1059
+ tag === "X" ||
1060
+ tag === "x" ||
1061
+ tag === "C"
1062
+ );
1063
+ }
1064
+
1065
+ function isReactFlightRowTag(tag: string, body: string): boolean {
1066
+ if (
1067
+ tag === "I" ||
1068
+ tag === "F" ||
1069
+ tag === "E" ||
1070
+ tag === "T" ||
1071
+ tag === "H" ||
1072
+ tag === "N" ||
1073
+ tag === "P" ||
1074
+ tag === "D" ||
1075
+ tag === "J" ||
1076
+ tag === "W" ||
1077
+ tag === "R" ||
1078
+ tag === "r" ||
1079
+ tag === "X" ||
1080
+ tag === "x" ||
1081
+ tag === "C"
1082
+ ) {
1083
+ return true;
1084
+ }
1085
+
1086
+ return isReactFlightBinaryRowTag(tag) && /^[AOoUSsLlGgMmV][0-9a-f]+,/i.test(body);
1087
+ }
1088
+
1089
+ function isReactFlightBinaryRowTag(tag: string | undefined): tag is ReactFlightBinaryRowTag {
1090
+ return (
1091
+ tag === "A" ||
1092
+ tag === "O" ||
1093
+ tag === "o" ||
1094
+ tag === "U" ||
1095
+ tag === "S" ||
1096
+ tag === "s" ||
1097
+ tag === "L" ||
1098
+ tag === "l" ||
1099
+ tag === "G" ||
1100
+ tag === "g" ||
1101
+ tag === "M" ||
1102
+ tag === "m" ||
1103
+ tag === "V"
1104
+ );
1105
+ }
1106
+
1107
+ function createReactFlightBinaryModel(
1108
+ tag: ReactFlightBinaryRowTag,
1109
+ bytes: Uint8Array,
1110
+ ): FlightArrayBufferModel | FlightTypedArrayModel | FlightDataViewModel {
1111
+ const byteValues = Array.from(bytes);
1112
+
1113
+ if (tag === "A") {
1114
+ return {
1115
+ kind: "array-buffer",
1116
+ bytes: byteValues,
1117
+ };
1118
+ }
1119
+
1120
+ if (tag === "V") {
1121
+ return {
1122
+ kind: "data-view",
1123
+ bytes: byteValues,
1124
+ };
1125
+ }
1126
+
1127
+ return {
1128
+ kind: "typed-array",
1129
+ arrayType: getReactFlightTypedArrayName(tag),
1130
+ bytes: byteValues,
1131
+ };
1132
+ }
1133
+
1134
+ function getReactFlightTypedArrayName(tag: Exclude<ReactFlightBinaryRowTag, "A" | "V">): FlightTypedArrayName {
1135
+ switch (tag) {
1136
+ case "O":
1137
+ return "Int8Array";
1138
+ case "o":
1139
+ return "Uint8Array";
1140
+ case "U":
1141
+ return "Uint8ClampedArray";
1142
+ case "S":
1143
+ return "Int16Array";
1144
+ case "s":
1145
+ return "Uint16Array";
1146
+ case "L":
1147
+ return "Int32Array";
1148
+ case "l":
1149
+ return "Uint32Array";
1150
+ case "G":
1151
+ return "Float32Array";
1152
+ case "g":
1153
+ return "Float64Array";
1154
+ case "M":
1155
+ return "BigInt64Array";
1156
+ case "m":
1157
+ return "BigUint64Array";
1158
+ default:
1159
+ throw new Error(`Unsupported React Flight typed array row tag: ${tag}`);
1160
+ }
1161
+ }
1162
+
1163
+ function decodeBase64Bytes(value: string): Uint8Array {
1164
+ // Issue 081: route through the native binding when available. The
1165
+ // native implementation accepts both URL-safe and standard alphabets
1166
+ // and tolerates missing padding, matching the JS fallback below.
1167
+ const native = getNativeFlight()?.decodeFlightBase64;
1168
+
1169
+ if (native !== undefined) {
1170
+ const result = native(value);
1171
+ // napi-rs returns a Buffer which is a Uint8Array subclass; the
1172
+ // callsite types it as Uint8Array so this is structurally fine.
1173
+ return result instanceof Uint8Array ? result : new Uint8Array(result);
1174
+ }
1175
+
1176
+ const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
1177
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
1178
+ const binary = globalThis.atob(padded);
1179
+ const bytes = new Uint8Array(binary.length);
1180
+
1181
+ for (let index = 0; index < binary.length; index += 1) {
1182
+ bytes[index] = binary.charCodeAt(index);
1183
+ }
1184
+
1185
+ return bytes;
1186
+ }
1187
+
1188
+ function parseReactFlightClientReference(id: number, payload: string): FlightClientReference {
1189
+ const value = JSON.parse(payload) as unknown;
1190
+
1191
+ if (Array.isArray(value)) {
1192
+ return {
1193
+ id,
1194
+ moduleId: String(value[0]),
1195
+ chunks: Array.isArray(value[1]) ? value[1].map(String) : [],
1196
+ exportName: String(value[2] ?? "default"),
1197
+ };
1198
+ }
1199
+
1200
+ const object = valueIsObject(value) ? value : {};
1201
+
1202
+ return {
1203
+ id,
1204
+ moduleId: String(object.id ?? object.moduleId ?? ""),
1205
+ chunks: Array.isArray(object.chunks) ? object.chunks.map(String) : [],
1206
+ exportName: String(object.name ?? object.exportName ?? "default"),
1207
+ };
1208
+ }
1209
+
1210
+ function parseReactFlightServerReference(
1211
+ id: number,
1212
+ payload: string,
1213
+ modelChunks: ReadonlyMap<number, unknown> = new Map(),
1214
+ errorChunks: ReadonlyMap<number, FlightErrorModel> = new Map(),
1215
+ ): FlightServerReference {
1216
+ const value = JSON.parse(payload) as unknown;
1217
+ const object = valueIsObject(value) ? value : {};
1218
+ const actionId = String(object.id ?? "");
1219
+ const separator = actionId.lastIndexOf("#");
1220
+ const bound = Array.isArray(object.bound)
1221
+ ? object.bound.map((entry) => decodeReactFlightModel(entry, modelChunks, errorChunks))
1222
+ : undefined;
1223
+
1224
+ return {
1225
+ id,
1226
+ moduleId: separator < 0 ? actionId : actionId.slice(0, separator),
1227
+ exportName:
1228
+ typeof object.name === "string"
1229
+ ? object.name
1230
+ : separator < 0
1231
+ ? "default"
1232
+ : actionId.slice(separator + 1),
1233
+ ...(bound === undefined ? {} : { bound }),
1234
+ };
1235
+ }
1236
+
1237
+ // Issue 079: hard cap on Flight tree depth to prevent stack-exhaustion
1238
+ // DoS from a deeply-nested payload. The cap is far higher than any
1239
+ // legitimate component tree.
1240
+ const MAX_FLIGHT_DECODE_DEPTH = 256;
1241
+
1242
+ class FlightDecodeError extends Error {
1243
+ constructor(message: string) {
1244
+ super(message);
1245
+ this.name = "FlightDecodeError";
1246
+ }
1247
+ }
1248
+
1249
+ function flightTooDeep(): never {
1250
+ throw new FlightDecodeError(
1251
+ `MR_FLIGHT_TOO_DEEP: nested deeper than ${MAX_FLIGHT_DECODE_DEPTH} levels`,
1252
+ );
1253
+ }
1254
+
1255
+ function decodeReactFlightModel(
1256
+ value: unknown,
1257
+ modelChunks: ReadonlyMap<number, unknown> = new Map(),
1258
+ errorChunks: ReadonlyMap<number, FlightErrorModel> = new Map(),
1259
+ depth = 0,
1260
+ ): FlightModel {
1261
+ if (depth > MAX_FLIGHT_DECODE_DEPTH) flightTooDeep();
1262
+ if (
1263
+ value === null ||
1264
+ typeof value === "number" ||
1265
+ typeof value === "boolean"
1266
+ ) {
1267
+ return value;
1268
+ }
1269
+
1270
+ if (typeof value === "string") {
1271
+ return decodeReactFlightString(value, modelChunks, errorChunks);
1272
+ }
1273
+
1274
+ if (Array.isArray(value)) {
1275
+ if (value[0] === "$") {
1276
+ return {
1277
+ kind: "element",
1278
+ type: decodeReactFlightElementType(value[1]),
1279
+ key: typeof value[2] === "string" ? value[2] : null,
1280
+ props: decodeReactFlightProps(
1281
+ valueIsObject(value[3]) ? value[3] : {},
1282
+ modelChunks,
1283
+ errorChunks,
1284
+ depth + 1,
1285
+ ),
1286
+ };
1287
+ }
1288
+
1289
+ return value.map((item) =>
1290
+ decodeReactFlightModel(item, modelChunks, errorChunks, depth + 1),
1291
+ );
1292
+ }
1293
+
1294
+ if (isReactFlightBinaryModel(value)) {
1295
+ return value;
1296
+ }
1297
+
1298
+ if (valueIsObject(value)) {
1299
+ return decodeReactFlightProps(value, modelChunks, errorChunks, depth + 1);
1300
+ }
1301
+
1302
+ return { kind: "undefined" };
1303
+ }
1304
+
1305
+ function decodeReactFlightString(
1306
+ value: string,
1307
+ modelChunks: ReadonlyMap<number, unknown>,
1308
+ errorChunks: ReadonlyMap<number, FlightErrorModel>,
1309
+ ): FlightModel {
1310
+ if (value === "$undefined" || value === "$u") {
1311
+ return { kind: "undefined" };
1312
+ }
1313
+
1314
+ if (value.startsWith("$$")) {
1315
+ return value.slice(1);
1316
+ }
1317
+
1318
+ if (value === "$I") {
1319
+ return { kind: "number", value: "Infinity" };
1320
+ }
1321
+
1322
+ if (value === "$-Infinity") {
1323
+ return { kind: "number", value: "-Infinity" };
1324
+ }
1325
+
1326
+ if (value === "$-0") {
1327
+ return { kind: "number", value: "-0" };
1328
+ }
1329
+
1330
+ if (value === "$N") {
1331
+ return { kind: "number", value: "NaN" };
1332
+ }
1333
+
1334
+ if (value.startsWith("$D")) {
1335
+ return { kind: "date", value: value.slice(2) };
1336
+ }
1337
+
1338
+ if (value.startsWith("$n")) {
1339
+ return { kind: "bigint", value: value.slice(2) };
1340
+ }
1341
+
1342
+ if (/^\$[AOoUSsLlGgMmV][0-9a-f]+$/.test(value)) {
1343
+ return decodeReactFlightChunk(value.slice(2), modelChunks, errorChunks);
1344
+ }
1345
+
1346
+ if (value.startsWith("$S")) {
1347
+ return { kind: "symbol", name: value.slice(2) };
1348
+ }
1349
+
1350
+ if (/^\$F[0-9a-f]+$/i.test(value)) {
1351
+ return {
1352
+ kind: "server-reference",
1353
+ id: parseReactFlightId(value.slice(2)),
1354
+ };
1355
+ }
1356
+
1357
+ if (/^\$L[0-9a-f]+$/i.test(value)) {
1358
+ return {
1359
+ kind: "client-reference",
1360
+ id: parseReactFlightId(value.slice(2)),
1361
+ };
1362
+ }
1363
+
1364
+ if (/^\$@[0-9a-f]*$/i.test(value)) {
1365
+ return {
1366
+ kind: "promise",
1367
+ id: value.length === 2 ? 0 : parseReactFlightId(value.slice(2)),
1368
+ };
1369
+ }
1370
+
1371
+ if (/^\$Q[0-9a-f]+$/i.test(value)) {
1372
+ const decoded = decodeReactFlightChunk(value.slice(2), modelChunks, errorChunks);
1373
+ const entries = Array.isArray(decoded)
1374
+ ? decoded.map((entry): [FlightModel, FlightModel] =>
1375
+ Array.isArray(entry) ? [entry[0] ?? { kind: "undefined" }, entry[1] ?? { kind: "undefined" }] : [entry, { kind: "undefined" }],
1376
+ )
1377
+ : [];
1378
+
1379
+ return {
1380
+ kind: "map",
1381
+ entries,
1382
+ };
1383
+ }
1384
+
1385
+ if (/^\$W[0-9a-f]+$/i.test(value)) {
1386
+ const decoded = decodeReactFlightChunk(value.slice(2), modelChunks, errorChunks);
1387
+
1388
+ return {
1389
+ kind: "set",
1390
+ values: Array.isArray(decoded) ? decoded : [],
1391
+ };
1392
+ }
1393
+
1394
+ if (/^\$K[0-9a-f]+$/i.test(value)) {
1395
+ const decoded = decodeReactFlightChunk(value.slice(2), modelChunks, errorChunks);
1396
+ const entries = Array.isArray(decoded)
1397
+ ? decoded.flatMap((entry): [string, FlightModel][] =>
1398
+ Array.isArray(entry) && typeof entry[0] === "string"
1399
+ ? [[entry[0], entry[1] ?? { kind: "undefined" }]]
1400
+ : [],
1401
+ )
1402
+ : [];
1403
+
1404
+ return {
1405
+ kind: "form-data",
1406
+ entries,
1407
+ };
1408
+ }
1409
+
1410
+ if (/^\$i[0-9a-f]+$/i.test(value)) {
1411
+ const decoded = decodeReactFlightChunk(value.slice(2), modelChunks, errorChunks);
1412
+
1413
+ return {
1414
+ kind: "iterable",
1415
+ values: Array.isArray(decoded) ? decoded : [],
1416
+ };
1417
+ }
1418
+
1419
+ if (/^\$Z[0-9a-f]+$/i.test(value)) {
1420
+ return errorChunks.get(parseReactFlightId(value.slice(2))) ?? {
1421
+ kind: "error",
1422
+ name: "Error",
1423
+ message: "Unknown React Flight error.",
1424
+ };
1425
+ }
1426
+
1427
+ if (value === "$Y" || value.startsWith("$E")) {
1428
+ return { kind: "undefined" };
1429
+ }
1430
+
1431
+ if (/^\$[0-9a-f]+$/i.test(value)) {
1432
+ return decodeReactFlightChunk(value.slice(1), modelChunks, errorChunks);
1433
+ }
1434
+
1435
+ return value;
1436
+ }
1437
+
1438
+ function decodeReactFlightChunk(
1439
+ id: string,
1440
+ modelChunks: ReadonlyMap<number, unknown>,
1441
+ errorChunks: ReadonlyMap<number, FlightErrorModel>,
1442
+ ): FlightModel {
1443
+ const numericId = parseReactFlightId(id);
1444
+ const error = errorChunks.get(numericId);
1445
+
1446
+ if (error !== undefined) {
1447
+ return error;
1448
+ }
1449
+
1450
+ if (!modelChunks.has(numericId)) {
1451
+ return {
1452
+ kind: "promise",
1453
+ id: numericId,
1454
+ };
1455
+ }
1456
+
1457
+ return decodeReactFlightModel(modelChunks.get(numericId), modelChunks, errorChunks);
1458
+ }
1459
+
1460
+ function decodeReactFlightElementType(value: unknown): FlightElementModel["type"] {
1461
+ if (value === "$Sreact.fragment") {
1462
+ return { kind: "fragment" };
1463
+ }
1464
+
1465
+ if (typeof value === "string" && /^\$L[0-9a-f]+$/i.test(value)) {
1466
+ return {
1467
+ kind: "client-reference",
1468
+ id: parseReactFlightId(value.slice(2)),
1469
+ };
1470
+ }
1471
+
1472
+ return typeof value === "string" ? value : String(value);
1473
+ }
1474
+
1475
+ function decodeReactFlightProps(
1476
+ value: Record<string, unknown>,
1477
+ modelChunks: ReadonlyMap<number, unknown>,
1478
+ errorChunks: ReadonlyMap<number, FlightErrorModel>,
1479
+ depth = 0,
1480
+ ): Record<string, FlightModel> {
1481
+ if (depth > MAX_FLIGHT_DECODE_DEPTH) flightTooDeep();
1482
+ return Object.fromEntries(
1483
+ Object.entries(value).map(([key, child]) => [
1484
+ key,
1485
+ decodeReactFlightModel(child, modelChunks, errorChunks, depth + 1),
1486
+ ]),
1487
+ );
1488
+ }
1489
+
1490
+ function isReactFlightBinaryModel(
1491
+ value: unknown,
1492
+ ): value is FlightArrayBufferModel | FlightTypedArrayModel | FlightDataViewModel {
1493
+ return (
1494
+ valueIsObject(value) &&
1495
+ (value.kind === "array-buffer" || value.kind === "typed-array" || value.kind === "data-view")
1496
+ );
1497
+ }
1498
+
1499
+ function parseReactFlightError(payload: string): FlightErrorModel {
1500
+ const value = JSON.parse(payload) as unknown;
1501
+ const object = valueIsObject(value) ? value : {};
1502
+ const digest = typeof object.digest === "string" ? object.digest : undefined;
1503
+
1504
+ return {
1505
+ kind: "error",
1506
+ name: typeof object.name === "string" ? object.name : "Error",
1507
+ message: typeof object.message === "string" ? object.message : "React Flight error.",
1508
+ ...(digest === undefined ? {} : { digest }),
1509
+ };
1510
+ }
1511
+
1512
+ function encodeReactFlightError(model: FlightErrorModel): Record<string, unknown> {
1513
+ return {
1514
+ digest: model.digest ?? "",
1515
+ name: model.name,
1516
+ message: model.message,
1517
+ stack: [],
1518
+ env: "Server",
1519
+ };
1520
+ }
1521
+
1522
+ function isFlightErrorModel(model: FlightModel): model is FlightErrorModel {
1523
+ return (
1524
+ typeof model === "object" &&
1525
+ model !== null &&
1526
+ !Array.isArray(model) &&
1527
+ model.kind === "error"
1528
+ );
1529
+ }
1530
+
1531
+ function valueIsObject(value: unknown): value is Record<string, unknown> {
1532
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1533
+ }
1534
+
1535
+ function formatReactFlightId(id: number): string {
1536
+ return id.toString(16);
1537
+ }
1538
+
1539
+ function parseReactFlightId(value: string): number {
1540
+ return Number.parseInt(value, 16);
1541
+ }
1542
+
1543
+ async function serializeFlightValue(
1544
+ value: unknown,
1545
+ state: FlightSerializationState,
1546
+ ): Promise<FlightModel> {
1547
+ const awaited = await value;
1548
+
1549
+ if (awaited === null) {
1550
+ return null;
1551
+ }
1552
+
1553
+ if (awaited === undefined) {
1554
+ return { kind: "undefined" };
1555
+ }
1556
+
1557
+ if (
1558
+ typeof awaited === "string" ||
1559
+ typeof awaited === "boolean"
1560
+ ) {
1561
+ return awaited;
1562
+ }
1563
+
1564
+ if (typeof awaited === "number") {
1565
+ if (Number.isNaN(awaited)) {
1566
+ return { kind: "number", value: "NaN" };
1567
+ }
1568
+
1569
+ if (awaited === Infinity) {
1570
+ return { kind: "number", value: "Infinity" };
1571
+ }
1572
+
1573
+ if (awaited === -Infinity) {
1574
+ return { kind: "number", value: "-Infinity" };
1575
+ }
1576
+
1577
+ if (Object.is(awaited, -0)) {
1578
+ return { kind: "number", value: "-0" };
1579
+ }
1580
+
1581
+ return awaited;
1582
+ }
1583
+
1584
+ if (typeof awaited === "bigint") {
1585
+ return { kind: "bigint", value: awaited.toString() };
1586
+ }
1587
+
1588
+ if (typeof awaited === "symbol") {
1589
+ return { kind: "symbol", name: Symbol.keyFor(awaited) ?? awaited.description ?? "" };
1590
+ }
1591
+
1592
+ if (Array.isArray(awaited)) {
1593
+ return await Promise.all(awaited.map((item) => serializeFlightValue(item, state)));
1594
+ }
1595
+
1596
+ if (isServerReference(awaited)) {
1597
+ return {
1598
+ kind: "server-reference",
1599
+ id: await getServerReferenceId(awaited, state),
1600
+ };
1601
+ }
1602
+
1603
+ if (isReactCompatElement(awaited)) {
1604
+ return await serializeElement(awaited, state);
1605
+ }
1606
+
1607
+ if (awaited instanceof Date) {
1608
+ return { kind: "date", value: awaited.toJSON() };
1609
+ }
1610
+
1611
+ if (awaited instanceof Map) {
1612
+ return {
1613
+ kind: "map",
1614
+ entries: await Promise.all(
1615
+ Array.from(awaited.entries()).map(async ([key, value]) => [
1616
+ await serializeFlightValue(key, state),
1617
+ await serializeFlightValue(value, state),
1618
+ ] as [FlightModel, FlightModel]),
1619
+ ),
1620
+ };
1621
+ }
1622
+
1623
+ if (awaited instanceof Set) {
1624
+ return {
1625
+ kind: "set",
1626
+ values: await Promise.all(
1627
+ Array.from(awaited.values()).map((value) => serializeFlightValue(value, state)),
1628
+ ),
1629
+ };
1630
+ }
1631
+
1632
+ if (isFormDataLike(awaited)) {
1633
+ return {
1634
+ kind: "form-data",
1635
+ entries: await Promise.all(
1636
+ Array.from(awaited.entries()).map(async ([key, value]) => [
1637
+ key,
1638
+ await serializeFlightValue(value, state),
1639
+ ] as [string, FlightModel]),
1640
+ ),
1641
+ };
1642
+ }
1643
+
1644
+ if (isIterableObject(awaited)) {
1645
+ return {
1646
+ kind: "iterable",
1647
+ values: await Promise.all(
1648
+ Array.from(awaited).map((value) => serializeFlightValue(value, state)),
1649
+ ),
1650
+ };
1651
+ }
1652
+
1653
+ if (awaited instanceof Error) {
1654
+ return {
1655
+ kind: "error",
1656
+ name: awaited.name,
1657
+ message: awaited.message,
1658
+ };
1659
+ }
1660
+
1661
+ if (typeof awaited === "object") {
1662
+ return await serializeObject(awaited as Record<string, unknown>, state);
1663
+ }
1664
+
1665
+ throw new TypeError(`Unsupported Flight value: ${typeof awaited}`);
1666
+ }
1667
+
1668
+ async function serializeElement(
1669
+ element: ReactCompatElementLike,
1670
+ state: FlightSerializationState,
1671
+ ): Promise<FlightElementModel | FlightModel> {
1672
+ if (typeof element.type === "function") {
1673
+ return await serializeFlightValue(element.type(element.props), state);
1674
+ }
1675
+
1676
+ if (isClientReference(element.type)) {
1677
+ return {
1678
+ kind: "element",
1679
+ type: {
1680
+ kind: "client-reference",
1681
+ id: getClientReferenceId(element.type, state),
1682
+ },
1683
+ key: element.key,
1684
+ props: await serializeProps(element.props, state),
1685
+ };
1686
+ }
1687
+
1688
+ if (typeof element.type === "string") {
1689
+ return {
1690
+ kind: "element",
1691
+ type: element.type,
1692
+ key: element.key,
1693
+ props: await serializeProps(element.props, state),
1694
+ };
1695
+ }
1696
+
1697
+ if (element.type === REACT_COMPAT_FRAGMENT_TYPE) {
1698
+ return {
1699
+ kind: "element",
1700
+ type: { kind: "fragment" },
1701
+ key: element.key,
1702
+ props: await serializeProps(element.props, state),
1703
+ };
1704
+ }
1705
+
1706
+ throw new TypeError("Unsupported Flight element type.");
1707
+ }
1708
+
1709
+ async function serializeProps(
1710
+ props: Record<string, unknown>,
1711
+ state: FlightSerializationState,
1712
+ ): Promise<Record<string, FlightModel>> {
1713
+ const entries = await Promise.all(
1714
+ Object.entries(props).map(async ([key, value]) => [
1715
+ key,
1716
+ await serializeFlightValue(value, state),
1717
+ ] as const),
1718
+ );
1719
+
1720
+ return Object.fromEntries(entries);
1721
+ }
1722
+
1723
+ async function serializeObject(
1724
+ object: Record<string, unknown>,
1725
+ state: FlightSerializationState,
1726
+ ): Promise<FlightObjectModel> {
1727
+ return Object.fromEntries(
1728
+ await Promise.all(
1729
+ Object.entries(object).map(async ([key, value]) => [
1730
+ key,
1731
+ await serializeFlightValue(value, state),
1732
+ ] as const),
1733
+ ),
1734
+ ) as FlightObjectModel;
1735
+ }
1736
+
1737
+ function getClientReferenceId(
1738
+ reference: ClientReference,
1739
+ state: FlightSerializationState,
1740
+ ): number {
1741
+ const key = `${reference.moduleId}:${reference.exportName}`;
1742
+ const existing = state.clientReferenceIndexes.get(key);
1743
+
1744
+ if (existing !== undefined) {
1745
+ return existing;
1746
+ }
1747
+
1748
+ const id = state.clientReferences.length;
1749
+ state.clientReferences.push({
1750
+ id,
1751
+ moduleId: reference.moduleId,
1752
+ exportName: reference.exportName,
1753
+ ...(reference.chunks === undefined ? {} : { chunks: reference.chunks }),
1754
+ });
1755
+ state.clientReferenceIndexes.set(key, id);
1756
+ return id;
1757
+ }
1758
+
1759
+ async function getServerReferenceId(
1760
+ reference: ServerReference,
1761
+ state: FlightSerializationState,
1762
+ ): Promise<number> {
1763
+ const serializedBound = reference.bound === undefined
1764
+ ? undefined
1765
+ : await Promise.all(reference.bound.map((value) => serializeFlightValue(value, state)));
1766
+ const key = `${reference.moduleId}:${reference.exportName}:${JSON.stringify(serializedBound ?? null)}`;
1767
+ const existing = state.serverReferenceIndexes.get(key);
1768
+
1769
+ if (existing !== undefined) {
1770
+ return existing;
1771
+ }
1772
+
1773
+ const id = state.serverReferences.length;
1774
+ state.serverReferences.push({
1775
+ id,
1776
+ moduleId: reference.moduleId,
1777
+ exportName: reference.exportName,
1778
+ ...(serializedBound === undefined ? {} : { bound: serializedBound }),
1779
+ });
1780
+ state.serverReferenceIndexes.set(key, id);
1781
+ return id;
1782
+ }
1783
+
1784
+ function isReactCompatElement(value: unknown): value is ReactCompatElementLike {
1785
+ return (
1786
+ typeof value === "object" &&
1787
+ value !== null &&
1788
+ (value as { $$typeof?: unknown }).$$typeof === REACT_COMPAT_ELEMENT_TYPE
1789
+ );
1790
+ }
1791
+
1792
+ function isFormDataLike(value: unknown): value is FormData {
1793
+ return typeof FormData !== "undefined" && value instanceof FormData;
1794
+ }
1795
+
1796
+ function isIterableObject(value: unknown): value is Iterable<unknown> {
1797
+ return typeof value === "object" && value !== null && Symbol.iterator in value;
1798
+ }
1799
+
1800
+ function serverActionKey(moduleId: string, exportName: string): string {
1801
+ return `${moduleId}#${exportName}`;
1802
+ }
1803
+
1804
+ async function readServerActionPayload(
1805
+ request: Request,
1806
+ maxBodyBytes: number,
1807
+ ): Promise<
1808
+ | {
1809
+ moduleId?: unknown;
1810
+ exportName?: unknown;
1811
+ bound?: unknown;
1812
+ args?: unknown;
1813
+ }
1814
+ | Response
1815
+ > {
1816
+ // Issue 076: bound the body before JSON.parse. The Content-Length
1817
+ // header (when present) is the cheap pre-check; the streaming reader
1818
+ // catches chunked / missing-length bodies too.
1819
+ const declaredLength = Number(request.headers.get("content-length") ?? "NaN");
1820
+ if (Number.isFinite(declaredLength) && declaredLength > maxBodyBytes) {
1821
+ return jsonResponse({ ok: false, error: "Payload too large." }, 413);
1822
+ }
1823
+
1824
+ const body = request.body;
1825
+ let text = "";
1826
+ if (body !== null) {
1827
+ const reader = body.getReader();
1828
+ const decoder = new TextDecoder();
1829
+ let total = 0;
1830
+ try {
1831
+ while (true) {
1832
+ const { done, value } = await reader.read();
1833
+ if (done) break;
1834
+ total += value.byteLength;
1835
+ if (total > maxBodyBytes) {
1836
+ await reader.cancel();
1837
+ return jsonResponse({ ok: false, error: "Payload too large." }, 413);
1838
+ }
1839
+ text += decoder.decode(value, { stream: true });
1840
+ }
1841
+ text += decoder.decode();
1842
+ } finally {
1843
+ reader.releaseLock?.();
1844
+ }
1845
+ } else {
1846
+ text = await request.text();
1847
+ }
1848
+
1849
+ try {
1850
+ return JSON.parse(text) as {
1851
+ moduleId?: unknown;
1852
+ exportName?: unknown;
1853
+ bound?: unknown;
1854
+ args?: unknown;
1855
+ };
1856
+ } catch {
1857
+ return jsonResponse({ ok: false, error: "Invalid JSON payload." }, 400);
1858
+ }
1859
+ }
1860
+
1861
+ function getServerAction(entry: ServerAction | ServerActionDescriptor): ServerAction {
1862
+ return typeof entry === "function" ? entry : entry.action;
1863
+ }
1864
+
1865
+ function isAllowedServerAction(
1866
+ reference: ServerActionRequestReference,
1867
+ allowedActionSet: ReadonlySet<string> | undefined,
1868
+ ): boolean {
1869
+ if (allowedActionSet === undefined) {
1870
+ return true;
1871
+ }
1872
+
1873
+ return allowedActionSet.has(serverActionKey(reference.moduleId, reference.exportName));
1874
+ }
1875
+
1876
+ function getServerActionArgsValidator(
1877
+ entry: ServerAction | ServerActionDescriptor,
1878
+ ): ServerActionDescriptor["validateArgs"] {
1879
+ return typeof entry === "function" ? undefined : entry.validateArgs;
1880
+ }
1881
+
1882
+ function runWithFlightCacheScope<T>(callback: () => T): T {
1883
+ const previousScope = getGlobalCacheScope();
1884
+ setGlobalCacheScope({
1885
+ functionCaches: new WeakMap(),
1886
+ controller: new AbortController(),
1887
+ ownerStack: ["renderToFlightResponse"],
1888
+ });
1889
+
1890
+ try {
1891
+ const result = callback();
1892
+
1893
+ if (isPromiseLike(result)) {
1894
+ return Promise.resolve(result).finally(() => {
1895
+ setGlobalCacheScope(previousScope);
1896
+ }) as T;
1897
+ }
1898
+
1899
+ setGlobalCacheScope(previousScope);
1900
+ return result;
1901
+ } catch (error) {
1902
+ setGlobalCacheScope(previousScope);
1903
+ throw error;
1904
+ }
1905
+ }
1906
+
1907
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
1908
+ return (
1909
+ (typeof value === "object" || typeof value === "function") &&
1910
+ value !== null &&
1911
+ typeof (value as { then?: unknown }).then === "function"
1912
+ );
1913
+ }
1914
+
1915
+ function getGlobalCacheScope(): ServerCacheScope | undefined {
1916
+ return (globalThis as { [CACHE_SCOPE_SYMBOL]?: ServerCacheScope })[CACHE_SCOPE_SYMBOL];
1917
+ }
1918
+
1919
+ function setGlobalCacheScope(scope: ServerCacheScope | undefined): void {
1920
+ if (scope === undefined) {
1921
+ delete (globalThis as { [CACHE_SCOPE_SYMBOL]?: ServerCacheScope })[CACHE_SCOPE_SYMBOL];
1922
+ return;
1923
+ }
1924
+
1925
+ (globalThis as { [CACHE_SCOPE_SYMBOL]?: ServerCacheScope })[CACHE_SCOPE_SYMBOL] = scope;
1926
+ }
1927
+
1928
+ function validateRequestOrigin(
1929
+ request: Request,
1930
+ allowedOrigins: ServerActionHandlerOptions["allowedOrigins"],
1931
+ ): Response | undefined {
1932
+ // Issue 076: secure default. `"any"` disables the check (explicit
1933
+ // opt-out for embedders behind their own auth boundary). Otherwise we
1934
+ // enforce same-origin by default and extend with the array if given.
1935
+ if (allowedOrigins === "any") return undefined;
1936
+
1937
+ const origin = request.headers.get("origin");
1938
+ // Browsers omit `Origin` on same-origin GETs but include it on
1939
+ // cross-site POSTs. Missing Origin therefore signals same-origin
1940
+ // (or non-browser traffic) and is allowed.
1941
+ if (origin === null) return undefined;
1942
+
1943
+ let expected: string | undefined;
1944
+ try {
1945
+ expected = new URL(request.url).origin;
1946
+ } catch {
1947
+ expected = undefined;
1948
+ }
1949
+
1950
+ if (expected !== undefined && origin === expected) return undefined;
1951
+ if (allowedOrigins !== undefined && allowedOrigins.includes(origin)) {
1952
+ return undefined;
1953
+ }
1954
+ return jsonResponse({ ok: false, error: "Origin not allowed." }, 403);
1955
+ }
1956
+
1957
+ function validateCsrfToken(
1958
+ request: Request,
1959
+ csrf: ServerActionHandlerOptions["csrf"],
1960
+ ): Response | undefined {
1961
+ // Issue 076: CSRF check is on by default. `false` disables it
1962
+ // (documented opt-out); `true` / object override the names.
1963
+ if (csrf === false) return undefined;
1964
+
1965
+ const config = typeof csrf === "object" ? csrf : {};
1966
+ const headerName = config.headerName ?? "x-mreact-csrf";
1967
+ const cookieName = config.cookieName ?? "mreact.csrf";
1968
+ const headerToken = request.headers.get(headerName);
1969
+ const cookieToken = readCookie(request.headers.get("cookie"), cookieName);
1970
+
1971
+ return headerToken !== null && cookieToken !== undefined && headerToken === cookieToken
1972
+ ? undefined
1973
+ : jsonResponse({ ok: false, error: "Invalid CSRF token." }, 403);
1974
+ }
1975
+
1976
+ function validateServerActionNonce(
1977
+ request: Request,
1978
+ replayProtection: ServerActionHandlerOptions["replayProtection"],
1979
+ ): { response?: Response; commit?: () => void } {
1980
+ if (replayProtection === undefined) {
1981
+ return {};
1982
+ }
1983
+
1984
+ const headerName = replayProtection.headerName ?? "x-mreact-action-nonce";
1985
+ const nonce = request.headers.get(headerName);
1986
+
1987
+ if (nonce === null || nonce.length === 0) {
1988
+ return {
1989
+ response: jsonResponse({ ok: false, error: "Missing server action nonce." }, 400),
1990
+ };
1991
+ }
1992
+
1993
+ if (replayProtection.seen.has(nonce)) {
1994
+ return {
1995
+ response: jsonResponse(
1996
+ { ok: false, error: "Server action nonce was already used." },
1997
+ 409,
1998
+ ),
1999
+ };
2000
+ }
2001
+
2002
+ // Issue 076: defer the .add() until the action succeeds so a failed
2003
+ // run does not consume a replay slot. The caller invokes `commit()`
2004
+ // on the success path only.
2005
+ return {
2006
+ commit: () => replayProtection.seen.add(nonce),
2007
+ };
2008
+ }
2009
+
2010
+ function readCookie(cookieHeader: string | null, name: string): string | undefined {
2011
+ if (cookieHeader === null) {
2012
+ return undefined;
2013
+ }
2014
+
2015
+ for (const part of cookieHeader.split(";")) {
2016
+ const [rawKey, ...rawValue] = part.trim().split("=");
2017
+
2018
+ if (rawKey === name) {
2019
+ const raw = rawValue.join("=");
2020
+ if (raw.indexOf("%") === -1) {
2021
+ return raw;
2022
+ }
2023
+
2024
+ // Issue 076 / 072: malformed `%`-escapes raise URIError; treat
2025
+ // the cookie as absent so a bogus cookie cannot abort the handler.
2026
+ try {
2027
+ return decodeURIComponent(raw);
2028
+ } catch {
2029
+ return undefined;
2030
+ }
2031
+ }
2032
+ }
2033
+
2034
+ return undefined;
2035
+ }
2036
+
2037
+ function jsonResponse(value: unknown, status: number): Response {
2038
+ return new Response(JSON.stringify(value), {
2039
+ status,
2040
+ headers: { "content-type": "application/json" },
2041
+ });
2042
+ }
2043
+
2044
+ function serializeJsonForHtml(value: unknown): string {
2045
+ return JSON.stringify(value)
2046
+ .replaceAll("<", "\\u003c")
2047
+ .replaceAll("\u2028", "\\u2028")
2048
+ .replaceAll("\u2029", "\\u2029");
2049
+ }
2050
+
2051
+ function escapeAttribute(value: string): string {
2052
+ return value
2053
+ .replaceAll("&", "&amp;")
2054
+ .replaceAll('"', "&quot;")
2055
+ .replaceAll("<", "&lt;")
2056
+ .replaceAll(">", "&gt;");
2057
+ }