@lovrozagar/flare 0.1.2 → 0.2.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.
Files changed (85) hide show
  1. package/README.md +42 -19
  2. package/package.json +8 -6
  3. package/src/broadcast/index.ts +1 -1
  4. package/src/broadcast/provider.tsx +2 -2
  5. package/src/caches/index.ts +26 -0
  6. package/src/components/await.tsx +36 -39
  7. package/src/components/dev-error-overlay.tsx +8 -9
  8. package/src/components/direction-script.tsx +2 -2
  9. package/src/components/locale-script.tsx +2 -2
  10. package/src/components/reset-css.tsx +1 -1
  11. package/src/components/ssr-context.tsx +7 -14
  12. package/src/components/theme-script.tsx +2 -2
  13. package/src/components/view-transition-css.tsx +1 -1
  14. package/src/defer/index.ts +22 -0
  15. package/src/direction/index.tsx +61 -36
  16. package/src/errors/index.ts +45 -11
  17. package/src/fonts/component.tsx +2 -1
  18. package/src/form/index.tsx +18 -12
  19. package/src/generators/index.ts +73 -30
  20. package/src/head-client/index.ts +86 -29
  21. package/src/history/index.ts +6 -0
  22. package/src/hydrate/index.tsx +90 -60
  23. package/src/hydration/index.ts +26 -6
  24. package/src/i18n/index.ts +11 -2
  25. package/src/image/index.tsx +7 -4
  26. package/src/intercept-outlet/index.tsx +6 -2
  27. package/src/lazy/index.tsx +50 -50
  28. package/src/link/index.tsx +97 -58
  29. package/src/loader-pipeline/index.ts +119 -38
  30. package/src/locale/cookie.ts +15 -0
  31. package/src/locale/index.tsx +24 -35
  32. package/src/locale.ts +1 -0
  33. package/src/middleware/builtins/api-proxy.ts +58 -4
  34. package/src/middleware/builtins/cdn-proxy.ts +24 -3
  35. package/src/middleware/builtins/i18n.ts +23 -25
  36. package/src/middleware/builtins/keepalive.ts +2 -3
  37. package/src/mount/index.ts +8 -1
  38. package/src/navigation/index.ts +575 -129
  39. package/src/navigation/types.ts +11 -1
  40. package/src/navigation-progress/index.tsx +17 -16
  41. package/src/ndjson-client/index.ts +42 -17
  42. package/src/ndjson-server/index.ts +78 -29
  43. package/src/outlet/index.tsx +192 -97
  44. package/src/outlet/types.ts +9 -1
  45. package/src/plugins/dev-cdn-cache.ts +18 -5
  46. package/src/plugins/dev-dashboard/client.tsx +130 -118
  47. package/src/plugins/dev-prerender.ts +6 -1
  48. package/src/plugins/dev-server.ts +12 -3
  49. package/src/plugins/image-plugin.ts +18 -4
  50. package/src/plugins/index.ts +10 -7
  51. package/src/plugins/prerender-plugin.ts +3 -3
  52. package/src/plugins/server-fn.ts +15 -20
  53. package/src/plugins/solid-jsx.d.ts +3 -3
  54. package/src/plugins/sx-ast/atomic.ts +5 -5
  55. package/src/plugins/sx-ast/index.ts +9 -0
  56. package/src/plugins/sx.d.ts +4 -5
  57. package/src/plugins/virtual.ts +11 -6
  58. package/src/prerender/index.ts +68 -32
  59. package/src/protocol.ts +65 -0
  60. package/src/query-client/index.tsx +73 -14
  61. package/src/rewrite/index.ts +10 -4
  62. package/src/route-builder/create-layout.ts +1 -1
  63. package/src/route-builder/create-root-layout.ts +1 -1
  64. package/src/route-builder/types.ts +5 -1
  65. package/src/router-primitives/types.ts +2 -0
  66. package/src/security/index.ts +41 -5
  67. package/src/server/index.ts +1 -2
  68. package/src/server-fn/get-input.ts +90 -0
  69. package/src/server-fn/http-error.ts +13 -0
  70. package/src/server-fn/index.ts +94 -70
  71. package/src/server-fn-query.ts +9 -17
  72. package/src/server-handler/etag.ts +4 -2
  73. package/src/server-handler/headers.ts +1 -2
  74. package/src/server-handler/index.ts +166 -58
  75. package/src/server-handler/types.ts +2 -0
  76. package/src/service-worker/template.ts +10 -42
  77. package/src/ssr/head-prefix.ts +2 -1
  78. package/src/ssr/hoist-head-markers.ts +25 -0
  79. package/src/ssr/index.tsx +109 -92
  80. package/src/state-parser/index.ts +38 -26
  81. package/src/styles/index.ts +89 -60
  82. package/src/testing/index.ts +8 -3
  83. package/src/theme/index.tsx +35 -32
  84. package/src/tracing/timing.ts +6 -1
  85. package/src/url/index.ts +9 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * GET server-fn input codec. Client-safe (no node:async_hooks).
3
+ *
4
+ * Nested objects/arrays JSON-encode so they do not become `[object Object]`.
5
+ * Primitive arrays use repeated keys (`tag=a&tag=b`) to match the existing
6
+ * multi-value GET parser. Strings that look like JSON literals are quoted so
7
+ * `{ id: "42" }` and `{ id: 42 }` round-trip as distinct values.
8
+ */
9
+
10
+ function isPrimitive(value: unknown): value is boolean | number | string | null {
11
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
12
+ }
13
+
14
+ /** Query tokens that JSON.parse would accept — quote strings that collide with these. */
15
+ function isJsonLiteralToken(val: string): boolean {
16
+ if (val === "true" || val === "false" || val === "null") return true;
17
+ if (val.length >= 2 && val.startsWith('"') && val.endsWith('"')) return true;
18
+ if ((val.startsWith("{") && val.endsWith("}")) || (val.startsWith("[") && val.endsWith("]"))) return true;
19
+ return /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(val);
20
+ }
21
+
22
+ function encodeGetPrimitive(value: boolean | number | string | null): string {
23
+ if (typeof value === "string") return isJsonLiteralToken(value) ? JSON.stringify(value) : value;
24
+ return JSON.stringify(value);
25
+ }
26
+
27
+ function appendGetParam(params: URLSearchParams, key: string, value: unknown): void {
28
+ if (value === undefined) return;
29
+ if (isPrimitive(value)) {
30
+ params.append(key, encodeGetPrimitive(value));
31
+ return;
32
+ }
33
+ if (Array.isArray(value) && value.length === 0) {
34
+ params.append(key, "[]");
35
+ return;
36
+ }
37
+ if (Array.isArray(value) && value.every(isPrimitive)) {
38
+ for (const item of value) {
39
+ params.append(key, encodeGetPrimitive(item));
40
+ }
41
+ return;
42
+ }
43
+ params.append(key, JSON.stringify(value));
44
+ }
45
+
46
+ export function serializeGetInput(input: unknown): string {
47
+ if (input === undefined || input === null) return "";
48
+ if (typeof input !== "object" || Array.isArray(input)) {
49
+ const params = new URLSearchParams();
50
+ appendGetParam(params, "value", input);
51
+ return params.toString();
52
+ }
53
+ const params = new URLSearchParams();
54
+ for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
55
+ appendGetParam(params, key, value);
56
+ }
57
+ return params.toString();
58
+ }
59
+
60
+ export function serverFnGetUrl(path: string, input: unknown): string {
61
+ if (input === undefined) return path;
62
+ const qs = serializeGetInput(input);
63
+ return qs.length > 0 ? `${path}?${qs}` : path;
64
+ }
65
+
66
+ function parseGetValue(val: string): unknown {
67
+ if (!isJsonLiteralToken(val)) return val;
68
+ try {
69
+ return JSON.parse(val) as unknown;
70
+ } catch {
71
+ return val;
72
+ }
73
+ }
74
+
75
+ export function parseGetSearchParams(searchParams: URLSearchParams): Record<string, unknown> | undefined {
76
+ const obj: Record<string, unknown> = Object.create(null);
77
+ let hasParams = false;
78
+ for (const [key, val] of searchParams) {
79
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
80
+ hasParams = true;
81
+ const parsed = parseGetValue(val);
82
+ const existing = obj[key];
83
+ if (existing !== undefined) {
84
+ obj[key] = Array.isArray(existing) ? [...(existing as unknown[]), parsed] : [existing, parsed];
85
+ } else {
86
+ obj[key] = parsed;
87
+ }
88
+ }
89
+ return hasParams ? obj : undefined;
90
+ }
@@ -0,0 +1,13 @@
1
+ import { type FlattenedError, ServerFnValidationError } from "../errors/index.ts";
2
+
3
+ /** Map a failed server-fn HTTP body to the same error the server module throws. */
4
+ export function throwServerFnHttpError(body: unknown, status: number, name: string): never {
5
+ if (typeof body === "object" && body !== null && "errors" in body) {
6
+ throw new ServerFnValidationError((body as { errors: FlattenedError }).errors);
7
+ }
8
+ const errMsg =
9
+ typeof body === "object" && body !== null && "message" in body
10
+ ? String((body as Record<string, unknown>).message)
11
+ : `Request failed (${status})`;
12
+ throw new Error(`Server function "${name}" failed: ${errMsg}`);
13
+ }
@@ -1,12 +1,13 @@
1
- import type { FlattenedError } from "../errors/index.ts";
1
+ import { FORM_FN_FIELD, parseServerFnPathname, serverFnPath } from "../protocol.ts";
2
2
  import {
3
3
  isNotFoundError,
4
4
  isRedirectResponse,
5
5
  isServerFnValidationError,
6
6
  isUnauthenticatedError,
7
7
  isUnauthorizedError,
8
- ServerFnValidationError,
9
8
  } from "../errors/index.ts";
9
+ import { parseGetSearchParams, serverFnGetUrl } from "./get-input.ts";
10
+ import { throwServerFnHttpError } from "./http-error.ts";
10
11
  import type { RevalidateFn, RevalidateOptions } from "../revalidation/index.ts";
11
12
  import { createRevalidateFn } from "../revalidation/index.ts";
12
13
  /* package self-reference: routes through `exports.browser` -> stub on client, real on server.
@@ -284,22 +285,79 @@ function jsonResponse(data: unknown, status: number): Response {
284
285
  });
285
286
  }
286
287
 
288
+ /** Max POST body for `/_flare/server-fn/*` (JSON or form). */
289
+ export const SERVER_FN_MAX_BODY_BYTES = 1_048_576;
290
+
291
+ export async function readLimitedBody(
292
+ request: Request,
293
+ maxBytes: number,
294
+ ): Promise<{ bytes: ArrayBuffer } | { error: Response }> {
295
+ const declared = Number(request.headers.get("content-length"));
296
+ if (Number.isFinite(declared) && declared > maxBytes) {
297
+ return { error: jsonResponse({ message: "Payload too large" }, 413) };
298
+ }
299
+
300
+ const stream = request.body;
301
+ if (!stream) {
302
+ return { bytes: new ArrayBuffer(0) };
303
+ }
304
+
305
+ const reader = stream.getReader();
306
+ const chunks: Uint8Array[] = [];
307
+ let size = 0;
308
+ for (;;) {
309
+ const { done, value } = await reader.read();
310
+ if (done) break;
311
+ if (!value) continue;
312
+ size += value.byteLength;
313
+ if (size > maxBytes) {
314
+ await reader.cancel().catch(() => {});
315
+ return { error: jsonResponse({ message: "Payload too large" }, 413) };
316
+ }
317
+ chunks.push(value);
318
+ }
319
+
320
+ const bytes = new Uint8Array(size);
321
+ let offset = 0;
322
+ for (const chunk of chunks) {
323
+ bytes.set(chunk, offset);
324
+ offset += chunk.byteLength;
325
+ }
326
+ return { bytes: bytes.buffer };
327
+ }
328
+
287
329
  /* ── CSRF Origin validation ────────────────────────────────────────── */
288
330
 
331
+ const ALLOWED_FETCH_SITES = new Set(["none", "same-origin", "same-site"]);
332
+
289
333
  /**
290
- * Validate that the request Origin header matches the request URL origin.
291
- * Browsers always send Origin on POST; some browsers omit it on GET.
292
- * When Origin is present (any method), validate it — cross-origin GET
293
- * server function calls are just as dangerous as POST for CSRF.
294
- * Missing Origin is allowed since CSRF is a browser-only attack vector
295
- * and non-browser clients (curl, etc) won't have cookies.
334
+ * CSRF: Origin when present (any method), else Sec-Fetch-Site, else Referer.
335
+ * Top-level GET navigations omit Origin; SameSite=Lax still sends cookies.
336
+ * Missing all three is allowed for non-browser clients (curl, server-to-server).
296
337
  */
297
338
  function validateOrigin(request: Request): boolean {
339
+ const requestOrigin = new URL(request.url).origin;
340
+
298
341
  const origin = request.headers.get("origin");
299
- if (!origin) return true;
342
+ if (origin !== null) {
343
+ return origin === requestOrigin;
344
+ }
300
345
 
301
- const requestOrigin = new URL(request.url).origin;
302
- return origin === requestOrigin;
346
+ const site = request.headers.get("sec-fetch-site");
347
+ if (site !== null) {
348
+ return ALLOWED_FETCH_SITES.has(site);
349
+ }
350
+
351
+ const referer = request.headers.get("referer");
352
+ if (referer !== null) {
353
+ try {
354
+ return new URL(referer).origin === requestOrigin;
355
+ } catch {
356
+ return false;
357
+ }
358
+ }
359
+
360
+ return true;
303
361
  }
304
362
 
305
363
  /* ── formDataToObject ───────────────────────────────────────────────── */
@@ -307,7 +365,7 @@ function validateOrigin(request: Request): boolean {
307
365
  export function formDataToObject(formData: FormData): Record<string, File | string | string[]> {
308
366
  const obj: Record<string, File | string | string[]> = Object.create(null);
309
367
  for (const [key, val] of formData) {
310
- if (key === "__flare_fn" || key === "__proto__" || key === "constructor" || key === "prototype") continue;
368
+ if (key === FORM_FN_FIELD || key === "__proto__" || key === "constructor" || key === "prototype") continue;
311
369
  if (val instanceof File) {
312
370
  obj[key] = val;
313
371
  continue;
@@ -330,20 +388,14 @@ export async function handleServerFnRequest(
330
388
  fns: Map<string, ServerFnRegistration>,
331
389
  authenticateFn?: (env: unknown, request: Request) => unknown | Promise<unknown>,
332
390
  ): Promise<Response> {
333
- /* parse URL: /_fn/{id}/{name} */
391
+ /* parse URL: /_flare/server-fn/{id}/{name} */
334
392
  const url = new URL(request.url);
335
- const segments = url.pathname.split("/").filter(Boolean);
336
- /* segments: ["_fn", id, name] */
337
- if (segments.length < 3 || segments[0] !== "_fn") {
393
+ const parsed = parseServerFnPathname(url.pathname);
394
+ if (!parsed) {
338
395
  return jsonResponse({ message: "Server function not found" }, 404);
339
396
  }
340
397
 
341
- const id = segments[1];
342
- const name = segments[2];
343
-
344
- if (!id || !name) {
345
- return jsonResponse({ message: "Server function not found" }, 404);
346
- }
398
+ const { id, name } = parsed;
347
399
 
348
400
  /* CSRF: validate Origin before any processing */
349
401
  if (!validateOrigin(request)) {
@@ -377,36 +429,26 @@ export async function handleServerFnRequest(
377
429
  /* parse input — FormData for form submissions, JSON for programmatic calls */
378
430
  let input: unknown;
379
431
  if (registration.method === "post") {
432
+ const limited = await readLimitedBody(request, SERVER_FN_MAX_BODY_BYTES);
433
+ if ("error" in limited) return limited.error;
380
434
  const contentType = request.headers.get("content-type") ?? "";
381
435
  if (contentType.includes("multipart/form-data") || contentType.includes("x-www-form-urlencoded")) {
382
- input = formDataToObject(await request.formData());
383
- } else {
384
- const text = await request.text();
385
- if (text) {
386
- try {
387
- input = JSON.parse(text);
388
- } catch {
389
- return jsonResponse({ message: "Invalid JSON" }, 400);
390
- }
436
+ const formRequest = new Request(request.url, {
437
+ body: limited.bytes,
438
+ headers: { "content-type": contentType },
439
+ method: "POST",
440
+ });
441
+ input = formDataToObject(await formRequest.formData());
442
+ } else if (limited.bytes.byteLength > 0) {
443
+ const text = new TextDecoder().decode(limited.bytes);
444
+ try {
445
+ input = JSON.parse(text);
446
+ } catch {
447
+ return jsonResponse({ message: "Invalid JSON" }, 400);
391
448
  }
392
449
  }
393
450
  } else {
394
- const params = url.searchParams;
395
- const obj: Record<string, string | string[]> = Object.create(null);
396
- let hasParams = false;
397
- for (const [key, val] of params) {
398
- if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
399
- hasParams = true;
400
- const existing = obj[key];
401
- if (existing !== undefined) {
402
- obj[key] = Array.isArray(existing) ? [...existing, val] : [existing, val];
403
- } else {
404
- obj[key] = val;
405
- }
406
- }
407
- if (hasParams) {
408
- input = obj;
409
- }
451
+ input = parseGetSearchParams(url.searchParams);
410
452
  }
411
453
 
412
454
  /* validate input — ServerFnValidationError propagates to outer catch for structured errors */
@@ -577,16 +619,12 @@ export function serverFnQueryOptions<TInput, TOutput>(
577
619
  }
578
620
 
579
621
  /* client: HTTP fetch */
580
- const url = `/_fn/${id}/${name}`;
622
+ const url = serverFnPath(id, name);
581
623
  const method = reg?.method ?? "post";
582
624
 
583
625
  const res =
584
626
  method === "get"
585
- ? await fetch(
586
- config?.input !== undefined
587
- ? `${url}?${new URLSearchParams(config.input as Record<string, string>)}`
588
- : url,
589
- )
627
+ ? await fetch(serverFnGetUrl(url, config?.input))
590
628
  : await fetch(url, {
591
629
  body: config?.input !== undefined ? JSON.stringify(config.input) : undefined,
592
630
  headers: { "content-type": "application/json" },
@@ -595,14 +633,7 @@ export function serverFnQueryOptions<TInput, TOutput>(
595
633
 
596
634
  if (!res.ok) {
597
635
  const body: unknown = await res.json().catch(() => null);
598
- if (typeof body === "object" && body !== null && "errors" in body) {
599
- throw new ServerFnValidationError((body as { errors: FlattenedError }).errors);
600
- }
601
- const errMsg =
602
- typeof body === "object" && body !== null && "message" in body
603
- ? String((body as Record<string, unknown>).message)
604
- : `Request failed (${res.status})`;
605
- throw new Error(`Server function "${name}" failed: ${errMsg}`);
636
+ throwServerFnHttpError(body, res.status, name);
606
637
  }
607
638
 
608
639
  const json = (await res.json()) as {
@@ -658,7 +689,7 @@ export function serverFnMutationOptions<TInput, TOutput>(
658
689
  }
659
690
 
660
691
  /* client: HTTP fetch — mutations always POST */
661
- const url = `/_fn/${id}/${name}`;
692
+ const url = serverFnPath(id, name);
662
693
  const res = await fetch(url, {
663
694
  body: input !== undefined ? JSON.stringify(input) : undefined,
664
695
  headers: { "content-type": "application/json" },
@@ -667,14 +698,7 @@ export function serverFnMutationOptions<TInput, TOutput>(
667
698
 
668
699
  if (!res.ok) {
669
700
  const body: unknown = await res.json().catch(() => null);
670
- if (typeof body === "object" && body !== null && "errors" in body) {
671
- throw new ServerFnValidationError((body as { errors: FlattenedError }).errors);
672
- }
673
- const errMsg =
674
- typeof body === "object" && body !== null && "message" in body
675
- ? String((body as Record<string, unknown>).message)
676
- : `Request failed (${res.status})`;
677
- throw new Error(`Server function "${name}" failed: ${errMsg}`);
701
+ throwServerFnHttpError(body, res.status, name);
678
702
  }
679
703
 
680
704
  const json = (await res.json()) as {
@@ -4,6 +4,10 @@
4
4
  * to avoid pulling in node:async_hooks on the client.
5
5
  */
6
6
 
7
+ import { serverFnGetUrl } from "./server-fn/get-input.ts";
8
+ import { throwServerFnHttpError } from "./server-fn/http-error.ts";
9
+ import { serverFnPath } from "./protocol.ts";
10
+
7
11
  export interface PiggybackedQuery {
8
12
  data: unknown;
9
13
  key: unknown[];
@@ -48,16 +52,12 @@ export function serverFnQueryOptions<TInput, TOutput>(
48
52
  return serverFn(config?.input as TInput);
49
53
  }
50
54
 
51
- const url = `/_fn/${id}/${name}`;
55
+ const url = serverFnPath(id, name);
52
56
  const method = reg?.method ?? "post";
53
57
 
54
58
  const res =
55
59
  method === "get"
56
- ? await fetch(
57
- config?.input !== undefined
58
- ? `${url}?${new URLSearchParams(config.input as Record<string, string>)}`
59
- : url,
60
- )
60
+ ? await fetch(serverFnGetUrl(url, config?.input))
61
61
  : await fetch(url, {
62
62
  body: config?.input !== undefined ? JSON.stringify(config.input) : undefined,
63
63
  headers: { "content-type": "application/json" },
@@ -66,11 +66,7 @@ export function serverFnQueryOptions<TInput, TOutput>(
66
66
 
67
67
  if (!res.ok) {
68
68
  const body: unknown = await res.json().catch(() => null);
69
- const errMsg =
70
- typeof body === "object" && body !== null && "message" in body
71
- ? String((body as Record<string, unknown>).message)
72
- : `Request failed (${res.status})`;
73
- throw new Error(`Server function request failed: ${errMsg}`);
69
+ throwServerFnHttpError(body, res.status, name);
74
70
  }
75
71
 
76
72
  const json = (await res.json()) as {
@@ -121,7 +117,7 @@ export function serverFnMutationOptions<TInput, TOutput>(
121
117
  return serverFn(input);
122
118
  }
123
119
 
124
- const url = `/_fn/${id}/${name}`;
120
+ const url = serverFnPath(id, name);
125
121
  const res = await fetch(url, {
126
122
  body: input !== undefined ? JSON.stringify(input) : undefined,
127
123
  headers: { "content-type": "application/json" },
@@ -130,11 +126,7 @@ export function serverFnMutationOptions<TInput, TOutput>(
130
126
 
131
127
  if (!res.ok) {
132
128
  const body: unknown = await res.json().catch(() => null);
133
- const errMsg =
134
- typeof body === "object" && body !== null && "message" in body
135
- ? String((body as Record<string, unknown>).message)
136
- : `Request failed (${res.status})`;
137
- throw new Error(`Server function request failed: ${errMsg}`);
129
+ throwServerFnHttpError(body, res.status, name);
138
130
  }
139
131
 
140
132
  const json = (await res.json()) as {
@@ -4,6 +4,8 @@
4
4
  * the actual bytes while content is semantically identical.
5
5
  */
6
6
 
7
+ import { HEADER_DATA } from "../protocol.ts";
8
+
7
9
  /** Compute weak ETag from content using SHA-256, truncated to 16 hex chars */
8
10
  export async function computeEtag(body: string): Promise<string> {
9
11
  const encoded = new TextEncoder().encode(body);
@@ -35,11 +37,11 @@ export function weakMatch(ifNoneMatch: string, etag: string): boolean {
35
37
  }
36
38
 
37
39
  /**
38
- * Build Vary header value. Always includes `x-d` (flare's NDJSON protocol header).
40
+ * Build Vary header value. Always includes `flare-data` (NDJSON protocol header).
39
41
  * Additional values are appended, with deduplication.
40
42
  */
41
43
  export function buildVaryHeader(additional?: string[]): string {
42
- const values = ["x-d"];
44
+ const values = [HEADER_DATA];
43
45
  if (additional) {
44
46
  for (const v of additional) {
45
47
  if (!values.some((existing) => existing.toLowerCase() === v.toLowerCase())) {
@@ -1,5 +1,4 @@
1
1
  export type FlareCacheStatus = "HIT" | "MISS" | "STALE";
2
2
  export type FlareRenderMode = "ISR" | "SSG" | "SSR";
3
3
 
4
- export const FLARE_CACHE_HEADER = "Flare-Cache" as const;
5
- export const FLARE_RENDER_HEADER = "Flare-Render" as const;
4
+ export { FLARE_CACHE_HEADER, FLARE_RENDER_HEADER } from "../protocol.ts";