@solidjs/web 2.0.0-rc.1 → 2.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@solidjs/web",
3
3
  "description": "Solid's web runtime: client rendering, hydration, SSR, and DOM-specific control flow (Portal, Dynamic).",
4
- "version": "2.0.0-rc.1",
4
+ "version": "2.0.0-rc.2",
5
5
  "author": "Ryan Carniato",
6
6
  "license": "MIT",
7
7
  "homepage": "https://solidjs.com",
@@ -378,11 +378,11 @@
378
378
  "seroval-plugins": "~1.5.4"
379
379
  },
380
380
  "peerDependencies": {
381
- "solid-js": "^2.0.0-rc.1"
381
+ "solid-js": "^2.0.0-rc.2"
382
382
  },
383
383
  "devDependencies": {
384
384
  "@codspeed/vitest-plugin": "^5.4.0",
385
- "solid-js": "2.0.0-rc.1"
385
+ "solid-js": "2.0.0-rc.2"
386
386
  },
387
387
  "scripts": {
388
388
  "build": "npm-run-all -nl build:clean types:copy-jsx build:js",
@@ -8,6 +8,9 @@ const state = globalThis[STATE] || (globalThis[STATE] = {
8
8
  materialized: new WeakMap(),
9
9
  materializedValues: new WeakSet()
10
10
  });
11
+ function setContainerTraceStreamMint(fn) {
12
+ state.streamOf = fn;
13
+ }
11
14
  const TRACE = Symbol.for("dom-expressions.container-trace");
12
15
  function materialize(marker) {
13
16
  let value = state.materialized.get(marker.$tr);
@@ -20,9 +23,10 @@ function materialize(marker) {
20
23
  }
21
24
  function parseTrace(value, ctx) {
22
25
  const trace = value[TRACE];
26
+ const sub = trace.subscribe();
23
27
  return {
24
28
  a: trace.array ? 1 : 0,
25
- i: ctx.parse(trace.subscribe())
29
+ i: ctx.parse(state.streamOf ? state.streamOf(sub) : sub)
26
30
  };
27
31
  }
28
32
  const ContainerTracePlugin = {
@@ -36,9 +40,10 @@ const ContainerTracePlugin = {
36
40
  },
37
41
  async async(value, ctx) {
38
42
  const trace = value[TRACE];
43
+ const sub = trace.subscribe();
39
44
  return {
40
45
  a: trace.array ? 1 : 0,
41
- i: await ctx.parse(trace.subscribe())
46
+ i: await ctx.parse(state.streamOf ? state.streamOf(sub) : sub)
42
47
  };
43
48
  },
44
49
  stream: parseTrace
@@ -56,6 +61,18 @@ const ContainerTracePlugin = {
56
61
  }
57
62
  };
58
63
 
64
+ setContainerTraceStreamMint(iterable => {
65
+ const stream = seroval.createStream();
66
+ (async () => {
67
+ try {
68
+ for await (const value of iterable) stream.next(value);
69
+ stream.return(undefined);
70
+ } catch (error) {
71
+ stream.throw(error);
72
+ }
73
+ })();
74
+ return stream;
75
+ });
59
76
  const DEFAULT_WEB_PLUGINS = Object.freeze([web.AbortSignalPlugin,
60
77
  web.CustomEventPlugin, web.DOMExceptionPlugin, web.EventPlugin,
61
78
  web.FormDataPlugin, web.HeadersPlugin, web.ReadableStreamPlugin, web.RequestPlugin, web.ResponsePlugin, web.URLSearchParamsPlugin, web.URLPlugin,
@@ -1,4 +1,4 @@
1
- import { Feature, fromCrossJSON } from 'seroval';
1
+ import { Feature, fromCrossJSON, createStream } from 'seroval';
2
2
  import { AbortSignalPlugin, CustomEventPlugin, DOMExceptionPlugin, EventPlugin, FormDataPlugin, HeadersPlugin, ReadableStreamPlugin, RequestPlugin, ResponsePlugin, URLSearchParamsPlugin, URLPlugin } from 'seroval-plugins/web';
3
3
 
4
4
  const STATE = Symbol.for("dom-expressions.container-trace-state");
@@ -6,6 +6,9 @@ const state = globalThis[STATE] || (globalThis[STATE] = {
6
6
  materialized: new WeakMap(),
7
7
  materializedValues: new WeakSet()
8
8
  });
9
+ function setContainerTraceStreamMint(fn) {
10
+ state.streamOf = fn;
11
+ }
9
12
  const TRACE = Symbol.for("dom-expressions.container-trace");
10
13
  function materialize(marker) {
11
14
  let value = state.materialized.get(marker.$tr);
@@ -18,9 +21,10 @@ function materialize(marker) {
18
21
  }
19
22
  function parseTrace(value, ctx) {
20
23
  const trace = value[TRACE];
24
+ const sub = trace.subscribe();
21
25
  return {
22
26
  a: trace.array ? 1 : 0,
23
- i: ctx.parse(trace.subscribe())
27
+ i: ctx.parse(state.streamOf ? state.streamOf(sub) : sub)
24
28
  };
25
29
  }
26
30
  const ContainerTracePlugin = {
@@ -34,9 +38,10 @@ const ContainerTracePlugin = {
34
38
  },
35
39
  async async(value, ctx) {
36
40
  const trace = value[TRACE];
41
+ const sub = trace.subscribe();
37
42
  return {
38
43
  a: trace.array ? 1 : 0,
39
- i: await ctx.parse(trace.subscribe())
44
+ i: await ctx.parse(state.streamOf ? state.streamOf(sub) : sub)
40
45
  };
41
46
  },
42
47
  stream: parseTrace
@@ -54,6 +59,18 @@ const ContainerTracePlugin = {
54
59
  }
55
60
  };
56
61
 
62
+ setContainerTraceStreamMint(iterable => {
63
+ const stream = createStream();
64
+ (async () => {
65
+ try {
66
+ for await (const value of iterable) stream.next(value);
67
+ stream.return(undefined);
68
+ } catch (error) {
69
+ stream.throw(error);
70
+ }
71
+ })();
72
+ return stream;
73
+ });
57
74
  const DEFAULT_WEB_PLUGINS = Object.freeze([AbortSignalPlugin,
58
75
  CustomEventPlugin, DOMExceptionPlugin, EventPlugin,
59
76
  FormDataPlugin, HeadersPlugin, ReadableStreamPlugin, RequestPlugin, ResponsePlugin, URLSearchParamsPlugin, URLPlugin,
@@ -8,6 +8,9 @@ const state = globalThis[STATE] || (globalThis[STATE] = {
8
8
  materialized: new WeakMap(),
9
9
  materializedValues: new WeakSet()
10
10
  });
11
+ function setContainerTraceStreamMint(fn) {
12
+ state.streamOf = fn;
13
+ }
11
14
  const TRACE = Symbol.for("dom-expressions.container-trace");
12
15
  function materialize(marker) {
13
16
  let value = state.materialized.get(marker.$tr);
@@ -20,9 +23,10 @@ function materialize(marker) {
20
23
  }
21
24
  function parseTrace(value, ctx) {
22
25
  const trace = value[TRACE];
26
+ const sub = trace.subscribe();
23
27
  return {
24
28
  a: trace.array ? 1 : 0,
25
- i: ctx.parse(trace.subscribe())
29
+ i: ctx.parse(state.streamOf ? state.streamOf(sub) : sub)
26
30
  };
27
31
  }
28
32
  const ContainerTracePlugin = {
@@ -36,9 +40,10 @@ const ContainerTracePlugin = {
36
40
  },
37
41
  async async(value, ctx) {
38
42
  const trace = value[TRACE];
43
+ const sub = trace.subscribe();
39
44
  return {
40
45
  a: trace.array ? 1 : 0,
41
- i: await ctx.parse(trace.subscribe())
46
+ i: await ctx.parse(state.streamOf ? state.streamOf(sub) : sub)
42
47
  };
43
48
  },
44
49
  stream: parseTrace
@@ -56,6 +61,18 @@ const ContainerTracePlugin = {
56
61
  }
57
62
  };
58
63
 
64
+ setContainerTraceStreamMint(iterable => {
65
+ const stream = seroval.createStream();
66
+ (async () => {
67
+ try {
68
+ for await (const value of iterable) stream.next(value);
69
+ stream.return(undefined);
70
+ } catch (error) {
71
+ stream.throw(error);
72
+ }
73
+ })();
74
+ return stream;
75
+ });
59
76
  const DEFAULT_WEB_PLUGINS = Object.freeze([web.AbortSignalPlugin,
60
77
  web.CustomEventPlugin, web.DOMExceptionPlugin, web.EventPlugin,
61
78
  web.FormDataPlugin, web.HeadersPlugin, web.ReadableStreamPlugin, web.RequestPlugin, web.ResponsePlugin, web.URLSearchParamsPlugin, web.URLPlugin,
@@ -1,4 +1,4 @@
1
- import { Feature, fromCrossJSON, toCrossJSONStream, Serializer, getCrossReferenceHeader } from 'seroval';
1
+ import { Feature, fromCrossJSON, createStream, toCrossJSONStream, Serializer, getCrossReferenceHeader } from 'seroval';
2
2
  export { OpaqueReference, createPlugin } from 'seroval';
3
3
  import { AbortSignalPlugin, CustomEventPlugin, DOMExceptionPlugin, EventPlugin, FormDataPlugin, HeadersPlugin, ReadableStreamPlugin, RequestPlugin, ResponsePlugin, URLSearchParamsPlugin, URLPlugin } from 'seroval-plugins/web';
4
4
 
@@ -7,6 +7,9 @@ const state = globalThis[STATE] || (globalThis[STATE] = {
7
7
  materialized: new WeakMap(),
8
8
  materializedValues: new WeakSet()
9
9
  });
10
+ function setContainerTraceStreamMint(fn) {
11
+ state.streamOf = fn;
12
+ }
10
13
  const TRACE = Symbol.for("dom-expressions.container-trace");
11
14
  function materialize(marker) {
12
15
  let value = state.materialized.get(marker.$tr);
@@ -19,9 +22,10 @@ function materialize(marker) {
19
22
  }
20
23
  function parseTrace(value, ctx) {
21
24
  const trace = value[TRACE];
25
+ const sub = trace.subscribe();
22
26
  return {
23
27
  a: trace.array ? 1 : 0,
24
- i: ctx.parse(trace.subscribe())
28
+ i: ctx.parse(state.streamOf ? state.streamOf(sub) : sub)
25
29
  };
26
30
  }
27
31
  const ContainerTracePlugin = {
@@ -35,9 +39,10 @@ const ContainerTracePlugin = {
35
39
  },
36
40
  async async(value, ctx) {
37
41
  const trace = value[TRACE];
42
+ const sub = trace.subscribe();
38
43
  return {
39
44
  a: trace.array ? 1 : 0,
40
- i: await ctx.parse(trace.subscribe())
45
+ i: await ctx.parse(state.streamOf ? state.streamOf(sub) : sub)
41
46
  };
42
47
  },
43
48
  stream: parseTrace
@@ -55,6 +60,18 @@ const ContainerTracePlugin = {
55
60
  }
56
61
  };
57
62
 
63
+ setContainerTraceStreamMint(iterable => {
64
+ const stream = createStream();
65
+ (async () => {
66
+ try {
67
+ for await (const value of iterable) stream.next(value);
68
+ stream.return(undefined);
69
+ } catch (error) {
70
+ stream.throw(error);
71
+ }
72
+ })();
73
+ return stream;
74
+ });
58
75
  const DEFAULT_WEB_PLUGINS = Object.freeze([AbortSignalPlugin,
59
76
  CustomEventPlugin, DOMExceptionPlugin, EventPlugin,
60
77
  FormDataPlugin, HeadersPlugin, ReadableStreamPlugin, RequestPlugin, ResponsePlugin, URLSearchParamsPlugin, URLPlugin,
@@ -396,6 +396,30 @@ const config = {
396
396
  responseHandler: undefined,
397
397
  serializeArgs: undefined
398
398
  };
399
+ const CALL_OBSERVERS = new Set();
400
+ function notifyCallObservers(type, id, instance, value, meta) {
401
+ if (CALL_OBSERVERS.size === 0) return;
402
+ const field = type === "request" ? "request" : "response";
403
+ const time = performance.now();
404
+ for (const observer of new Set(CALL_OBSERVERS)) {
405
+ try {
406
+ observer({
407
+ type,
408
+ id,
409
+ instance,
410
+ [field]: value.clone(),
411
+ meta,
412
+ time
413
+ });
414
+ } catch (error) {
415
+ console.error(error);
416
+ }
417
+ }
418
+ }
419
+ function observeServerFunctionCalls(observer) {
420
+ CALL_OBSERVERS.add(observer);
421
+ return () => CALL_OBSERVERS.delete(observer);
422
+ }
399
423
  function serializeArguments(args) {
400
424
  if (!config.serializeArgs) {
401
425
  throw new Error("Server function arguments are sent as JSON by default and these " + "arguments are not JSON-serializable. Call enableRichArguments() " + '(from "@solidjs/web/server-functions/rich-args") once at startup ' + "to send Dates, Maps, Sets, typed arrays, etc. through the codec — " + "or pass a single Blob/FormData/File argument, which has a native " + "HTTP encoding.");
@@ -425,6 +449,11 @@ function provideRPC() {
425
449
  decodeResponse
426
450
  });
427
451
  }
452
+ function serverFunctionFailure(response, value) {
453
+ const error = value ?? new Error(`Server function call failed with status ${response.status}`);
454
+ if (error instanceof Error && !("status" in error)) error.status = response.status;
455
+ return error;
456
+ }
428
457
  async function createRequest(base, id, instance, options, meta) {
429
458
  const headers = {
430
459
  ...options.headers,
@@ -445,7 +474,12 @@ async function createRequest(base, id, instance, options, meta) {
445
474
  meta
446
475
  })) || init;
447
476
  }
448
- return fetch(base, init);
477
+ if (CALL_OBSERVERS.size === 0) return fetch(base, init);
478
+ const request = new Request(new URL(base, globalThis.location?.href || "http://localhost"), init);
479
+ notifyCallObservers("request", id, instance, request, meta);
480
+ const response = await fetch(request);
481
+ notifyCallObservers("response", id, instance, response, meta);
482
+ return response;
449
483
  }
450
484
  async function initializeResponse(base, id, instance, options, args, meta) {
451
485
  if (args.length === 0) {
@@ -528,6 +562,7 @@ async function fetchServerFunction(base, id, options, args, meta, callArgs = arg
528
562
  });
529
563
  if (handled !== undefined) return handled;
530
564
  }
565
+ const failed = response.headers.has(ERROR_HEADER) || response.status >= 500;
531
566
  if (response.headers.has(SINGLE_FLIGHT_HEADER)) {
532
567
  const consumer = getFlightDataConsumer();
533
568
  if (consumer) {
@@ -535,8 +570,8 @@ async function fetchServerFunction(base, id, options, args, meta, callArgs = arg
535
570
  await consumer(payload.data, {
536
571
  response
537
572
  });
538
- if (response.headers.has(ERROR_HEADER) && !response.headers.has("Location") && !response.headers.has(REVALIDATE_HEADER)) {
539
- throw payload.value;
573
+ if (failed && !response.headers.has("Location") && !response.headers.has(REVALIDATE_HEADER)) {
574
+ throw serverFunctionFailure(response, payload.value);
540
575
  }
541
576
  return payload.value;
542
577
  }
@@ -545,8 +580,8 @@ async function fetchServerFunction(base, id, options, args, meta, callArgs = arg
545
580
  return response;
546
581
  }
547
582
  const result = await decodeResponse(response.clone());
548
- if (response.headers.has(ERROR_HEADER)) {
549
- throw result;
583
+ if (failed) {
584
+ throw serverFunctionFailure(response, result);
550
585
  }
551
586
  if (controller && result?.[Symbol.asyncIterator]) {
552
587
  return {
@@ -658,10 +693,10 @@ function live(fn) {
658
693
  iterable.onstatus && iterable.onstatus(state, error);
659
694
  } catch {}
660
695
  };
661
- const emitClosed = () => {
696
+ const emitClosed = error => {
662
697
  if (ended) return;
663
698
  ended = true;
664
- emit("closed");
699
+ emit("closed", error);
665
700
  };
666
701
  const closeIt = value => {
667
702
  const current = it;
@@ -713,6 +748,11 @@ function live(fn) {
713
748
  return r;
714
749
  } catch (error) {
715
750
  if (!connected) throw error;
751
+ if (error !== null && typeof error === "object" && typeof error.status === "number" && error.status >= 400 && error.status < 500) {
752
+ stopped = true;
753
+ emitClosed(error);
754
+ throw error;
755
+ }
716
756
  it = undefined;
717
757
  emit("reconnecting", error);
718
758
  await new Promise(resolve => {
@@ -787,6 +827,7 @@ exports.getServerFunctionsCodec = getServerFunctionsCodec;
787
827
  exports.hasFlashCookie = hasFlashCookie;
788
828
  exports.isServerFunction = isServerFunction;
789
829
  exports.live = live;
830
+ exports.observeServerFunctionCalls = observeServerFunctionCalls;
790
831
  exports.registerServerReference = registerServerReference;
791
832
  exports.serializeString = serializeString;
792
833
  exports.subscribeFlightData = subscribeFlightData;
@@ -394,6 +394,30 @@ const config = {
394
394
  responseHandler: undefined,
395
395
  serializeArgs: undefined
396
396
  };
397
+ const CALL_OBSERVERS = new Set();
398
+ function notifyCallObservers(type, id, instance, value, meta) {
399
+ if (CALL_OBSERVERS.size === 0) return;
400
+ const field = type === "request" ? "request" : "response";
401
+ const time = performance.now();
402
+ for (const observer of new Set(CALL_OBSERVERS)) {
403
+ try {
404
+ observer({
405
+ type,
406
+ id,
407
+ instance,
408
+ [field]: value.clone(),
409
+ meta,
410
+ time
411
+ });
412
+ } catch (error) {
413
+ console.error(error);
414
+ }
415
+ }
416
+ }
417
+ function observeServerFunctionCalls(observer) {
418
+ CALL_OBSERVERS.add(observer);
419
+ return () => CALL_OBSERVERS.delete(observer);
420
+ }
397
421
  function serializeArguments(args) {
398
422
  if (!config.serializeArgs) {
399
423
  throw new Error("Server function arguments are sent as JSON by default and these " + "arguments are not JSON-serializable. Call enableRichArguments() " + '(from "@solidjs/web/server-functions/rich-args") once at startup ' + "to send Dates, Maps, Sets, typed arrays, etc. through the codec — " + "or pass a single Blob/FormData/File argument, which has a native " + "HTTP encoding.");
@@ -423,6 +447,11 @@ function provideRPC() {
423
447
  decodeResponse
424
448
  });
425
449
  }
450
+ function serverFunctionFailure(response, value) {
451
+ const error = value ?? new Error(`Server function call failed with status ${response.status}`);
452
+ if (error instanceof Error && !("status" in error)) error.status = response.status;
453
+ return error;
454
+ }
426
455
  async function createRequest(base, id, instance, options, meta) {
427
456
  const headers = {
428
457
  ...options.headers,
@@ -443,7 +472,12 @@ async function createRequest(base, id, instance, options, meta) {
443
472
  meta
444
473
  })) || init;
445
474
  }
446
- return fetch(base, init);
475
+ if (CALL_OBSERVERS.size === 0) return fetch(base, init);
476
+ const request = new Request(new URL(base, globalThis.location?.href || "http://localhost"), init);
477
+ notifyCallObservers("request", id, instance, request, meta);
478
+ const response = await fetch(request);
479
+ notifyCallObservers("response", id, instance, response, meta);
480
+ return response;
447
481
  }
448
482
  async function initializeResponse(base, id, instance, options, args, meta) {
449
483
  if (args.length === 0) {
@@ -526,6 +560,7 @@ async function fetchServerFunction(base, id, options, args, meta, callArgs = arg
526
560
  });
527
561
  if (handled !== undefined) return handled;
528
562
  }
563
+ const failed = response.headers.has(ERROR_HEADER) || response.status >= 500;
529
564
  if (response.headers.has(SINGLE_FLIGHT_HEADER)) {
530
565
  const consumer = getFlightDataConsumer();
531
566
  if (consumer) {
@@ -533,8 +568,8 @@ async function fetchServerFunction(base, id, options, args, meta, callArgs = arg
533
568
  await consumer(payload.data, {
534
569
  response
535
570
  });
536
- if (response.headers.has(ERROR_HEADER) && !response.headers.has("Location") && !response.headers.has(REVALIDATE_HEADER)) {
537
- throw payload.value;
571
+ if (failed && !response.headers.has("Location") && !response.headers.has(REVALIDATE_HEADER)) {
572
+ throw serverFunctionFailure(response, payload.value);
538
573
  }
539
574
  return payload.value;
540
575
  }
@@ -543,8 +578,8 @@ async function fetchServerFunction(base, id, options, args, meta, callArgs = arg
543
578
  return response;
544
579
  }
545
580
  const result = await decodeResponse(response.clone());
546
- if (response.headers.has(ERROR_HEADER)) {
547
- throw result;
581
+ if (failed) {
582
+ throw serverFunctionFailure(response, result);
548
583
  }
549
584
  if (controller && result?.[Symbol.asyncIterator]) {
550
585
  return {
@@ -656,10 +691,10 @@ function live(fn) {
656
691
  iterable.onstatus && iterable.onstatus(state, error);
657
692
  } catch {}
658
693
  };
659
- const emitClosed = () => {
694
+ const emitClosed = error => {
660
695
  if (ended) return;
661
696
  ended = true;
662
- emit("closed");
697
+ emit("closed", error);
663
698
  };
664
699
  const closeIt = value => {
665
700
  const current = it;
@@ -711,6 +746,11 @@ function live(fn) {
711
746
  return r;
712
747
  } catch (error) {
713
748
  if (!connected) throw error;
749
+ if (error !== null && typeof error === "object" && typeof error.status === "number" && error.status >= 400 && error.status < 500) {
750
+ stopped = true;
751
+ emitClosed(error);
752
+ throw error;
753
+ }
714
754
  it = undefined;
715
755
  emit("reconnecting", error);
716
756
  await new Promise(resolve => {
@@ -760,4 +800,4 @@ function getServerFunctionInvocation() {
760
800
  return undefined;
761
801
  }
762
802
 
763
- export { ChunkReader, ERROR_HEADER, FLASH_COOKIE, FUNCTION_HEADER, GET, INSTANCE_HEADER, REVALIDATE_HEADER, SINGLE_FLIGHT_HEADER, clearFlashCookie, configureServerFunctionsClient, createChunk, createServerReference, decodeErrorHeaderValue, decodeResponse, decodeResponsePayload, deserializeStream, encodeErrorHeaderValue, frameAddress, getFlightDataConsumer, getServerFunctionInvocation, getServerFunctionMetadata, getServerFunctionsCodec, hasFlashCookie, isServerFunction, live, registerServerReference, serializeString, subscribeFlightData, withMeta };
803
+ export { ChunkReader, ERROR_HEADER, FLASH_COOKIE, FUNCTION_HEADER, GET, INSTANCE_HEADER, REVALIDATE_HEADER, SINGLE_FLIGHT_HEADER, clearFlashCookie, configureServerFunctionsClient, createChunk, createServerReference, decodeErrorHeaderValue, decodeResponse, decodeResponsePayload, deserializeStream, encodeErrorHeaderValue, frameAddress, getFlightDataConsumer, getServerFunctionInvocation, getServerFunctionMetadata, getServerFunctionsCodec, hasFlashCookie, isServerFunction, live, observeServerFunctionCalls, registerServerReference, serializeString, subscribeFlightData, withMeta };
@@ -362,6 +362,7 @@ async function decodeResponsePayload(response, codecOptions) {
362
362
  };
363
363
  }
364
364
 
365
+ typeof setImmediate === "function" ? setImmediate : fn => setTimeout(fn, 0);
365
366
  const RequestContext = Symbol.for("solid.RequestContext");
366
367
  function getRequestEvent() {
367
368
  return globalThis[RequestContext] ? globalThis[RequestContext].getStore() || solidJs.sharedConfig.context && solidJs.sharedConfig.context.event || console.warn("RequestEvent is missing. This is most likely due to accessing `getRequestEvent` non-managed async scope in a partially polyfilled environment. Try moving it above all `await` calls.") : undefined;
@@ -493,7 +494,8 @@ const config = {
493
494
  transformFlightResult: undefined,
494
495
  transformDirectResult: undefined,
495
496
  handleNoJS: undefined,
496
- endpoint: "/_server"
497
+ endpoint: "/_server",
498
+ csrf: true
497
499
  };
498
500
  function configureServerFunctionsServer({
499
501
  provideEvent,
@@ -504,6 +506,7 @@ function configureServerFunctionsServer({
504
506
  transformDirectResult,
505
507
  handleNoJS,
506
508
  endpoint,
509
+ csrf,
507
510
  codec
508
511
  } = {}) {
509
512
  if (provideEvent !== undefined) config.provideEvent = provideEvent;
@@ -514,6 +517,7 @@ function configureServerFunctionsServer({
514
517
  if (transformDirectResult !== undefined) config.transformDirectResult = transformDirectResult;
515
518
  if (handleNoJS !== undefined) config.handleNoJS = handleNoJS;
516
519
  if (endpoint !== undefined) config.endpoint = endpoint;
520
+ if (csrf !== undefined) config.csrf = csrf;
517
521
  if (codec !== undefined) configureServerFunctionsCodec(codec);
518
522
  }
519
523
  function provideEvent(event, fn) {
@@ -950,31 +954,96 @@ function sanitizeServerError(value) {
950
954
  if (isSafeError(value)) return value;
951
955
  return new Error(GENERIC_SERVER_ERROR_MESSAGE);
952
956
  }
957
+ function observeServerFunctionCalls() {
958
+ return () => {};
959
+ }
960
+ async function matchesOrigin(origin, request, matcher) {
961
+ if (matcher === undefined) return origin === new URL(request.url).origin;
962
+ if (typeof matcher === "function") return !!(await matcher(origin, request));
963
+ return Array.isArray(matcher) ? matcher.includes(origin) : origin === matcher;
964
+ }
965
+ async function allowsServerFunctionRequest(request, options) {
966
+ const fetchSite = request.headers.get("Sec-Fetch-Site");
967
+ if (fetchSite === "same-origin") return true;
968
+ if (fetchSite === "same-site" || fetchSite === "cross-site" || fetchSite === "none") {
969
+ return false;
970
+ }
971
+ const origin = request.headers.get("Origin");
972
+ if (origin !== null) return matchesOrigin(origin, request, options.origin);
973
+ const referer = request.headers.get("Referer");
974
+ if (referer !== null) {
975
+ try {
976
+ return matchesOrigin(new URL(referer).origin, request, options.origin);
977
+ } catch {
978
+ return false;
979
+ }
980
+ }
981
+ return options.allowRequestsWithoutOriginCheck === true;
982
+ }
983
+ const CSRF_VARY = ["Sec-Fetch-Site", "Origin", "Referer"];
984
+ function withCSRFVary(response) {
985
+ const current = response.headers.get("Vary");
986
+ if (current === "*") return response;
987
+ const values = current ? current.split(",").map(value => value.trim()) : [];
988
+ const names = new Set(values.map(value => value.toLowerCase()));
989
+ for (const value of CSRF_VARY) {
990
+ if (!names.has(value.toLowerCase())) values.push(value);
991
+ }
992
+ const vary = values.join(", ");
993
+ try {
994
+ response.headers.set("Vary", vary);
995
+ return response;
996
+ } catch {
997
+ const headers = new Headers(response.headers);
998
+ headers.set("Vary", vary);
999
+ return new Response(response.body, {
1000
+ status: response.status,
1001
+ statusText: response.statusText,
1002
+ headers
1003
+ });
1004
+ }
1005
+ }
1006
+ function forbiddenResponse() {
1007
+ return withCSRFVary(new Response(DEV ? "Forbidden" : null, {
1008
+ status: 403,
1009
+ headers: {
1010
+ "Cache-Control": "no-store"
1011
+ }
1012
+ }));
1013
+ }
953
1014
  async function handleServerFunctionRequest(request, options = {}) {
954
1015
  const codec = options.codec !== undefined ? options.codec : getServerFunctionsCodec();
955
1016
  const url = new URL(request.url);
1017
+ const csrf = options.csrf !== undefined ? options.csrf : config.csrf;
1018
+ const protectsRequest = csrf !== false;
1019
+ if (protectsRequest && !(await allowsServerFunctionRequest(request, csrf === true ? {} : csrf))) {
1020
+ return forbiddenResponse();
1021
+ }
956
1022
  const instance = request.headers.get(INSTANCE_HEADER);
957
1023
  const functionId = resolveFunctionId(request, url);
958
1024
  if (!functionId) {
959
- return new Response(DEV ? "Server function not found" : null, {
1025
+ const response = new Response(DEV ? "Server function not found" : null, {
960
1026
  status: 404
961
1027
  });
1028
+ return protectsRequest ? withCSRFVary(response) : response;
962
1029
  }
963
1030
  let serverFunction;
964
1031
  try {
965
1032
  serverFunction = getServerFunction(functionId);
966
1033
  } catch {
967
- return new Response(DEV ? `Unknown server function: ${functionId}` : null, {
1034
+ const response = new Response(DEV ? `Unknown server function: ${functionId}` : null, {
968
1035
  status: 404
969
1036
  });
1037
+ return protectsRequest ? withCSRFVary(response) : response;
970
1038
  }
971
1039
  if (request.method === "GET" && METHODS.get(functionId) !== "GET") {
972
- return new Response(DEV ? `Method not allowed for server function: ${functionId}` : null, {
1040
+ const response = new Response(DEV ? `Method not allowed for server function: ${functionId}` : null, {
973
1041
  status: 405,
974
1042
  headers: {
975
1043
  Allow: "POST"
976
1044
  }
977
1045
  });
1046
+ return protectsRequest ? withCSRFVary(response) : response;
978
1047
  }
979
1048
  const event = options.createEvent ? options.createEvent(request) : {
980
1049
  request,
@@ -1133,7 +1202,8 @@ async function handleServerFunctionRequest(request, options = {}) {
1133
1202
  return encodeResult(safe, headers, 200, codec, request.signal);
1134
1203
  }
1135
1204
  };
1136
- return commitEventResponse(await dispatch(), event);
1205
+ const response = commitEventResponse(await dispatch(), event);
1206
+ return protectsRequest ? withCSRFVary(response) : response;
1137
1207
  }
1138
1208
 
1139
1209
  exports.ERROR_HEADER = ERROR_HEADER;
@@ -1162,6 +1232,7 @@ exports.handleServerFunctionRequest = handleServerFunctionRequest;
1162
1232
  exports.hasFlashCookie = hasFlashCookie;
1163
1233
  exports.isServerFunction = isServerFunction;
1164
1234
  exports.live = live;
1235
+ exports.observeServerFunctionCalls = observeServerFunctionCalls;
1165
1236
  exports.registerServerFunction = registerServerFunction;
1166
1237
  exports.registerServerReference = registerServerReference;
1167
1238
  exports.sanitizeServerError = sanitizeServerError;