@nice-code/action 0.15.0 → 0.17.0

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 (44) hide show
  1. package/README.md +582 -582
  2. package/build/{ActionDevtoolsCore-9PsnscvK.mjs → ActionDevtoolsCore-BcItqP-C.mjs} +7 -7
  3. package/build/ActionDevtoolsCore-BcItqP-C.mjs.map +1 -0
  4. package/build/{ActionDevtoolsCore-CCRLYASa.d.cts → ActionDevtoolsCore-C5XrQI1K.d.mts} +3 -3
  5. package/build/{ActionDevtoolsCore-DtgXwPBZ.cjs → ActionDevtoolsCore-Cb_QR44N.cjs} +7 -7
  6. package/build/ActionDevtoolsCore-Cb_QR44N.cjs.map +1 -0
  7. package/build/{ActionDevtoolsCore-CYGD2o6C.d.mts → ActionDevtoolsCore-Dd1qJAwK.d.cts} +3 -3
  8. package/build/{ActionPayload.types-BN-rXFBK.d.cts → ActionPayload.types-BchJrBIX.d.mts} +1966 -1139
  9. package/build/{ActionPayload.types-D28ELKXC.d.mts → ActionPayload.types-snDlSIF-.d.cts} +1966 -1139
  10. package/build/devtools/browser/index.cjs +5 -5
  11. package/build/devtools/browser/index.cjs.map +1 -1
  12. package/build/devtools/browser/index.d.cts +1 -1
  13. package/build/devtools/browser/index.d.mts +1 -1
  14. package/build/devtools/browser/index.mjs +5 -5
  15. package/build/devtools/browser/index.mjs.map +1 -1
  16. package/build/devtools/server/index.cjs +1 -1
  17. package/build/devtools/server/index.cjs.map +1 -1
  18. package/build/devtools/server/index.d.cts +1 -1
  19. package/build/devtools/server/index.d.mts +1 -1
  20. package/build/devtools/server/index.mjs +1 -1
  21. package/build/devtools/server/index.mjs.map +1 -1
  22. package/build/index.cjs +2667 -1752
  23. package/build/index.cjs.map +1 -1
  24. package/build/index.d.cts +2 -2
  25. package/build/index.d.mts +2 -2
  26. package/build/index.mjs +2635 -1738
  27. package/build/index.mjs.map +1 -1
  28. package/build/platform/cloudflare/index.cjs +56 -0
  29. package/build/platform/cloudflare/index.cjs.map +1 -0
  30. package/build/platform/cloudflare/index.d.cts +67 -0
  31. package/build/platform/cloudflare/index.d.mts +67 -0
  32. package/build/platform/cloudflare/index.mjs +54 -0
  33. package/build/platform/cloudflare/index.mjs.map +1 -0
  34. package/build/react-query/index.cjs.map +1 -1
  35. package/build/react-query/index.d.cts +1 -1
  36. package/build/react-query/index.d.mts +1 -1
  37. package/build/react-query/index.mjs.map +1 -1
  38. package/build/wsAcceptorCarrier-CXGlQU_f.mjs +80 -0
  39. package/build/wsAcceptorCarrier-CXGlQU_f.mjs.map +1 -0
  40. package/build/wsAcceptorCarrier-DHRbsY1X.cjs +103 -0
  41. package/build/wsAcceptorCarrier-DHRbsY1X.cjs.map +1 -0
  42. package/package.json +15 -4
  43. package/build/ActionDevtoolsCore-9PsnscvK.mjs.map +0 -1
  44. package/build/ActionDevtoolsCore-DtgXwPBZ.cjs.map +0 -1
package/build/index.cjs CHANGED
@@ -1,14 +1,15 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_rolldown_runtime = require("./rolldown-runtime-emK7D4bc.cjs");
3
3
  const require_RunningAction_types = require("./RunningAction.types-DjCX1xp5.cjs");
4
+ const require_wsAcceptorCarrier = require("./wsAcceptorCarrier-DHRbsY1X.cjs");
4
5
  let nanoid = require("nanoid");
5
6
  let _nice_code_error = require("@nice-code/error");
6
- let std_env = require("std-env");
7
7
  let _nice_code_common_errors = require("@nice-code/common-errors");
8
- let msgpackr = require("msgpackr");
8
+ let std_env = require("std-env");
9
9
  let _nice_code_util = require("@nice-code/util");
10
10
  let valibot = require("valibot");
11
11
  valibot = require_rolldown_runtime.__toESM(valibot, 1);
12
+ let msgpackr = require("msgpackr");
12
13
  const UNSET_RUNTIME_ENV_ID = "_unset_";
13
14
  //#endregion
14
15
  //#region src/ActionRuntime/utils/runtimeCoordinateToStringIds.ts
@@ -638,6 +639,156 @@ const isActionPayload_Result_JsonObject = (obj) => {
638
639
  return isAction_Base_JsonObject(obj) && obj.result != null && obj.form === "data" && obj.type === "result";
639
640
  };
640
641
  //#endregion
642
+ //#region src/ActionDefinition/Schema/ActionSchema.ts
643
+ /**
644
+ * What a sender should expect back from an action — declared on its schema so both ends agree without
645
+ * any wire flag (each derives the mode from the shared `domain:id`).
646
+ *
647
+ * - `payload` — a typed output (the action has `.output(...)`); the sender awaits it.
648
+ * - `ack` — an empty success confirming receipt (no output); the sender may await it to know the
649
+ * receiver handled it (or to surface an error). This is the default for an action with no output.
650
+ * - `none` — fire-and-forget: the receiver sends no reply and the sender doesn't wait. The sender's
651
+ * running action completes as soon as the frame is on the wire (no pending reply, no timeout).
652
+ */
653
+ let EActionResponseMode = /* @__PURE__ */ function(EActionResponseMode) {
654
+ EActionResponseMode["payload"] = "payload";
655
+ EActionResponseMode["ack"] = "ack";
656
+ EActionResponseMode["none"] = "none";
657
+ return EActionResponseMode;
658
+ }({});
659
+ var ActionSchema = class {
660
+ _errorDeclarations = [];
661
+ inputOptions;
662
+ outputOptions;
663
+ _responseMode;
664
+ get inputSchema() {
665
+ return this.inputOptions?.schema;
666
+ }
667
+ get outputSchema() {
668
+ return this.outputOptions?.schema;
669
+ }
670
+ /**
671
+ * The response contract for this action. Defaults are inferred — `payload` when an output schema is
672
+ * declared, otherwise `ack` — and made explicit by {@link ack} / {@link fireAndForget}.
673
+ */
674
+ get responseMode() {
675
+ if (this._responseMode != null) return this._responseMode;
676
+ return this.outputOptions != null ? "payload" : "ack";
677
+ }
678
+ /**
679
+ * Mark this action as expecting only an acknowledgment (an empty success). Mostly for clarity — an
680
+ * output-less action already acks by default — but it documents intent and reads as the deliberate
681
+ * counterpart to {@link fireAndForget}.
682
+ */
683
+ ack() {
684
+ this._responseMode = "ack";
685
+ return this;
686
+ }
687
+ /**
688
+ * Mark this action as fire-and-forget: the receiver sends no reply, and the sender's running action
689
+ * completes the moment the frame is sent (no awaited reply, no timeout). Ideal for high-frequency
690
+ * server→client pushes (presence, ticks) where an ack would only add wire chatter.
691
+ */
692
+ fireAndForget() {
693
+ this._responseMode = "none";
694
+ return this;
695
+ }
696
+ /**
697
+ * Declare the input schema (JSON-native or with explicit SERDE type param).
698
+ * For non-JSON-native inputs, prefer the 3-argument form below to avoid
699
+ * needing explicit type parameters.
700
+ */
701
+ input(options) {
702
+ this.inputOptions = options;
703
+ return this;
704
+ }
705
+ /**
706
+ * Declare the output schema (JSON-native or with explicit SERDE type param).
707
+ * For non-JSON-native outputs, prefer the 3-argument form below to avoid
708
+ * needing explicit type parameters.
709
+ */
710
+ output(options) {
711
+ this.outputOptions = options;
712
+ return this;
713
+ }
714
+ throws(domain, ids) {
715
+ this._errorDeclarations.push({
716
+ _domain: domain,
717
+ _ids: ids
718
+ });
719
+ return this;
720
+ }
721
+ /**
722
+ * Serialize raw input to a JSON-serializable form.
723
+ * Uses the schema's serialization.serialize if defined; otherwise the input
724
+ * is already JSON-native and is returned as-is.
725
+ */
726
+ serializeInput(rawInput) {
727
+ if (this.inputOptions?.serialization) return this.inputOptions.serialization.serialize(rawInput);
728
+ return rawInput;
729
+ }
730
+ /**
731
+ * Deserialize a JSON value back into the raw input type.
732
+ * Uses serialization.deserialize if defined; otherwise the value is cast
733
+ * directly (it's already in the correct shape).
734
+ */
735
+ deserializeInput(serialized) {
736
+ if (this.inputOptions?.serialization) return this.inputOptions.serialization.deserialize(serialized);
737
+ return serialized;
738
+ }
739
+ /**
740
+ * Validate raw input against the schema defined via `.input({ schema })`.
741
+ * Throws `action_input_validation_failed` if validation fails.
742
+ * Returns the validated (and possibly coerced) value on success.
743
+ * If no input schema was declared, the value is passed through as-is.
744
+ */
745
+ validateInput(value, meta) {
746
+ if (this.inputOptions?.schema == null) return value;
747
+ const result = this.inputOptions.schema["~standard"].validate(value);
748
+ if (result instanceof Promise) throw err_nice_action.fromId("action_input_validation_promise", {
749
+ domain: meta.domain,
750
+ actionId: meta.actionId
751
+ });
752
+ if (result.issues != null) throw err_nice_action.fromId("action_input_validation_failed", {
753
+ domain: meta.domain,
754
+ actionId: meta.actionId,
755
+ validationMessage: (0, _nice_code_common_errors.extractMessageFromStandardSchema)(result)
756
+ });
757
+ return result.value;
758
+ }
759
+ validateOutput(value, meta) {
760
+ if (this.outputOptions?.schema == null) return value;
761
+ const result = this.outputOptions.schema["~standard"].validate(value);
762
+ if (result instanceof Promise) throw err_nice_action.fromId("action_output_validation_promise", {
763
+ domain: meta.domain,
764
+ actionId: meta.actionId
765
+ });
766
+ if (result.issues != null) throw err_nice_action.fromId("action_output_validation_failed", {
767
+ domain: meta.domain,
768
+ actionId: meta.actionId,
769
+ validationMessage: (0, _nice_code_common_errors.extractMessageFromStandardSchema)(result)
770
+ });
771
+ return result.value;
772
+ }
773
+ /**
774
+ * Serialize raw output to a JSON-serializable form.
775
+ */
776
+ serializeOutput(rawOutput) {
777
+ if (this.outputOptions?.serialization) return this.outputOptions.serialization.serialize(rawOutput);
778
+ return rawOutput;
779
+ }
780
+ /**
781
+ * Deserialize a JSON value back into the raw output type.
782
+ */
783
+ deserializeOutput(serialized) {
784
+ if (this.outputOptions?.serialization) return this.outputOptions.serialization.deserialize(serialized);
785
+ return serialized;
786
+ }
787
+ };
788
+ const actionSchema = () => {
789
+ return new ActionSchema();
790
+ };
791
+ //#endregion
641
792
  //#region src/utils/getAssumedRuntimeEnvironment.ts
642
793
  const getAssumedRuntimeInfo = () => {
643
794
  return {
@@ -673,6 +824,137 @@ function peekHandlerCuid() {
673
824
  return _stack[_stack.length - 1];
674
825
  }
675
826
  //#endregion
827
+ //#region src/ActionRuntime/Handler/PeerLink/Connector/err_nice_external_client.ts
828
+ const err_nice_external_client = err_nice_action.createChildDomain({
829
+ domain: "err_nice_external_client",
830
+ schema: {}
831
+ });
832
+ //#endregion
833
+ //#region src/ActionRuntime/Transport/err_nice_transport.ts
834
+ let EErrId_NiceTransport = /* @__PURE__ */ function(EErrId_NiceTransport) {
835
+ EErrId_NiceTransport["timeout"] = "timeout";
836
+ EErrId_NiceTransport["not_found"] = "not_found";
837
+ EErrId_NiceTransport["unsupported"] = "unsupported";
838
+ EErrId_NiceTransport["initialization_failed"] = "initialization_failed";
839
+ EErrId_NiceTransport["send_failed"] = "send_failed";
840
+ EErrId_NiceTransport["invalid_action_response"] = "invalid_action_response";
841
+ return EErrId_NiceTransport;
842
+ }({});
843
+ const err_nice_transport = err_nice_external_client.createChildDomain({
844
+ domain: "err_nice_transport",
845
+ schema: {
846
+ ["timeout"]: (0, _nice_code_error.err)({ message: ({ timeout }) => `ActionConnect transport timed out after ${timeout}ms.` }),
847
+ ["not_found"]: (0, _nice_code_error.err)({ message: ({ actionId }) => `No connected transport found for action "${actionId}".` }),
848
+ ["unsupported"]: (0, _nice_code_error.err)({ message: ({ transportShapes }) => `${transportShapes.length} Transport(s) [${transportShapes.join(", ")}] found but returned "unsupported" status.` }),
849
+ ["initialization_failed"]: (0, _nice_code_error.err)({ message: ({ actionId }) => `Transports found for action "${actionId}", but none are ready.` }),
850
+ ["send_failed"]: (0, _nice_code_error.err)({
851
+ message: ({ actionId, httpStatusCode, message }) => `Failed to send action "${actionId}" [${httpStatusCode ?? "Unknown status"}]: ${message ?? "Unknown error"}.`,
852
+ httpStatusCode: ({ httpStatusCode }) => httpStatusCode ?? 500
853
+ }),
854
+ ["invalid_action_response"]: (0, _nice_code_error.err)({ message: ({ actionId }) => `Invalid action response JSON structure for action "${actionId}"` })
855
+ }
856
+ });
857
+ //#endregion
858
+ //#region src/ActionRuntime/Transport/ConnectionTransportManager.ts
859
+ var ConnectionTransportManager = class {
860
+ _cache;
861
+ _transports = [];
862
+ constructor(_cache) {
863
+ this._cache = _cache;
864
+ }
865
+ addTransport(transport) {
866
+ this._transports.push(transport);
867
+ }
868
+ /**
869
+ * The highest-priority transport (first declared). Used to label an action's route *before* the
870
+ * transport has finished connecting — so a still-connecting action shows its (expected) destination
871
+ * instead of an "unknown" hop. {@link getReadyTransport} still decides the real winner, and the
872
+ * caller corrects the hop if a lower-priority transport ends up serving the action.
873
+ */
874
+ getPreferredTransport() {
875
+ return this._transports[0];
876
+ }
877
+ async getReadyTransport(routeActionParams) {
878
+ const action = routeActionParams.action;
879
+ const candidates = [];
880
+ const unavailableTransports = [];
881
+ for (const transport of this._transports) {
882
+ const cacheKey = transport.getCacheKey(routeActionParams);
883
+ if (cacheKey != null) {
884
+ const cached = this._cache.get(cacheKey);
885
+ if (cached != null) {
886
+ if (cached instanceof Promise) {
887
+ candidates.push(cached);
888
+ continue;
889
+ }
890
+ candidates.push(Promise.resolve({
891
+ ...cached,
892
+ transport
893
+ }));
894
+ break;
895
+ }
896
+ }
897
+ const statusInfo = transport.getTransport(routeActionParams);
898
+ if (statusInfo.status === "ready") {
899
+ const readyData = statusInfo.readyData;
900
+ if (cacheKey != null) {
901
+ const entry = {
902
+ methods: readyData,
903
+ transport
904
+ };
905
+ this._cache.set(cacheKey, entry);
906
+ readyData.addOnDisconnectListener?.(() => {
907
+ if (this._cache.get(cacheKey) === entry) this._cache.delete(cacheKey);
908
+ });
909
+ }
910
+ candidates.push(Promise.resolve({
911
+ methods: readyData,
912
+ transport
913
+ }));
914
+ break;
915
+ }
916
+ if (statusInfo.status === "unsupported") {
917
+ unavailableTransports.push(transport);
918
+ continue;
919
+ }
920
+ if (statusInfo.status === "initializing") {
921
+ const promise = statusInfo.initializationPromise.then((info) => {
922
+ if (info.status === "failed") throw info.error;
923
+ if (info.status === "unsupported") throw err_nice_transport.fromId("unsupported", { transportShapes: [transport.type] });
924
+ const readyData = info.readyData;
925
+ const entry = {
926
+ methods: readyData,
927
+ transport
928
+ };
929
+ if (cacheKey != null) if (this._cache.get(cacheKey) === promise) {
930
+ this._cache.set(cacheKey, entry);
931
+ readyData.addOnDisconnectListener?.(() => {
932
+ if (this._cache.get(cacheKey) === entry) this._cache.delete(cacheKey);
933
+ });
934
+ } else readyData.disconnect?.();
935
+ return entry;
936
+ }).catch((e) => {
937
+ if (cacheKey != null && this._cache.get(cacheKey) === promise) this._cache.delete(cacheKey);
938
+ throw e;
939
+ });
940
+ if (cacheKey != null) this._cache.set(cacheKey, promise);
941
+ candidates.push(promise);
942
+ }
943
+ }
944
+ if (candidates.length === 0) {
945
+ if (unavailableTransports.length > 0) throw err_nice_transport.fromId("unsupported", { transportShapes: unavailableTransports.map((t) => t.type) });
946
+ throw err_nice_transport.fromId("not_found", { actionId: action.id });
947
+ }
948
+ let lastError;
949
+ for (const candidate of candidates) try {
950
+ return await candidate;
951
+ } catch (e) {
952
+ lastError = e;
953
+ }
954
+ throw err_nice_transport.fromId("initialization_failed", { actionId: action.id }).withOriginError(lastError);
955
+ }
956
+ };
957
+ //#endregion
676
958
  //#region src/ActionRuntime/ActionDomainManager.ts
677
959
  var ActionDomainManager = class {
678
960
  _domains = /* @__PURE__ */ new Map();
@@ -845,183 +1127,33 @@ var ActionHandler = class {
845
1127
  }
846
1128
  };
847
1129
  //#endregion
848
- //#region src/ActionRuntime/Handler/ExternalClient/err_nice_external_client.ts
849
- const err_nice_external_client = err_nice_action.createChildDomain({
850
- domain: "err_nice_external_client",
851
- schema: {}
852
- });
853
- //#endregion
854
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/err_nice_transport.ts
855
- let EErrId_NiceTransport = /* @__PURE__ */ function(EErrId_NiceTransport) {
856
- EErrId_NiceTransport["timeout"] = "timeout";
857
- EErrId_NiceTransport["not_found"] = "not_found";
858
- EErrId_NiceTransport["unsupported"] = "unsupported";
859
- EErrId_NiceTransport["initialization_failed"] = "initialization_failed";
860
- EErrId_NiceTransport["send_failed"] = "send_failed";
861
- EErrId_NiceTransport["invalid_action_response"] = "invalid_action_response";
862
- return EErrId_NiceTransport;
863
- }({});
864
- const err_nice_transport = err_nice_external_client.createChildDomain({
865
- domain: "err_nice_transport",
866
- schema: {
867
- ["timeout"]: (0, _nice_code_error.err)({ message: ({ timeout }) => `ActionConnect transport timed out after ${timeout}ms.` }),
868
- ["not_found"]: (0, _nice_code_error.err)({ message: ({ actionId }) => `No connected transport found for action "${actionId}".` }),
869
- ["unsupported"]: (0, _nice_code_error.err)({ message: ({ transportTypes }) => `${transportTypes.length} Transport(s) [${transportTypes.join(", ")}] found but returned "unsupported" status.` }),
870
- ["initialization_failed"]: (0, _nice_code_error.err)({ message: ({ actionId }) => `Transports found for action "${actionId}", but none are ready.` }),
871
- ["send_failed"]: (0, _nice_code_error.err)({
872
- message: ({ actionId, httpStatusCode, message }) => `Failed to send action "${actionId}" [${httpStatusCode ?? "Unknown status"}]: ${message ?? "Unknown error"}.`,
873
- httpStatusCode: ({ httpStatusCode }) => httpStatusCode ?? 500
874
- }),
875
- ["invalid_action_response"]: (0, _nice_code_error.err)({ message: ({ actionId }) => `Invalid action response JSON structure for action "${actionId}"` })
876
- }
877
- });
878
- //#endregion
879
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/Transport.types.ts
880
- let ETransportType = /* @__PURE__ */ function(ETransportType) {
881
- ETransportType["ws"] = "ws";
882
- ETransportType["http"] = "http";
883
- ETransportType["custom"] = "custom";
884
- return ETransportType;
885
- }({});
1130
+ //#region src/ActionRuntime/Handler/PeerLink/PeerLinkHandler.ts
886
1131
  /**
1132
+ * Shared base for every handler that routes a domain set to/from *another runtime* (a "peer") — the
1133
+ * unified peer-link concept. Both specializations extend this as siblings, differing only in *who
1134
+ * establishes the connection*, which is a transport trait, not a routing one:
887
1135
  *
888
- * TRANSPORT READINESS RESPONSE
1136
+ * - {@link ConnectorHandler} — **dial-out**: this runtime opens connection(s) to one peer
1137
+ * over a transport stack (with caching + fallback). The classic "client → backend" link.
1138
+ * - {@link AcceptorHandler} — **accept-in**: connections are accepted from many peers and fed in
1139
+ * via `receive()`; it keeps a per-connection registry and can push to any of them.
889
1140
  *
1141
+ * To the runtime there is no "client" vs "server" — both are peer-link handlers (`handlerType =
1142
+ * external`) keyed to a peer coordinate, chosen by the return-path dispatch via {@link sendReturnPayload}.
890
1143
  */
891
- let ETransportStatus = /* @__PURE__ */ function(ETransportStatus) {
892
- ETransportStatus["uninitialized"] = "uninitialized";
893
- ETransportStatus["unsupported"] = "unsupported";
894
- ETransportStatus["initializing"] = "initializing";
895
- ETransportStatus["ready"] = "ready";
896
- ETransportStatus["failed"] = "failed";
897
- return ETransportStatus;
898
- }({});
899
- //#endregion
900
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/ConnectionTransportManager.ts
901
- var ConnectionTransportManager = class {
902
- _cache;
903
- _transports = [];
904
- constructor(_cache) {
905
- this._cache = _cache;
906
- }
907
- addTransport(transport) {
908
- this._transports.push(transport);
909
- }
910
- /**
911
- * The highest-priority transport (first declared). Used to label an action's route *before* the
912
- * transport has finished connecting — so a still-connecting action shows its (expected) destination
913
- * instead of an "unknown" hop. {@link getReadyTransport} still decides the real winner, and the
914
- * caller corrects the hop if a lower-priority transport ends up serving the action.
915
- */
916
- getPreferredTransport() {
917
- return this._transports[0];
918
- }
919
- async getReadyTransport(routeActionParams) {
920
- const action = routeActionParams.action;
921
- const candidates = [];
922
- const unavailableTransports = [];
923
- for (const transport of this._transports) {
924
- const cacheKey = transport.getCacheKey(routeActionParams);
925
- if (cacheKey != null) {
926
- const cached = this._cache.get(cacheKey);
927
- if (cached != null) {
928
- if (cached instanceof Promise) {
929
- candidates.push(cached);
930
- continue;
931
- }
932
- candidates.push(Promise.resolve({
933
- ...cached,
934
- transport
935
- }));
936
- break;
937
- }
938
- }
939
- const statusInfo = transport.getTransport(routeActionParams);
940
- if (statusInfo.status === "ready") {
941
- const readyData = statusInfo.readyData;
942
- if (cacheKey != null) {
943
- const entry = {
944
- methods: readyData,
945
- transport
946
- };
947
- this._cache.set(cacheKey, entry);
948
- readyData.addOnDisconnectListener?.(() => {
949
- if (this._cache.get(cacheKey) === entry) this._cache.delete(cacheKey);
950
- });
951
- }
952
- candidates.push(Promise.resolve({
953
- methods: readyData,
954
- transport
955
- }));
956
- break;
957
- }
958
- if (statusInfo.status === "unsupported") {
959
- unavailableTransports.push(transport);
960
- continue;
961
- }
962
- if (statusInfo.status === "initializing") {
963
- const promise = statusInfo.initializationPromise.then((info) => {
964
- if (info.status === "failed") throw info.error;
965
- if (info.status === "unsupported") throw err_nice_transport.fromId("unsupported", { transportTypes: [transport.type] });
966
- const readyData = info.readyData;
967
- const entry = {
968
- methods: readyData,
969
- transport
970
- };
971
- if (cacheKey != null) if (this._cache.get(cacheKey) === promise) {
972
- this._cache.set(cacheKey, entry);
973
- readyData.addOnDisconnectListener?.(() => {
974
- if (this._cache.get(cacheKey) === entry) this._cache.delete(cacheKey);
975
- });
976
- } else readyData.disconnect?.();
977
- return entry;
978
- }).catch((e) => {
979
- if (cacheKey != null && this._cache.get(cacheKey) === promise) this._cache.delete(cacheKey);
980
- throw e;
981
- });
982
- if (cacheKey != null) this._cache.set(cacheKey, promise);
983
- candidates.push(promise);
984
- }
985
- }
986
- if (candidates.length === 0) {
987
- if (unavailableTransports.length > 0) throw err_nice_transport.fromId("unsupported", { transportTypes: unavailableTransports.map((t) => t.type) });
988
- throw err_nice_transport.fromId("not_found", { actionId: action.id });
989
- }
990
- let lastError;
991
- for (const candidate of candidates) try {
992
- return await candidate;
993
- } catch (e) {
994
- lastError = e;
995
- }
996
- throw err_nice_transport.fromId("initialization_failed", { actionId: action.id }).withOriginError(lastError);
997
- }
998
- };
999
- //#endregion
1000
- //#region src/ActionRuntime/Handler/ExternalClient/ActionExternalClientHandler.ts
1001
- var ActionExternalClientHandler = class extends ActionHandler {
1002
- externalClient;
1003
- handlerType = "external";
1004
- cuid;
1005
- _defaultTimeout;
1006
- _transportCache = /* @__PURE__ */ new Map();
1007
- transportManager = new ConnectionTransportManager(this._transportCache);
1008
- _incomingActionDataListeners = [];
1144
+ var PeerLinkHandler = class extends ActionHandler {
1145
+ /** The peer runtime this handler links to (an env-only coordinate for an accept-in handler). */
1146
+ peerClient;
1147
+ handlerType = "peer";
1009
1148
  actionRouter = new ActionRouter({
1010
1149
  contextType: "handler_route",
1011
1150
  handler: this
1012
1151
  });
1013
- constructor({ runtimeCoordinate: externalClientSpecifier, transports, defaultTimeout }) {
1152
+ /** Listeners installed by the runtime (`resolveIncomingActionPayload`) for inbound peer frames. */
1153
+ _incomingActionDataListeners = [];
1154
+ constructor(peerCoordinate) {
1014
1155
  super();
1015
- this.externalClient = externalClientSpecifier;
1016
- this.cuid = (0, nanoid.nanoid)();
1017
- this._defaultTimeout = defaultTimeout ?? 1e4;
1018
- for (const transport of transports) {
1019
- const connection = transport._createConnection({ resolvers: { onIncomingActionDataJson: (json) => {
1020
- for (const l of this._incomingActionDataListeners) l(json);
1021
- } } });
1022
- connection.definition = transport;
1023
- this.transportManager.addTransport(connection);
1024
- }
1156
+ this.peerClient = peerCoordinate;
1025
1157
  }
1026
1158
  forDomain(domain) {
1027
1159
  this.actionRouter.forDomain(domain, true);
@@ -1038,6 +1170,49 @@ var ActionExternalClientHandler = class extends ActionHandler {
1038
1170
  _setIncomingActionDataListener(listener) {
1039
1171
  this._incomingActionDataListeners.push(listener);
1040
1172
  }
1173
+ /** Hand a decoded inbound frame to the runtime (called by each specialization's receive path). */
1174
+ _emitIncoming(json) {
1175
+ for (const listener of this._incomingActionDataListeners) listener(json);
1176
+ }
1177
+ /**
1178
+ * Whether this handler currently holds a *live* connection bound to `origin`. The runtime's return-path
1179
+ * dispatch ({@link ActionRuntime.getReturnHandlerForOrigin}) prefers a handler that owns the origin's
1180
+ * connection over a mere coordinate match, so with several duplex acceptors a result/push routes back
1181
+ * over the carrier the client connected on. Defaults to `false`; an acceptor overrides it from its
1182
+ * connection registry.
1183
+ */
1184
+ ownsLiveConnectionFor(_origin) {
1185
+ return false;
1186
+ }
1187
+ /** Release any long-lived connections this handler owns (a teardown). No-op by default. */
1188
+ clearTransportCache() {}
1189
+ };
1190
+ //#endregion
1191
+ //#region src/ActionRuntime/Handler/PeerLink/Connector/ConnectorHandler.ts
1192
+ /**
1193
+ * Dial-out peer link: this runtime opens connection(s) to one peer over a transport stack (cached, with
1194
+ * preference-ordered fallback). The classic "client → backend" handler — but to the runtime it's just a
1195
+ * {@link PeerLinkHandler} like the accept-in server one.
1196
+ */
1197
+ var ConnectorHandler = class extends PeerLinkHandler {
1198
+ /**
1199
+ * Dial-out can receive (and so return) an unsolicited push only over a duplex transport. With every
1200
+ * transport exchange-only (HTTP), the peer can never push to us — so this link can't deliver one back.
1201
+ */
1202
+ canPush;
1203
+ _defaultTimeout;
1204
+ _transportCache = /* @__PURE__ */ new Map();
1205
+ transportManager = new ConnectionTransportManager(this._transportCache);
1206
+ constructor({ runtimeCoordinate: peerSpecifier, transports, defaultTimeout }) {
1207
+ super(peerSpecifier);
1208
+ this._defaultTimeout = defaultTimeout ?? 1e4;
1209
+ this.canPush = transports.some((transport) => transport.type === "duplex");
1210
+ for (const transport of transports) {
1211
+ const connection = transport._createConnection({ resolvers: { onIncomingActionDataJson: (json) => this._emitIncoming(json) } });
1212
+ connection.definition = transport;
1213
+ this.transportManager.addTransport(connection);
1214
+ }
1215
+ }
1041
1216
  async handleActionRequest(action, config) {
1042
1217
  const localRuntime = config?.targetLocalRuntime ?? ActionRuntime.getDefault();
1043
1218
  const localClient = localRuntime.coordinate;
@@ -1047,7 +1222,7 @@ var ActionExternalClientHandler = class extends ActionHandler {
1047
1222
  const routeParams = {
1048
1223
  action,
1049
1224
  localClient,
1050
- externalClient: this.externalClient
1225
+ externalClient: this.peerClient
1051
1226
  };
1052
1227
  const preferredTransport = this.transportManager.getPreferredTransport();
1053
1228
  const routeItem = preferredTransport != null ? {
@@ -1086,6 +1261,7 @@ var ActionExternalClientHandler = class extends ActionHandler {
1086
1261
  };
1087
1262
  if (action.type === "request" && methods.updateRunConfig != null) sendInput.timeout = methods.updateRunConfig(sendInput)?.timeout ?? incomingTimeout;
1088
1263
  methods.sendActionData(sendInput);
1264
+ if (action.type === "request" && action.schema.responseMode === "none") runningAction._completeWithResult(action.successResult(void 0));
1089
1265
  } catch (err) {
1090
1266
  runningAction._abort(err);
1091
1267
  }
@@ -1103,12 +1279,12 @@ var ActionExternalClientHandler = class extends ActionHandler {
1103
1279
  const { methods } = await this.transportManager.getReadyTransport({
1104
1280
  action: payload,
1105
1281
  localClient,
1106
- externalClient: this.externalClient
1282
+ externalClient: this.peerClient
1107
1283
  });
1108
1284
  if (methods.sendReturnData == null) return false;
1109
1285
  methods.sendReturnData(payload, {
1110
1286
  localClient,
1111
- externalClient: this.externalClient
1287
+ externalClient: this.peerClient
1112
1288
  });
1113
1289
  return true;
1114
1290
  } catch {
@@ -1118,15 +1294,15 @@ var ActionExternalClientHandler = class extends ActionHandler {
1118
1294
  toJsonObject() {
1119
1295
  return {
1120
1296
  type: this.handlerType,
1121
- client: this.externalClient
1297
+ client: this.peerClient
1122
1298
  };
1123
1299
  }
1124
1300
  toHandlerRouteItem(transport, input) {
1125
1301
  return {
1126
1302
  type: this.handlerType,
1127
- client: this.externalClient,
1303
+ client: this.peerClient,
1128
1304
  transOrd: transport.transOrd,
1129
- transType: transport.type,
1305
+ transShape: transport.type,
1130
1306
  transInfo: transport.getRouteInfo(input)
1131
1307
  };
1132
1308
  }
@@ -1136,8 +1312,8 @@ var ActionExternalClientHandler = class extends ActionHandler {
1136
1312
  this._transportCache.clear();
1137
1313
  }
1138
1314
  };
1139
- const createExternalClientHandler = (config) => {
1140
- return new ActionExternalClientHandler(config);
1315
+ const createConnectorHandler = (config) => {
1316
+ return new ConnectorHandler(config);
1141
1317
  };
1142
1318
  //#endregion
1143
1319
  //#region src/ActionRuntime/ActionRuntime.ts
@@ -1147,7 +1323,7 @@ var ActionRuntime = class {
1147
1323
  runtimeInfo = getAssumedRuntimeInfo();
1148
1324
  actionRouter;
1149
1325
  _pendingRunningActions = /* @__PURE__ */ new Map();
1150
- _registeredExternalHandlers = [];
1326
+ _registeredPeerHandlers = [];
1151
1327
  _applied = false;
1152
1328
  static getDefault() {
1153
1329
  return getDefaultActionRuntime();
@@ -1222,24 +1398,24 @@ var ActionRuntime = class {
1222
1398
  */
1223
1399
  _getHandlerForAction(action, options) {
1224
1400
  const handlers = this.actionRouter.getRouteDataEntriesForAction(action);
1225
- const targetExternalClient = options?.targetExternalClient;
1401
+ const targetPeer = options?.targetPeer;
1226
1402
  const possibleHandlers = handlers.filter((handler) => {
1227
- if (handler.handlerType === "external") {
1228
- if (targetExternalClient && !targetExternalClient.isSameFor(handler.externalClient).id) return false;
1403
+ if (handler.handlerType === "peer") {
1404
+ if (targetPeer && !targetPeer.isSameFor(handler.peerClient).id) return false;
1229
1405
  return true;
1230
1406
  }
1231
- if (targetExternalClient != null) return false;
1407
+ if (targetPeer != null) return false;
1232
1408
  if (action.type === "request") return true;
1233
1409
  return false;
1234
1410
  });
1235
1411
  if (possibleHandlers.length === 0) return;
1236
- const scoringExternalClient = targetExternalClient ?? RuntimeCoordinate.unknown;
1412
+ const scoringPeer = targetPeer ?? RuntimeCoordinate.unknown;
1237
1413
  let handlerScore = -1;
1238
1414
  let handler;
1239
1415
  for (const possibleHandler of possibleHandlers) {
1240
- if (possibleHandler.handlerType === "local" && handler == null) return possibleHandler;
1241
- if (possibleHandler.handlerType === "external") {
1242
- const score = scoringExternalClient.similarityLevel(possibleHandler.externalClient);
1416
+ if (possibleHandler.handlerType === "local") return possibleHandler;
1417
+ if (possibleHandler.handlerType === "peer") {
1418
+ const score = scoringPeer.similarityLevel(possibleHandler.peerClient);
1243
1419
  if (score > handlerScore) {
1244
1420
  handlerScore = score;
1245
1421
  handler = possibleHandler;
@@ -1253,7 +1429,7 @@ var ActionRuntime = class {
1253
1429
  if (handler == null) throw err_nice_action.fromId("no_action_execution_handler", {
1254
1430
  actionId: action.id,
1255
1431
  domain: action.domain,
1256
- specifiedClient: options?.targetExternalClient
1432
+ specifiedClient: options?.targetPeer
1257
1433
  });
1258
1434
  return handler;
1259
1435
  }
@@ -1265,9 +1441,9 @@ var ActionRuntime = class {
1265
1441
  */
1266
1442
  addHandlers(handlers) {
1267
1443
  for (const handler of handlers) {
1268
- if (handler.handlerType === "external") {
1444
+ if (handler.handlerType === "peer") {
1269
1445
  handler._setIncomingActionDataListener((json) => this.resolveIncomingActionPayload(json));
1270
- this._registeredExternalHandlers.push(handler);
1446
+ this._registeredPeerHandlers.push(handler);
1271
1447
  }
1272
1448
  const handlerRouter = handler.getActionRouter();
1273
1449
  this.actionRouter.addDomainsFromOther(handlerRouter);
@@ -1278,18 +1454,18 @@ var ActionRuntime = class {
1278
1454
  }
1279
1455
  /**
1280
1456
  * Declare an external "backend client" in one call: build an
1281
- * {@link ActionExternalClientHandler} for `externalCoordinate` carrying the given
1457
+ * {@link ConnectorHandler} for `externalCoordinate` carrying the given
1282
1458
  * `transports`, route the listed `domains`/`actions` to it, register it (plus any
1283
1459
  * `localHandlers` — e.g. server→client push handlers that share the same channel)
1284
1460
  * on this runtime, and `apply()`. Returns the external handler so the caller can
1285
1461
  * later `clearTransportCache()` it.
1286
1462
  *
1287
- * Sugar over `new ActionExternalClientHandler(...).forDomain(...)` + `addHandlers([...])`,
1463
+ * Sugar over `new ConnectorHandler(...).forDomain(...)` + `addHandlers([...])`,
1288
1464
  * so a single runtime can host one handler per backend target with its transports
1289
1465
  * declared once and reused across every action routed to that backend.
1290
1466
  */
1291
1467
  connectTo(externalCoordinate, options) {
1292
- const handler = new ActionExternalClientHandler({
1468
+ const handler = new ConnectorHandler({
1293
1469
  runtimeCoordinate: externalCoordinate,
1294
1470
  transports: options.transports,
1295
1471
  defaultTimeout: options.defaultTimeout
@@ -1318,25 +1494,40 @@ var ActionRuntime = class {
1318
1494
  * Find the best registered external handler that can reach `originClient` directly.
1319
1495
  * Used to locate the return-path channel for dispatching results back to the action origin.
1320
1496
  * Returns `undefined` if no handler matches (score > 0 required, i.e. at least id must match).
1497
+ *
1498
+ * A handler that currently holds the origin's *live* connection always wins over a mere coordinate
1499
+ * match — so with several duplex acceptors (e.g. WS + WebRTC) a result/push routes back over the carrier
1500
+ * the client actually connected on, never a same-coordinate sibling that lacks the socket. Only when no
1501
+ * handler owns a live connection do we fall back to the plain best-coordinate-score pick (the
1502
+ * single-acceptor and connector-only cases, unchanged).
1321
1503
  */
1322
1504
  getReturnHandlerForOrigin(originClient) {
1323
1505
  if (originClient.envId === "_unset_") return void 0;
1324
1506
  let bestScore = -1;
1325
1507
  let bestHandler;
1326
- for (const handler of this._registeredExternalHandlers) {
1327
- const score = originClient.similarityLevel(handler.externalClient);
1508
+ let bestOwnedScore = -1;
1509
+ let bestOwnedHandler;
1510
+ for (const handler of this._registeredPeerHandlers) {
1511
+ if (!handler.canPush) continue;
1512
+ const score = originClient.similarityLevel(handler.peerClient);
1328
1513
  if (score > bestScore) {
1329
1514
  bestScore = score;
1330
1515
  bestHandler = handler;
1331
1516
  }
1517
+ if (score > bestOwnedScore && handler.ownsLiveConnectionFor(originClient)) {
1518
+ bestOwnedScore = score;
1519
+ bestOwnedHandler = handler;
1520
+ }
1332
1521
  }
1522
+ if (bestOwnedHandler != null && bestOwnedScore > 0) return bestOwnedHandler;
1333
1523
  return bestScore > 0 ? bestHandler : void 0;
1334
1524
  }
1335
1525
  resetRuntime() {
1336
1526
  for (const ra of this._pendingRunningActions.values()) ra._abort(err_nice_action.fromId("runtime_reset"));
1337
- for (const handler of this._registeredExternalHandlers) handler.clearTransportCache();
1527
+ for (const handler of this._registeredPeerHandlers) handler.clearTransportCache();
1338
1528
  }
1339
1529
  _trySetupReturnDispatch(runningAction) {
1530
+ if (runningAction.context.schema.responseMode === "none") return;
1340
1531
  const originClient = runningAction.context.originClient;
1341
1532
  if (originClient.envId === "_unset_" || originClient.isSameFor(this._coordinate).id) return;
1342
1533
  runningAction.addUpdateListeners([(update) => {
@@ -1815,467 +2006,7 @@ const createActionRootDomain = (definition) => {
1815
2006
  return new ActionRootDomain(definition);
1816
2007
  };
1817
2008
  //#endregion
1818
- //#region src/ActionDefinition/Schema/ActionSchema.ts
1819
- var ActionSchema = class {
1820
- _errorDeclarations = [];
1821
- inputOptions;
1822
- outputOptions;
1823
- get inputSchema() {
1824
- return this.inputOptions?.schema;
1825
- }
1826
- get outputSchema() {
1827
- return this.outputOptions?.schema;
1828
- }
1829
- /**
1830
- * Declare the input schema (JSON-native or with explicit SERDE type param).
1831
- * For non-JSON-native inputs, prefer the 3-argument form below to avoid
1832
- * needing explicit type parameters.
1833
- */
1834
- input(options) {
1835
- this.inputOptions = options;
1836
- return this;
1837
- }
1838
- /**
1839
- * Declare the output schema (JSON-native or with explicit SERDE type param).
1840
- * For non-JSON-native outputs, prefer the 3-argument form below to avoid
1841
- * needing explicit type parameters.
1842
- */
1843
- output(options) {
1844
- this.outputOptions = options;
1845
- return this;
1846
- }
1847
- throws(domain, ids) {
1848
- this._errorDeclarations.push({
1849
- _domain: domain,
1850
- _ids: ids
1851
- });
1852
- return this;
1853
- }
1854
- /**
1855
- * Serialize raw input to a JSON-serializable form.
1856
- * Uses the schema's serialization.serialize if defined; otherwise the input
1857
- * is already JSON-native and is returned as-is.
1858
- */
1859
- serializeInput(rawInput) {
1860
- if (this.inputOptions?.serialization) return this.inputOptions.serialization.serialize(rawInput);
1861
- return rawInput;
1862
- }
1863
- /**
1864
- * Deserialize a JSON value back into the raw input type.
1865
- * Uses serialization.deserialize if defined; otherwise the value is cast
1866
- * directly (it's already in the correct shape).
1867
- */
1868
- deserializeInput(serialized) {
1869
- if (this.inputOptions?.serialization) return this.inputOptions.serialization.deserialize(serialized);
1870
- return serialized;
1871
- }
1872
- /**
1873
- * Validate raw input against the schema defined via `.input({ schema })`.
1874
- * Throws `action_input_validation_failed` if validation fails.
1875
- * Returns the validated (and possibly coerced) value on success.
1876
- * If no input schema was declared, the value is passed through as-is.
1877
- */
1878
- validateInput(value, meta) {
1879
- if (this.inputOptions?.schema == null) return value;
1880
- const result = this.inputOptions.schema["~standard"].validate(value);
1881
- if (result instanceof Promise) throw err_nice_action.fromId("action_input_validation_promise", {
1882
- domain: meta.domain,
1883
- actionId: meta.actionId
1884
- });
1885
- if (result.issues != null) throw err_nice_action.fromId("action_input_validation_failed", {
1886
- domain: meta.domain,
1887
- actionId: meta.actionId,
1888
- validationMessage: (0, _nice_code_common_errors.extractMessageFromStandardSchema)(result)
1889
- });
1890
- return result.value;
1891
- }
1892
- validateOutput(value, meta) {
1893
- if (this.outputOptions?.schema == null) return value;
1894
- const result = this.outputOptions.schema["~standard"].validate(value);
1895
- if (result instanceof Promise) throw err_nice_action.fromId("action_output_validation_promise", {
1896
- domain: meta.domain,
1897
- actionId: meta.actionId
1898
- });
1899
- if (result.issues != null) throw err_nice_action.fromId("action_output_validation_failed", {
1900
- domain: meta.domain,
1901
- actionId: meta.actionId,
1902
- validationMessage: (0, _nice_code_common_errors.extractMessageFromStandardSchema)(result)
1903
- });
1904
- return result.value;
1905
- }
1906
- /**
1907
- * Serialize raw output to a JSON-serializable form.
1908
- */
1909
- serializeOutput(rawOutput) {
1910
- if (this.outputOptions?.serialization) return this.outputOptions.serialization.serialize(rawOutput);
1911
- return rawOutput;
1912
- }
1913
- /**
1914
- * Deserialize a JSON value back into the raw output type.
1915
- */
1916
- deserializeOutput(serialized) {
1917
- if (this.outputOptions?.serialization) return this.outputOptions.serialization.deserialize(serialized);
1918
- return serialized;
1919
- }
1920
- };
1921
- const actionSchema = () => {
1922
- return new ActionSchema();
1923
- };
1924
- //#endregion
1925
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/Transport.ts
1926
- /**
1927
- * Reusable transport definition. Devs construct these (`HttpTransport.create({ createRequest })`,
1928
- * `WebSocketTransport.create({ createWebSocket })`, …) and pass them to an
1929
- * `ActionExternalClientHandler`. A single
1930
- * definition can be shared across multiple handlers — each handler builds its own live
1931
- * {@link TransportConnection} via {@link TransportConnection._createConnection}.
1932
- */
1933
- var Transport = class {};
1934
- //#endregion
1935
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/helpers/addTransportStatusMetadata.ts
1936
- function addTransportStatusMetadata(transportStatus) {
1937
- if (transportStatus.status === "ready") return {
1938
- status: "ready",
1939
- readyData: transportStatus.readyData
1940
- };
1941
- if (transportStatus.status === "initializing") return {
1942
- status: "initializing",
1943
- initializationPromise: transportStatus.initializationPromise,
1944
- timeStarted: Date.now()
1945
- };
1946
- if (transportStatus.status === "failed") return {
1947
- status: "failed",
1948
- error: transportStatus.error,
1949
- timeFailed: Date.now()
1950
- };
1951
- if (transportStatus.status === "unsupported") return { status: "unsupported" };
1952
- return { status: "uninitialized" };
1953
- }
1954
- //#endregion
1955
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/TransportConnection.ts
1956
- let transportOrd = 0;
1957
- /**
1958
- * Live, per-handler transport runtime built from a reusable {@link Transport} definition. Holds the
1959
- * connection-scoped state (ordinal, initialized config, sockets / abort sets) that must not be shared
1960
- * across handlers. Construct these via `definition._createConnection(...)`, never directly.
1961
- */
1962
- var TransportConnection = class {
1963
- def;
1964
- transOrd = transportOrd++;
1965
- type;
1966
- initialized;
1967
- /** Backref to the public definition that created this connection (used for devtools route info). */
1968
- definition;
1969
- constructor(def) {
1970
- this.def = def;
1971
- this.type = def.type;
1972
- this.initialized = def.initialize();
1973
- }
1974
- /**
1975
- * Devtools route info for an action routed through this live connection. Defaults to the stateless
1976
- * {@link definition}'s info; connections override to enrich it from live state (e.g. the actual
1977
- * resolved socket URL) when the definition couldn't resolve it on its own.
1978
- */
1979
- getRouteInfo(input) {
1980
- return this.definition?.getRouteInfo(input);
1981
- }
1982
- _getCacheKey(input) {
1983
- const parts = this.initialized.getTransportCacheKey?.(input);
1984
- if (parts == null) return null;
1985
- return parts.join("\0");
1986
- }
1987
- getCacheKey(input) {
1988
- const inner = this._getCacheKey(input);
1989
- if (inner == null) return null;
1990
- return `${this.transOrd}:${inner}`;
1991
- }
1992
- getTransport(input) {
1993
- return this._processTransportStatus(input);
1994
- }
1995
- _processTransportStatus(input) {
1996
- const transportStatusInfo = addTransportStatusMetadata(this.initialized.getTransport(input));
1997
- if (transportStatusInfo.status !== "initializing" && transportStatusInfo.status !== "ready") return transportStatusInfo;
1998
- if (transportStatusInfo.status === "initializing") {
1999
- const promiseForReadyData = transportStatusInfo.initializationPromise.then((result) => {
2000
- if (result.status === "ready") return {
2001
- status: "ready",
2002
- readyData: this._finalizeTransportMethods(result.readyData)
2003
- };
2004
- return result;
2005
- });
2006
- return {
2007
- status: "initializing",
2008
- timeStarted: transportStatusInfo.timeStarted,
2009
- initializationPromise: promiseForReadyData
2010
- };
2011
- }
2012
- return {
2013
- status: "ready",
2014
- readyData: this._finalizeTransportMethods(transportStatusInfo.readyData)
2015
- };
2016
- }
2017
- };
2018
- //#endregion
2019
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/Custom/CustomConnection.ts
2020
- var CustomConnection = class extends TransportConnection {
2021
- constructor(def) {
2022
- super({
2023
- ...def,
2024
- type: "custom"
2025
- });
2026
- }
2027
- _finalizeTransportMethods(inputs) {
2028
- return {
2029
- sendActionData: inputs.sendActionData,
2030
- sendReturnData: inputs.sendReturnData,
2031
- updateRunConfig: inputs.updateRunConfig
2032
- };
2033
- }
2034
- };
2035
- //#endregion
2036
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/Custom/CustomTransport.ts
2037
- /**
2038
- * Reusable custom transport definition for channels nice-action doesn't model natively. Create one
2039
- * with `CustomTransport.create({ sendActionData })` for the simple case, or
2040
- * `CustomTransport.createAdvanced({ getTransport })` for full control over the lifecycle.
2041
- */
2042
- var CustomTransport = class CustomTransport extends Transport {
2043
- options;
2044
- type = "custom";
2045
- constructor(options) {
2046
- super();
2047
- this.options = options;
2048
- }
2049
- static create(options) {
2050
- return new CustomTransport({
2051
- ...options,
2052
- mode: "send"
2053
- });
2054
- }
2055
- static createAdvanced(options) {
2056
- return new CustomTransport({
2057
- ...options,
2058
- mode: "advanced"
2059
- });
2060
- }
2061
- _createConnection(_ctx) {
2062
- const options = this.options;
2063
- let getTransport;
2064
- if (options.mode === "advanced") getTransport = options.getTransport;
2065
- else getTransport = () => ({
2066
- status: "ready",
2067
- readyData: {
2068
- sendActionData: options.sendActionData,
2069
- sendReturnData: options.sendReturnData,
2070
- updateRunConfig: options.updateRunConfig,
2071
- closeTransport: options.closeTransport ?? (() => {})
2072
- }
2073
- });
2074
- return new CustomConnection({ initialize: () => ({
2075
- getTransportCacheKey: options.getTransportCacheKey,
2076
- getTransport
2077
- }) });
2078
- }
2079
- getRouteInfo(input) {
2080
- if (this.options.getRouteInfo != null) return this.options.getRouteInfo(input);
2081
- return {
2082
- type: "custom",
2083
- summary: this.options.label ?? "custom"
2084
- };
2085
- }
2086
- };
2087
- //#endregion
2088
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/err_nice_transport_ws.ts
2089
- let EErrId_NiceTransport_WebSocket = /* @__PURE__ */ function(EErrId_NiceTransport_WebSocket) {
2090
- EErrId_NiceTransport_WebSocket["ws_disconnected"] = "ws_disconnected";
2091
- EErrId_NiceTransport_WebSocket["ws_create_failed"] = "ws_create_failed";
2092
- EErrId_NiceTransport_WebSocket["ws_error"] = "ws_error";
2093
- return EErrId_NiceTransport_WebSocket;
2094
- }({});
2095
- const err_nice_transport_ws = err_nice_transport.createChildDomain({
2096
- domain: "ws_transport",
2097
- schema: {
2098
- ["ws_disconnected"]: (0, _nice_code_error.err)({ message: () => `WebSocket transport disconnected.` }),
2099
- ["ws_create_failed"]: (0, _nice_code_error.err)({ message: ({ originalError }) => `Failed to create WebSocket transport.${originalError ? ` Original error: ${originalError.message}` : ""}` }),
2100
- ["ws_error"]: (0, _nice_code_error.err)({ message: ({ originalError }) => `WebSocket transport error.${originalError ? ` Original error: ${originalError.message}` : ""}` })
2101
- }
2102
- });
2103
- //#endregion
2104
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/Http/HttpConnection.ts
2105
- var HttpConnection = class extends TransportConnection {
2106
- constructor(def) {
2107
- super({
2108
- ...def,
2109
- type: "http"
2110
- });
2111
- }
2112
- _finalizeTransportMethods(methods) {
2113
- return {
2114
- sendActionData: (input) => {
2115
- const request = methods.createRequest(input);
2116
- this.send({
2117
- ...input,
2118
- params: { request },
2119
- runningAction: input.runningAction
2120
- }).catch((err) => input.runningAction._abort(err));
2121
- },
2122
- updateRunConfig: methods.updateRunConfig
2123
- };
2124
- }
2125
- async send(input) {
2126
- const { action, runningAction, timeout, params: { request } } = input;
2127
- const wire = action.toJsonObject();
2128
- const ac = new AbortController();
2129
- let timedOut = false;
2130
- const timeoutId = setTimeout(() => {
2131
- timedOut = true;
2132
- ac.abort();
2133
- }, timeout);
2134
- const unsubscribe = input.runningAction.addUpdateListeners([(update) => {
2135
- if (update.type === "finished") {
2136
- clearTimeout(timeoutId);
2137
- ac.abort();
2138
- }
2139
- }]);
2140
- try {
2141
- const res = await fetch(request.url, {
2142
- method: "POST",
2143
- headers: {
2144
- "Content-Type": "application/json",
2145
- ...request.headers
2146
- },
2147
- body: request.body ?? JSON.stringify(wire),
2148
- signal: ac.signal
2149
- });
2150
- if (!res.ok) {
2151
- if (action.type === "request") try {
2152
- const jsonData = await res.json();
2153
- if (isActionPayload_Result_JsonObject(jsonData)) runningAction._completeWithResult(action._domain.hydrateResultPayload(jsonData));
2154
- else if ((0, _nice_code_error.isNiceErrorObject)(jsonData)) runningAction._completeWithResult(action.errorResult((0, _nice_code_error.castNiceError)(jsonData)));
2155
- else runningAction._completeWithResult(action.errorResult(err_nice_transport.fromId("invalid_action_response", { actionId: action.id })));
2156
- } catch (e) {
2157
- throw err_nice_transport.fromId("send_failed", {
2158
- actionState: action.type,
2159
- actionId: action.id,
2160
- httpStatusCode: res.status,
2161
- message: e.message
2162
- }).withOriginError(e);
2163
- }
2164
- else {
2165
- let text;
2166
- try {
2167
- text = await res.text();
2168
- } catch (e) {
2169
- console.warn(`Failed to read error response body for failed HTTP request in HttpConnection:`, e);
2170
- }
2171
- throw err_nice_transport.fromId("send_failed", {
2172
- actionState: action.type,
2173
- actionId: action.id,
2174
- httpStatusCode: res.status,
2175
- message: text ?? `HTTP error with status ${res.status}`
2176
- });
2177
- }
2178
- return;
2179
- }
2180
- if (action.type === "request") {
2181
- const json = await res.json();
2182
- if (!isActionPayload_Result_JsonObject(json)) throw err_nice_transport.fromId("invalid_action_response", { actionId: action.id });
2183
- runningAction._completeWithResult(action._domain.hydrateResultPayload(json));
2184
- }
2185
- } catch (err) {
2186
- if (timedOut) throw err_nice_transport.fromId("timeout", { timeout });
2187
- if (err instanceof _nice_code_error.NiceError) throw err;
2188
- throw err_nice_transport.fromId("send_failed", {
2189
- actionState: action.type,
2190
- actionId: action.id,
2191
- message: err instanceof Error ? err.message : String(err)
2192
- }).withOriginError(err instanceof Error ? err : void 0);
2193
- } finally {
2194
- clearTimeout(timeoutId);
2195
- unsubscribe();
2196
- }
2197
- }
2198
- };
2199
- //#endregion
2200
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/Http/HttpTransport.ts
2201
- function shortPath(url) {
2202
- try {
2203
- return new URL(url).pathname || url;
2204
- } catch {
2205
- return url;
2206
- }
2207
- }
2208
- /**
2209
- * Reusable HTTP transport definition. Create one with `HttpTransport.create({ createRequest })` — the
2210
- * single `createRequest` function lets you keep it simple (`() => ({ url })`) or derive the request
2211
- * per action.
2212
- */
2213
- var HttpTransport = class HttpTransport extends Transport {
2214
- options;
2215
- type = "http";
2216
- constructor(options) {
2217
- super();
2218
- this.options = options;
2219
- }
2220
- static create(options) {
2221
- return new HttpTransport(options);
2222
- }
2223
- _createConnection(_ctx) {
2224
- return new HttpConnection({ initialize: () => ({
2225
- getTransportCacheKey: this.options.getTransportCacheKey,
2226
- getTransport: () => ({
2227
- status: "ready",
2228
- readyData: {
2229
- createRequest: this.options.createRequest,
2230
- updateRunConfig: this.options.updateRunConfig
2231
- }
2232
- })
2233
- }) });
2234
- }
2235
- getRouteInfo(input) {
2236
- const { url } = this.options.createRequest(input);
2237
- return {
2238
- type: "http",
2239
- method: "POST",
2240
- url,
2241
- summary: `POST ${shortPath(url)}`
2242
- };
2243
- }
2244
- };
2245
- //#endregion
2246
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/WebSocket/actionFrameCrypto.ts
2247
- const ENCRYPTED_ENVELOPE_LENGTH = 2;
2248
- /**
2249
- * Build the encrypt/decrypt transform for a connection whose handshake settled on the `encrypted`
2250
- * level. Keyed by the link + `linkedClientId`, so it reuses the cached shared AES-GCM key.
2251
- */
2252
- function createActionFrameCrypto({ link, linkedClientId }) {
2253
- return {
2254
- async encryptFrame(frame) {
2255
- const { nonce, ciphertext } = await link.encryptBytesForLinkedClient({
2256
- linkedClientId,
2257
- dataToEncrypt: frame
2258
- });
2259
- return (0, msgpackr.pack)([nonce, ciphertext]);
2260
- },
2261
- async decryptFrame(frame) {
2262
- if (typeof frame === "string") throw new Error("[ws-crypto] expected an encrypted binary frame, received text");
2263
- const envelope = (0, msgpackr.unpack)(frame instanceof ArrayBuffer ? new Uint8Array(frame) : frame);
2264
- if (!Array.isArray(envelope) || envelope.length !== ENCRYPTED_ENVELOPE_LENGTH) throw new Error("[ws-crypto] malformed encrypted frame envelope");
2265
- const [nonce, ciphertext] = envelope;
2266
- if (!(nonce instanceof Uint8Array) || !(ciphertext instanceof Uint8Array)) throw new Error("[ws-crypto] malformed encrypted frame fields");
2267
- return await link.decryptBytesFromLinkedClient({
2268
- linkedClientId,
2269
- dataToDecrypt: {
2270
- nonce,
2271
- ciphertext
2272
- }
2273
- });
2274
- }
2275
- };
2276
- }
2277
- //#endregion
2278
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/WebSocket/actionWsHandshake.ts
2009
+ //#region src/ActionRuntime/Transport/crypto/actionHandshake.ts
2279
2010
  /**
2280
2011
  * Authenticated handshake for the WebSocket channel. Run once per connection, before any action
2281
2012
  * frames flow, it:
@@ -2627,250 +2358,840 @@ function createServerHandshake(config) {
2627
2358
  };
2628
2359
  }
2629
2360
  //#endregion
2630
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/WebSocket/actionWireCodec.ts
2361
+ //#region src/utils/decodeActionFrame.ts
2631
2362
  /**
2632
- * Shared building blocks for the binary action codecs (the stateless {@link createBinaryWsAdapter} and
2633
- * the per-connection `createBinaryWsSessionFactory`). Both map a `domain:id` route to a tiny integer
2634
- * and reduce the verbose JSON wire to a positional tuple — they only differ in how much context they
2635
- * carry per frame, so the dictionary + payload (de)assembly live here.
2363
+ * Decode a single inbound channel frame (text or binary) into validated action wire JSON, or
2364
+ * `undefined` if it isn't a recognisable action payload.
2365
+ *
2366
+ * Shared by the WebSocket transport's message listener and the server-side `AcceptorHandler` so
2367
+ * both decode identically: a binary `decoder.incoming` (e.g. msgpackr) takes precedence, and plain
2368
+ * text frames fall back to JSON — keeping binary and JSON clients interoperable on one channel.
2636
2369
  */
2370
+ function decodeActionFrame(frame, decoder) {
2371
+ const decoded = decoder?.incoming?.(frame) ?? (typeof frame === "string" ? parseJsonActionFrame(frame) : void 0);
2372
+ return decoded != null && isActionPayload_Any_JsonObject(decoded) ? decoded : void 0;
2373
+ }
2374
+ function parseJsonActionFrame(message) {
2375
+ try {
2376
+ const json = JSON.parse(message);
2377
+ return isActionPayload_Any_JsonObject(json) ? json : void 0;
2378
+ } catch {
2379
+ return;
2380
+ }
2381
+ }
2382
+ //#endregion
2383
+ //#region src/ActionRuntime/Transport/crypto/actionFrameCrypto.ts
2384
+ const ENCRYPTED_ENVELOPE_LENGTH = 2;
2637
2385
  /**
2638
- * Tiny integer codes for the payload type, so the verbose `"request"`/`"result"`/`"progress"`
2639
- * strings never hit the wire. The index in {@link ReversePayloadType} must line up with the value.
2386
+ * Build the encrypt/decrypt transform for a connection whose handshake settled on the `encrypted`
2387
+ * level. Keyed by the link + `linkedClientId`, so it reuses the cached shared AES-GCM key.
2640
2388
  */
2641
- const PayloadTypeToInt = {
2642
- ["request"]: 0,
2643
- ["result"]: 1,
2644
- ["progress"]: 2
2645
- };
2646
- const ReversePayloadType = [
2647
- "request",
2648
- "result",
2649
- "progress"
2650
- ];
2651
- /**
2652
- * Build the positional `domain:id` ↔ integer dictionary. Both ends of a channel MUST build it from
2653
- * the same domains in the same order — the mapping is positional, so a mismatch routes to the wrong
2654
- * action. Add new transported domains to the end of the list.
2655
- */
2656
- function buildActionRouteDictionary(domains) {
2657
- const routeToInt = /* @__PURE__ */ new Map();
2658
- const intToRoute = [];
2659
- for (const dom of domains) for (const actionId of Object.keys(dom.actionSchema)) {
2660
- const routeKey = `${dom.domain}:${actionId}`;
2661
- if (routeToInt.has(routeKey)) continue;
2662
- routeToInt.set(routeKey, intToRoute.length);
2663
- intToRoute.push({
2664
- domain: dom.domain,
2665
- id: actionId,
2666
- allDomains: dom.allDomains
2667
- });
2668
- }
2389
+ function createActionFrameCrypto({ link, linkedClientId }) {
2669
2390
  return {
2670
- routeToInt,
2671
- intToRoute
2391
+ async encryptFrame(frame) {
2392
+ const { nonce, ciphertext } = await link.encryptBytesForLinkedClient({
2393
+ linkedClientId,
2394
+ dataToEncrypt: frame
2395
+ });
2396
+ return (0, msgpackr.pack)([nonce, ciphertext]);
2397
+ },
2398
+ async decryptFrame(frame) {
2399
+ if (typeof frame === "string") throw new Error("[ws-crypto] expected an encrypted binary frame, received text");
2400
+ const envelope = (0, msgpackr.unpack)(frame instanceof ArrayBuffer ? new Uint8Array(frame) : frame);
2401
+ if (!Array.isArray(envelope) || envelope.length !== ENCRYPTED_ENVELOPE_LENGTH) throw new Error("[ws-crypto] malformed encrypted frame envelope");
2402
+ const [nonce, ciphertext] = envelope;
2403
+ if (!(nonce instanceof Uint8Array) || !(ciphertext instanceof Uint8Array)) throw new Error("[ws-crypto] malformed encrypted frame fields");
2404
+ return await link.decryptBytesFromLinkedClient({
2405
+ linkedClientId,
2406
+ dataToDecrypt: {
2407
+ nonce,
2408
+ ciphertext
2409
+ }
2410
+ });
2411
+ }
2672
2412
  };
2673
2413
  }
2674
- /** Pull the type-specific payload (`input` / `result` / `progress`) out of a wire JSON object. */
2675
- function extractWirePayload(json) {
2676
- if (json.type === "request") return json.input;
2677
- if (json.type === "result") return json.result;
2678
- if (json.type === "progress") return json.progress;
2414
+ //#endregion
2415
+ //#region src/ActionRuntime/Transport/crypto/frameBytes.ts
2416
+ /** Normalize any frame form to bytes (for the AES-GCM layer, which works on `Uint8Array`). */
2417
+ function toFrameBytes(frame) {
2418
+ if (typeof frame === "string") return new TextEncoder().encode(frame);
2419
+ if (frame instanceof ArrayBuffer) return new Uint8Array(frame);
2420
+ return frame;
2679
2421
  }
2680
- /**
2681
- * Reassemble a full wire JSON object from its decoded parts. `inputHash`/`outputHash` are emitted
2682
- * empty — the hydration constructors recompute them — and the result still satisfies
2683
- * `isActionPayload_Any_JsonObject` so it flows through validation like a JSON frame.
2684
- */
2685
- function assembleWireJson(routeMeta, payloadType, time, context, payloadData) {
2686
- const base = {
2687
- form: "data",
2688
- domain: routeMeta.domain,
2689
- id: routeMeta.id,
2690
- allDomains: routeMeta.allDomains,
2691
- time,
2692
- context
2693
- };
2694
- if (payloadType === "request") return {
2695
- ...base,
2696
- type: "request",
2697
- input: payloadData,
2698
- inputHash: ""
2422
+ //#endregion
2423
+ //#region src/ActionRuntime/Transport/SecureSession/frameCryptoPipe.ts
2424
+ function createFrameCryptoPipe(config) {
2425
+ const { write, isOpen, crypto, label = "link" } = config;
2426
+ let sendChain = Promise.resolve();
2427
+ const send = (frame) => {
2428
+ if (isOpen != null && !isOpen()) return;
2429
+ if (crypto == null) {
2430
+ write(frame);
2431
+ return;
2432
+ }
2433
+ const bytes = toFrameBytes(frame);
2434
+ sendChain = sendChain.then(() => crypto).then((c) => c.encryptFrame(bytes)).then((encrypted) => {
2435
+ if (isOpen == null || isOpen()) write(encrypted);
2436
+ }).catch((err) => console.error(`[${label}] failed to encrypt/send frame`, err));
2699
2437
  };
2700
- if (payloadType === "result") return {
2701
- ...base,
2702
- type: "result",
2703
- result: payloadData,
2704
- outputHash: ""
2438
+ const decryptIncoming = async (frame) => {
2439
+ if (crypto == null) return frame;
2440
+ try {
2441
+ return await (await crypto).decryptFrame(frame);
2442
+ } catch (err) {
2443
+ console.error(`[${label}] failed to decrypt incoming frame`, err);
2444
+ return;
2445
+ }
2705
2446
  };
2706
2447
  return {
2707
- ...base,
2708
- type: "progress",
2709
- progress: payloadData
2448
+ send,
2449
+ decryptIncoming
2710
2450
  };
2711
2451
  }
2712
2452
  //#endregion
2713
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/WebSocket/createBinaryWsAdapter.ts
2714
- /**
2715
- * Positional layout of the stateless binary envelope. A flat tuple (rather than an object) strips the
2716
- * repeated `domain`/`id`/`form`/`type` and context key names from every frame, and we carry only the
2717
- * context fields the receiver can't recompute: `cuid` (correlation) and `originClient` (return
2718
- * routing).
2719
- *
2720
- * [ routeInt, typeInt, time, cuid, originClient, payloadData ]
2721
- *
2722
- * Dropped vs the JSON wire: `form`/`type` strings, `inputHash`/`outputHash` (recomputed on hydrate),
2723
- * `context.timeCreated` (reconstructed from `time`) and `context.routing` (rebuilt empty — the
2724
- * receiver re-stamps its own route items as it handles the action). For the leanest possible frames
2725
- * (integer correlation, identity dropped after a handshake), use `createBinaryWsSessionFactory`.
2726
- */
2727
- const ENVELOPE$1 = {
2728
- route: 0,
2729
- type: 1,
2730
- time: 2,
2731
- cuid: 3,
2732
- originClient: 4,
2733
- payload: 5
2734
- };
2735
- const ENVELOPE_LENGTH$1 = 6;
2453
+ //#region src/ActionRuntime/Transport/SecureSession/acceptorSecureSession.ts
2736
2454
  /**
2737
- * Builds a *stateless* `formatMessage` pipeline for {@link WebSocketTransport}, packing action
2738
- * payloads into a compact msgpackr binary frame instead of JSON. The `domain`/`id` route collapses to
2739
- * a single integer drawn from a shared dictionary; `form`/`type`, the recomputable
2740
- * `inputHash`/`outputHash`, and the per-frame `context.routing`/`context.timeCreated` are all dropped
2741
- * (see {@link ENVELOPE}).
2742
- *
2743
- * No validation runs here: `incoming` blindly reconstructs the wire JSON shape and hands it back to
2744
- * the connection, which flows into `ActionRuntime` → `domain.hydrateAnyAction()` where the Valibot
2745
- * schemas validate it exactly as they would for a JSON frame.
2746
- *
2747
- * Both ends of the socket MUST construct the adapter with the same domains in the same order — the
2748
- * integer dictionary is positional. Mismatched dictionaries will route to the wrong action.
2455
+ * The acceptor (accept-in) counterpart to the connector's `establishLinkSession`: one connection's
2456
+ * secure session the server-side handshake driving, the ordered frame crypto, and the per-connection
2457
+ * phase (undecided plain | authenticated). The crypto itself (ordered encrypt-send / decrypt) lives in
2458
+ * the shared {@link createFrameCryptoPipe}, the same primitive the connector uses — so the secure logic
2459
+ * lives once for both link roles.
2749
2460
  *
2750
- * Because `incoming` returns `undefined` for text frames, a binary server can still serve plain-JSON
2751
- * clients on the same runtime (the connection falls back to its built-in JSON parser).
2461
+ * The handler owns one of these per accepted connection and feeds it inbound frames via {@link receive};
2462
+ * identity binding, persistence, codec, and return routing stay in the handler (driven through the
2463
+ * config callbacks). Inbound processing is serialized per connection because the handshake and decryption
2464
+ * are async — this keeps handshake ordering and frame order intact.
2752
2465
  */
2753
- function createBinaryWsAdapter(domains) {
2754
- const { routeToInt, intToRoute } = buildActionRouteDictionary(domains);
2755
- return {
2756
- outgoing: (input) => {
2757
- const json = input.action.toJsonObject();
2758
- const routeKey = `${json.domain}:${json.id}`;
2759
- const routeInt = routeToInt.get(routeKey);
2760
- if (routeInt == null) throw new Error(`[binary-ws] Cannot pack unregistered action route: ${routeKey}`);
2761
- const envelope = new Array(ENVELOPE_LENGTH$1);
2762
- envelope[ENVELOPE$1.route] = routeInt;
2763
- envelope[ENVELOPE$1.type] = PayloadTypeToInt[json.type];
2764
- envelope[ENVELOPE$1.time] = json.time;
2765
- envelope[ENVELOPE$1.cuid] = json.context.cuid;
2766
- envelope[ENVELOPE$1.originClient] = json.context.originClient;
2767
- envelope[ENVELOPE$1.payload] = extractWirePayload(json);
2768
- return (0, msgpackr.pack)(envelope);
2769
- },
2770
- incoming: (frame) => {
2771
- let buffer;
2772
- if (frame instanceof ArrayBuffer) buffer = new Uint8Array(frame);
2773
- else if (frame instanceof Uint8Array) buffer = frame;
2774
- else return;
2775
- try {
2776
- const envelope = (0, msgpackr.unpack)(buffer);
2777
- if (!Array.isArray(envelope) || envelope.length !== ENVELOPE_LENGTH$1) return void 0;
2778
- const routeMeta = intToRoute[envelope[ENVELOPE$1.route]];
2779
- const payloadType = ReversePayloadType[envelope[ENVELOPE$1.type]];
2780
- if (routeMeta == null || payloadType == null) return void 0;
2781
- const time = envelope[ENVELOPE$1.time];
2782
- return assembleWireJson(routeMeta, payloadType, time, {
2783
- cuid: envelope[ENVELOPE$1.cuid],
2784
- timeCreated: time,
2785
- routing: [],
2786
- originClient: envelope[ENVELOPE$1.originClient]
2787
- }, envelope[ENVELOPE$1.payload]);
2788
- } catch (e) {
2789
- console.error("[binary-ws] Failed to unpack binary action frame", e);
2466
+ var AcceptorSecureSession = class {
2467
+ config;
2468
+ _handshake;
2469
+ /** The ordered encrypt-send / decrypt pipe — present only for an `encrypted` connection. */
2470
+ _pipe;
2471
+ _authed = false;
2472
+ _plain = false;
2473
+ /** Serializes inbound processing (handshake + decryption are async). */
2474
+ _inboundChain = Promise.resolve();
2475
+ constructor(config) {
2476
+ this.config = config;
2477
+ }
2478
+ /** Feed one inbound frame. Serialized per connection; routes back through the config callbacks. */
2479
+ receive(frame) {
2480
+ this._inboundChain = this._inboundChain.then(() => this._receive(frame)).catch((err) => console.error("[ws-server] failed to process inbound frame", err));
2481
+ }
2482
+ async _receive(frame) {
2483
+ if (this._plain) {
2484
+ this.config.routePlain(frame);
2485
+ return;
2486
+ }
2487
+ if (!this._authed) {
2488
+ const message = typeof frame === "string" ? decodeHandshakeMessage(frame) : void 0;
2489
+ if (message == null) {
2490
+ if (this.config.noneAllowed) {
2491
+ this._plain = true;
2492
+ this.config.routePlain(frame);
2493
+ }
2790
2494
  return;
2791
2495
  }
2496
+ await this.config.link.initialize();
2497
+ if (this._handshake == null) this._handshake = createServerHandshake({
2498
+ link: this.config.link,
2499
+ localCoordinate: this.config.localCoordinate,
2500
+ dictionaryVersion: this.config.dictionaryVersion,
2501
+ securityLevel: this.config.securityLevel,
2502
+ verifyKeyResolver: this.config.verifyKeyResolver
2503
+ });
2504
+ if (message.t === "hello") this.config.send(encodeHandshakeMessage(await this._handshake.onHello(message)));
2505
+ else if (message.t === "prove") {
2506
+ const reply = await this._handshake.onProve(message);
2507
+ this.config.send(encodeHandshakeMessage(reply));
2508
+ const result = this._handshake.getResult();
2509
+ if (reply.t === "accept" && result != null) this._complete(result);
2510
+ }
2511
+ return;
2792
2512
  }
2793
- };
2794
- }
2795
- //#endregion
2796
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/WebSocket/createBinaryWsSessionFactory.ts
2797
- /**
2798
- * Positional layout of the *session* binary envelope — the leanest frame. Compared to the stateless
2799
- * adapter it replaces the 21-char `cuid` with a small per-connection integer and only carries
2800
- * `originClient` on the very first request of each direction (the peer remembers it afterwards).
2801
- *
2802
- * [ routeInt, typeInt, corrId, time, originClient?, payloadData ]
2803
- */
2804
- const ENVELOPE = {
2805
- route: 0,
2806
- type: 1,
2807
- corr: 2,
2808
- time: 3,
2809
- originClient: 4,
2810
- payload: 5
2811
- };
2812
- const ENVELOPE_LENGTH = 6;
2813
- /**
2814
- * How long a pending correlation entry is kept before it's swept. A correlation only matters until its
2815
- * action resolves or times out, so anything older than the longest realistic action timeout can be
2816
- * dropped — this bounds memory when requests time out or a connection dies mid-flight (their replies
2817
- * would never arrive, leaving the entry orphaned). Generous default so live correlations are never
2818
- * pruned (the default transport timeout is 10s).
2819
- */
2820
- const DEFAULT_CORRELATION_TTL_MS = 5 * 6e4;
2821
- function isKnownIdentity(coordinate) {
2822
- return coordinate != null && coordinate.envId !== "_unset_";
2823
- }
2824
- /**
2825
- * Drop entries older than `ttlMs`. Maps keep insertion order and entries are inserted in time order,
2826
- * so the oldest are first — stop sweeping at the first live entry.
2827
- */
2828
- function pruneExpired(map, now, ttlMs) {
2829
- for (const [key, entry] of map) {
2830
- if (now - entry.time <= ttlMs) break;
2831
- map.delete(key);
2513
+ const bytes = this._pipe != null ? await this._pipe.decryptIncoming(frame) : frame;
2514
+ if (bytes === void 0) return;
2515
+ this.config.routeAction(bytes);
2516
+ }
2517
+ _complete(result) {
2518
+ this._authed = true;
2519
+ this._handshake = void 0;
2520
+ if (result.securityLevel === "encrypted") this._pipe = this._buildPipe(createActionFrameCrypto({
2521
+ link: this.config.link,
2522
+ linkedClientId: result.linkedClientId
2523
+ }));
2524
+ this.config.onAuthenticated({
2525
+ client: new RuntimeCoordinate(result.remote),
2526
+ securityLevel: result.securityLevel,
2527
+ linkedClientId: result.linkedClientId,
2528
+ keyMaterial: result.encryptionKeyMaterial
2529
+ });
2832
2530
  }
2833
- }
2531
+ /**
2532
+ * Restore an already-authenticated session after eviction — no handshake. For an `encrypted`
2533
+ * connection the shared key is re-derived asynchronously (the acceptor re-links the client off its own
2534
+ * persisted identity); the pipe's crypto IS that promise, so the connection's first in/out frame
2535
+ * naturally waits for the key before encrypt/decrypt — no separate gate.
2536
+ */
2537
+ rehydrate(state) {
2538
+ this._authed = true;
2539
+ if (state.securityLevel !== "encrypted" || state.keyMaterial == null) return;
2540
+ const { link } = this.config;
2541
+ const { linkedClientId, keyMaterial } = state;
2542
+ const cryptoReady = link.initialize().then(() => link.linkClient({
2543
+ linkedClientId,
2544
+ verifyPublicKey: keyMaterial.verifyPublicKey,
2545
+ exchangePublicKey: keyMaterial.exchangePublicKey,
2546
+ saltString: keyMaterial.saltString,
2547
+ infoString: keyMaterial.infoString,
2548
+ bindVerifyKeysIntoDerivation: keyMaterial.bindVerifyKeysIntoDerivation
2549
+ })).then(() => createActionFrameCrypto({
2550
+ link,
2551
+ linkedClientId
2552
+ }));
2553
+ cryptoReady.catch((err) => console.error("[ws-server] failed to restore encrypted session", err));
2554
+ this._pipe = this._buildPipe(cryptoReady);
2555
+ }
2556
+ /** Send an outbound frame: through the encrypt pipe when encrypted, otherwise raw. */
2557
+ send(frame) {
2558
+ if (this._pipe != null) {
2559
+ this._pipe.send(frame);
2560
+ return;
2561
+ }
2562
+ this.config.send(frame);
2563
+ }
2564
+ _buildPipe(crypto) {
2565
+ return createFrameCryptoPipe({
2566
+ write: this.config.send,
2567
+ crypto,
2568
+ label: "ws-server"
2569
+ });
2570
+ }
2571
+ };
2572
+ //#endregion
2573
+ //#region src/ActionRuntime/Handler/PeerLink/Acceptor/AcceptorHandler.ts
2834
2574
  /**
2835
- * Builds a factory of *stateful, per-connection* codecs for {@link WebSocketTransport} /
2836
- * `ActionServerHandler` the maximally compact binary wire. Call the returned factory once per live
2837
- * connection (each socket on the client, each accepted connection on the server) so every channel
2838
- * gets its own correlation + identity state.
2575
+ * Server-side handler for backends that accept many client connections over a single open channel
2576
+ * (WebSockets, Durable Objects, …). It is transport-agnostic: you feed it inbound frames with
2577
+ * {@link receive} and tell it how to write outbound frames via the `send` option.
2839
2578
  *
2840
- * On top of everything {@link createBinaryWsAdapter} drops, a session also drops:
2841
- * - **`cuid`** — replaced by a per-connection integer correlation id. The initiator maps it to its
2842
- * real cuid; the responder echoes it; each side reconstructs the cuid from its own map. Correlation
2843
- * only needs to be unique per socket, so a counter suffices.
2844
- * - **`originClient` after the first request** the first request each side sends carries its
2845
- * identity; the peer remembers it and injects it into later frames. Replies omit it entirely (a
2846
- * reply carries the initiator's own origin, which the initiator already knows).
2579
+ * Add it alongside your local execution handler:
2580
+ * ```ts
2581
+ * const serverHandler = createAcceptorHandler({ clientEnv, formatMessage, send: (ws, f) => ws.send(f) });
2582
+ * runtime.addHandlers([localHandler, serverHandler]);
2583
+ * // per inbound message (e.g. a Durable Object's webSocketMessage):
2584
+ * serverHandler.receive(ws, message);
2585
+ * ```
2847
2586
  *
2848
- * Both ends MUST build the factory from the same domains in the same order (positional dictionary).
2849
- * Text frames still return `undefined` from `incoming`, so JSON clients remain interoperable.
2587
+ * Inbound requests route to your local handler; the runtime's return dispatch then calls this
2588
+ * handler back (it is an external handler keyed to `clientEnv`) to send the result to the originating
2589
+ * connection. The handler keeps a per-connection identity registry so each result lands on the right
2590
+ * socket, and remembers each connection's encoding so binary and JSON clients can share the channel.
2850
2591
  *
2851
- * Hibernation note: after a server connection is evicted its session resets, so a still-connected
2852
- * client (whose session persists) will keep omitting `originClient`. The server must therefore restore
2853
- * the connection→client binding from its own store (see `ActionServerHandler.rehydrateConnection`) and
2854
- * inject `originClient` from there — the session alone can't recover it.
2592
+ * It registers an empty action router, so it is never chosen to *execute* an inbound request — only
2593
+ * to ferry results/pushes back out.
2855
2594
  */
2856
- function createBinaryWsSessionFactory(domains, options) {
2857
- const { routeToInt, intToRoute } = buildActionRouteDictionary(domains);
2858
- const unknownIdentity = RuntimeCoordinate.unknown.toJsonObject();
2859
- const ttlMs = options?.correlationTtlMs ?? DEFAULT_CORRELATION_TTL_MS;
2860
- return () => {
2861
- let outCounter = 0;
2862
- const corrToCuid = /* @__PURE__ */ new Map();
2863
- const cuidToCorr = /* @__PURE__ */ new Map();
2864
- let selfIdentity;
2865
- let peerIdentity;
2866
- return {
2867
- outgoing: (input) => {
2868
- const json = input.action.toJsonObject();
2869
- const routeKey = `${json.domain}:${json.id}`;
2870
- const routeInt = routeToInt.get(routeKey);
2871
- if (routeInt == null) throw new Error(`[binary-ws] Cannot pack unregistered action route: ${routeKey}`);
2872
- const now = Date.now();
2873
- pruneExpired(corrToCuid, now, ttlMs);
2595
+ var AcceptorHandler = class extends PeerLinkHandler {
2596
+ /** Accept-in over a live (duplex) connection registry — it pushes results/broadcasts to bound sockets. */
2597
+ canPush = true;
2598
+ _formatMessage;
2599
+ _createFormatMessage;
2600
+ _send;
2601
+ _runtime;
2602
+ _serverTimeout;
2603
+ _onConnectionBound;
2604
+ _security;
2605
+ /** Normalized accepted levels; whether `none` (plain) is allowed; whether any level needs a handshake. */
2606
+ _allowedLevels;
2607
+ _noneAllowed;
2608
+ _handshakeMode;
2609
+ _connByClient = /* @__PURE__ */ new Map();
2610
+ _clientByConn = /* @__PURE__ */ new Map();
2611
+ _connEncoding = /* @__PURE__ */ new Map();
2612
+ _codecByConn = /* @__PURE__ */ new Map();
2613
+ _sessionByConn = /* @__PURE__ */ new Map();
2614
+ constructor(options) {
2615
+ super(options.clientEnv);
2616
+ this._formatMessage = options.formatMessage;
2617
+ this._createFormatMessage = options.createFormatMessage;
2618
+ this._send = options.send;
2619
+ this._runtime = options.runtime;
2620
+ this._serverTimeout = options.defaultTimeout ?? 1e4;
2621
+ this._onConnectionBound = options.onConnectionBound;
2622
+ this._security = options.security;
2623
+ this._allowedLevels = options.security == null ? [] : Array.isArray(options.security.securityLevel) ? options.security.securityLevel : [options.security.securityLevel];
2624
+ this._noneAllowed = this._allowedLevels.includes("none");
2625
+ this._handshakeMode = this._allowedLevels.some((level) => level !== "none");
2626
+ }
2627
+ /**
2628
+ * The codec for a connection: a per-connection session (cached) when a factory was provided, else
2629
+ * the single shared `formatMessage`.
2630
+ */
2631
+ _codecFor(connection) {
2632
+ if (this._createFormatMessage != null) {
2633
+ let codec = this._codecByConn.get(connection);
2634
+ if (codec == null) {
2635
+ codec = this._createFormatMessage();
2636
+ this._codecByConn.set(connection, codec);
2637
+ }
2638
+ return codec;
2639
+ }
2640
+ if (this._formatMessage != null) return this._formatMessage;
2641
+ throw err_nice_transport.fromId("not_found", { actionId: "server-handler-codec (provide formatMessage or createFormatMessage)" });
2642
+ }
2643
+ /**
2644
+ * Register (or replace) the connection-bound persistence callback after construction. Used by
2645
+ * lifecycle helpers like {@link createHibernatableWsServerAdapter} so persistence and replay are
2646
+ * owned by one place instead of being split across the constructor options.
2647
+ */
2648
+ setOnConnectionBound(onConnectionBound) {
2649
+ this._onConnectionBound = onConnectionBound;
2650
+ }
2651
+ /**
2652
+ * Feed one inbound frame from a connection into the runtime. Decodes text or binary, binds the
2653
+ * connection to the requesting client's identity, then routes it (requests execute locally;
2654
+ * results/progress resolve pending server-initiated actions).
2655
+ */
2656
+ receive(connection, frame) {
2657
+ const security = this._security;
2658
+ if (security == null || !this._handshakeMode) {
2659
+ this._receivePlain(connection, frame);
2660
+ return;
2661
+ }
2662
+ this._sessionFor(connection, security).receive(frame);
2663
+ }
2664
+ _receivePlain(connection, frame) {
2665
+ const wire = decodeActionFrame(frame, this._codecFor(connection));
2666
+ if (wire == null) return;
2667
+ const encoding = typeof frame === "string" ? "json" : "binary";
2668
+ this._connEncoding.set(connection, encoding);
2669
+ if (wire.type === "request") this._resolveRequestIdentity(connection, wire, encoding);
2670
+ this._emitIncoming(wire);
2671
+ }
2672
+ /**
2673
+ * The secure session for a connection (built lazily on its first secure-mode frame), with the
2674
+ * handler-owned effects — raw send, identity binding + persistence, and inbound routing — wired in as
2675
+ * callbacks. The session owns all crypto/handshake/chain state; the handler keeps only the registry.
2676
+ */
2677
+ _sessionFor(connection, security) {
2678
+ let session = this._sessionByConn.get(connection);
2679
+ if (session == null) {
2680
+ session = new AcceptorSecureSession({
2681
+ link: security.link,
2682
+ localCoordinate: security.localCoordinate,
2683
+ dictionaryVersion: security.dictionaryVersion,
2684
+ securityLevel: security.securityLevel,
2685
+ verifyKeyResolver: security.verifyKeyResolver,
2686
+ noneAllowed: this._noneAllowed,
2687
+ send: (frame) => this._send(connection, frame),
2688
+ onAuthenticated: (auth) => this._onConnectionAuthenticated(connection, auth),
2689
+ routePlain: (frame) => this._receivePlain(connection, frame),
2690
+ routeAction: (bytes) => this._routeAuthedActionBytes(connection, bytes)
2691
+ });
2692
+ this._sessionByConn.set(connection, session);
2693
+ }
2694
+ return session;
2695
+ }
2696
+ /** Bind + persist a connection's authenticated identity once its handshake completes. */
2697
+ _onConnectionAuthenticated(connection, auth) {
2698
+ this._bindConnection(connection, auth.client);
2699
+ this._connEncoding.set(connection, "binary");
2700
+ this._onConnectionBound?.(connection, {
2701
+ client: auth.client.toJsonObject(),
2702
+ encoding: "binary",
2703
+ secure: {
2704
+ securityLevel: auth.securityLevel,
2705
+ linkedClientId: auth.linkedClientId,
2706
+ keyMaterial: auth.keyMaterial
2707
+ }
2708
+ });
2709
+ }
2710
+ /** Decode a decrypted authenticated frame, inject the *authenticated* identity, and route it. */
2711
+ _routeAuthedActionBytes(connection, bytes) {
2712
+ const wire = decodeActionFrame(bytes, this._codecFor(connection));
2713
+ if (wire == null) return;
2714
+ if (wire.type === "request") {
2715
+ const bound = this._clientByConn.get(connection);
2716
+ if (bound != null) wire.context.originClient = bound.toJsonObject();
2717
+ }
2718
+ this._emitIncoming(wire);
2719
+ }
2720
+ /**
2721
+ * Ensure an inbound request carries the client's identity and that this connection is bound to it,
2722
+ * so its result can be routed back. A session codec omits `originClient` after the first request, so
2723
+ * when it's missing we restore it from the (possibly rehydrated) binding instead. (Plain mode only;
2724
+ * secure mode binds the authenticated coordinate at handshake time.)
2725
+ */
2726
+ _resolveRequestIdentity(connection, wire, encoding) {
2727
+ const wireOrigin = wire.context.originClient;
2728
+ if (wireOrigin != null && wireOrigin.envId !== "_unset_") {
2729
+ const clientCoord = new RuntimeCoordinate(wireOrigin);
2730
+ const isNewBinding = this._clientByConn.get(connection)?.stringId !== clientCoord.stringId;
2731
+ this._bindConnection(connection, clientCoord);
2732
+ if (isNewBinding) this._onConnectionBound?.(connection, {
2733
+ client: clientCoord.toJsonObject(),
2734
+ encoding
2735
+ });
2736
+ return;
2737
+ }
2738
+ const bound = this._clientByConn.get(connection);
2739
+ if (bound != null) wire.context.originClient = bound.toJsonObject();
2740
+ }
2741
+ /**
2742
+ * Restore a connection→client binding without an inbound frame — for transports that resume after
2743
+ * eviction. Pair it with the {@link IAcceptorHandlerOptions.onConnectionBound} hook: persist
2744
+ * the binding there, then replay each live connection here when the channel comes back (e.g. a
2745
+ * Durable Object iterating `ctx.getWebSockets()` as it wakes from hibernation).
2746
+ */
2747
+ rehydrateConnection(connection, binding) {
2748
+ this._bindConnection(connection, new RuntimeCoordinate(binding.client));
2749
+ this._connEncoding.set(connection, binding.encoding);
2750
+ const secure = binding.secure;
2751
+ const security = this._security;
2752
+ if (secure == null || security == null) return;
2753
+ this._sessionFor(connection, security).rehydrate(secure);
2754
+ }
2755
+ toJsonObject() {
2756
+ return {
2757
+ type: this.handlerType,
2758
+ client: this.peerClient
2759
+ };
2760
+ }
2761
+ toHandlerRouteItem() {
2762
+ return {
2763
+ type: this.handlerType,
2764
+ client: this.peerClient,
2765
+ transShape: "duplex",
2766
+ transOrd: 0
2767
+ };
2768
+ }
2769
+ /** Forget a connection (call on socket close) so stale entries don't misroute later results. */
2770
+ dropConnection(connection) {
2771
+ const coord = this._clientByConn.get(connection);
2772
+ if (coord != null && this._connByClient.get(coord.stringId) === connection) this._connByClient.delete(coord.stringId);
2773
+ this._clientByConn.delete(connection);
2774
+ this._connEncoding.delete(connection);
2775
+ this._codecByConn.delete(connection);
2776
+ this._sessionByConn.delete(connection);
2777
+ }
2778
+ /** Live connection for a client coordinate, if currently registered. */
2779
+ getConnectionForClient(client) {
2780
+ return this._connByClient.get(client.stringId);
2781
+ }
2782
+ /** This acceptor owns the origin's return path when it currently holds a live connection bound to it. */
2783
+ ownsLiveConnectionFor(origin) {
2784
+ return this._connByClient.has(origin.stringId);
2785
+ }
2786
+ /** Whether this acceptor currently tracks `connection` — used to pick the owning handler among several. */
2787
+ hasConnection(connection) {
2788
+ return this._clientByConn.has(connection);
2789
+ }
2790
+ /**
2791
+ * Send (and optionally await) a server-initiated action to a specific connected client. Pass the
2792
+ * connection token directly (e.g. the `ws`) or a client `RuntimeCoordinate` to look one up.
2793
+ */
2794
+ pushToClient(runtime, target, request, options) {
2795
+ const connection = this._resolveConnection(target);
2796
+ return this._dispatch(runtime, connection, request, options?.timeout);
2797
+ }
2798
+ /**
2799
+ * Build a local handler whose cases are connection-aware: each case receives the primed request and
2800
+ * the originating client's live connection (resolved from `originClient`), so handlers don't repeat
2801
+ * the `getConnectionForClient(action.context.originClient)` lookup. Cases may return raw output or
2802
+ * nothing, just like {@link ActionLocalHandler.forDomainActionCases}. Add the returned handler to the
2803
+ * runtime alongside this server handler:
2804
+ * ```ts
2805
+ * runtime.addHandlers([serverHandler.forConnectionDomainCases(domain, { … }), serverHandler]);
2806
+ * ```
2807
+ */
2808
+ forConnectionDomainCases(domain, cases) {
2809
+ return this.forConnectionDomainCasesMulti([domain], cases);
2810
+ }
2811
+ /**
2812
+ * Like {@link forConnectionDomainCases} but spanning several domains with one merged case map — used
2813
+ * by channel-derived wiring (`acceptChannelConnections`) where the channel's `toAcceptor` domains are
2814
+ * served together. Each domain takes only the cases whose ids it owns, so a single map can cover
2815
+ * several domains and unrelated ids are ignored.
2816
+ */
2817
+ forConnectionDomainCasesMulti(domains, cases) {
2818
+ const handler = new ActionLocalHandler();
2819
+ for (const domain of domains) {
2820
+ const ownedIds = new Set(Object.keys(domain.actionsMap()));
2821
+ const wrapped = {};
2822
+ for (const id in cases) {
2823
+ if (!ownedIds.has(id)) continue;
2824
+ const caseFn = cases[id];
2825
+ if (caseFn == null) continue;
2826
+ wrapped[id] = (request) => caseFn(request, this.getConnectionForClient(request.context.originClient));
2827
+ }
2828
+ handler.forDomainActionCases(domain, wrapped);
2829
+ }
2830
+ return handler;
2831
+ }
2832
+ /**
2833
+ * Fan a server-initiated request out to every currently-bound connection. A fresh request is built
2834
+ * per connection (each push mutates its own action context) and dispatched fire-and-forget. Pass
2835
+ * `except` to skip the originating socket and `where` to filter by connection (e.g. read its
2836
+ * attachment for a role). Iterating bound connections (rather than every accepted socket) skips
2837
+ * sockets that are still mid-handshake and so can't yet receive a frame.
2838
+ */
2839
+ broadcast(makeRequest, options) {
2840
+ const runtime = options?.runtime ?? this._runtime;
2841
+ if (runtime == null) throw err_nice_transport.fromId("not_found", { actionId: "server-handler-runtime (construct with `runtime` or pass `options.runtime`)" });
2842
+ for (const connection of this._clientByConn.keys()) {
2843
+ if (options?.except != null && connection === options.except) continue;
2844
+ if (options?.where != null && !options.where(connection)) continue;
2845
+ try {
2846
+ this.pushToClient(runtime, connection, makeRequest(), { timeout: options?.timeout });
2847
+ } catch (error) {
2848
+ if (options?.onError != null) options.onError(error, connection);
2849
+ else console.error("[ws-server] broadcast push failed", error);
2850
+ }
2851
+ }
2852
+ }
2853
+ async sendReturnPayload(payload, config) {
2854
+ const connection = this._connByClient.get(payload.context.originClient.stringId);
2855
+ if (connection == null) return false;
2856
+ this._sendPayload(connection, payload, config.targetLocalRuntime.coordinate);
2857
+ return true;
2858
+ }
2859
+ async handleActionRequest(action, config) {
2860
+ const runtime = config?.targetLocalRuntime ?? ActionRuntime.getDefault();
2861
+ const connection = this._resolveSingleConnection();
2862
+ return this._dispatch(runtime, connection, action, config?.timeout);
2863
+ }
2864
+ _dispatch(runtime, connection, action, timeout) {
2865
+ const timeoutMs = timeout ?? this._serverTimeout;
2866
+ action.context._setOriginClient(runtime.coordinate);
2867
+ action.context.addRouteItem({
2868
+ runtime: runtime.coordinate,
2869
+ handler: this.toHandlerRouteItem(),
2870
+ time: Date.now()
2871
+ });
2872
+ const runningAction = new RunningAction({
2873
+ context: action.context,
2874
+ request: action,
2875
+ parentCuid: peekHandlerCuid(),
2876
+ callSite: action._callSite
2877
+ });
2878
+ runtime.registerRunningAction(runningAction);
2879
+ if (action.schema.responseMode === "none") {
2880
+ try {
2881
+ this._sendPayload(connection, action, runtime.coordinate);
2882
+ runningAction._completeWithResult(action.successResult(void 0));
2883
+ } catch (err) {
2884
+ runningAction._abort(err);
2885
+ }
2886
+ return runningAction;
2887
+ }
2888
+ const timeoutId = setTimeout(() => {
2889
+ runningAction._abort(err_nice_transport.fromId("timeout", { timeout: timeoutMs }));
2890
+ }, timeoutMs);
2891
+ runningAction.addUpdateListeners([(update) => {
2892
+ if (update.type === "finished") clearTimeout(timeoutId);
2893
+ }]);
2894
+ try {
2895
+ this._sendPayload(connection, action, runtime.coordinate);
2896
+ } catch (err) {
2897
+ runningAction._abort(err);
2898
+ }
2899
+ return runningAction;
2900
+ }
2901
+ _sendPayload(connection, payload, localClient) {
2902
+ const frame = (this._connEncoding.get(connection) ?? "binary") === "json" ? JSON.stringify(payload.toJsonObject()) : this._codecFor(connection).outgoing({
2903
+ action: payload,
2904
+ localClient,
2905
+ externalClient: this.peerClient
2906
+ });
2907
+ const session = this._sessionByConn.get(connection);
2908
+ if (session == null) {
2909
+ this._send(connection, frame);
2910
+ return;
2911
+ }
2912
+ session.send(frame);
2913
+ }
2914
+ _bindConnection(connection, client) {
2915
+ this._connByClient.set(client.stringId, connection);
2916
+ this._clientByConn.set(connection, client);
2917
+ }
2918
+ _resolveConnection(target) {
2919
+ if (target instanceof RuntimeCoordinate) {
2920
+ const connection = this._connByClient.get(target.stringId);
2921
+ if (connection == null) throw err_nice_transport.fromId("not_found", { actionId: target.stringId });
2922
+ return connection;
2923
+ }
2924
+ return target;
2925
+ }
2926
+ _resolveSingleConnection() {
2927
+ if (this._clientByConn.size !== 1) throw err_nice_transport.fromId("not_found", { actionId: "server-handler-target (use pushToClient with an explicit connection or client coordinate)" });
2928
+ return this._clientByConn.keys().next().value;
2929
+ }
2930
+ };
2931
+ const createAcceptorHandler = (options) => {
2932
+ return new AcceptorHandler(options);
2933
+ };
2934
+ //#endregion
2935
+ //#region src/ActionRuntime/Handler/PeerLink/Acceptor/createSecureActionServer.ts
2936
+ /** Default accepted set: negotiate per connection to whatever the client picks. */
2937
+ const DEFAULT_SERVER_SECURITY_LEVELS$1 = [
2938
+ "none",
2939
+ "authenticated",
2940
+ "encrypted"
2941
+ ];
2942
+ /**
2943
+ * Build an {@link AcceptorHandler} for the secure binary channel with the boilerplate folded in:
2944
+ * it creates the {@link ClientCryptoKeyLink} and the storage-backed TOFU resolver from a single
2945
+ * `storageAdapter`, installs the channel's per-connection codec, and assembles the `security` block
2946
+ * from the runtime coordinate + channel version (accepting all three levels by default).
2947
+ *
2948
+ * For a hibernatable transport (e.g. a Durable Object), pair it with
2949
+ * {@link createHibernatableWsServerAdapter} to wire persistence + replay.
2950
+ */
2951
+ function createSecureAcceptorHandler(options) {
2952
+ const link = options.link ?? new _nice_code_util.ClientCryptoKeyLink({ storageAdapter: options.storageAdapter });
2953
+ return new AcceptorHandler({
2954
+ clientEnv: options.clientEnv,
2955
+ createFormatMessage: options.channel.createCodec,
2956
+ send: options.send,
2957
+ runtime: options.runtime,
2958
+ defaultTimeout: options.defaultTimeout,
2959
+ security: {
2960
+ securityLevel: options.securityLevel ?? DEFAULT_SERVER_SECURITY_LEVELS$1,
2961
+ link,
2962
+ localCoordinate: options.runtime.coordinate.toJsonObject(),
2963
+ dictionaryVersion: options.channel.dictionaryVersion,
2964
+ verifyKeyResolver: options.verifyKeyResolver ?? createStorageTofuVerifyKeyResolver(options.storageAdapter)
2965
+ }
2966
+ });
2967
+ }
2968
+ //#endregion
2969
+ //#region src/ActionRuntime/Channel/ActionChannel.ts
2970
+ /**
2971
+ * Declare a transport-agnostic channel by role. Use it for HTTP, custom transports, or as the routing
2972
+ * half of a richer channel. The order of each list is part of the contract for wire formats that pack
2973
+ * positionally (see `defineSecureChannel`) — add new domains to the end of their list. (`domains` is
2974
+ * accepted as a legacy alias for `toAcceptor`.)
2975
+ */
2976
+ function defineChannel(options) {
2977
+ return {
2978
+ toAcceptorDomains: options.toAcceptor,
2979
+ toConnectorDomains: options.toConnector
2980
+ };
2981
+ }
2982
+ /**
2983
+ * Wire a connection to the acceptor straight from a channel: route the channel's `toAcceptor` domains to
2984
+ * the acceptor over `transports`, and register local handlers for its `toConnector` pushes from
2985
+ * `onPush`. The channel is the single source of truth for *what* is routed in each direction — the
2986
+ * caller only supplies the transport(s) and the push handlers, never restated domain lists. Pass several
2987
+ * transports to make the connector→acceptor path transport-agnostic (e.g. secure WS preferred, HTTP
2988
+ * fallback).
2989
+ *
2990
+ * Sugar over {@link ActionRuntime.connectTo}. Returns the acceptor handler so the caller can later
2991
+ * `clearTransportCache()` it.
2992
+ */
2993
+ function connectChannel(runtime, acceptorCoordinate, options) {
2994
+ const pushHandlers = options.onPush != null ? options.channel.toConnectorDomains.map((domain) => domain.wrapAsPartialLocalHandler(options.onPush)) : [];
2995
+ return runtime.connectTo(acceptorCoordinate, {
2996
+ transports: options.transports,
2997
+ domains: [...options.channel.toAcceptorDomains],
2998
+ localHandlers: pushHandlers,
2999
+ defaultTimeout: options.defaultTimeout
3000
+ });
3001
+ }
3002
+ /**
3003
+ * Register an acceptor handler's execution for a channel straight from its definition: the channel's
3004
+ * `toAcceptor` domains are served together with one merged, connection-aware case map (each case gets
3005
+ * the primed request + the originating connection, as with
3006
+ * {@link AcceptorHandler.forConnectionDomainCases}). The domain list is taken from the channel,
3007
+ * never restated. Add the returned handler to the runtime alongside the acceptor handler:
3008
+ * ```ts
3009
+ * runtime.addHandlers([acceptChannelConnections(serverHandler, channel, { … }), serverHandler]);
3010
+ * ```
3011
+ */
3012
+ function acceptChannelConnections(serverHandler, channel, cases) {
3013
+ return serverHandler.forConnectionDomainCasesMulti(channel.toAcceptorDomains, cases);
3014
+ }
3015
+ /**
3016
+ * Build the secure {@link AcceptorHandler} for a channel — the accept-in counterpart to
3017
+ * {@link connectChannel}. It folds in the same boilerplate as {@link createSecureAcceptorHandler} (the
3018
+ * `ClientCryptoKeyLink` + storage-backed TOFU resolver from one `storageAdapter`, the channel's codec +
3019
+ * dictionary version, the `security` block from the runtime coordinate) but takes the `(runtime, channel,
3020
+ * options)` shape of the channel family. Pair it with {@link acceptChannelConnections} for execution:
3021
+ * ```ts
3022
+ * const acceptor = acceptChannel(runtime, gameChannel, { clientEnv, storageAdapter, send });
3023
+ * runtime.addHandlers([acceptChannelConnections(acceptor, gameChannel, { … }), acceptor]);
3024
+ * ```
3025
+ */
3026
+ function acceptChannel(runtime, channel, options) {
3027
+ return createSecureAcceptorHandler({
3028
+ channel,
3029
+ runtime,
3030
+ clientEnv: options.clientEnv,
3031
+ storageAdapter: options.storageAdapter,
3032
+ link: options.link,
3033
+ send: options.send,
3034
+ securityLevel: options.securityLevel,
3035
+ verifyKeyResolver: options.verifyKeyResolver,
3036
+ defaultTimeout: options.defaultTimeout
3037
+ });
3038
+ }
3039
+ //#endregion
3040
+ //#region src/ActionRuntime/Transport/codec/actionWireCodec.ts
3041
+ /**
3042
+ * Tiny integer codes for the payload type, so the verbose `"request"`/`"result"`/`"progress"`
3043
+ * strings never hit the wire. The index in {@link ReversePayloadType} must line up with the value.
3044
+ */
3045
+ const PayloadTypeToInt = {
3046
+ ["request"]: 0,
3047
+ ["result"]: 1,
3048
+ ["progress"]: 2
3049
+ };
3050
+ const ReversePayloadType = [
3051
+ "request",
3052
+ "result",
3053
+ "progress"
3054
+ ];
3055
+ /**
3056
+ * Build the positional `domain:id` ↔ integer dictionary. Both ends of a channel MUST build it from
3057
+ * the same domains in the same order — the mapping is positional, so a mismatch routes to the wrong
3058
+ * action. Add new transported domains to the end of the list.
3059
+ */
3060
+ function buildActionRouteDictionary(domains) {
3061
+ const routeToInt = /* @__PURE__ */ new Map();
3062
+ const intToRoute = [];
3063
+ for (const dom of domains) for (const actionId of Object.keys(dom.actionSchema)) {
3064
+ const routeKey = `${dom.domain}:${actionId}`;
3065
+ if (routeToInt.has(routeKey)) continue;
3066
+ routeToInt.set(routeKey, intToRoute.length);
3067
+ intToRoute.push({
3068
+ domain: dom.domain,
3069
+ id: actionId,
3070
+ allDomains: dom.allDomains
3071
+ });
3072
+ }
3073
+ return {
3074
+ routeToInt,
3075
+ intToRoute
3076
+ };
3077
+ }
3078
+ /** Pull the type-specific payload (`input` / `result` / `progress`) out of a wire JSON object. */
3079
+ function extractWirePayload(json) {
3080
+ if (json.type === "request") return json.input;
3081
+ if (json.type === "result") return json.result;
3082
+ if (json.type === "progress") return json.progress;
3083
+ }
3084
+ /**
3085
+ * Reassemble a full wire JSON object from its decoded parts. `inputHash`/`outputHash` are emitted
3086
+ * empty — the hydration constructors recompute them — and the result still satisfies
3087
+ * `isActionPayload_Any_JsonObject` so it flows through validation like a JSON frame.
3088
+ */
3089
+ function assembleWireJson(routeMeta, payloadType, time, context, payloadData) {
3090
+ const base = {
3091
+ form: "data",
3092
+ domain: routeMeta.domain,
3093
+ id: routeMeta.id,
3094
+ allDomains: routeMeta.allDomains,
3095
+ time,
3096
+ context
3097
+ };
3098
+ if (payloadType === "request") return {
3099
+ ...base,
3100
+ type: "request",
3101
+ input: payloadData,
3102
+ inputHash: ""
3103
+ };
3104
+ if (payloadType === "result") return {
3105
+ ...base,
3106
+ type: "result",
3107
+ result: payloadData,
3108
+ outputHash: ""
3109
+ };
3110
+ return {
3111
+ ...base,
3112
+ type: "progress",
3113
+ progress: payloadData
3114
+ };
3115
+ }
3116
+ //#endregion
3117
+ //#region src/ActionRuntime/Transport/codec/createBinaryWireSessionFactory.ts
3118
+ /**
3119
+ * Positional layout of the *session* binary envelope — the leanest frame. Compared to the stateless
3120
+ * adapter it replaces the 21-char `cuid` with a small per-connection integer and only carries
3121
+ * `originClient` on the very first request of each direction (the peer remembers it afterwards).
3122
+ *
3123
+ * [ routeInt, typeInt, corrId, time, originClient?, payloadData ]
3124
+ */
3125
+ const ENVELOPE$1 = {
3126
+ route: 0,
3127
+ type: 1,
3128
+ corr: 2,
3129
+ time: 3,
3130
+ originClient: 4,
3131
+ payload: 5
3132
+ };
3133
+ const ENVELOPE_LENGTH$1 = 6;
3134
+ /**
3135
+ * How long a pending correlation entry is kept before it's swept. A correlation only matters until its
3136
+ * action resolves or times out, so anything older than the longest realistic action timeout can be
3137
+ * dropped — this bounds memory when requests time out or a connection dies mid-flight (their replies
3138
+ * would never arrive, leaving the entry orphaned). Generous default so live correlations are never
3139
+ * pruned (the default transport timeout is 10s).
3140
+ */
3141
+ const DEFAULT_CORRELATION_TTL_MS = 5 * 6e4;
3142
+ function isKnownIdentity(coordinate) {
3143
+ return coordinate != null && coordinate.envId !== "_unset_";
3144
+ }
3145
+ /**
3146
+ * Drop entries older than `ttlMs`. Maps keep insertion order and entries are inserted in time order,
3147
+ * so the oldest are first — stop sweeping at the first live entry.
3148
+ */
3149
+ function pruneExpired(map, now, ttlMs) {
3150
+ for (const [key, entry] of map) {
3151
+ if (now - entry.time <= ttlMs) break;
3152
+ map.delete(key);
3153
+ }
3154
+ }
3155
+ /**
3156
+ * Builds a factory of *stateful, per-connection* codecs for {@link LinkTransport} /
3157
+ * `AcceptorHandler` — the maximally compact binary wire. Call the returned factory once per live
3158
+ * connection (each socket on the client, each accepted connection on the server) so every channel
3159
+ * gets its own correlation + identity state.
3160
+ *
3161
+ * On top of everything {@link createBinaryWireAdapter} drops, a session also drops:
3162
+ * - **`cuid`** — replaced by a per-connection integer correlation id. The initiator maps it to its
3163
+ * real cuid; the responder echoes it; each side reconstructs the cuid from its own map. Correlation
3164
+ * only needs to be unique per socket, so a counter suffices.
3165
+ * - **`originClient` after the first request** — the first request each side sends carries its
3166
+ * identity; the peer remembers it and injects it into later frames. Replies omit it entirely (a
3167
+ * reply carries the initiator's own origin, which the initiator already knows).
3168
+ *
3169
+ * Both ends MUST build the factory from the same domains in the same order (positional dictionary).
3170
+ * Text frames still return `undefined` from `incoming`, so JSON clients remain interoperable.
3171
+ *
3172
+ * Hibernation note: after a server connection is evicted its session resets, so a still-connected
3173
+ * client (whose session persists) will keep omitting `originClient`. The server must therefore restore
3174
+ * the connection→client binding from its own store (see `AcceptorHandler.rehydrateConnection`) and
3175
+ * inject `originClient` from there — the session alone can't recover it.
3176
+ */
3177
+ function createBinaryWireSessionFactory(domains, options) {
3178
+ const { routeToInt, intToRoute } = buildActionRouteDictionary(domains);
3179
+ const unknownIdentity = RuntimeCoordinate.unknown.toJsonObject();
3180
+ const ttlMs = options?.correlationTtlMs ?? DEFAULT_CORRELATION_TTL_MS;
3181
+ return () => {
3182
+ let outCounter = 0;
3183
+ const corrToCuid = /* @__PURE__ */ new Map();
3184
+ const cuidToCorr = /* @__PURE__ */ new Map();
3185
+ let selfIdentity;
3186
+ let peerIdentity;
3187
+ return {
3188
+ outgoing: (input) => {
3189
+ const json = input.action.toJsonObject();
3190
+ const routeKey = `${json.domain}:${json.id}`;
3191
+ const routeInt = routeToInt.get(routeKey);
3192
+ if (routeInt == null) throw new Error(`[binary-wire] Cannot pack unregistered action route: ${routeKey}`);
3193
+ const now = Date.now();
3194
+ pruneExpired(corrToCuid, now, ttlMs);
2874
3195
  pruneExpired(cuidToCorr, now, ttlMs);
2875
3196
  let corr;
2876
3197
  let wireIdentity;
@@ -2888,13 +3209,13 @@ function createBinaryWsSessionFactory(domains, options) {
2888
3209
  corr = cuidToCorr.get(json.context.cuid)?.value ?? -1;
2889
3210
  if (json.type === "result") cuidToCorr.delete(json.context.cuid);
2890
3211
  }
2891
- const envelope = new Array(ENVELOPE_LENGTH);
2892
- envelope[ENVELOPE.route] = routeInt;
2893
- envelope[ENVELOPE.type] = PayloadTypeToInt[json.type];
2894
- envelope[ENVELOPE.corr] = corr;
2895
- envelope[ENVELOPE.time] = json.time;
2896
- envelope[ENVELOPE.originClient] = wireIdentity;
2897
- envelope[ENVELOPE.payload] = extractWirePayload(json);
3212
+ const envelope = new Array(ENVELOPE_LENGTH$1);
3213
+ envelope[ENVELOPE$1.route] = routeInt;
3214
+ envelope[ENVELOPE$1.type] = PayloadTypeToInt[json.type];
3215
+ envelope[ENVELOPE$1.corr] = corr;
3216
+ envelope[ENVELOPE$1.time] = json.time;
3217
+ envelope[ENVELOPE$1.originClient] = wireIdentity;
3218
+ envelope[ENVELOPE$1.payload] = extractWirePayload(json);
2898
3219
  return (0, msgpackr.pack)(envelope);
2899
3220
  },
2900
3221
  incoming: (frame) => {
@@ -2904,16 +3225,16 @@ function createBinaryWsSessionFactory(domains, options) {
2904
3225
  else return;
2905
3226
  try {
2906
3227
  const envelope = (0, msgpackr.unpack)(buffer);
2907
- if (!Array.isArray(envelope) || envelope.length !== ENVELOPE_LENGTH) return void 0;
2908
- const routeMeta = intToRoute[envelope[ENVELOPE.route]];
2909
- const payloadType = ReversePayloadType[envelope[ENVELOPE.type]];
3228
+ if (!Array.isArray(envelope) || envelope.length !== ENVELOPE_LENGTH$1) return void 0;
3229
+ const routeMeta = intToRoute[envelope[ENVELOPE$1.route]];
3230
+ const payloadType = ReversePayloadType[envelope[ENVELOPE$1.type]];
2910
3231
  if (routeMeta == null || payloadType == null) return void 0;
2911
3232
  const now = Date.now();
2912
3233
  pruneExpired(corrToCuid, now, ttlMs);
2913
3234
  pruneExpired(cuidToCorr, now, ttlMs);
2914
- const corr = envelope[ENVELOPE.corr];
2915
- const time = envelope[ENVELOPE.time];
2916
- const wireIdentity = envelope[ENVELOPE.originClient];
3235
+ const corr = envelope[ENVELOPE$1.corr];
3236
+ const time = envelope[ENVELOPE$1.time];
3237
+ const wireIdentity = envelope[ENVELOPE$1.originClient];
2917
3238
  let cuid;
2918
3239
  let originClient;
2919
3240
  if (payloadType === "request") {
@@ -2934,9 +3255,9 @@ function createBinaryWsSessionFactory(domains, options) {
2934
3255
  timeCreated: time,
2935
3256
  routing: [],
2936
3257
  originClient
2937
- }, envelope[ENVELOPE.payload]);
3258
+ }, envelope[ENVELOPE$1.payload]);
2938
3259
  } catch (e) {
2939
- console.error("[binary-ws] Failed to unpack binary action session frame", e);
3260
+ console.error("[binary-wire] Failed to unpack binary action session frame", e);
2940
3261
  return;
2941
3262
  }
2942
3263
  }
@@ -2944,438 +3265,433 @@ function createBinaryWsSessionFactory(domains, options) {
2944
3265
  };
2945
3266
  }
2946
3267
  //#endregion
2947
- //#region src/utils/decodeActionFrame.ts
2948
- /**
2949
- * Decode a single inbound channel frame (text or binary) into validated action wire JSON, or
2950
- * `undefined` if it isn't a recognisable action payload.
2951
- *
2952
- * Shared by the WebSocket transport's message listener and the server-side `ActionServerHandler` so
2953
- * both decode identically: a binary `decoder.incoming` (e.g. msgpackr) takes precedence, and plain
2954
- * text frames fall back to JSON — keeping binary and JSON clients interoperable on one channel.
2955
- */
2956
- function decodeActionFrame(frame, decoder) {
2957
- const decoded = decoder?.incoming?.(frame) ?? (typeof frame === "string" ? parseJsonActionFrame(frame) : void 0);
2958
- return decoded != null && isActionPayload_Any_JsonObject(decoded) ? decoded : void 0;
2959
- }
2960
- function parseJsonActionFrame(message) {
2961
- try {
2962
- const json = JSON.parse(message);
2963
- return isActionPayload_Any_JsonObject(json) ? json : void 0;
2964
- } catch {
2965
- return;
2966
- }
2967
- }
2968
- //#endregion
2969
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/helpers/createUnsetTransportResolvers.ts
2970
- const createUnsetTransportResolvers = (type) => ({ onIncomingActionDataJson: (json) => {
2971
- console.warn(`Received incoming action JSON [${json.domain}:${json.id}] on Transport [${type}] but no incoming data listener has been set.`);
2972
- } });
2973
- //#endregion
2974
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/WebSocket/ws_util.ts
3268
+ //#region src/ActionRuntime/Channel/secureChannel.ts
2975
3269
  /**
2976
- * Send a text or binary frame over a socket. A binary formatter may hand back a `Uint8Array` whose
2977
- * backing buffer is typed as `ArrayBufferLike` (msgpackr pools buffers / may be `SharedArrayBuffer`),
2978
- * which `WebSocket.send`'s `BufferSource` parameter rejects copy it into a fresh `ArrayBuffer`-backed
2979
- * view so the type (and the bytes) are safe to send.
3270
+ * Derive a stable wire-dictionary version from the ordered route list (FNV-1a over `domain:id,…`), so
3271
+ * the version moves automatically whenever the transported domains change a stale peer is then
3272
+ * rejected by the handshake instead of silently misrouting a positionally-packed frame.
2980
3273
  */
2981
- function sendFrame(ws, data) {
2982
- if (typeof data === "string" || data instanceof ArrayBuffer) {
2983
- ws.send(data);
2984
- return;
3274
+ function deriveDictionaryVersion(domains) {
3275
+ const { intToRoute } = buildActionRouteDictionary(domains);
3276
+ const signature = intToRoute.map((route) => `${route.domain}:${route.id}`).join(",");
3277
+ let hash = 2166136261;
3278
+ for (let i = 0; i < signature.length; i++) {
3279
+ hash ^= signature.charCodeAt(i);
3280
+ hash = Math.imul(hash, 16777619);
2985
3281
  }
2986
- ws.send(new Uint8Array(data));
3282
+ return `auto:${(hash >>> 0).toString(16).padStart(8, "0")}`;
2987
3283
  }
2988
- /** Normalize any frame form to bytes (for the AES-GCM layer, which works on `Uint8Array`). */
2989
- function toFrameBytes(frame) {
2990
- if (typeof frame === "string") return new TextEncoder().encode(frame);
2991
- if (frame instanceof ArrayBuffer) return new Uint8Array(frame);
2992
- return frame;
3284
+ /**
3285
+ * Bundle a secure channel's shared identity from its transported domains. Both ends MUST call this
3286
+ * with the same domains in the same order (the binary wire dictionary is positional). The
3287
+ * `dictionaryVersion` is derived from those domains unless you pin an explicit one.
3288
+ *
3289
+ * Declare the domains *by role* — `toAcceptor` (connector→acceptor requests) and `toConnector`
3290
+ * (acceptor→connector pushes) — so the routing for both ends is derived from the channel (see
3291
+ * {@link connectChannel} and `acceptChannelConnections`) instead of being restated at each end. The
3292
+ * wire dictionary spans `[...toAcceptor, ...toConnector]` in that order; add new domains to the end of
3293
+ * their list to keep older peers compatible. (`domains` is still accepted as a legacy alias for
3294
+ * `toAcceptor`.)
3295
+ */
3296
+ function defineSecureChannel(options) {
3297
+ const base = defineChannel({
3298
+ toAcceptor: options.toAcceptor,
3299
+ toConnector: options.toConnector
3300
+ });
3301
+ const allDomains = [...base.toAcceptorDomains, ...base.toConnectorDomains];
3302
+ return {
3303
+ ...base,
3304
+ dictionaryVersion: options.dictionaryVersion ?? deriveDictionaryVersion(allDomains),
3305
+ createCodec: createBinaryWireSessionFactory(allDomains, options.sessionOptions)
3306
+ };
2993
3307
  }
2994
- /** Compact a WebSocket URL to `host/pathname` for devtools display, falling back to the raw url. */
2995
- function shortWs(url) {
3308
+ //#endregion
3309
+ //#region src/ActionRuntime/Transport/SecureSession/exchangeProtocol.ts
3310
+ function encodeExchange(envelope) {
3311
+ return JSON.stringify(envelope);
3312
+ }
3313
+ function decodeExchangeRequest(raw) {
3314
+ return parse(raw);
3315
+ }
3316
+ function decodeExchangeReply(raw) {
3317
+ return parse(raw);
3318
+ }
3319
+ function parse(raw) {
2996
3320
  try {
2997
- const u = new URL(url);
2998
- return `${u.host}${u.pathname}`;
3321
+ return JSON.parse(raw);
2999
3322
  } catch {
3000
- return url;
3323
+ return;
3001
3324
  }
3002
3325
  }
3326
+ function bytesToBase64(bytes) {
3327
+ let binary = "";
3328
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
3329
+ return btoa(binary);
3330
+ }
3331
+ function base64ToBytes(base64) {
3332
+ const binary = atob(base64);
3333
+ const bytes = new Uint8Array(binary.length);
3334
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
3335
+ return bytes;
3336
+ }
3003
3337
  //#endregion
3004
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/WebSocket/WebSocketConnection.ts
3005
- const HANDSHAKE_TIMEOUT_MS = 15e3;
3006
- var WebSocketConnection = class extends TransportConnection {
3007
- resolvers;
3008
- /** URL of the most recently resolved live socket surfaced to devtools when the definition can't. */
3009
- _liveSocketUrl;
3010
- /** Sockets we closed on purpose (via `disconnect`), so their `close` event stays quiet. */
3011
- _intentionalCloses = /* @__PURE__ */ new WeakSet();
3012
- constructor(def, resolvers) {
3013
- super({
3014
- ...def,
3015
- type: "ws"
3016
- });
3017
- this.resolvers = resolvers ?? createUnsetTransportResolvers("ws");
3018
- }
3019
- _getCacheKey(_input) {
3020
- return this.initialized.getTransportCacheKey?.(_input).join("\0") ?? "";
3338
+ //#region src/ActionRuntime/Transport/SecureSession/exchangeAcceptor.ts
3339
+ const textEncoder$1 = new TextEncoder();
3340
+ const textDecoder$1 = new TextDecoder();
3341
+ /**
3342
+ * Acceptor (accept-in) side of the secure exchange protocol — the HTTP counterpart to
3343
+ * {@link AcceptorSecureSession}. Each POST body is one {@link decodeExchangeRequest} envelope; the
3344
+ * acceptor drives the server handshake over the two `hs` POSTs (correlated by `hsid`, since stateless
3345
+ * requests can't rely on channel ordering), mints a session **token** on accept, and on every later `act`
3346
+ * POST resolves the session by token, decrypts the body (at `encrypted`), routes it through the runtime,
3347
+ * and returns the (encrypted) result inline as the reply.
3348
+ *
3349
+ * Sessions and in-flight handshakes are held in memory — fine for a single-instance server. (Surviving a
3350
+ * Durable-Object eviction would persist each token's `keyMaterial` and re-derive the key on a miss, the
3351
+ * same primitive `AcceptorSecureSession.rehydrate` uses; left as a follow-up.)
3352
+ */
3353
+ var ExchangeAcceptor = class {
3354
+ _security;
3355
+ _runtime;
3356
+ _allowedLevels;
3357
+ _noneAllowed;
3358
+ _pendingHandshakes = /* @__PURE__ */ new Map();
3359
+ _sessions = /* @__PURE__ */ new Map();
3360
+ constructor(config) {
3361
+ this._security = config.security;
3362
+ this._runtime = config.runtime;
3363
+ this._allowedLevels = Array.isArray(config.security.securityLevel) ? config.security.securityLevel : [config.security.securityLevel];
3364
+ this._noneAllowed = this._allowedLevels.includes("none");
3021
3365
  }
3022
- _processTransportStatus(input) {
3023
- const transportStatusInfo = addTransportStatusMetadata(this.initialized.getTransport(input));
3024
- if (transportStatusInfo.status !== "initializing" && transportStatusInfo.status !== "ready") return transportStatusInfo;
3025
- if (transportStatusInfo.status === "ready") {
3026
- const ws = transportStatusInfo.readyData.ws;
3027
- if (ws.readyState !== WebSocket.OPEN || this._isSecure(transportStatusInfo.readyData)) {
3028
- const readyData = transportStatusInfo.readyData;
3029
- const initialization = async () => {
3030
- await this._awaitOpen(ws);
3031
- return {
3032
- status: "ready",
3033
- readyData: await this._finalize(readyData)
3034
- };
3035
- };
3366
+ /** Process one POST body (an exchange envelope), returning the reply body to send back. */
3367
+ async handlePost(body) {
3368
+ const request = decodeExchangeRequest(body);
3369
+ if (request == null) return this._err("malformed exchange request");
3370
+ if (request.k === "hs") return encodeExchange(await this._handleHandshake(request));
3371
+ return encodeExchange(await this._handleAction(request));
3372
+ }
3373
+ async _handleHandshake(request) {
3374
+ const message = decodeHandshakeMessage(request.m);
3375
+ if (message == null) return {
3376
+ k: "err",
3377
+ message: "malformed handshake message"
3378
+ };
3379
+ const security = this._security;
3380
+ await security.link.initialize();
3381
+ let handshake = this._pendingHandshakes.get(request.hsid);
3382
+ if (handshake == null) {
3383
+ handshake = createServerHandshake({
3384
+ link: security.link,
3385
+ localCoordinate: security.localCoordinate,
3386
+ dictionaryVersion: security.dictionaryVersion,
3387
+ securityLevel: security.securityLevel,
3388
+ verifyKeyResolver: security.verifyKeyResolver
3389
+ });
3390
+ this._pendingHandshakes.set(request.hsid, handshake);
3391
+ }
3392
+ if (message.t === "hello") return {
3393
+ k: "hs",
3394
+ m: encodeHandshakeMessage(await handshake.onHello(message))
3395
+ };
3396
+ if (message.t === "prove") {
3397
+ const reply = await handshake.onProve(message);
3398
+ this._pendingHandshakes.delete(request.hsid);
3399
+ const result = handshake.getResult();
3400
+ if (reply.t === "accept" && result != null) {
3401
+ const token = (0, nanoid.nanoid)();
3402
+ this._sessions.set(token, {
3403
+ client: new RuntimeCoordinate(result.remote),
3404
+ securityLevel: result.securityLevel,
3405
+ crypto: result.securityLevel === "encrypted" ? createActionFrameCrypto({
3406
+ link: security.link,
3407
+ linkedClientId: result.linkedClientId
3408
+ }) : void 0
3409
+ });
3036
3410
  return {
3037
- status: "initializing",
3038
- timeStarted: Date.now(),
3039
- initializationPromise: initialization()
3411
+ k: "hs",
3412
+ m: encodeHandshakeMessage(reply),
3413
+ t: token
3040
3414
  };
3041
3415
  }
3416
+ return {
3417
+ k: "hs",
3418
+ m: encodeHandshakeMessage(reply)
3419
+ };
3042
3420
  }
3043
- if (transportStatusInfo.status === "initializing") return {
3044
- status: "initializing",
3045
- initializationPromise: transportStatusInfo.initializationPromise.then(async (result) => {
3046
- if (result.status === "ready") {
3047
- await this._awaitOpen(result.readyData.ws);
3048
- return {
3049
- status: "ready",
3050
- readyData: await this._finalize(result.readyData)
3051
- };
3052
- }
3053
- return result;
3054
- }),
3055
- timeStarted: transportStatusInfo.timeStarted
3056
- };
3057
3421
  return {
3058
- status: "ready",
3059
- readyData: this._finalizeTransportMethods(transportStatusInfo.readyData)
3060
- };
3061
- }
3062
- getRouteInfo(input) {
3063
- const base = this.definition?.getRouteInfo(input);
3064
- if (base?.url != null || this._liveSocketUrl == null) return base;
3065
- return {
3066
- type: "ws",
3067
- ...base,
3068
- url: this._liveSocketUrl,
3069
- summary: `ws ${shortWs(this._liveSocketUrl)}`
3422
+ k: "err",
3423
+ message: `unexpected handshake message ${message.t}`
3070
3424
  };
3071
3425
  }
3072
- _isSecure(wsData) {
3073
- return wsData.secureChannel != null && wsData.secureChannel.securityLevel !== "none";
3074
- }
3075
- _awaitOpen(ws) {
3076
- if (ws.readyState === WebSocket.OPEN) return Promise.resolve();
3077
- return new Promise((resolve, reject) => {
3078
- ws.addEventListener("open", () => resolve(), { once: true });
3079
- ws.addEventListener("error", (event) => reject(event), { once: true });
3080
- ws.addEventListener("close", (event) => reject(/* @__PURE__ */ new Error(`WebSocket closed before open: code=${event.code}`)), { once: true });
3081
- });
3082
- }
3083
- /** Non-secure connections finalize synchronously; secure ones run the handshake first. */
3084
- _finalize(wsData) {
3085
- return this._isSecure(wsData) ? this._finalizeSecureMethods(wsData) : this._finalizeTransportMethods(wsData);
3086
- }
3087
- _finalizeTransportMethods(wsData) {
3088
- const ws = wsData.ws;
3089
- const disconnectListeners = [];
3090
- const abortSet = /* @__PURE__ */ new Set();
3091
- this._captureSocketUrl(ws);
3092
- this._attachLifecycle(ws, disconnectListeners, abortSet);
3093
- ws.addEventListener("message", async (event) => {
3094
- const frame = await this._normalizeFrame(event.data);
3095
- if (frame !== void 0) await this._handleIncomingActionFrame(frame, wsData, void 0);
3096
- });
3097
- return this._buildSendMethods(ws, wsData, void 0, disconnectListeners, abortSet);
3098
- }
3099
- /**
3100
- * Secure path: a single message listener feeds the handshake until it completes, then routes action
3101
- * frames (decrypting for the `encrypted` level). Frames that arrive in the gap between accept and
3102
- * activation are buffered and flushed, so nothing is lost.
3103
- */
3104
- async _finalizeSecureMethods(wsData) {
3105
- const ws = wsData.ws;
3106
- const disconnectListeners = [];
3107
- const abortSet = /* @__PURE__ */ new Set();
3108
- this._captureSocketUrl(ws);
3109
- this._attachLifecycle(ws, disconnectListeners, abortSet);
3110
- let active = false;
3111
- let crypto;
3112
- const handshakeQueue = [];
3113
- const handshakeWaiters = [];
3114
- const pendingActionFrames = [];
3115
- ws.addEventListener("message", async (event) => {
3116
- const frame = await this._normalizeFrame(event.data);
3117
- if (frame === void 0) return;
3118
- if (active) {
3119
- await this._handleIncomingActionFrame(frame, wsData, crypto);
3120
- return;
3121
- }
3122
- if (typeof frame === "string") {
3123
- const message = decodeHandshakeMessage(frame);
3124
- if (message != null) {
3125
- const waiter = handshakeWaiters.shift();
3126
- if (waiter != null) waiter(message);
3127
- else handshakeQueue.push(message);
3128
- return;
3129
- }
3130
- }
3131
- pendingActionFrames.push(frame);
3132
- });
3133
- const nextHandshakeMessage = () => {
3134
- const queued = handshakeQueue.shift();
3135
- if (queued != null) return Promise.resolve(queued);
3136
- return new Promise((resolve, reject) => {
3137
- const timeout = setTimeout(() => reject(/* @__PURE__ */ new Error("[ws-handshake] timed out waiting for server reply")), HANDSHAKE_TIMEOUT_MS);
3138
- handshakeWaiters.push((message) => {
3139
- clearTimeout(timeout);
3140
- resolve(message);
3141
- });
3142
- });
3143
- };
3144
- crypto = await this._runClientHandshake(ws, wsData.secureChannel, nextHandshakeMessage);
3145
- active = true;
3146
- for (const frame of pendingActionFrames) await this._handleIncomingActionFrame(frame, wsData, crypto);
3147
- pendingActionFrames.length = 0;
3148
- return this._buildSendMethods(ws, wsData, crypto, disconnectListeners, abortSet);
3149
- }
3150
- async _runClientHandshake(ws, secure, nextHandshakeMessage) {
3151
- await secure.link.initialize();
3152
- const handshake = createClientHandshake({
3153
- link: secure.link,
3154
- localCoordinate: secure.localCoordinate,
3155
- dictionaryVersion: secure.dictionaryVersion,
3156
- securityLevel: secure.securityLevel
3157
- });
3158
- sendFrame(ws, encodeHandshakeMessage(await handshake.createHello()));
3159
- const welcome = await nextHandshakeMessage();
3160
- if (welcome.t === "reject") throw new Error(`[ws-handshake] rejected by server: ${welcome.reason}`);
3161
- if (welcome.t !== "welcome") throw new Error(`[ws-handshake] expected welcome, got ${welcome.t}`);
3162
- sendFrame(ws, encodeHandshakeMessage(await handshake.onWelcome(welcome)));
3163
- const accept = await nextHandshakeMessage();
3164
- if (accept.t === "reject") throw new Error(`[ws-handshake] rejected by server: ${accept.reason}`);
3165
- if (accept.t !== "accept") throw new Error(`[ws-handshake] expected accept, got ${accept.t}`);
3166
- const result = await handshake.onAccept(accept);
3167
- return result.securityLevel === "encrypted" ? createActionFrameCrypto({
3168
- link: secure.link,
3169
- linkedClientId: result.linkedClientId
3170
- }) : void 0;
3171
- }
3172
- _buildSendMethods(ws, wsData, crypto, disconnectListeners, abortSet) {
3173
- let sendChain = Promise.resolve();
3174
- const enqueueSend = (frame) => {
3175
- if (ws.readyState !== WebSocket.OPEN) return;
3176
- if (crypto == null) {
3177
- sendFrame(ws, frame);
3178
- return;
3179
- }
3180
- const bytes = toFrameBytes(frame);
3181
- sendChain = sendChain.then(() => crypto.encryptFrame(bytes)).then((encrypted) => {
3182
- if (ws.readyState === WebSocket.OPEN) sendFrame(ws, encrypted);
3183
- }).catch((err) => console.error("[ws] failed to encrypt/send frame", err));
3426
+ async _handleAction(request) {
3427
+ let session;
3428
+ let candidate;
3429
+ if (request.t != null) {
3430
+ session = this._sessions.get(request.t);
3431
+ if (session == null) return {
3432
+ k: "err",
3433
+ message: "unknown or expired session token"
3434
+ };
3435
+ if ("c" in request) {
3436
+ if (session.crypto == null) return {
3437
+ k: "err",
3438
+ message: "session is not encrypted"
3439
+ };
3440
+ const plain = await session.crypto.decryptFrame(base64ToBytes(request.c));
3441
+ candidate = JSON.parse(textDecoder$1.decode(plain));
3442
+ } else candidate = request.w;
3443
+ } else {
3444
+ if (!this._noneAllowed || "c" in request) return {
3445
+ k: "err",
3446
+ message: "missing session token"
3447
+ };
3448
+ candidate = request.w;
3449
+ }
3450
+ if (!isActionPayload_Any_JsonObject(candidate)) return {
3451
+ k: "err",
3452
+ message: "malformed action wire"
3184
3453
  };
3185
- const sendActionData = (inputs) => {
3186
- const { action, runningAction, timeout } = inputs;
3187
- if (ws.readyState !== WebSocket.OPEN) {
3188
- if (action.type === "request") runningAction._abort(err_nice_transport_ws.fromId("ws_disconnected"));
3189
- return;
3190
- }
3191
- if (action.type === "request") {
3192
- abortSet.add(runningAction);
3193
- const timeoutId = setTimeout(() => {
3194
- runningAction._abort(err_nice_transport.fromId("timeout", { timeout }));
3195
- }, timeout);
3196
- runningAction.addUpdateListeners([(update) => {
3197
- if (update.type === "finished") {
3198
- clearTimeout(timeoutId);
3199
- abortSet.delete(runningAction);
3200
- }
3201
- }]);
3202
- }
3203
- enqueueSend(wsData.formatMessage?.outgoing(inputs) ?? JSON.stringify(inputs.action.toJsonObject()));
3454
+ const wire = candidate;
3455
+ if (session != null && wire.type === "request") wire.context.originClient = session.client.toJsonObject();
3456
+ const resultWire = (await (await this._runtime.handleActionPayloadWire(wire)).waitForResultPayload()).toJsonObject();
3457
+ if (session?.crypto != null) return {
3458
+ k: "act",
3459
+ c: bytesToBase64(await session.crypto.encryptFrame(textEncoder$1.encode(JSON.stringify(resultWire))))
3204
3460
  };
3205
3461
  return {
3206
- sendActionData,
3207
- updateRunConfig: wsData.updateRunConfig,
3208
- addOnDisconnectListener: (cb) => {
3209
- disconnectListeners.push(cb);
3210
- },
3211
- disconnect: () => {
3212
- this._intentionalCloses.add(ws);
3213
- try {
3214
- ws.close();
3215
- } catch {}
3216
- },
3217
- sendReturnData: (payload, clients) => {
3218
- enqueueSend((clients != null ? wsData.formatMessage?.outgoing({
3219
- action: payload,
3220
- ...clients
3221
- }) : void 0) ?? JSON.stringify(payload.toJsonObject()));
3222
- }
3462
+ k: "act",
3463
+ w: resultWire
3223
3464
  };
3224
3465
  }
3225
- /** Decode (and, when encrypted, decrypt) one inbound action frame and hand it to the runtime. */
3226
- async _handleIncomingActionFrame(frame, wsData, crypto) {
3227
- let decoded = frame;
3228
- if (crypto != null) try {
3229
- decoded = await crypto.decryptFrame(frame);
3230
- } catch (err) {
3231
- console.error("[ws] failed to decrypt incoming frame", err);
3232
- return;
3233
- }
3234
- const rawJson = decodeActionFrame(decoded, wsData.formatMessage);
3235
- if (rawJson != null) this.resolvers.onIncomingActionDataJson(rawJson);
3236
- }
3237
- /** Accept text + binary frames (ArrayBuffer / Uint8Array / Blob); Blobs are converted to a buffer. */
3238
- async _normalizeFrame(data) {
3239
- if (typeof data === "string" || data instanceof ArrayBuffer || data instanceof Uint8Array) return data;
3240
- if (typeof Blob !== "undefined" && data instanceof Blob) return await data.arrayBuffer();
3241
- }
3242
- _captureSocketUrl(ws) {
3243
- if (ws.url != null && ws.url !== "") this._liveSocketUrl = ws.url;
3244
- }
3245
- _attachLifecycle(ws, disconnectListeners, abortSet) {
3246
- ws.addEventListener("close", (event) => {
3247
- if (!this._intentionalCloses.has(ws)) console.error("WebSocket closed:", event);
3248
- for (const cb of disconnectListeners) cb();
3249
- this._abortAll(abortSet, err_nice_transport_ws.fromId("ws_disconnected"));
3466
+ _err(message) {
3467
+ return encodeExchange({
3468
+ k: "err",
3469
+ message
3250
3470
  });
3251
- ws.addEventListener("error", (event) => {
3252
- console.error("WebSocket error:", event);
3253
- for (const cb of disconnectListeners) cb();
3254
- this._abortAll(abortSet, err_nice_transport_ws.fromId("ws_error", { originalError: event instanceof Error ? event : void 0 }));
3255
- });
3256
- }
3257
- _abortAll(abortSet, error) {
3258
- const snapshot = [...abortSet];
3259
- for (const ra of snapshot) ra._abort(error);
3260
3471
  }
3261
3472
  };
3262
3473
  //#endregion
3263
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/WebSocket/WebSocketTransport.ts
3474
+ //#region src/ActionRuntime/Handler/PeerLink/Acceptor/createActionFetchHandler.ts
3475
+ /** Permissive defaults — fine for a public action endpoint; override (or disable) via `cors`. */
3476
+ const DEFAULT_CORS_HEADERS = {
3477
+ "Access-Control-Allow-Origin": "*",
3478
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
3479
+ "Access-Control-Allow-Headers": "Content-Type",
3480
+ "Access-Control-Max-Age": "86400"
3481
+ };
3264
3482
  /**
3265
- * Reusable WebSocket transport definition. Create one with `WebSocketTransport.create({ createWebSocket })`
3266
- * for the common case, or `WebSocketTransport.createAdvanced({ getTransport })` for full control over
3267
- * readiness. The underlying socket is cached (via `getTransportCacheKey`) and reused across actions.
3483
+ * Build the `fetch` handler a server/Durable-Object exposes for action traffic, folding in the
3484
+ * boilerplate every endpoint repeats: CORS (incl. the `OPTIONS` preflight), routing the `/action`
3485
+ * `POST` body through the runtime (`handleActionPayloadWire` `waitForResultPayload`
3486
+ * `toHttpResponse`), an optional WebSocket-upgrade hook, and a `404` fallback.
3487
+ *
3488
+ * It only touches web-standard `Request`/`Response`, so it stays transport-agnostic — the one
3489
+ * environment-specific bit (the WS upgrade) is injected via {@link IActionFetchHandlerOptions.onWebSocketUpgrade}:
3490
+ * ```ts
3491
+ * this.fetchHandler = createActionFetchHandler(this.runtime, {
3492
+ * onWebSocketUpgrade: () => {
3493
+ * const pair = new WebSocketPair();
3494
+ * this.ctx.acceptWebSocket(pair[1]);
3495
+ * return new Response(null, { status: 101, webSocket: pair[0] });
3496
+ * },
3497
+ * });
3498
+ * // async fetch(request) { return this.fetchHandler(request); }
3499
+ * ```
3268
3500
  */
3269
- var WebSocketTransport = class WebSocketTransport extends Transport {
3270
- options;
3271
- type = "ws";
3272
- constructor(options) {
3273
- super();
3274
- this.options = options;
3275
- }
3276
- static create(options) {
3277
- return new WebSocketTransport({
3278
- ...options,
3279
- mode: "socket"
3280
- });
3281
- }
3282
- static createAdvanced(options) {
3283
- return new WebSocketTransport({
3284
- ...options,
3285
- mode: "advanced"
3501
+ function createActionFetchHandler(runtime, options = {}) {
3502
+ const corsHeaders = options.cors === false ? {} : options.cors ?? DEFAULT_CORS_HEADERS;
3503
+ const isActionPath = options.isActionPath ?? ((url) => url.pathname.endsWith("/action"));
3504
+ const isWebSocketPath = options.isWebSocketPath ?? ((url) => url.pathname.endsWith("/ws"));
3505
+ const exchangeAcceptor = options.security != null ? new ExchangeAcceptor({
3506
+ runtime,
3507
+ security: options.security
3508
+ }) : void 0;
3509
+ const withCors = (response) => {
3510
+ if (options.cors === false) return response;
3511
+ const headers = new Headers(response.headers);
3512
+ for (const [key, value] of Object.entries(corsHeaders)) headers.set(key, value);
3513
+ return new Response(response.body, {
3514
+ status: response.status,
3515
+ headers
3286
3516
  });
3287
- }
3288
- _createConnection(ctx) {
3289
- const options = this.options;
3290
- let getTransport;
3291
- if (options.mode === "advanced") getTransport = options.getTransport;
3292
- else getTransport = (input) => ({
3293
- status: "ready",
3294
- readyData: {
3295
- ws: options.createWebSocket(input),
3296
- formatMessage: options.createFormatMessage?.() ?? options.formatMessage,
3297
- updateRunConfig: options.updateRunConfig,
3298
- secureChannel: options.security
3517
+ };
3518
+ return async (request) => {
3519
+ if (request.method === "OPTIONS") return withCors(new Response(null, { status: 204 }));
3520
+ const url = new URL(request.url);
3521
+ const isWebSocketUpgrade = options.isWebSocketUpgrade ?? ((req, u) => req.headers.get("Upgrade") === "websocket" && isWebSocketPath(u));
3522
+ if (options.onWebSocketUpgrade != null && isWebSocketUpgrade(request, url)) return options.onWebSocketUpgrade(request, url);
3523
+ if (request.method === "POST" && isActionPath(url)) {
3524
+ if (exchangeAcceptor != null) {
3525
+ const reply = await exchangeAcceptor.handlePost(await request.text());
3526
+ return withCors(new Response(reply, {
3527
+ status: 200,
3528
+ headers: { "Content-Type": "application/json" }
3529
+ }));
3299
3530
  }
3300
- });
3301
- return new WebSocketConnection({ initialize: () => ({
3302
- getTransportCacheKey: options.getTransportCacheKey,
3303
- getTransport
3304
- }) }, ctx.resolvers);
3305
- }
3306
- getRouteInfo(input) {
3307
- if (this.options.getRouteInfo != null) return this.options.getRouteInfo(input);
3308
- return {
3309
- type: "ws",
3310
- summary: "ws"
3311
- };
3312
- }
3313
- };
3531
+ return withCors((await (await runtime.handleActionPayloadWire(await request.json())).waitForResultPayload()).toHttpResponse({ useErrorStatus: options.useErrorStatus }));
3532
+ }
3533
+ return withCors(new Response("Not found", { status: 404 }));
3534
+ };
3535
+ }
3314
3536
  //#endregion
3315
- //#region src/ActionRuntime/Handler/ExternalClient/Transport/WebSocket/secureWsChannel.ts
3537
+ //#region src/ActionRuntime/Handler/PeerLink/Acceptor/Hibernation/createHibernatableWsServerAdapter.ts
3316
3538
  /**
3317
- * Derive a stable wire-dictionary version from the ordered route list (FNV-1a over `domain:id,…`), so
3318
- * the version moves automatically whenever the transported domains change a stale peer is then
3319
- * rejected by the handshake instead of silently misrouting a positionally-packed frame.
3539
+ * Wire the hibernation lifecycle for an acceptor handler on a transport whose connections outlive process
3540
+ * eviction (e.g. a Durable Object's hibernatable WebSockets). It owns persistence end to end:
3541
+ * registers `setAttachment` as the handler's connection-bound callback and immediately replays every
3542
+ * live connection's stored binding via `getAttachment`, so results/pushes still route after a wake.
3543
+ *
3544
+ * Layered on top of the generic {@link AcceptorHandler} — it touches only the handler's neutral
3545
+ * `setOnConnectionBound` / `rehydrateConnection` / `receive` / `dropConnection` surface, so no
3546
+ * hibernation concern leaks into the handler itself.
3547
+ *
3548
+ * Construct it once when the handler is built, then forward connection events:
3549
+ * ```ts
3550
+ * const duplex = createHibernatableWsServerAdapter({ handler, getConnections, getAttachment, setAttachment });
3551
+ * // webSocketMessage(ws, msg) => duplex.receive(ws, msg);
3552
+ * // webSocketClose/Error(ws) => duplex.drop(ws);
3553
+ * ```
3320
3554
  */
3321
- function deriveDictionaryVersion(domains) {
3322
- const { intToRoute } = buildActionRouteDictionary(domains);
3323
- const signature = intToRoute.map((route) => `${route.domain}:${route.id}`).join(",");
3324
- let hash = 2166136261;
3325
- for (let i = 0; i < signature.length; i++) {
3326
- hash ^= signature.charCodeAt(i);
3327
- hash = Math.imul(hash, 16777619);
3555
+ function createHibernatableWsServerAdapter(options) {
3556
+ const { handler, getConnections, getAttachment, setAttachment } = options;
3557
+ handler.setOnConnectionBound(setAttachment);
3558
+ for (const connection of getConnections()) {
3559
+ const binding = getAttachment(connection);
3560
+ if (binding != null) handler.rehydrateConnection(connection, binding);
3328
3561
  }
3329
- return `auto:${(hash >>> 0).toString(16).padStart(8, "0")}`;
3330
- }
3331
- /**
3332
- * Bundle a secure channel's shared identity from its transported domains. Both ends MUST call this
3333
- * with the same domains in the same order (the binary wire dictionary is positional). The
3334
- * `dictionaryVersion` is derived from those domains unless you pin an explicit one.
3335
- */
3336
- function defineSecureWsChannel(options) {
3337
3562
  return {
3338
- dictionaryVersion: options.dictionaryVersion ?? deriveDictionaryVersion(options.domains),
3339
- createCodec: createBinaryWsSessionFactory(options.domains, options.sessionOptions)
3563
+ receive: (connection, frame) => handler.receive(connection, frame),
3564
+ drop: (connection) => handler.dropConnection(connection)
3340
3565
  };
3341
3566
  }
3567
+ //#endregion
3568
+ //#region src/ActionRuntime/Channel/serveChannel.ts
3569
+ /** Default accepted set, shared by every carrier: negotiate per connection to whatever the client picks. */
3570
+ const DEFAULT_SERVER_SECURITY_LEVELS = [
3571
+ "none",
3572
+ "authenticated",
3573
+ "encrypted"
3574
+ ];
3342
3575
  /**
3343
- * Build a {@link WebSocketTransport} for the secure binary channel with the boilerplate folded in: it
3344
- * creates the {@link ClientCryptoKeyLink} from `storageAdapter`, opens an `arraybuffer` socket to
3345
- * `url`, caches it per endpoint, installs the channel's per-connection codec, and assembles the
3346
- * `security` block from the runtime coordinate + channel version. Pass `createWebSocket` /
3347
- * `getTransportCacheKey` to take over those bits when you need to.
3576
+ * Serve a secure channel over one or more carriers from a single call — the accept-in dual of
3577
+ * `connectChannel`. It builds the crypto identity (a {@link ClientCryptoKeyLink} + a storage-backed TOFU
3578
+ * resolver) and the security block (coordinate, dictionary version, accepted levels) *once* from
3579
+ * `(runtime, channel)` and fans them across every carrier, so the WebSocket and the secure-HTTP endpoint
3580
+ * can never drift apart. It registers your handlers (plus the duplex acceptor it builds) on the runtime,
3581
+ * wires hibernation when the duplex carrier declares it, and returns a single {@link IChannelServer} whose
3582
+ * `fetch` / `duplex` / `pushToClient` you forward straight to the host:
3583
+ * ```ts
3584
+ * const server = serveChannel(runtime, channel, {
3585
+ * clientEnv, storage,
3586
+ * carriers: [wsAcceptorCarrier({ send, upgrade, hibernation }), httpAcceptorCarrier()],
3587
+ * handlers: [localHandler],
3588
+ * });
3589
+ * // fetch(req) => server.fetch(req)
3590
+ * // webSocketMessage(conn, m) => server.duplex?.receive(conn, m)
3591
+ * // webSocketClose/Error(conn) => server.duplex?.drop(conn)
3592
+ * ```
3593
+ *
3594
+ * `TConn` (the live-connection token a duplex carrier hands back through `send`/`receive`/`drop`) is
3595
+ * inferred from the carriers — `WebSocket` for `wsAcceptorCarrier`, the data-channel type for a WebRTC
3596
+ * carrier, and so on — so it stays carrier-agnostic.
3348
3597
  */
3349
- function createSecureWebSocketTransport(options) {
3350
- const link = new _nice_code_util.ClientCryptoKeyLink({ storageAdapter: options.storageAdapter });
3351
- return WebSocketTransport.create({
3352
- createWebSocket: options.createWebSocket ?? (() => {
3353
- const ws = new WebSocket(options.url);
3354
- ws.binaryType = "arraybuffer";
3355
- return ws;
3356
- }),
3357
- getTransportCacheKey: options.getTransportCacheKey ?? (() => [options.url]),
3358
- createFormatMessage: options.channel.createCodec,
3359
- updateRunConfig: options.updateRunConfig,
3360
- getRouteInfo: options.getRouteInfo,
3361
- security: {
3362
- securityLevel: options.securityLevel,
3363
- link,
3364
- localCoordinate: options.runtime.coordinate.toJsonObject(),
3365
- dictionaryVersion: options.channel.dictionaryVersion
3366
- }
3598
+ function serveChannel(runtime, channel, options) {
3599
+ const duplexCarriers = options.carriers.filter((carrier) => !require_wsAcceptorCarrier.isExchangeAcceptorCarrier(carrier));
3600
+ const exchangeCarriers = options.carriers.filter(require_wsAcceptorCarrier.isExchangeAcceptorCarrier);
3601
+ if (exchangeCarriers.length > 1) throw new Error("serveChannel: at most one exchange carrier is supported");
3602
+ const exchangeCarrier = exchangeCarriers[0];
3603
+ const exchangeSecure = exchangeCarrier != null && (exchangeCarrier.secure ?? true);
3604
+ const anyDuplexSecure = duplexCarriers.some((carrier) => carrier.secure ?? true);
3605
+ const securityLevel = options.securityLevel ?? DEFAULT_SERVER_SECURITY_LEVELS;
3606
+ let secure;
3607
+ if (anyDuplexSecure || exchangeSecure) {
3608
+ const storage = options.storage;
3609
+ if (storage == null) throw new Error("serveChannel: a secure carrier requires `storage`. Pass it, or set `secure: false` on the carrier for a plain endpoint.");
3610
+ secure = {
3611
+ storage,
3612
+ link: options.link ?? new _nice_code_util.ClientCryptoKeyLink({ storageAdapter: storage }),
3613
+ verifyKeyResolver: options.verifyKeyResolver ?? createStorageTofuVerifyKeyResolver(storage)
3614
+ };
3615
+ }
3616
+ const handlers = [];
3617
+ for (const carrier of duplexCarriers) {
3618
+ const handler = (carrier.secure ?? true) && secure != null ? acceptChannel(runtime, channel, {
3619
+ clientEnv: options.clientEnv,
3620
+ storageAdapter: secure.storage,
3621
+ link: secure.link,
3622
+ verifyKeyResolver: secure.verifyKeyResolver,
3623
+ securityLevel,
3624
+ send: carrier.send,
3625
+ defaultTimeout: options.defaultTimeout
3626
+ }) : createAcceptorHandler({
3627
+ clientEnv: options.clientEnv,
3628
+ createFormatMessage: channel.createCodec,
3629
+ send: carrier.send,
3630
+ runtime,
3631
+ defaultTimeout: options.defaultTimeout
3632
+ });
3633
+ const router = carrier.hibernation != null ? createHibernatableWsServerAdapter({
3634
+ handler,
3635
+ ...carrier.hibernation
3636
+ }) : {
3637
+ receive: (connection, frame) => handler.receive(connection, frame),
3638
+ drop: (connection) => handler.dropConnection(connection)
3639
+ };
3640
+ carrier._activate(router);
3641
+ handlers.push(handler);
3642
+ }
3643
+ runtime.addHandlers([...options.handlers ?? [], ...handlers]);
3644
+ const exchangeSecurity = exchangeSecure && secure != null ? {
3645
+ link: secure.link,
3646
+ verifyKeyResolver: secure.verifyKeyResolver,
3647
+ localCoordinate: runtime.coordinate.toJsonObject(),
3648
+ dictionaryVersion: channel.dictionaryVersion,
3649
+ securityLevel
3650
+ } : void 0;
3651
+ const defaultIsUpgrade = (request) => request.headers.get("Upgrade") === "websocket";
3652
+ const upgraders = [];
3653
+ for (const carrier of duplexCarriers) {
3654
+ if (carrier.upgrade == null) continue;
3655
+ upgraders.push({
3656
+ isUpgrade: carrier.isUpgrade ?? defaultIsUpgrade,
3657
+ upgrade: carrier.upgrade
3658
+ });
3659
+ }
3660
+ const fetch = createActionFetchHandler(runtime, {
3661
+ cors: exchangeCarrier?.cors,
3662
+ onWebSocketUpgrade: upgraders.length === 0 ? void 0 : (request, url) => (upgraders.find((u) => u.isUpgrade(request, url)) ?? upgraders[0]).upgrade(request, url),
3663
+ isWebSocketUpgrade: upgraders.length === 0 ? void 0 : (request, url) => upgraders.some((u) => u.isUpgrade(request, url)),
3664
+ isActionPath: exchangeCarrier != null ? exchangeCarrier.isActionPath ?? (() => true) : () => false,
3665
+ security: exchangeSecurity,
3666
+ useErrorStatus: exchangeCarrier?.useErrorStatus
3367
3667
  });
3668
+ const duplex = duplexCarriers.length === 1 ? duplexCarriers[0] : void 0;
3669
+ const pushToClient = (target, request, pushOptions) => {
3670
+ const owner = target instanceof RuntimeCoordinate ? handlers.find((handler) => handler.ownsLiveConnectionFor(target)) : handlers.find((handler) => handler.hasConnection(target));
3671
+ if (owner == null) throw new Error("serveChannel: no duplex carrier holds a connection for the push target");
3672
+ return owner.pushToClient(runtime, target, request, pushOptions);
3673
+ };
3674
+ return {
3675
+ handlers,
3676
+ fetch,
3677
+ duplex,
3678
+ pushToClient
3679
+ };
3368
3680
  }
3369
3681
  //#endregion
3370
- //#region src/ActionRuntime/Handler/Server/WsConnectionStateStore.ts
3682
+ //#region src/ActionRuntime/Handler/PeerLink/Acceptor/Hibernation/ConnectionStateStore.ts
3371
3683
  /**
3372
- * A typed per-connection state store that co-owns the app state and the server handler's routing
3684
+ * A typed per-connection state store that co-owns the app state and the acceptor handler's routing
3373
3685
  * binding in one attachment, so neither the consumer nor the handler has to hand-merge the two. Create
3374
- * it through {@link ActionServerHandler.createConnectionState} (which also wires binding persistence and
3375
- * replays surviving connections after a wake), then `get`/`set`/`clearApp` the app state directly.
3686
+ * it through {@link createConnectionStateStore} (which also wires binding persistence and replays
3687
+ * surviving connections after a wake), then `get`/`set`/`clearApp` the app state directly.
3688
+ *
3689
+ * The mechanism is carrier-neutral — it only needs read/write/enumerate callbacks for the connection's
3690
+ * attachment — but it pays off on transports whose connections outlive process eviction (e.g. a
3691
+ * Durable Object's hibernatable WebSockets), which is why it lives beside the hibernation adapter.
3376
3692
  *
3377
3693
  * ```ts
3378
- * const players = serverHandler.createConnectionState({
3694
+ * const players = createConnectionStateStore(serverHandler, {
3379
3695
  * schema: vs_player,
3380
3696
  * read: (ws) => ws.deserializeAttachment(),
3381
3697
  * write: (ws, v) => ws.serializeAttachment(v),
@@ -3385,7 +3701,7 @@ function createSecureWebSocketTransport(options) {
3385
3701
  * const player = players.get(ws);
3386
3702
  * ```
3387
3703
  */
3388
- var WsConnectionStateStore = class {
3704
+ var ConnectionStateStore = class {
3389
3705
  options;
3390
3706
  constructor(options) {
3391
3707
  this.options = options;
@@ -3448,534 +3764,1116 @@ var WsConnectionStateStore = class {
3448
3764
  return result.value;
3449
3765
  }
3450
3766
  };
3451
- //#endregion
3452
- //#region src/ActionRuntime/Handler/Server/ActionServerHandler.ts
3453
3767
  /**
3454
- * Server-side handler for backends that accept many client connections over a single open channel
3455
- * (WebSockets, Durable Objects, …). It is transport-agnostic: you feed it inbound frames with
3456
- * {@link receive} and tell it how to write outbound frames via the `send` option.
3457
- *
3458
- * Add it alongside your local execution handler:
3459
- * ```ts
3460
- * const serverHandler = createServerHandler({ clientEnv, formatMessage, send: (ws, f) => ws.send(f) });
3461
- * runtime.addHandlers([localHandler, serverHandler]);
3462
- * // per inbound message (e.g. a Durable Object's webSocketMessage):
3463
- * serverHandler.receive(ws, message);
3464
- * ```
3465
- *
3466
- * Inbound requests route to your local handler; the runtime's return dispatch then calls this
3467
- * handler back (it is an external handler keyed to `clientEnv`) to send the result to the originating
3468
- * connection. The handler keeps a per-connection identity registry so each result lands on the right
3469
- * socket, and remembers each connection's encoding so binary and JSON clients can share the channel.
3768
+ * Build a per-connection {@link ConnectionStateStore} bound to an {@link AcceptorHandler}: it registers
3769
+ * itself as the handler's connection-bound persistence callback (so bindings are written without
3770
+ * overwriting app state) and immediately replays every live connection's stored binding via
3771
+ * {@link AcceptorHandler.rehydrateConnection} — so on a transport that resumes after eviction (e.g. a
3772
+ * Durable Object waking from hibernation) both the app identity and the action routing come back from a
3773
+ * single attachment, with no storage reads and no hand-rolled merge.
3470
3774
  *
3471
- * It registers an empty action router, so it is never chosen to *execute* an inbound request — only
3472
- * to ferry results/pushes back out.
3775
+ * Lives outside the handler so the generic {@link AcceptorHandler} stays free of any attachment/
3776
+ * hibernation concern it exposes only the neutral `setOnConnectionBound` + `rehydrateConnection`
3777
+ * hooks this builder drives.
3473
3778
  */
3474
- var ActionServerHandler = class extends ActionExternalClientHandler {
3475
- _formatMessage;
3476
- _createFormatMessage;
3477
- _send;
3478
- _runtime;
3479
- _serverTimeout;
3480
- _onConnectionBound;
3481
- /** Incoming-data listeners installed by the runtime (`resolveIncomingActionPayload`). */
3482
- _incomingListeners = [];
3483
- _security;
3484
- /** Normalized accepted levels; whether `none` (plain) is allowed; whether any level needs a handshake. */
3485
- _allowedLevels;
3486
- _noneAllowed;
3487
- _handshakeMode;
3488
- _connByClient = /* @__PURE__ */ new Map();
3489
- _clientByConn = /* @__PURE__ */ new Map();
3490
- _connEncoding = /* @__PURE__ */ new Map();
3491
- _codecByConn = /* @__PURE__ */ new Map();
3492
- _handshakeByConn = /* @__PURE__ */ new Map();
3493
- _cryptoByConn = /* @__PURE__ */ new Map();
3494
- _authedConns = /* @__PURE__ */ new Set();
3495
- _plainConns = /* @__PURE__ */ new Set();
3496
- _inboundChainByConn = /* @__PURE__ */ new Map();
3497
- _outboundChainByConn = /* @__PURE__ */ new Map();
3498
- constructor(options) {
3499
- super({
3500
- runtimeCoordinate: options.clientEnv,
3501
- transports: []
3502
- });
3503
- this._formatMessage = options.formatMessage;
3504
- this._createFormatMessage = options.createFormatMessage;
3505
- this._send = options.send;
3506
- this._runtime = options.runtime;
3507
- this._serverTimeout = options.defaultTimeout ?? 1e4;
3508
- this._onConnectionBound = options.onConnectionBound;
3509
- this._security = options.security;
3510
- this._allowedLevels = options.security == null ? [] : Array.isArray(options.security.securityLevel) ? options.security.securityLevel : [options.security.securityLevel];
3511
- this._noneAllowed = this._allowedLevels.includes("none");
3512
- this._handshakeMode = this._allowedLevels.some((level) => level !== "none");
3779
+ function createConnectionStateStore(handler, options) {
3780
+ const store = new ConnectionStateStore(options);
3781
+ handler.setOnConnectionBound((connection, binding) => store._persistBinding(connection, binding));
3782
+ for (const connection of options.getConnections()) {
3783
+ const binding = store._readBinding(connection);
3784
+ if (binding != null) handler.rehydrateConnection(connection, binding);
3513
3785
  }
3514
- /**
3515
- * The codec for a connection: a per-connection session (cached) when a factory was provided, else
3516
- * the single shared `formatMessage`.
3517
- */
3518
- _codecFor(connection) {
3519
- if (this._createFormatMessage != null) {
3520
- let codec = this._codecByConn.get(connection);
3521
- if (codec == null) {
3522
- codec = this._createFormatMessage();
3523
- this._codecByConn.set(connection, codec);
3786
+ return store;
3787
+ }
3788
+ //#endregion
3789
+ //#region src/ActionRuntime/Transport/Carrier/duplex/inMemory/createInMemoryChannel.ts
3790
+ /**
3791
+ * Two cross-wired in-process byte channels — a loopback carrier with no socket. The client end is a
3792
+ * {@link IDuplexCarrier} you hand to a {@link LinkTransport}; the server end plugs into an
3793
+ * `AcceptorHandler` (`send: (_, f) => serverEndpoint.send(f)`, and `serverEndpoint.onMessage(f =>
3794
+ * handler.receive(conn, f))`). Frames are delivered on a microtask, so each side observes the other
3795
+ * asynchronously — exactly like a real transport — which makes this ideal for tests and for running
3796
+ * two runtimes in one process (or proving a non-WS carrier end to end).
3797
+ */
3798
+ function createInMemoryChannelPair() {
3799
+ let clientMessage;
3800
+ let clientClose;
3801
+ let serverMessage;
3802
+ let serverClose;
3803
+ let open = true;
3804
+ const closeBoth = () => {
3805
+ if (!open) return;
3806
+ open = false;
3807
+ queueMicrotask(() => {
3808
+ clientClose?.();
3809
+ serverClose?.();
3810
+ });
3811
+ };
3812
+ return {
3813
+ clientChannel: {
3814
+ ready: Promise.resolve(),
3815
+ isOpen: () => open,
3816
+ send: (frame) => {
3817
+ if (!open) return;
3818
+ queueMicrotask(() => serverMessage?.(frame));
3819
+ },
3820
+ attach: ({ onMessage, onClose }) => {
3821
+ clientMessage = onMessage;
3822
+ clientClose = onClose;
3823
+ },
3824
+ close: closeBoth,
3825
+ label: "in-memory"
3826
+ },
3827
+ serverEndpoint: {
3828
+ send: (frame) => {
3829
+ if (!open) return;
3830
+ queueMicrotask(() => clientMessage?.(frame));
3831
+ },
3832
+ onMessage: (handler) => {
3833
+ serverMessage = handler;
3834
+ },
3835
+ onClose: (handler) => {
3836
+ serverClose = handler;
3837
+ },
3838
+ close: closeBoth
3839
+ }
3840
+ };
3841
+ }
3842
+ //#endregion
3843
+ //#region src/ActionRuntime/Transport/Carrier/duplex/inMemory/inMemoryCarrier.ts
3844
+ /**
3845
+ * A loopback duplex carrier with no socket — two cross-wired in-process ends. The connector end is an
3846
+ * {@link IDuplexCarrierSource} for {@link secureTransport}; the acceptor end plugs into an
3847
+ * `AcceptorHandler`. Ideal for tests and for running two runtimes in one process, or proving a
3848
+ * non-WS carrier end to end.
3849
+ */
3850
+ function inMemoryCarrier() {
3851
+ const { clientChannel, serverEndpoint } = createInMemoryChannelPair();
3852
+ return {
3853
+ carrier: {
3854
+ carrierLabel: "memory",
3855
+ open: () => clientChannel,
3856
+ getCacheKey: () => ["memory"]
3857
+ },
3858
+ serverEndpoint
3859
+ };
3860
+ }
3861
+ //#endregion
3862
+ //#region src/ActionRuntime/Transport/Carrier/duplex/rtc/rtcDataChannelByteChannel.ts
3863
+ /**
3864
+ * Adapt a WebRTC `RTCDataChannel` to the carrier-agnostic {@link IDuplexCarrier}, so two browsers
3865
+ * (or two mobile apps) linked peer-to-peer — no server in the middle — run the *same* secure session as
3866
+ * a WebSocket. Hand it to `createSecureLinkTransport({ openChannel: () => rtcDataChannelByteChannel(dc) })`.
3867
+ *
3868
+ * The data channel must already be created (its negotiation/signaling is the app's concern); this only
3869
+ * drives bytes over it. Binary frames are requested as `ArrayBuffer` so the binary session codec unpacks
3870
+ * them synchronously; a `Blob` (if the channel hands one back) is normalized to a buffer.
3871
+ */
3872
+ function rtcDataChannelByteChannel(dc) {
3873
+ dc.binaryType = "arraybuffer";
3874
+ let intentional = false;
3875
+ let onMessage;
3876
+ let onClose;
3877
+ const preAttach = [];
3878
+ const deliver = (frame) => {
3879
+ if (onMessage != null) onMessage(frame);
3880
+ else preAttach.push(frame);
3881
+ };
3882
+ dc.addEventListener("message", async (event) => {
3883
+ const frame = await normalizeFrame$1(event.data);
3884
+ if (frame !== void 0) deliver(frame);
3885
+ });
3886
+ dc.addEventListener("close", () => {
3887
+ if (!intentional) console.error("RTCDataChannel closed");
3888
+ onClose?.();
3889
+ });
3890
+ dc.addEventListener("error", (event) => {
3891
+ console.error("RTCDataChannel error:", event);
3892
+ onClose?.();
3893
+ });
3894
+ return {
3895
+ ready: new Promise((resolve, reject) => {
3896
+ if (dc.readyState === "open") {
3897
+ resolve();
3898
+ return;
3524
3899
  }
3525
- return codec;
3900
+ dc.addEventListener("open", () => resolve(), { once: true });
3901
+ dc.addEventListener("error", (event) => reject(event), { once: true });
3902
+ dc.addEventListener("close", () => reject(/* @__PURE__ */ new Error("RTCDataChannel closed before open")), { once: true });
3903
+ }),
3904
+ isOpen: () => dc.readyState === "open",
3905
+ send: (frame) => {
3906
+ if (typeof frame === "string" || frame instanceof ArrayBuffer) dc.send(frame);
3907
+ else dc.send(new Uint8Array(frame));
3908
+ },
3909
+ attach: (handlers) => {
3910
+ onMessage = handlers.onMessage;
3911
+ onClose = handlers.onClose;
3912
+ for (const frame of preAttach) handlers.onMessage(frame);
3913
+ preAttach.length = 0;
3914
+ },
3915
+ close: () => {
3916
+ intentional = true;
3917
+ try {
3918
+ dc.close();
3919
+ } catch {}
3920
+ },
3921
+ get label() {
3922
+ return dc.label != null && dc.label !== "" ? dc.label : void 0;
3526
3923
  }
3527
- if (this._formatMessage != null) return this._formatMessage;
3528
- throw err_nice_transport.fromId("not_found", { actionId: "server-handler-codec (provide formatMessage or createFormatMessage)" });
3529
- }
3530
- _setIncomingActionDataListener(listener) {
3531
- this._incomingListeners.push(listener);
3924
+ };
3925
+ }
3926
+ async function normalizeFrame$1(data) {
3927
+ if (typeof data === "string" || data instanceof ArrayBuffer || data instanceof Uint8Array) return data;
3928
+ if (typeof Blob !== "undefined" && data instanceof Blob) return await data.arrayBuffer();
3929
+ }
3930
+ //#endregion
3931
+ //#region src/ActionRuntime/Transport/Carrier/duplex/rtc/rtcCarrier.ts
3932
+ /**
3933
+ * A WebRTC {@link IDuplexCarrierSource} over an already-negotiated `RTCDataChannel` (signaling is the
3934
+ * app's concern). Hand it to {@link secureTransport} so two browsers/apps linked peer-to-peer run the
3935
+ * identical secure session as a WebSocket.
3936
+ */
3937
+ function rtcCarrier(dataChannel, options = {}) {
3938
+ return {
3939
+ carrierLabel: "webrtc",
3940
+ open: () => rtcDataChannelByteChannel(dataChannel),
3941
+ getCacheKey: options.getTransportCacheKey ?? (() => ["webrtc"]),
3942
+ getRouteInfo: options.getRouteInfo
3943
+ };
3944
+ }
3945
+ //#endregion
3946
+ //#region src/ActionRuntime/Transport/Carrier/duplex/ws/err_nice_transport_ws.ts
3947
+ let EErrId_NiceTransport_WebSocket = /* @__PURE__ */ function(EErrId_NiceTransport_WebSocket) {
3948
+ EErrId_NiceTransport_WebSocket["ws_disconnected"] = "ws_disconnected";
3949
+ EErrId_NiceTransport_WebSocket["ws_create_failed"] = "ws_create_failed";
3950
+ EErrId_NiceTransport_WebSocket["ws_error"] = "ws_error";
3951
+ return EErrId_NiceTransport_WebSocket;
3952
+ }({});
3953
+ const err_nice_transport_ws = err_nice_transport.createChildDomain({
3954
+ domain: "ws_transport",
3955
+ schema: {
3956
+ ["ws_disconnected"]: (0, _nice_code_error.err)({ message: () => `WebSocket transport disconnected.` }),
3957
+ ["ws_create_failed"]: (0, _nice_code_error.err)({ message: ({ originalError }) => `Failed to create WebSocket transport.${originalError ? ` Original error: ${originalError.message}` : ""}` }),
3958
+ ["ws_error"]: (0, _nice_code_error.err)({ message: ({ originalError }) => `WebSocket transport error.${originalError ? ` Original error: ${originalError.message}` : ""}` })
3532
3959
  }
3533
- /**
3534
- * Register (or replace) the connection-bound persistence callback after construction. Used by
3535
- * lifecycle helpers like {@link createHibernatableWsServerAdapter} so persistence and replay are
3536
- * owned by one place instead of being split across the constructor options.
3537
- */
3538
- setOnConnectionBound(onConnectionBound) {
3539
- this._onConnectionBound = onConnectionBound;
3960
+ });
3961
+ //#endregion
3962
+ //#region src/ActionRuntime/Transport/Carrier/duplex/ws/ws_util.ts
3963
+ /**
3964
+ * Send a text or binary frame over a socket. A binary formatter may hand back a `Uint8Array` whose
3965
+ * backing buffer is typed as `ArrayBufferLike` (msgpackr pools buffers / may be `SharedArrayBuffer`),
3966
+ * which `WebSocket.send`'s `BufferSource` parameter rejects — copy it into a fresh `ArrayBuffer`-backed
3967
+ * view so the type (and the bytes) are safe to send.
3968
+ */
3969
+ function sendFrame(ws, data) {
3970
+ if (typeof data === "string" || data instanceof ArrayBuffer) {
3971
+ ws.send(data);
3972
+ return;
3540
3973
  }
3541
- /**
3542
- * Create a typed per-connection state store that co-owns the consumer's app state and this handler's
3543
- * routing binding in one attachment. It registers itself as the connection-bound persistence callback
3544
- * (so bindings are written without overwriting app state) and immediately replays every live
3545
- * connection's stored binding via {@link rehydrateConnection} — so on a transport that resumes after
3546
- * eviction (e.g. a Durable Object waking from hibernation) both the app identity and the action
3547
- * routing come back from a single attachment, with no storage reads and no hand-rolled merge.
3548
- *
3549
- * This supersedes {@link createHibernatableWsServerAdapter} for app code that also pins its own state
3550
- * to the connection. Construct it once when the handler is built, then `get`/`set` app state directly.
3551
- */
3552
- createConnectionState(options) {
3553
- const store = new WsConnectionStateStore(options);
3554
- this.setOnConnectionBound((connection, binding) => store._persistBinding(connection, binding));
3555
- for (const connection of options.getConnections()) {
3556
- const binding = store._readBinding(connection);
3557
- if (binding != null) this.rehydrateConnection(connection, binding);
3558
- }
3559
- return store;
3974
+ ws.send(new Uint8Array(data));
3975
+ }
3976
+ /** Compact a WebSocket URL to `host/pathname` for devtools display, falling back to the raw url. */
3977
+ function shortWs(url) {
3978
+ try {
3979
+ const u = new URL(url);
3980
+ return `${u.host}${u.pathname}`;
3981
+ } catch {
3982
+ return url;
3560
3983
  }
3561
- /**
3562
- * Feed one inbound frame from a connection into the runtime. Decodes text or binary, binds the
3563
- * connection to the requesting client's identity, then routes it (requests execute locally;
3564
- * results/progress resolve pending server-initiated actions).
3565
- */
3566
- receive(connection, frame) {
3567
- if (this._security == null || !this._handshakeMode) {
3568
- this._receivePlain(connection, frame);
3569
- return;
3984
+ }
3985
+ //#endregion
3986
+ //#region src/ActionRuntime/Transport/Carrier/duplex/ws/webSocketByteChannel.ts
3987
+ /**
3988
+ * Adapt a `WebSocket` to the carrier-agnostic {@link IDuplexCarrier}, so the WebSocket becomes
3989
+ * "just another carrier" under the shared secure session. It owns every WebSocket-specific concern —
3990
+ * awaiting `open`, normalizing `Blob` frames to bytes, suppressing the log on a deliberate `close`, and
3991
+ * buffering any frame that arrives before the session attaches its handler — leaving the session itself
3992
+ * carrier-neutral.
3993
+ */
3994
+ function webSocketByteChannel(ws) {
3995
+ let intentional = false;
3996
+ let onMessage;
3997
+ let onClose;
3998
+ const preAttach = [];
3999
+ const deliver = (frame) => {
4000
+ if (onMessage != null) onMessage(frame);
4001
+ else preAttach.push(frame);
4002
+ };
4003
+ ws.addEventListener("message", async (event) => {
4004
+ const frame = await normalizeFrame(event.data);
4005
+ if (frame !== void 0) deliver(frame);
4006
+ });
4007
+ ws.addEventListener("close", (event) => {
4008
+ if (!intentional) console.error("WebSocket closed:", event);
4009
+ onClose?.();
4010
+ });
4011
+ ws.addEventListener("error", (event) => {
4012
+ console.error("WebSocket error:", event);
4013
+ onClose?.();
4014
+ });
4015
+ return {
4016
+ ready: new Promise((resolve, reject) => {
4017
+ if (ws.readyState === WebSocket.OPEN) {
4018
+ resolve();
4019
+ return;
4020
+ }
4021
+ ws.addEventListener("open", () => resolve(), { once: true });
4022
+ ws.addEventListener("error", (event) => reject(event), { once: true });
4023
+ ws.addEventListener("close", (event) => reject(/* @__PURE__ */ new Error(`WebSocket closed before open: code=${event.code}`)), { once: true });
4024
+ }),
4025
+ isOpen: () => ws.readyState === WebSocket.OPEN,
4026
+ send: (frame) => sendFrame(ws, frame),
4027
+ attach: (handlers) => {
4028
+ onMessage = handlers.onMessage;
4029
+ onClose = handlers.onClose;
4030
+ for (const frame of preAttach) handlers.onMessage(frame);
4031
+ preAttach.length = 0;
4032
+ },
4033
+ close: () => {
4034
+ intentional = true;
4035
+ try {
4036
+ ws.close();
4037
+ } catch {}
4038
+ },
4039
+ get label() {
4040
+ return ws.url != null && ws.url !== "" ? ws.url : void 0;
3570
4041
  }
3571
- const next = (this._inboundChainByConn.get(connection) ?? Promise.resolve()).then(() => this._receiveSecure(connection, frame)).catch((err) => console.error("[ws-server] failed to process inbound frame", err));
3572
- this._inboundChainByConn.set(connection, next);
3573
- }
3574
- _receivePlain(connection, frame) {
3575
- const wire = decodeActionFrame(frame, this._codecFor(connection));
3576
- if (wire == null) return;
3577
- const encoding = typeof frame === "string" ? "json" : "binary";
3578
- this._connEncoding.set(connection, encoding);
3579
- if (wire.type === "request") this._resolveRequestIdentity(connection, wire, encoding);
3580
- for (const listener of this._incomingListeners) listener(wire);
4042
+ };
4043
+ }
4044
+ /** Accept text + binary frames (ArrayBuffer / Uint8Array / Blob); Blobs are converted to a buffer. */
4045
+ async function normalizeFrame(data) {
4046
+ if (typeof data === "string" || data instanceof ArrayBuffer || data instanceof Uint8Array) return data;
4047
+ if (typeof Blob !== "undefined" && data instanceof Blob) return await data.arrayBuffer();
4048
+ }
4049
+ //#endregion
4050
+ //#region src/ActionRuntime/Transport/Carrier/duplex/ws/wsCarrier.ts
4051
+ /**
4052
+ * A WebSocket {@link IDuplexCarrierSource}: opens an `arraybuffer` socket to `url` (cached per endpoint)
4053
+ * and adapts it to a carrier. Hand it to {@link secureTransport} (or `LinkTransport`) — the WebSocket is
4054
+ * now "just another carrier" under the shared secure session, with no WS-specific transport class.
4055
+ */
4056
+ function wsCarrier(url, options = {}) {
4057
+ return {
4058
+ carrierLabel: "ws",
4059
+ open: (input) => webSocketByteChannel(options.createWebSocket?.(input) ?? defaultWebSocket(url)),
4060
+ getCacheKey: options.getTransportCacheKey ?? (() => [url]),
4061
+ getRouteInfo: options.getRouteInfo ?? (() => ({
4062
+ carrierLabel: "ws",
4063
+ url,
4064
+ summary: `ws ${shortWs(url)}`
4065
+ }))
4066
+ };
4067
+ }
4068
+ function defaultWebSocket(url) {
4069
+ const ws = new WebSocket(url);
4070
+ ws.binaryType = "arraybuffer";
4071
+ return ws;
4072
+ }
4073
+ //#endregion
4074
+ //#region src/ActionRuntime/Transport/Carrier/exchange/http/httpAcceptorCarrier.ts
4075
+ /**
4076
+ * An HTTP {@link IExchangeAcceptorCarrier}: the accept-in dual of {@link httpCarrier}. It serves the
4077
+ * secure exchange protocol (handshake → token session → encrypted frames) over web-standard
4078
+ * `Request`/`Response`. The crypto identity, runtime coordinate, dictionary version, and accepted security
4079
+ * levels are all supplied centrally by `serveChannel`, so this only needs to say which requests carry an
4080
+ * action envelope and how to answer CORS.
4081
+ */
4082
+ function httpAcceptorCarrier(options = {}) {
4083
+ return {
4084
+ shape: "exchange",
4085
+ carrierLabel: options.carrierLabel ?? "http",
4086
+ secure: options.secure,
4087
+ isActionPath: options.isActionPath,
4088
+ cors: options.cors,
4089
+ useErrorStatus: options.useErrorStatus
4090
+ };
4091
+ }
4092
+ //#endregion
4093
+ //#region src/ActionRuntime/Transport/Carrier/exchange/http/httpCarrier.ts
4094
+ function shortPath(url) {
4095
+ try {
4096
+ return new URL(url).pathname || url;
4097
+ } catch {
4098
+ return url;
3581
4099
  }
3582
- async _receiveSecure(connection, frame) {
3583
- const security = this._security;
3584
- if (security == null) return;
3585
- if (this._plainConns.has(connection)) {
3586
- this._receivePlain(connection, frame);
3587
- return;
3588
- }
3589
- if (!this._authedConns.has(connection)) {
3590
- const message = typeof frame === "string" ? decodeHandshakeMessage(frame) : void 0;
3591
- if (message == null) {
3592
- if (this._noneAllowed) {
3593
- this._plainConns.add(connection);
3594
- this._receivePlain(connection, frame);
4100
+ }
4101
+ /**
4102
+ * An HTTP {@link IExchangeCarrierSource}: each `exchange` POSTs one frame body to the action endpoint and
4103
+ * resolves with the response body as the single correlated reply. Hand it to {@link secureTransport} —
4104
+ * HTTP then runs the *same* secure session as a duplex carrier (handshake → token → encrypted frames),
4105
+ * the request/reply correlation provided for free by the HTTP transaction.
4106
+ *
4107
+ * `createRequest` derives the URL/headers per action (keep it simple with `() => ({ url })`). The body is
4108
+ * the session's responsibility, so it is never built here.
4109
+ */
4110
+ function httpCarrier(createRequest, options = {}) {
4111
+ const doFetch = options.fetch ?? fetch;
4112
+ return {
4113
+ shape: "exchange",
4114
+ carrierLabel: "http",
4115
+ open: (input) => {
4116
+ const request = createRequest(input);
4117
+ return {
4118
+ label: request.url,
4119
+ exchange: async (frame, opts) => {
4120
+ return await (await doFetch(request.url, {
4121
+ method: "POST",
4122
+ headers: {
4123
+ "Content-Type": "application/json",
4124
+ ...request.headers
4125
+ },
4126
+ body: typeof frame === "string" ? frame : new Uint8Array(frame),
4127
+ signal: opts?.signal
4128
+ })).text();
3595
4129
  }
4130
+ };
4131
+ },
4132
+ getCacheKey: options.getTransportCacheKey ?? ((input) => [createRequest(input).url]),
4133
+ getRouteInfo: options.getRouteInfo ?? ((input) => {
4134
+ const { url } = createRequest(input);
4135
+ return {
4136
+ carrierLabel: "http",
4137
+ method: "POST",
4138
+ url,
4139
+ summary: `POST ${shortPath(url)}`
4140
+ };
4141
+ })
4142
+ };
4143
+ }
4144
+ //#endregion
4145
+ //#region src/ActionRuntime/Transport/codec/createBinaryWireAdapter.ts
4146
+ /**
4147
+ * Positional layout of the stateless binary envelope. A flat tuple (rather than an object) strips the
4148
+ * repeated `domain`/`id`/`form`/`type` and context key names from every frame, and we carry only the
4149
+ * context fields the receiver can't recompute: `cuid` (correlation) and `originClient` (return
4150
+ * routing).
4151
+ *
4152
+ * [ routeInt, typeInt, time, cuid, originClient, payloadData ]
4153
+ *
4154
+ * Dropped vs the JSON wire: `form`/`type` strings, `inputHash`/`outputHash` (recomputed on hydrate),
4155
+ * `context.timeCreated` (reconstructed from `time`) and `context.routing` (rebuilt empty — the
4156
+ * receiver re-stamps its own route items as it handles the action). For the leanest possible frames
4157
+ * (integer correlation, identity dropped after a handshake), use `createBinaryWireSessionFactory`.
4158
+ */
4159
+ const ENVELOPE = {
4160
+ route: 0,
4161
+ type: 1,
4162
+ time: 2,
4163
+ cuid: 3,
4164
+ originClient: 4,
4165
+ payload: 5
4166
+ };
4167
+ const ENVELOPE_LENGTH = 6;
4168
+ /**
4169
+ * Builds a *stateless* `formatMessage` pipeline for {@link LinkTransport}, packing action
4170
+ * payloads into a compact msgpackr binary frame instead of JSON. The `domain`/`id` route collapses to
4171
+ * a single integer drawn from a shared dictionary; `form`/`type`, the recomputable
4172
+ * `inputHash`/`outputHash`, and the per-frame `context.routing`/`context.timeCreated` are all dropped
4173
+ * (see {@link ENVELOPE}).
4174
+ *
4175
+ * No validation runs here: `incoming` blindly reconstructs the wire JSON shape and hands it back to
4176
+ * the connection, which flows into `ActionRuntime` → `domain.hydrateAnyAction()` where the Valibot
4177
+ * schemas validate it exactly as they would for a JSON frame.
4178
+ *
4179
+ * Both ends of the socket MUST construct the adapter with the same domains in the same order — the
4180
+ * integer dictionary is positional. Mismatched dictionaries will route to the wrong action.
4181
+ *
4182
+ * Because `incoming` returns `undefined` for text frames, a binary server can still serve plain-JSON
4183
+ * clients on the same runtime (the connection falls back to its built-in JSON parser).
4184
+ */
4185
+ function createBinaryWireAdapter(domains) {
4186
+ const { routeToInt, intToRoute } = buildActionRouteDictionary(domains);
4187
+ return {
4188
+ outgoing: (input) => {
4189
+ const json = input.action.toJsonObject();
4190
+ const routeKey = `${json.domain}:${json.id}`;
4191
+ const routeInt = routeToInt.get(routeKey);
4192
+ if (routeInt == null) throw new Error(`[binary-wire] Cannot pack unregistered action route: ${routeKey}`);
4193
+ const envelope = new Array(ENVELOPE_LENGTH);
4194
+ envelope[ENVELOPE.route] = routeInt;
4195
+ envelope[ENVELOPE.type] = PayloadTypeToInt[json.type];
4196
+ envelope[ENVELOPE.time] = json.time;
4197
+ envelope[ENVELOPE.cuid] = json.context.cuid;
4198
+ envelope[ENVELOPE.originClient] = json.context.originClient;
4199
+ envelope[ENVELOPE.payload] = extractWirePayload(json);
4200
+ return (0, msgpackr.pack)(envelope);
4201
+ },
4202
+ incoming: (frame) => {
4203
+ let buffer;
4204
+ if (frame instanceof ArrayBuffer) buffer = new Uint8Array(frame);
4205
+ else if (frame instanceof Uint8Array) buffer = frame;
4206
+ else return;
4207
+ try {
4208
+ const envelope = (0, msgpackr.unpack)(buffer);
4209
+ if (!Array.isArray(envelope) || envelope.length !== ENVELOPE_LENGTH) return void 0;
4210
+ const routeMeta = intToRoute[envelope[ENVELOPE.route]];
4211
+ const payloadType = ReversePayloadType[envelope[ENVELOPE.type]];
4212
+ if (routeMeta == null || payloadType == null) return void 0;
4213
+ const time = envelope[ENVELOPE.time];
4214
+ return assembleWireJson(routeMeta, payloadType, time, {
4215
+ cuid: envelope[ENVELOPE.cuid],
4216
+ timeCreated: time,
4217
+ routing: [],
4218
+ originClient: envelope[ENVELOPE.originClient]
4219
+ }, envelope[ENVELOPE.payload]);
4220
+ } catch (e) {
4221
+ console.error("[binary-wire] Failed to unpack binary action frame", e);
3596
4222
  return;
3597
4223
  }
3598
- await security.link.initialize();
3599
- let handshake = this._handshakeByConn.get(connection);
3600
- if (handshake == null) {
3601
- handshake = createServerHandshake({
3602
- link: security.link,
3603
- localCoordinate: security.localCoordinate,
3604
- dictionaryVersion: security.dictionaryVersion,
3605
- securityLevel: security.securityLevel,
3606
- verifyKeyResolver: security.verifyKeyResolver
3607
- });
3608
- this._handshakeByConn.set(connection, handshake);
3609
- }
3610
- if (message.t === "hello") this._send(connection, encodeHandshakeMessage(await handshake.onHello(message)));
3611
- else if (message.t === "prove") {
3612
- const reply = await handshake.onProve(message);
3613
- this._send(connection, encodeHandshakeMessage(reply));
3614
- const result = handshake.getResult();
3615
- if (reply.t === "accept" && result != null) this._completeServerHandshake(connection, result);
3616
- }
3617
- return;
3618
- }
3619
- let bytes = frame;
3620
- const cryptoReady = this._cryptoByConn.get(connection);
3621
- if (cryptoReady != null) try {
3622
- bytes = await (await cryptoReady).decryptFrame(frame);
3623
- } catch (err) {
3624
- console.error("[ws-server] failed to decrypt inbound frame", err);
3625
- return;
3626
4224
  }
3627
- const wire = decodeActionFrame(bytes, this._codecFor(connection));
3628
- if (wire == null) return;
3629
- if (wire.type === "request") {
3630
- const bound = this._clientByConn.get(connection);
3631
- if (bound != null) wire.context.originClient = bound.toJsonObject();
4225
+ };
4226
+ }
4227
+ //#endregion
4228
+ //#region src/ActionRuntime/Transport/Transport.ts
4229
+ /**
4230
+ * Reusable transport definition. Devs construct these (`secureTransport({ carrier: wsCarrier(url) })`,
4231
+ * `plainTransport({ carrier: httpCarrier(...) })`, …) and pass them to a
4232
+ * `ConnectorHandler`. A single
4233
+ * definition can be shared across multiple handlers — each handler builds its own live
4234
+ * {@link TransportConnection} via {@link TransportConnection._createConnection}.
4235
+ */
4236
+ var Transport = class {};
4237
+ //#endregion
4238
+ //#region src/ActionRuntime/Transport/SecureSession/establishExchangeSession.ts
4239
+ const textEncoder = new TextEncoder();
4240
+ const textDecoder = new TextDecoder();
4241
+ /** Plain path (no handshake/token): every action rides a bare `act` envelope, plaintext both ways. */
4242
+ function finalizePlainExchangeMethods(ctx) {
4243
+ return buildExchangeMethods(ctx, {});
4244
+ }
4245
+ /** Secure path: run the handshake (two exchanges) once at bring-up, then reuse the token + crypto. */
4246
+ async function finalizeSecureExchangeMethods(ctx) {
4247
+ return buildExchangeMethods(ctx, await runConnectorExchangeHandshake(ctx.carrier, ctx.secure));
4248
+ }
4249
+ function buildExchangeMethods(ctx, state) {
4250
+ const sendActionData = (inputs) => {
4251
+ runExchange(ctx.carrier, state, inputs).catch((err) => inputs.runningAction._abort(err));
4252
+ };
4253
+ return {
4254
+ sendActionData,
4255
+ updateRunConfig: ctx.updateRunConfig
4256
+ };
4257
+ }
4258
+ async function runExchange(carrier, state, inputs) {
4259
+ const { action, runningAction, timeout } = inputs;
4260
+ const ac = new AbortController();
4261
+ let timedOut = false;
4262
+ const timeoutId = setTimeout(() => {
4263
+ timedOut = true;
4264
+ ac.abort();
4265
+ }, timeout);
4266
+ const unsubscribe = runningAction.addUpdateListeners([(update) => {
4267
+ if (update.type === "finished") {
4268
+ clearTimeout(timeoutId);
4269
+ ac.abort();
3632
4270
  }
3633
- for (const listener of this._incomingListeners) listener(wire);
3634
- }
3635
- _completeServerHandshake(connection, result) {
3636
- const clientCoord = new RuntimeCoordinate(result.remote);
3637
- this._bindConnection(connection, clientCoord);
3638
- this._connEncoding.set(connection, "binary");
3639
- this._authedConns.add(connection);
3640
- this._handshakeByConn.delete(connection);
3641
- if (result.securityLevel === "encrypted" && this._security != null) this._cryptoByConn.set(connection, Promise.resolve(createActionFrameCrypto({
3642
- link: this._security.link,
3643
- linkedClientId: result.linkedClientId
3644
- })));
3645
- this._onConnectionBound?.(connection, {
3646
- client: clientCoord.toJsonObject(),
3647
- encoding: "binary",
3648
- secure: {
3649
- securityLevel: result.securityLevel,
3650
- linkedClientId: result.linkedClientId,
3651
- keyMaterial: result.encryptionKeyMaterial
3652
- }
4271
+ }]);
4272
+ try {
4273
+ const request = await buildRequestEnvelope(state, action);
4274
+ const replyRaw = await carrier.exchange(encodeExchange(request), { signal: ac.signal });
4275
+ if (action.type !== "request") return;
4276
+ const reply = decodeExchangeReply(asString(replyRaw));
4277
+ if (reply == null) throw err_nice_transport.fromId("invalid_action_response", { actionId: action.id });
4278
+ if (reply.k === "err") throw err_nice_transport.fromId("send_failed", {
4279
+ actionState: action.type,
4280
+ actionId: action.id,
4281
+ message: reply.message
3653
4282
  });
4283
+ const wire = await extractReplyWire(state, reply);
4284
+ if (wire == null || !isActionPayload_Result_JsonObject(wire)) throw err_nice_transport.fromId("invalid_action_response", { actionId: action.id });
4285
+ runningAction._completeWithResult(action._domain.hydrateResultPayload(wire));
4286
+ } catch (err) {
4287
+ if (timedOut) throw err_nice_transport.fromId("timeout", { timeout });
4288
+ throw err;
4289
+ } finally {
4290
+ clearTimeout(timeoutId);
4291
+ unsubscribe();
3654
4292
  }
3655
- /**
3656
- * Ensure an inbound request carries the client's identity and that this connection is bound to it,
3657
- * so its result can be routed back. A session codec omits `originClient` after the first request, so
3658
- * when it's missing we restore it from the (possibly rehydrated) binding instead. (Plain mode only;
3659
- * secure mode binds the authenticated coordinate at handshake time.)
3660
- */
3661
- _resolveRequestIdentity(connection, wire, encoding) {
3662
- const wireOrigin = wire.context.originClient;
3663
- if (wireOrigin != null && wireOrigin.envId !== "_unset_") {
3664
- const clientCoord = new RuntimeCoordinate(wireOrigin);
3665
- const isNewBinding = this._clientByConn.get(connection)?.stringId !== clientCoord.stringId;
3666
- this._bindConnection(connection, clientCoord);
3667
- if (isNewBinding) this._onConnectionBound?.(connection, {
3668
- client: clientCoord.toJsonObject(),
3669
- encoding
3670
- });
3671
- return;
3672
- }
3673
- const bound = this._clientByConn.get(connection);
3674
- if (bound != null) wire.context.originClient = bound.toJsonObject();
3675
- }
3676
- /**
3677
- * Restore a connection→client binding without an inbound frame — for transports that resume after
3678
- * eviction. Pair it with the {@link IActionServerHandlerOptions.onConnectionBound} hook: persist
3679
- * the binding there, then replay each live connection here when the channel comes back (e.g. a
3680
- * Durable Object iterating `ctx.getWebSockets()` as it wakes from hibernation).
3681
- */
3682
- rehydrateConnection(connection, binding) {
3683
- this._bindConnection(connection, new RuntimeCoordinate(binding.client));
3684
- this._connEncoding.set(connection, binding.encoding);
3685
- const secure = binding.secure;
3686
- if (secure == null) return;
3687
- this._authedConns.add(connection);
3688
- if (secure.securityLevel === "encrypted" && secure.keyMaterial != null && this._security != null) {
3689
- const security = this._security;
3690
- const { linkedClientId, keyMaterial } = secure;
3691
- const cryptoReady = security.link.initialize().then(() => security.link.linkClient({
3692
- linkedClientId,
3693
- verifyPublicKey: keyMaterial.verifyPublicKey,
3694
- exchangePublicKey: keyMaterial.exchangePublicKey,
3695
- saltString: keyMaterial.saltString,
3696
- infoString: keyMaterial.infoString,
3697
- bindVerifyKeysIntoDerivation: keyMaterial.bindVerifyKeysIntoDerivation
3698
- })).then(() => createActionFrameCrypto({
3699
- link: security.link,
3700
- linkedClientId
3701
- }));
3702
- this._cryptoByConn.set(connection, cryptoReady);
3703
- const gate = cryptoReady.then(() => {}, (err) => console.error("[ws-server] failed to restore encrypted session", err));
3704
- this._inboundChainByConn.set(connection, gate);
3705
- this._outboundChainByConn.set(connection, gate);
3706
- }
3707
- }
3708
- toHandlerRouteItem() {
4293
+ }
4294
+ async function buildRequestEnvelope(state, action) {
4295
+ const wire = action.toJsonObject();
4296
+ if (state.crypto != null) {
4297
+ const ciphertext = await state.crypto.encryptFrame(textEncoder.encode(JSON.stringify(wire)));
3709
4298
  return {
3710
- type: this.handlerType,
3711
- client: this.externalClient,
3712
- transType: "custom",
3713
- transOrd: 0
4299
+ k: "act",
4300
+ t: state.token,
4301
+ c: bytesToBase64(ciphertext)
3714
4302
  };
3715
4303
  }
3716
- /** Forget a connection (call on socket close) so stale entries don't misroute later results. */
3717
- dropConnection(connection) {
3718
- const coord = this._clientByConn.get(connection);
3719
- if (coord != null && this._connByClient.get(coord.stringId) === connection) this._connByClient.delete(coord.stringId);
3720
- this._clientByConn.delete(connection);
3721
- this._connEncoding.delete(connection);
3722
- this._codecByConn.delete(connection);
3723
- this._handshakeByConn.delete(connection);
3724
- this._cryptoByConn.delete(connection);
3725
- this._authedConns.delete(connection);
3726
- this._plainConns.delete(connection);
3727
- this._inboundChainByConn.delete(connection);
3728
- this._outboundChainByConn.delete(connection);
3729
- }
3730
- /** Live connection for a client coordinate, if currently registered. */
3731
- getConnectionForClient(client) {
3732
- return this._connByClient.get(client.stringId);
4304
+ return {
4305
+ k: "act",
4306
+ t: state.token,
4307
+ w: wire
4308
+ };
4309
+ }
4310
+ async function extractReplyWire(state, reply) {
4311
+ if (reply.k !== "act") return void 0;
4312
+ if ("c" in reply) {
4313
+ if (state.crypto == null) return void 0;
4314
+ const plain = await state.crypto.decryptFrame(base64ToBytes(reply.c));
4315
+ return JSON.parse(textDecoder.decode(plain));
4316
+ }
4317
+ return reply.w;
4318
+ }
4319
+ async function runConnectorExchangeHandshake(carrier, secure) {
4320
+ await secure.link.initialize();
4321
+ const handshake = createClientHandshake({
4322
+ link: secure.link,
4323
+ localCoordinate: secure.localCoordinate,
4324
+ dictionaryVersion: secure.dictionaryVersion,
4325
+ securityLevel: secure.securityLevel
4326
+ });
4327
+ const hsid = (0, nanoid.nanoid)();
4328
+ const hello = await handshake.createHello();
4329
+ const welcomeReply = decodeExchangeReply(asString(await carrier.exchange(encodeExchange({
4330
+ k: "hs",
4331
+ hsid,
4332
+ m: encodeHandshakeMessage(hello)
4333
+ }))));
4334
+ if (welcomeReply?.k !== "hs") throw new Error("[exchange-handshake] expected a welcome reply");
4335
+ const welcome = decodeHandshakeMessage(welcomeReply.m);
4336
+ if (welcome == null) throw new Error("[exchange-handshake] malformed welcome");
4337
+ if (welcome.t === "reject") throw new Error(`[exchange-handshake] rejected by peer: ${welcome.reason}`);
4338
+ if (welcome.t !== "welcome") throw new Error(`[exchange-handshake] expected welcome, got ${welcome.t}`);
4339
+ const prove = await handshake.onWelcome(welcome);
4340
+ const acceptReply = decodeExchangeReply(asString(await carrier.exchange(encodeExchange({
4341
+ k: "hs",
4342
+ hsid,
4343
+ m: encodeHandshakeMessage(prove)
4344
+ }))));
4345
+ if (acceptReply?.k !== "hs") throw new Error("[exchange-handshake] expected an accept reply");
4346
+ const accept = decodeHandshakeMessage(acceptReply.m);
4347
+ if (accept == null) throw new Error("[exchange-handshake] malformed accept");
4348
+ if (accept.t === "reject") throw new Error(`[exchange-handshake] rejected by peer: ${accept.reason}`);
4349
+ if (accept.t !== "accept") throw new Error(`[exchange-handshake] expected accept, got ${accept.t}`);
4350
+ if (acceptReply.t == null) throw new Error("[exchange-handshake] accept missing session token");
4351
+ const result = await handshake.onAccept(accept);
4352
+ const crypto = result.securityLevel === "encrypted" ? createActionFrameCrypto({
4353
+ link: secure.link,
4354
+ linkedClientId: result.linkedClientId
4355
+ }) : void 0;
4356
+ return {
4357
+ token: acceptReply.t,
4358
+ crypto
4359
+ };
4360
+ }
4361
+ function asString(frame) {
4362
+ if (typeof frame === "string") return frame;
4363
+ return textDecoder.decode(frame instanceof ArrayBuffer ? new Uint8Array(frame) : frame);
4364
+ }
4365
+ //#endregion
4366
+ //#region src/ActionRuntime/Transport/helpers/addTransportStatusMetadata.ts
4367
+ function addTransportStatusMetadata(transportStatus) {
4368
+ if (transportStatus.status === "ready") return {
4369
+ status: "ready",
4370
+ readyData: transportStatus.readyData
4371
+ };
4372
+ if (transportStatus.status === "initializing") return {
4373
+ status: "initializing",
4374
+ initializationPromise: transportStatus.initializationPromise,
4375
+ timeStarted: Date.now()
4376
+ };
4377
+ if (transportStatus.status === "failed") return {
4378
+ status: "failed",
4379
+ error: transportStatus.error,
4380
+ timeFailed: Date.now()
4381
+ };
4382
+ if (transportStatus.status === "unsupported") return { status: "unsupported" };
4383
+ return { status: "uninitialized" };
4384
+ }
4385
+ //#endregion
4386
+ //#region src/ActionRuntime/Transport/TransportConnection.ts
4387
+ let transportOrd = 0;
4388
+ /**
4389
+ * Live, per-handler transport runtime built from a reusable {@link Transport} definition. Holds the
4390
+ * connection-scoped state (ordinal, initialized config, sockets / abort sets) that must not be shared
4391
+ * across handlers. Construct these via `definition._createConnection(...)`, never directly.
4392
+ */
4393
+ var TransportConnection = class {
4394
+ def;
4395
+ transOrd = transportOrd++;
4396
+ type;
4397
+ initialized;
4398
+ /** Backref to the public definition that created this connection (used for devtools route info). */
4399
+ definition;
4400
+ constructor(def) {
4401
+ this.def = def;
4402
+ this.type = def.type;
4403
+ this.initialized = def.initialize();
3733
4404
  }
3734
4405
  /**
3735
- * Send (and optionally await) a server-initiated action to a specific connected client. Pass the
3736
- * connection token directly (e.g. the `ws`) or a client `RuntimeCoordinate` to look one up.
4406
+ * Devtools route info for an action routed through this live connection. Defaults to the stateless
4407
+ * {@link definition}'s info; connections override to enrich it from live state (e.g. the actual
4408
+ * resolved socket URL) when the definition couldn't resolve it on its own.
3737
4409
  */
3738
- pushToClient(runtime, target, request, options) {
3739
- const connection = this._resolveConnection(target);
3740
- return this._dispatch(runtime, connection, request, options?.timeout);
4410
+ getRouteInfo(input) {
4411
+ return this.definition?.getRouteInfo(input);
3741
4412
  }
3742
4413
  /**
3743
- * Build a local handler whose cases are connection-aware: each case receives the primed request and
3744
- * the originating client's live connection (resolved from `originClient`), so handlers don't repeat
3745
- * the `getConnectionForClient(action.context.originClient)` lookup. Cases may return raw output or
3746
- * nothing, just like {@link ActionLocalHandler.forDomainActionCases}. Add the returned handler to the
3747
- * runtime alongside this server handler:
3748
- * ```ts
3749
- * runtime.addHandlers([serverHandler.forConnectionDomainCases(domain, { … }), serverHandler]);
3750
- * ```
4414
+ * Whether a `ready`-status transport still needs asynchronous bring-up before its methods exist
4415
+ * awaiting the carrier to open and/or running a handshake. Default `false`: a stateless transport
4416
+ * (HTTP) is usable the instant `getTransport` reports `ready`, so it stays a terminal *synchronous*
4417
+ * fallback in {@link ConnectionTransportManager}. Stream carriers (Link/WS) override to `true`.
3751
4418
  */
3752
- forConnectionDomainCases(domain, cases) {
3753
- const handler = new ActionLocalHandler();
3754
- const executor = cases;
3755
- const wrapped = {};
3756
- const wrappedRecord = wrapped;
3757
- for (const id in cases) {
3758
- const caseFn = executor[id];
3759
- if (caseFn == null) continue;
3760
- wrappedRecord[id] = (request) => caseFn(request, this.getConnectionForClient(request.context.originClient));
3761
- }
3762
- return handler.forDomainActionCases(domain, wrapped);
4419
+ _needsAsyncBringUp(_readyData) {
4420
+ return false;
4421
+ }
4422
+ /** Await the carrier becoming ready to send (e.g. a socket `open`). Default: nothing to await. */
4423
+ _awaitCarrierReady(_readyData) {
4424
+ return Promise.resolve();
3763
4425
  }
3764
4426
  /**
3765
- * Fan a server-initiated request out to every currently-bound connection. A fresh request is built
3766
- * per connection (each push mutates its own action context) and dispatched fire-and-forget. Pass
3767
- * `except` to skip the originating socket and `where` to filter by connection (e.g. read its
3768
- * attachment for a role). Iterating bound connections (rather than every accepted socket) skips
3769
- * sockets that are still mid-handshake and so can't yet receive a frame.
4427
+ * Finalize during async bring-up may run a handshake, so it can be async. Defaults to the
4428
+ * synchronous {@link _finalizeTransportMethods}; secure stream carriers override to branch plain/secure.
3770
4429
  */
3771
- broadcast(makeRequest, options) {
3772
- const runtime = options?.runtime ?? this._runtime;
3773
- if (runtime == null) throw err_nice_transport.fromId("not_found", { actionId: "server-handler-runtime (construct with `runtime` or pass `options.runtime`)" });
3774
- for (const connection of this._clientByConn.keys()) {
3775
- if (options?.except != null && connection === options.except) continue;
3776
- if (options?.where != null && !options.where(connection)) continue;
3777
- try {
3778
- this.pushToClient(runtime, connection, makeRequest(), { timeout: options?.timeout });
3779
- } catch (error) {
3780
- if (options?.onError != null) options.onError(error, connection);
3781
- else console.error("[ws-server] broadcast push failed", error);
3782
- }
3783
- }
3784
- }
3785
- async sendReturnPayload(payload, config) {
3786
- const connection = this._connByClient.get(payload.context.originClient.stringId);
3787
- if (connection == null) return false;
3788
- this._sendPayload(connection, payload, config.targetLocalRuntime.coordinate);
3789
- return true;
3790
- }
3791
- async handleActionRequest(action, config) {
3792
- const runtime = config?.targetLocalRuntime ?? ActionRuntime.getDefault();
3793
- const connection = this._resolveSingleConnection();
3794
- return this._dispatch(runtime, connection, action, config?.timeout);
4430
+ _finalizeReady(readyData) {
4431
+ return this._finalizeTransportMethods(readyData);
3795
4432
  }
3796
- _dispatch(runtime, connection, action, timeout) {
3797
- const timeoutMs = timeout ?? this._serverTimeout;
3798
- action.context._setOriginClient(runtime.coordinate);
3799
- action.context.addRouteItem({
3800
- runtime: runtime.coordinate,
3801
- handler: this.toHandlerRouteItem(),
3802
- time: Date.now()
3803
- });
3804
- const runningAction = new RunningAction({
3805
- context: action.context,
3806
- request: action,
3807
- parentCuid: peekHandlerCuid(),
3808
- callSite: action._callSite
3809
- });
3810
- runtime.registerRunningAction(runningAction);
3811
- const timeoutId = setTimeout(() => {
3812
- runningAction._abort(err_nice_transport.fromId("timeout", { timeout: timeoutMs }));
3813
- }, timeoutMs);
3814
- runningAction.addUpdateListeners([(update) => {
3815
- if (update.type === "finished") clearTimeout(timeoutId);
3816
- }]);
3817
- try {
3818
- this._sendPayload(connection, action, runtime.coordinate);
3819
- } catch (err) {
3820
- runningAction._abort(err);
3821
- }
3822
- return runningAction;
4433
+ _getCacheKey(input) {
4434
+ const parts = this.initialized.getTransportCacheKey?.(input);
4435
+ if (parts == null) return null;
4436
+ return parts.join("\0");
3823
4437
  }
3824
- _sendPayload(connection, payload, localClient) {
3825
- const frame = (this._connEncoding.get(connection) ?? "binary") === "json" ? JSON.stringify(payload.toJsonObject()) : this._codecFor(connection).outgoing({
3826
- action: payload,
3827
- localClient,
3828
- externalClient: this.externalClient
3829
- });
3830
- const cryptoReady = this._cryptoByConn.get(connection);
3831
- if (cryptoReady == null) {
3832
- this._send(connection, frame);
3833
- return;
3834
- }
3835
- const bytes = toFrameBytes(frame);
3836
- const next = (this._outboundChainByConn.get(connection) ?? Promise.resolve()).then(() => cryptoReady).then((crypto) => crypto.encryptFrame(bytes)).then((encrypted) => this._send(connection, encrypted)).catch((err) => console.error("[ws-server] failed to encrypt/send frame", err));
3837
- this._outboundChainByConn.set(connection, next);
4438
+ getCacheKey(input) {
4439
+ const inner = this._getCacheKey(input);
4440
+ if (inner == null) return null;
4441
+ return `${this.transOrd}:${inner}`;
3838
4442
  }
3839
- _bindConnection(connection, client) {
3840
- this._connByClient.set(client.stringId, connection);
3841
- this._clientByConn.set(connection, client);
4443
+ getTransport(input) {
4444
+ return this._processTransportStatus(input);
3842
4445
  }
3843
- _resolveConnection(target) {
3844
- if (target instanceof RuntimeCoordinate) {
3845
- const connection = this._connByClient.get(target.stringId);
3846
- if (connection == null) throw err_nice_transport.fromId("not_found", { actionId: target.stringId });
3847
- return connection;
4446
+ _processTransportStatus(input) {
4447
+ const statusInfo = addTransportStatusMetadata(this.initialized.getTransport(input));
4448
+ if (statusInfo.status === "ready") {
4449
+ if (!this._needsAsyncBringUp(statusInfo.readyData)) return {
4450
+ status: "ready",
4451
+ readyData: this._finalizeTransportMethods(statusInfo.readyData)
4452
+ };
4453
+ return {
4454
+ status: "initializing",
4455
+ timeStarted: Date.now(),
4456
+ initializationPromise: this._bringUp(statusInfo.readyData)
4457
+ };
3848
4458
  }
3849
- return target;
4459
+ if (statusInfo.status === "initializing") {
4460
+ const initializationPromise = statusInfo.initializationPromise.then((result) => result.status === "ready" ? this._bringUp(result.readyData) : result);
4461
+ return {
4462
+ status: "initializing",
4463
+ timeStarted: statusInfo.timeStarted,
4464
+ initializationPromise
4465
+ };
4466
+ }
4467
+ return statusInfo;
3850
4468
  }
3851
- _resolveSingleConnection() {
3852
- if (this._clientByConn.size !== 1) throw err_nice_transport.fromId("not_found", { actionId: "server-handler-target (use pushToClient with an explicit connection or client coordinate)" });
3853
- return this._clientByConn.keys().next().value;
4469
+ /** Await carrier readiness, then finalize (possibly running a handshake) into the live methods. */
4470
+ async _bringUp(readyData) {
4471
+ await this._awaitCarrierReady(readyData);
4472
+ return {
4473
+ status: "ready",
4474
+ readyData: await this._finalizeReady(readyData)
4475
+ };
3854
4476
  }
3855
4477
  };
3856
- const createServerHandler = (options) => {
3857
- return new ActionServerHandler(options);
4478
+ //#endregion
4479
+ //#region src/ActionRuntime/Transport/Exchange/ExchangeConnection.ts
4480
+ /**
4481
+ * Carrier-agnostic live connection for the exchange (request → single reply) shape — the HTTP
4482
+ * counterpart to {@link LinkConnection}. It owns only the bring-up (run the secure handshake on first
4483
+ * use); the request/reply lifecycle + crypto live in the shared `establishExchangeSession`.
4484
+ */
4485
+ var ExchangeConnection = class extends TransportConnection {
4486
+ constructor(def) {
4487
+ super({
4488
+ ...def,
4489
+ type: "exchange"
4490
+ });
4491
+ }
4492
+ _getCacheKey(input) {
4493
+ return this.initialized.getTransportCacheKey?.(input).join("\0") ?? "";
4494
+ }
4495
+ _needsAsyncBringUp(data) {
4496
+ return data.secureChannel != null && data.secureChannel.securityLevel !== "none";
4497
+ }
4498
+ _finalizeReady(data) {
4499
+ const secure = data.secureChannel;
4500
+ if (secure != null && secure.securityLevel !== "none") return finalizeSecureExchangeMethods({
4501
+ ...this._sessionContext(data),
4502
+ secure
4503
+ });
4504
+ return this._finalizeTransportMethods(data);
4505
+ }
4506
+ _finalizeTransportMethods(data) {
4507
+ return finalizePlainExchangeMethods(this._sessionContext(data));
4508
+ }
4509
+ _sessionContext(data) {
4510
+ return {
4511
+ carrier: data.carrier,
4512
+ updateRunConfig: data.updateRunConfig,
4513
+ secure: data.secureChannel
4514
+ };
4515
+ }
3858
4516
  };
3859
4517
  //#endregion
3860
- //#region src/ActionRuntime/Handler/Server/createActionFetchHandler.ts
3861
- /** Permissive defaults — fine for a public action endpoint; override (or disable) via `cors`. */
3862
- const DEFAULT_CORS_HEADERS = {
3863
- "Access-Control-Allow-Origin": "*",
3864
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
3865
- "Access-Control-Allow-Headers": "Content-Type",
3866
- "Access-Control-Max-Age": "86400"
4518
+ //#region src/ActionRuntime/Transport/Exchange/ExchangeTransport.ts
4519
+ /**
4520
+ * A carrier-agnostic exchange (request → single reply) transport: it drives nice-action's secure session
4521
+ * over any {@link IExchangeCarrier} (HTTP being the one built-in). The duplex counterpart is
4522
+ * {@link LinkTransport}; this is the no-push half — its reply rides the response to its own request, so it
4523
+ * can't deliver an unsolicited frame (the runtime never picks it for the return path).
4524
+ */
4525
+ var ExchangeTransport = class ExchangeTransport extends Transport {
4526
+ options;
4527
+ type = "exchange";
4528
+ constructor(options) {
4529
+ super();
4530
+ this.options = options;
4531
+ }
4532
+ static create(options) {
4533
+ return new ExchangeTransport(options);
4534
+ }
4535
+ _createConnection(_ctx) {
4536
+ const options = this.options;
4537
+ return new ExchangeConnection({ initialize: () => ({
4538
+ getTransportCacheKey: options.getTransportCacheKey,
4539
+ getTransport: (input) => ({
4540
+ status: "ready",
4541
+ readyData: {
4542
+ carrier: options.openCarrier(input),
4543
+ secureChannel: options.security,
4544
+ updateRunConfig: options.updateRunConfig
4545
+ }
4546
+ })
4547
+ }) });
4548
+ }
4549
+ getRouteInfo(input) {
4550
+ if (this.options.getRouteInfo != null) return this.options.getRouteInfo(input);
4551
+ return {
4552
+ carrierLabel: this.options.label ?? "exchange",
4553
+ summary: this.options.label ?? "exchange"
4554
+ };
4555
+ }
3867
4556
  };
4557
+ //#endregion
4558
+ //#region src/ActionRuntime/Transport/helpers/createUnsetTransportResolvers.ts
4559
+ const createUnsetTransportResolvers = (transportLabel) => ({ onIncomingActionDataJson: (json) => {
4560
+ console.warn(`Received incoming action JSON [${json.domain}:${json.id}] on Transport [${transportLabel}] but no incoming data listener has been set.`);
4561
+ } });
4562
+ //#endregion
4563
+ //#region src/ActionRuntime/Transport/SecureSession/establishLinkSession.ts
4564
+ const HANDSHAKE_TIMEOUT_MS = 15e3;
4565
+ /** Plain path (no handshake): route every inbound frame to the runtime; send without crypto. */
4566
+ function finalizePlainLinkMethods(ctx) {
4567
+ const disconnectListeners = [];
4568
+ const abortSet = /* @__PURE__ */ new Set();
4569
+ const pipe = makePipe(ctx, void 0);
4570
+ ctx.channel.attach({
4571
+ onMessage: (frame) => void handleIncomingActionFrame(ctx, pipe, frame),
4572
+ onClose: () => onChannelClosed(ctx, disconnectListeners, abortSet),
4573
+ onError: () => onChannelClosed(ctx, disconnectListeners, abortSet)
4574
+ });
4575
+ return buildSendMethods(ctx, pipe, disconnectListeners, abortSet);
4576
+ }
3868
4577
  /**
3869
- * Build the `fetch` handler a server/Durable-Object exposes for action traffic, folding in the
3870
- * boilerplate every endpoint repeats: CORS (incl. the `OPTIONS` preflight), routing the `/action`
3871
- * `POST` body through the runtime (`handleActionPayloadWire` `waitForResultPayload`
3872
- * `toHttpResponse`), an optional WebSocket-upgrade hook, and a `404` fallback.
3873
- *
3874
- * It only touches web-standard `Request`/`Response`, so it stays transport-agnostic — the one
3875
- * environment-specific bit (the WS upgrade) is injected via {@link IActionFetchHandlerOptions.onWebSocketUpgrade}:
3876
- * ```ts
3877
- * this.fetchHandler = createActionFetchHandler(this.runtime, {
3878
- * onWebSocketUpgrade: () => {
3879
- * const pair = new WebSocketPair();
3880
- * this.ctx.acceptWebSocket(pair[1]);
3881
- * return new Response(null, { status: 101, webSocket: pair[0] });
3882
- * },
3883
- * });
3884
- * // async fetch(request) { return this.fetchHandler(request); }
3885
- * ```
4578
+ * Secure path: a single message handler feeds the handshake until it completes, then routes action
4579
+ * frames (decrypting at the `encrypted` level). Frames that race ahead of activation are buffered and
4580
+ * flushed once the handshake lands, so nothing is lost.
3886
4581
  */
3887
- function createActionFetchHandler(runtime, options = {}) {
3888
- const corsHeaders = options.cors === false ? {} : options.cors ?? DEFAULT_CORS_HEADERS;
3889
- const isActionPath = options.isActionPath ?? ((url) => url.pathname.endsWith("/action"));
3890
- const isWebSocketPath = options.isWebSocketPath ?? ((url) => url.pathname.endsWith("/ws"));
3891
- const withCors = (response) => {
3892
- if (options.cors === false) return response;
3893
- const headers = new Headers(response.headers);
3894
- for (const [key, value] of Object.entries(corsHeaders)) headers.set(key, value);
3895
- return new Response(response.body, {
3896
- status: response.status,
3897
- headers
4582
+ async function finalizeSecureLinkMethods(ctx) {
4583
+ const disconnectListeners = [];
4584
+ const abortSet = /* @__PURE__ */ new Set();
4585
+ const session = {};
4586
+ let active = false;
4587
+ const handshakeQueue = [];
4588
+ const handshakeWaiters = [];
4589
+ const pendingActionFrames = [];
4590
+ ctx.channel.attach({
4591
+ onMessage: (frame) => {
4592
+ if (active && session.pipe != null) {
4593
+ handleIncomingActionFrame(ctx, session.pipe, frame);
4594
+ return;
4595
+ }
4596
+ if (typeof frame === "string") {
4597
+ const message = decodeHandshakeMessage(frame);
4598
+ if (message != null) {
4599
+ const waiter = handshakeWaiters.shift();
4600
+ if (waiter != null) waiter(message);
4601
+ else handshakeQueue.push(message);
4602
+ return;
4603
+ }
4604
+ }
4605
+ pendingActionFrames.push(frame);
4606
+ },
4607
+ onClose: () => onChannelClosed(ctx, disconnectListeners, abortSet),
4608
+ onError: () => onChannelClosed(ctx, disconnectListeners, abortSet)
4609
+ });
4610
+ const nextHandshakeMessage = () => {
4611
+ const queued = handshakeQueue.shift();
4612
+ if (queued != null) return Promise.resolve(queued);
4613
+ return new Promise((resolve, reject) => {
4614
+ const timeout = setTimeout(() => reject(/* @__PURE__ */ new Error("[link-handshake] timed out waiting for peer reply")), HANDSHAKE_TIMEOUT_MS);
4615
+ handshakeWaiters.push((message) => {
4616
+ clearTimeout(timeout);
4617
+ resolve(message);
4618
+ });
3898
4619
  });
3899
4620
  };
3900
- return async (request) => {
3901
- if (request.method === "OPTIONS") return withCors(new Response(null, { status: 204 }));
3902
- const url = new URL(request.url);
3903
- if (options.onWebSocketUpgrade != null && request.headers.get("Upgrade") === "websocket" && isWebSocketPath(url)) return options.onWebSocketUpgrade(request, url);
3904
- if (request.method === "POST" && isActionPath(url)) return withCors((await (await runtime.handleActionPayloadWire(await request.json())).waitForResultPayload()).toHttpResponse({ useErrorStatus: options.useErrorStatus }));
3905
- return withCors(new Response("Not found", { status: 404 }));
4621
+ const pipe = makePipe(ctx, await runClientHandshake(ctx.channel, ctx.secure, nextHandshakeMessage));
4622
+ session.pipe = pipe;
4623
+ active = true;
4624
+ for (const frame of pendingActionFrames) await handleIncomingActionFrame(ctx, pipe, frame);
4625
+ pendingActionFrames.length = 0;
4626
+ return buildSendMethods(ctx, pipe, disconnectListeners, abortSet);
4627
+ }
4628
+ function makePipe(ctx, crypto) {
4629
+ return createFrameCryptoPipe({
4630
+ write: (frame) => ctx.channel.send(frame),
4631
+ isOpen: () => ctx.channel.isOpen(),
4632
+ crypto
4633
+ });
4634
+ }
4635
+ async function runClientHandshake(channel, secure, nextHandshakeMessage) {
4636
+ await secure.link.initialize();
4637
+ const handshake = createClientHandshake({
4638
+ link: secure.link,
4639
+ localCoordinate: secure.localCoordinate,
4640
+ dictionaryVersion: secure.dictionaryVersion,
4641
+ securityLevel: secure.securityLevel
4642
+ });
4643
+ channel.send(encodeHandshakeMessage(await handshake.createHello()));
4644
+ const welcome = await nextHandshakeMessage();
4645
+ if (welcome.t === "reject") throw new Error(`[link-handshake] rejected by peer: ${welcome.reason}`);
4646
+ if (welcome.t !== "welcome") throw new Error(`[link-handshake] expected welcome, got ${welcome.t}`);
4647
+ channel.send(encodeHandshakeMessage(await handshake.onWelcome(welcome)));
4648
+ const accept = await nextHandshakeMessage();
4649
+ if (accept.t === "reject") throw new Error(`[link-handshake] rejected by peer: ${accept.reason}`);
4650
+ if (accept.t !== "accept") throw new Error(`[link-handshake] expected accept, got ${accept.t}`);
4651
+ const result = await handshake.onAccept(accept);
4652
+ return result.securityLevel === "encrypted" ? createActionFrameCrypto({
4653
+ link: secure.link,
4654
+ linkedClientId: result.linkedClientId
4655
+ }) : void 0;
4656
+ }
4657
+ function buildSendMethods(ctx, pipe, disconnectListeners, abortSet) {
4658
+ const channel = ctx.channel;
4659
+ const sendActionData = (inputs) => {
4660
+ const { action, runningAction, timeout } = inputs;
4661
+ if (!channel.isOpen()) {
4662
+ if (action.type === "request") runningAction._abort(ctx.makeDisconnectError(action.id));
4663
+ return;
4664
+ }
4665
+ if (action.type === "request") {
4666
+ abortSet.add(runningAction);
4667
+ const timeoutId = setTimeout(() => {
4668
+ runningAction._abort(err_nice_transport.fromId("timeout", { timeout }));
4669
+ }, timeout);
4670
+ runningAction.addUpdateListeners([(update) => {
4671
+ if (update.type === "finished") {
4672
+ clearTimeout(timeoutId);
4673
+ abortSet.delete(runningAction);
4674
+ }
4675
+ }]);
4676
+ }
4677
+ pipe.send(ctx.formatMessage?.outgoing(inputs) ?? JSON.stringify(inputs.action.toJsonObject()));
4678
+ };
4679
+ return {
4680
+ sendActionData,
4681
+ updateRunConfig: ctx.updateRunConfig,
4682
+ addOnDisconnectListener: (cb) => {
4683
+ disconnectListeners.push(cb);
4684
+ },
4685
+ disconnect: () => {
4686
+ try {
4687
+ channel.close();
4688
+ } catch {}
4689
+ },
4690
+ sendReturnData: (payload, clients) => {
4691
+ const formatted = clients != null ? ctx.formatMessage?.outgoing({
4692
+ action: payload,
4693
+ ...clients
4694
+ }) : void 0;
4695
+ pipe.send(formatted ?? JSON.stringify(payload.toJsonObject()));
4696
+ }
3906
4697
  };
3907
4698
  }
4699
+ async function handleIncomingActionFrame(ctx, pipe, frame) {
4700
+ const decoded = await pipe.decryptIncoming(frame);
4701
+ if (decoded === void 0) return;
4702
+ const rawJson = decodeActionFrame(decoded, ctx.formatMessage);
4703
+ if (rawJson != null) ctx.resolvers.onIncomingActionDataJson(rawJson);
4704
+ }
4705
+ function onChannelClosed(ctx, disconnectListeners, abortSet) {
4706
+ for (const cb of disconnectListeners) cb();
4707
+ const error = ctx.makeDisconnectError("—");
4708
+ for (const ra of [...abortSet]) ra._abort(error);
4709
+ }
3908
4710
  //#endregion
3909
- //#region src/ActionRuntime/Handler/Server/createSecureActionServer.ts
3910
- /** Default accepted set: negotiate per connection to whatever the client picks. */
3911
- const DEFAULT_SERVER_SECURITY_LEVELS = [
3912
- "none",
3913
- "authenticated",
3914
- "encrypted"
3915
- ];
3916
- /**
3917
- * Build an {@link ActionServerHandler} for the secure binary channel with the boilerplate folded in:
3918
- * it creates the {@link ClientCryptoKeyLink} and the storage-backed TOFU resolver from a single
3919
- * `storageAdapter`, installs the channel's per-connection codec, and assembles the `security` block
3920
- * from the runtime coordinate + channel version (accepting all three levels by default).
3921
- *
3922
- * For a hibernatable transport (e.g. a Durable Object), pair it with
3923
- * {@link createHibernatableWsServerAdapter} to wire persistence + replay.
3924
- */
3925
- function createSecureActionServerHandler(options) {
3926
- const link = new _nice_code_util.ClientCryptoKeyLink({ storageAdapter: options.storageAdapter });
3927
- return new ActionServerHandler({
3928
- clientEnv: options.clientEnv,
3929
- createFormatMessage: options.channel.createCodec,
3930
- send: options.send,
3931
- runtime: options.runtime,
3932
- defaultTimeout: options.defaultTimeout,
3933
- security: {
3934
- securityLevel: options.securityLevel ?? DEFAULT_SERVER_SECURITY_LEVELS,
3935
- link,
3936
- localCoordinate: options.runtime.coordinate.toJsonObject(),
3937
- dictionaryVersion: options.channel.dictionaryVersion,
3938
- verifyKeyResolver: options.verifyKeyResolver ?? createStorageTofuVerifyKeyResolver(options.storageAdapter)
3939
- }
4711
+ //#region src/ActionRuntime/Transport/Link/LinkConnection.ts
4712
+ /** Abort error for a closed link channel (carrier-neutral the carrier itself isn't named). */
4713
+ function linkDisconnectError(actionId) {
4714
+ return err_nice_transport.fromId("send_failed", {
4715
+ actionId,
4716
+ actionState: "request",
4717
+ message: "link channel disconnected"
3940
4718
  });
3941
4719
  }
3942
4720
  /**
3943
- * Wire the hibernation lifecycle for a server handler on a transport whose sockets outlive process
3944
- * eviction (e.g. a Durable Object's hibernatable WebSockets). It owns persistence end to end:
3945
- * registers `setAttachment` as the handler's connection-bound callback and immediately replays every
3946
- * live connection's stored binding via `getAttachment`, so results/pushes still route after a wake.
3947
- *
3948
- * Construct it once when the handler is built, then forward socket events:
3949
- * ```ts
3950
- * const wsServer = createHibernatableWsServerAdapter({ handler, getWebSockets, getAttachment, setAttachment });
3951
- * // webSocketMessage(ws, msg) => wsServer.receive(ws, msg);
3952
- * // webSocketClose/Error(ws) => wsServer.drop(ws);
3953
- * ```
4721
+ * Carrier-agnostic live connection. It owns only the *bring-up* (open the carrier, then run the secure
4722
+ * session); the session itself handshake, frame crypto, codec, send/receive lives in the shared
4723
+ * {@link finalizeSecureLinkMethods}/{@link finalizePlainLinkMethods}, so a WebSocket, a WebRTC data
4724
+ * channel, a Bluetooth characteristic, and an in-memory pipe all run the identical secure layer.
3954
4725
  */
3955
- function createHibernatableWsServerAdapter(options) {
3956
- const { handler, getWebSockets, getAttachment, setAttachment } = options;
3957
- handler.setOnConnectionBound(setAttachment);
3958
- for (const connection of getWebSockets()) {
3959
- const binding = getAttachment(connection);
3960
- if (binding != null) handler.rehydrateConnection(connection, binding);
4726
+ var LinkConnection = class extends TransportConnection {
4727
+ resolvers;
4728
+ constructor(def, resolvers) {
4729
+ super({
4730
+ ...def,
4731
+ type: "duplex"
4732
+ });
4733
+ this.resolvers = resolvers ?? createUnsetTransportResolvers("link");
3961
4734
  }
3962
- return {
3963
- receive: (connection, frame) => handler.receive(connection, frame),
3964
- drop: (connection) => handler.dropConnection(connection)
4735
+ _getCacheKey(input) {
4736
+ return this.initialized.getTransportCacheKey?.(input).join("\0") ?? "";
4737
+ }
4738
+ _needsAsyncBringUp() {
4739
+ return true;
4740
+ }
4741
+ _awaitCarrierReady(data) {
4742
+ return data.channel.ready;
4743
+ }
4744
+ _finalizeReady(data) {
4745
+ const secure = data.secureChannel;
4746
+ if (secure != null && secure.securityLevel !== "none") return finalizeSecureLinkMethods({
4747
+ ...this._sessionContext(data),
4748
+ secure
4749
+ });
4750
+ return this._finalizeTransportMethods(data);
4751
+ }
4752
+ _sessionContext(data) {
4753
+ return {
4754
+ channel: data.channel,
4755
+ resolvers: this.resolvers,
4756
+ formatMessage: data.formatMessage,
4757
+ updateRunConfig: data.updateRunConfig,
4758
+ makeDisconnectError: linkDisconnectError
4759
+ };
4760
+ }
4761
+ _finalizeTransportMethods(data) {
4762
+ return finalizePlainLinkMethods(this._sessionContext(data));
4763
+ }
4764
+ };
4765
+ //#endregion
4766
+ //#region src/ActionRuntime/Transport/Link/LinkTransport.ts
4767
+ /**
4768
+ * A carrier-agnostic transport: it drives nice-action's secure session + action routing over any
4769
+ * {@link IDuplexCarrier}. The WebSocket transport is the special case that opens a `WebSocket`;
4770
+ * this opens whatever `openChannel` returns, so the identical secure layer works over WebRTC, Bluetooth,
4771
+ * or an in-memory pipe. Reported with an overridable carrier label in the devtools (defaults to "link").
4772
+ */
4773
+ var LinkTransport = class LinkTransport extends Transport {
4774
+ options;
4775
+ type = "duplex";
4776
+ constructor(options) {
4777
+ super();
4778
+ this.options = options;
4779
+ }
4780
+ static create(options) {
4781
+ return new LinkTransport(options);
4782
+ }
4783
+ _createConnection(ctx) {
4784
+ const options = this.options;
4785
+ return new LinkConnection({ initialize: () => ({
4786
+ getTransportCacheKey: options.getTransportCacheKey,
4787
+ getTransport: (input) => ({
4788
+ status: "ready",
4789
+ readyData: {
4790
+ channel: options.openChannel(input),
4791
+ formatMessage: options.createFormatMessage?.() ?? options.formatMessage,
4792
+ updateRunConfig: options.updateRunConfig,
4793
+ secureChannel: options.security
4794
+ }
4795
+ })
4796
+ }) }, ctx.resolvers);
4797
+ }
4798
+ getRouteInfo(input) {
4799
+ if (this.options.getRouteInfo != null) return this.options.getRouteInfo(input);
4800
+ return {
4801
+ carrierLabel: this.options.label ?? "link",
4802
+ summary: this.options.label ?? "link"
4803
+ };
4804
+ }
4805
+ };
4806
+ //#endregion
4807
+ //#region src/ActionRuntime/Transport/Carrier/Carrier.types.ts
4808
+ /**
4809
+ * Narrow a carrier source to the exchange shape via its `shape` discriminant — the one branch the
4810
+ * transport factories ({@link secureTransport}, {@link plainTransport}) use to pick the duplex vs
4811
+ * exchange transport. A duplex source carries no `shape`, so the `else` branch is the duplex one.
4812
+ */
4813
+ function isExchangeCarrierSource(carrier) {
4814
+ return "shape" in carrier && carrier.shape === "exchange";
4815
+ }
4816
+ //#endregion
4817
+ //#region src/ActionRuntime/Transport/plainTransport.ts
4818
+ function plainTransport(options) {
4819
+ const carrier = options.carrier;
4820
+ if (isExchangeCarrierSource(carrier)) return ExchangeTransport.create({
4821
+ openCarrier: carrier.open,
4822
+ getTransportCacheKey: carrier.getCacheKey,
4823
+ getRouteInfo: carrier.getRouteInfo,
4824
+ label: options.label ?? carrier.carrierLabel,
4825
+ updateRunConfig: options.updateRunConfig
4826
+ });
4827
+ return LinkTransport.create({
4828
+ openChannel: carrier.open,
4829
+ formatMessage: options.formatMessage,
4830
+ createFormatMessage: options.createFormatMessage,
4831
+ getTransportCacheKey: carrier.getCacheKey,
4832
+ getRouteInfo: carrier.getRouteInfo,
4833
+ label: options.label ?? carrier.carrierLabel,
4834
+ updateRunConfig: options.updateRunConfig
4835
+ });
4836
+ }
4837
+ //#endregion
4838
+ //#region src/ActionRuntime/Transport/secureTransport.ts
4839
+ function secureTransport(options) {
4840
+ const link = new _nice_code_util.ClientCryptoKeyLink({ storageAdapter: options.storageAdapter });
4841
+ const security = {
4842
+ securityLevel: options.securityLevel,
4843
+ link,
4844
+ localCoordinate: options.runtime.coordinate.toJsonObject(),
4845
+ dictionaryVersion: options.channel.dictionaryVersion
3965
4846
  };
4847
+ const carrier = options.carrier;
4848
+ if (isExchangeCarrierSource(carrier)) return ExchangeTransport.create({
4849
+ openCarrier: carrier.open,
4850
+ getTransportCacheKey: carrier.getCacheKey,
4851
+ getRouteInfo: carrier.getRouteInfo,
4852
+ label: carrier.carrierLabel,
4853
+ security
4854
+ });
4855
+ return LinkTransport.create({
4856
+ openChannel: carrier.open,
4857
+ createFormatMessage: options.channel.createCodec,
4858
+ getTransportCacheKey: carrier.getCacheKey,
4859
+ getRouteInfo: carrier.getRouteInfo,
4860
+ label: carrier.carrierLabel,
4861
+ security
4862
+ });
3966
4863
  }
3967
4864
  //#endregion
4865
+ exports.AcceptorHandler = AcceptorHandler;
3968
4866
  exports.ActionCore = ActionCore;
3969
4867
  exports.ActionDomain = ActionDomain;
3970
- exports.ActionExternalClientHandler = ActionExternalClientHandler;
3971
4868
  exports.ActionLocalHandler = ActionLocalHandler;
3972
4869
  exports.ActionRootDomain = ActionRootDomain;
3973
4870
  exports.ActionRuntime = ActionRuntime;
3974
4871
  exports.ActionSchema = ActionSchema;
3975
- exports.ActionServerHandler = ActionServerHandler;
3976
- exports.CustomTransport = CustomTransport;
4872
+ exports.ConnectionStateStore = ConnectionStateStore;
4873
+ exports.ConnectorHandler = ConnectorHandler;
3977
4874
  exports.EActionPayloadType = EActionPayloadType;
3978
4875
  exports.EActionProgressType = EActionProgressType;
4876
+ exports.EActionResponseMode = EActionResponseMode;
3979
4877
  exports.EErrId_NiceAction = EErrId_NiceAction;
3980
4878
  exports.EErrId_NiceTransport = EErrId_NiceTransport;
3981
4879
  exports.EErrId_NiceTransport_WebSocket = EErrId_NiceTransport_WebSocket;
@@ -3984,41 +4882,58 @@ exports.ERunningActionFinishedType = require_RunningAction_types.ERunningActionF
3984
4882
  exports.ERunningActionState = require_RunningAction_types.ERunningActionState;
3985
4883
  exports.ERunningActionUpdateType = require_RunningAction_types.ERunningActionUpdateType;
3986
4884
  exports.ESecurityLevel = ESecurityLevel;
3987
- exports.ETransportStatus = ETransportStatus;
3988
- exports.ETransportType = ETransportType;
3989
- exports.HttpTransport = HttpTransport;
4885
+ exports.ETransportShape = require_wsAcceptorCarrier.ETransportShape;
4886
+ exports.ETransportStatus = require_wsAcceptorCarrier.ETransportStatus;
4887
+ exports.ExchangeAcceptor = ExchangeAcceptor;
4888
+ exports.ExchangeTransport = ExchangeTransport;
4889
+ exports.LinkTransport = LinkTransport;
4890
+ exports.PeerLinkHandler = PeerLinkHandler;
3990
4891
  exports.RunningAction = RunningAction;
3991
4892
  exports.RuntimeCoordinate = RuntimeCoordinate;
3992
4893
  exports.Transport = Transport;
3993
- exports.WebSocketTransport = WebSocketTransport;
3994
- exports.WsConnectionStateStore = WsConnectionStateStore;
4894
+ exports.acceptChannel = acceptChannel;
4895
+ exports.acceptChannelConnections = acceptChannelConnections;
3995
4896
  exports.actionSchema = actionSchema;
4897
+ exports.connectChannel = connectChannel;
4898
+ exports.createAcceptorHandler = createAcceptorHandler;
3996
4899
  exports.createActionFetchHandler = createActionFetchHandler;
3997
4900
  exports.createActionFrameCrypto = createActionFrameCrypto;
3998
4901
  exports.createActionRootDomain = createActionRootDomain;
3999
- exports.createBinaryWsAdapter = createBinaryWsAdapter;
4000
- exports.createBinaryWsSessionFactory = createBinaryWsSessionFactory;
4902
+ exports.createBinaryWireAdapter = createBinaryWireAdapter;
4903
+ exports.createBinaryWireSessionFactory = createBinaryWireSessionFactory;
4001
4904
  exports.createClientHandshake = createClientHandshake;
4002
- exports.createExternalClientHandler = createExternalClientHandler;
4905
+ exports.createConnectionStateStore = createConnectionStateStore;
4906
+ exports.createConnectorHandler = createConnectorHandler;
4003
4907
  exports.createHibernatableWsServerAdapter = createHibernatableWsServerAdapter;
4908
+ exports.createInMemoryChannelPair = createInMemoryChannelPair;
4004
4909
  exports.createInMemoryTofuVerifyKeyResolver = createInMemoryTofuVerifyKeyResolver;
4005
4910
  exports.createLocalHandler = createLocalHandler;
4006
- exports.createSecureActionServerHandler = createSecureActionServerHandler;
4007
- exports.createSecureWebSocketTransport = createSecureWebSocketTransport;
4008
- exports.createServerHandler = createServerHandler;
4911
+ exports.createSecureAcceptorHandler = createSecureAcceptorHandler;
4009
4912
  exports.createServerHandshake = createServerHandshake;
4010
4913
  exports.createStorageTofuVerifyKeyResolver = createStorageTofuVerifyKeyResolver;
4011
4914
  exports.decodeActionFrame = decodeActionFrame;
4012
4915
  exports.decodeHandshakeMessage = decodeHandshakeMessage;
4013
- exports.defineSecureWsChannel = defineSecureWsChannel;
4916
+ exports.defineChannel = defineChannel;
4917
+ exports.defineSecureChannel = defineSecureChannel;
4014
4918
  exports.encodeHandshakeMessage = encodeHandshakeMessage;
4015
4919
  exports.err_nice_action = err_nice_action;
4016
4920
  exports.err_nice_external_client = err_nice_external_client;
4017
4921
  exports.err_nice_transport = err_nice_transport;
4018
4922
  exports.err_nice_transport_ws = err_nice_transport_ws;
4923
+ exports.httpAcceptorCarrier = httpAcceptorCarrier;
4924
+ exports.httpCarrier = httpCarrier;
4925
+ exports.inMemoryCarrier = inMemoryCarrier;
4019
4926
  exports.isActionPayload_Any_JsonObject = isActionPayload_Any_JsonObject;
4020
4927
  exports.isActionPayload_Request_JsonObject = isActionPayload_Request_JsonObject;
4021
4928
  exports.isActionPayload_Result_JsonObject = isActionPayload_Result_JsonObject;
4929
+ exports.isExchangeAcceptorCarrier = require_wsAcceptorCarrier.isExchangeAcceptorCarrier;
4930
+ exports.plainTransport = plainTransport;
4931
+ exports.rtcCarrier = rtcCarrier;
4932
+ exports.rtcDataChannelByteChannel = rtcDataChannelByteChannel;
4022
4933
  exports.runtimeLinkId = runtimeLinkId;
4934
+ exports.secureTransport = secureTransport;
4935
+ exports.serveChannel = serveChannel;
4936
+ exports.wsAcceptorCarrier = require_wsAcceptorCarrier.wsAcceptorCarrier;
4937
+ exports.wsCarrier = wsCarrier;
4023
4938
 
4024
4939
  //# sourceMappingURL=index.cjs.map