@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.
- package/README.md +582 -582
- package/build/{ActionDevtoolsCore-9PsnscvK.mjs → ActionDevtoolsCore-BcItqP-C.mjs} +7 -7
- package/build/ActionDevtoolsCore-BcItqP-C.mjs.map +1 -0
- package/build/{ActionDevtoolsCore-CCRLYASa.d.cts → ActionDevtoolsCore-C5XrQI1K.d.mts} +3 -3
- package/build/{ActionDevtoolsCore-DtgXwPBZ.cjs → ActionDevtoolsCore-Cb_QR44N.cjs} +7 -7
- package/build/ActionDevtoolsCore-Cb_QR44N.cjs.map +1 -0
- package/build/{ActionDevtoolsCore-CYGD2o6C.d.mts → ActionDevtoolsCore-Dd1qJAwK.d.cts} +3 -3
- package/build/{ActionPayload.types-BN-rXFBK.d.cts → ActionPayload.types-BchJrBIX.d.mts} +1966 -1139
- package/build/{ActionPayload.types-D28ELKXC.d.mts → ActionPayload.types-snDlSIF-.d.cts} +1966 -1139
- package/build/devtools/browser/index.cjs +5 -5
- package/build/devtools/browser/index.cjs.map +1 -1
- package/build/devtools/browser/index.d.cts +1 -1
- package/build/devtools/browser/index.d.mts +1 -1
- package/build/devtools/browser/index.mjs +5 -5
- package/build/devtools/browser/index.mjs.map +1 -1
- package/build/devtools/server/index.cjs +1 -1
- package/build/devtools/server/index.cjs.map +1 -1
- package/build/devtools/server/index.d.cts +1 -1
- package/build/devtools/server/index.d.mts +1 -1
- package/build/devtools/server/index.mjs +1 -1
- package/build/devtools/server/index.mjs.map +1 -1
- package/build/index.cjs +2667 -1752
- package/build/index.cjs.map +1 -1
- package/build/index.d.cts +2 -2
- package/build/index.d.mts +2 -2
- package/build/index.mjs +2635 -1738
- package/build/index.mjs.map +1 -1
- package/build/platform/cloudflare/index.cjs +56 -0
- package/build/platform/cloudflare/index.cjs.map +1 -0
- package/build/platform/cloudflare/index.d.cts +67 -0
- package/build/platform/cloudflare/index.d.mts +67 -0
- package/build/platform/cloudflare/index.mjs +54 -0
- package/build/platform/cloudflare/index.mjs.map +1 -0
- package/build/react-query/index.cjs.map +1 -1
- package/build/react-query/index.d.cts +1 -1
- package/build/react-query/index.d.mts +1 -1
- package/build/react-query/index.mjs.map +1 -1
- package/build/wsAcceptorCarrier-CXGlQU_f.mjs +80 -0
- package/build/wsAcceptorCarrier-CXGlQU_f.mjs.map +1 -0
- package/build/wsAcceptorCarrier-DHRbsY1X.cjs +103 -0
- package/build/wsAcceptorCarrier-DHRbsY1X.cjs.map +1 -0
- package/package.json +15 -4
- package/build/ActionDevtoolsCore-9PsnscvK.mjs.map +0 -1
- 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
|
|
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/
|
|
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
|
-
*
|
|
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
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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
|
-
|
|
1152
|
+
/** Listeners installed by the runtime (`resolveIncomingActionPayload`) for inbound peer frames. */
|
|
1153
|
+
_incomingActionDataListeners = [];
|
|
1154
|
+
constructor(peerCoordinate) {
|
|
1014
1155
|
super();
|
|
1015
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1297
|
+
client: this.peerClient
|
|
1122
1298
|
};
|
|
1123
1299
|
}
|
|
1124
1300
|
toHandlerRouteItem(transport, input) {
|
|
1125
1301
|
return {
|
|
1126
1302
|
type: this.handlerType,
|
|
1127
|
-
client: this.
|
|
1303
|
+
client: this.peerClient,
|
|
1128
1304
|
transOrd: transport.transOrd,
|
|
1129
|
-
|
|
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
|
|
1140
|
-
return new
|
|
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
|
-
|
|
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
|
|
1401
|
+
const targetPeer = options?.targetPeer;
|
|
1226
1402
|
const possibleHandlers = handlers.filter((handler) => {
|
|
1227
|
-
if (handler.handlerType === "
|
|
1228
|
-
if (
|
|
1403
|
+
if (handler.handlerType === "peer") {
|
|
1404
|
+
if (targetPeer && !targetPeer.isSameFor(handler.peerClient).id) return false;
|
|
1229
1405
|
return true;
|
|
1230
1406
|
}
|
|
1231
|
-
if (
|
|
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
|
|
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"
|
|
1241
|
-
if (possibleHandler.handlerType === "
|
|
1242
|
-
const score =
|
|
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?.
|
|
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 === "
|
|
1444
|
+
if (handler.handlerType === "peer") {
|
|
1269
1445
|
handler._setIncomingActionDataListener((json) => this.resolveIncomingActionPayload(json));
|
|
1270
|
-
this.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1327
|
-
|
|
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.
|
|
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/
|
|
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/
|
|
2361
|
+
//#region src/utils/decodeActionFrame.ts
|
|
2631
2362
|
/**
|
|
2632
|
-
*
|
|
2633
|
-
*
|
|
2634
|
-
*
|
|
2635
|
-
*
|
|
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
|
-
*
|
|
2639
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
2671
|
-
|
|
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
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
if (
|
|
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
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
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
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
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
|
-
|
|
2708
|
-
|
|
2709
|
-
progress: payloadData
|
|
2448
|
+
send,
|
|
2449
|
+
decryptIncoming
|
|
2710
2450
|
};
|
|
2711
2451
|
}
|
|
2712
2452
|
//#endregion
|
|
2713
|
-
//#region src/ActionRuntime/
|
|
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
|
-
*
|
|
2738
|
-
*
|
|
2739
|
-
*
|
|
2740
|
-
*
|
|
2741
|
-
*
|
|
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
|
-
*
|
|
2751
|
-
*
|
|
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
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
if (
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
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
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
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
|
-
*
|
|
2836
|
-
*
|
|
2837
|
-
*
|
|
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
|
-
*
|
|
2841
|
-
*
|
|
2842
|
-
*
|
|
2843
|
-
*
|
|
2844
|
-
*
|
|
2845
|
-
*
|
|
2846
|
-
*
|
|
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
|
-
*
|
|
2849
|
-
*
|
|
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
|
-
*
|
|
2852
|
-
*
|
|
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
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
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-
|
|
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/
|
|
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
|
-
*
|
|
2977
|
-
*
|
|
2978
|
-
*
|
|
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
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
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
|
-
|
|
3282
|
+
return `auto:${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
2987
3283
|
}
|
|
2988
|
-
/**
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
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
|
-
|
|
2995
|
-
|
|
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
|
-
|
|
2998
|
-
return `${u.host}${u.pathname}`;
|
|
3321
|
+
return JSON.parse(raw);
|
|
2999
3322
|
} catch {
|
|
3000
|
-
return
|
|
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/
|
|
3005
|
-
const
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
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
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
if (
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
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
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
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
|
-
|
|
3059
|
-
|
|
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
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
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
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
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
|
-
|
|
3207
|
-
|
|
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
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
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/
|
|
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
|
-
*
|
|
3266
|
-
*
|
|
3267
|
-
*
|
|
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
|
-
|
|
3270
|
-
options;
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
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
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
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
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
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/
|
|
3537
|
+
//#region src/ActionRuntime/Handler/PeerLink/Acceptor/Hibernation/createHibernatableWsServerAdapter.ts
|
|
3316
3538
|
/**
|
|
3317
|
-
*
|
|
3318
|
-
*
|
|
3319
|
-
*
|
|
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
|
|
3322
|
-
const {
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
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
|
-
|
|
3339
|
-
|
|
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
|
-
*
|
|
3344
|
-
*
|
|
3345
|
-
*
|
|
3346
|
-
* `
|
|
3347
|
-
*
|
|
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
|
|
3350
|
-
const
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
link,
|
|
3364
|
-
|
|
3365
|
-
|
|
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/
|
|
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
|
|
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
|
|
3375
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
3455
|
-
*
|
|
3456
|
-
*
|
|
3457
|
-
*
|
|
3458
|
-
*
|
|
3459
|
-
*
|
|
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
|
-
*
|
|
3472
|
-
*
|
|
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
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
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
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
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
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
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
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
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
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
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
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
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
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
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
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
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
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
const
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
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
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
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
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
transOrd: 0
|
|
4299
|
+
k: "act",
|
|
4300
|
+
t: state.token,
|
|
4301
|
+
c: bytesToBase64(ciphertext)
|
|
3714
4302
|
};
|
|
3715
4303
|
}
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
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
|
-
*
|
|
3736
|
-
*
|
|
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
|
-
|
|
3739
|
-
|
|
3740
|
-
return this._dispatch(runtime, connection, request, options?.timeout);
|
|
4410
|
+
getRouteInfo(input) {
|
|
4411
|
+
return this.definition?.getRouteInfo(input);
|
|
3741
4412
|
}
|
|
3742
4413
|
/**
|
|
3743
|
-
*
|
|
3744
|
-
* the
|
|
3745
|
-
* the `
|
|
3746
|
-
*
|
|
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
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
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
|
-
*
|
|
3766
|
-
*
|
|
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
|
-
|
|
3772
|
-
|
|
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
|
-
|
|
3797
|
-
const
|
|
3798
|
-
|
|
3799
|
-
|
|
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
|
-
|
|
3825
|
-
const
|
|
3826
|
-
|
|
3827
|
-
|
|
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
|
-
|
|
3840
|
-
this.
|
|
3841
|
-
this._clientByConn.set(connection, client);
|
|
4443
|
+
getTransport(input) {
|
|
4444
|
+
return this._processTransportStatus(input);
|
|
3842
4445
|
}
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
if (
|
|
3847
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
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
|
-
|
|
3857
|
-
|
|
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/
|
|
3861
|
-
/**
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
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
|
-
*
|
|
3870
|
-
*
|
|
3871
|
-
*
|
|
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
|
|
3888
|
-
const
|
|
3889
|
-
const
|
|
3890
|
-
const
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
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
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
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/
|
|
3910
|
-
/**
|
|
3911
|
-
|
|
3912
|
-
"
|
|
3913
|
-
|
|
3914
|
-
|
|
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
|
-
*
|
|
3944
|
-
*
|
|
3945
|
-
*
|
|
3946
|
-
*
|
|
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
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
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
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
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.
|
|
3976
|
-
exports.
|
|
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.
|
|
3988
|
-
exports.
|
|
3989
|
-
exports.
|
|
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.
|
|
3994
|
-
exports.
|
|
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.
|
|
4000
|
-
exports.
|
|
4902
|
+
exports.createBinaryWireAdapter = createBinaryWireAdapter;
|
|
4903
|
+
exports.createBinaryWireSessionFactory = createBinaryWireSessionFactory;
|
|
4001
4904
|
exports.createClientHandshake = createClientHandshake;
|
|
4002
|
-
exports.
|
|
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.
|
|
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.
|
|
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
|