dsh-interconnect 0.10.0 → 0.11.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 +20 -15
- package/assets/dsh-interconnect.md +5 -5
- package/dsh.plugin.json +2 -2
- package/lib/index.js +278 -154
- package/lib/interconnect/index.js +280 -155
- package/lib/interconnect/index.js.map +3 -3
- package/lib/skill-interconnect/index.js +1 -1
- package/lib/skill-interconnect/index.js.map +2 -2
- package/lib/tool-interconnect/index.js +70 -58
- package/lib/tool-interconnect/index.js.map +2 -2
- package/lib/types/interconnect/index.d.ts +64 -26
- package/lib/types/interconnect/index.d.ts.map +1 -1
- package/lib/types/interconnect/types.d.ts +7 -46
- package/lib/types/interconnect/types.d.ts.map +1 -1
- package/lib/types/skill-interconnect/index.d.ts.map +1 -1
- package/lib/types/tool-interconnect/index.d.ts +7 -3
- package/lib/types/tool-interconnect/index.d.ts.map +1 -1
- package/package.json +2 -5
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/interconnect/index.ts
|
|
2
2
|
import { Service } from "@deepseek-ai/cordis";
|
|
3
|
+
import { createHash, randomUUID, timingSafeEqual as constantTimeEqual } from "node:crypto";
|
|
3
4
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
4
5
|
import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
|
-
import { hasApiRemoteSubagentOwner } from "@deepseek-ai/dsh-api-remotes";
|
|
6
6
|
|
|
7
7
|
// ../.dsh/source/staging-20260807T143236Z/vendor/cosmokit/src/misc.ts
|
|
8
8
|
function isNullable(value) {
|
|
@@ -819,64 +819,81 @@ defineMethod("transform", [
|
|
|
819
819
|
|
|
820
820
|
// src/interconnect/index.ts
|
|
821
821
|
import WebSocket, { WebSocketServer } from "ws";
|
|
822
|
-
|
|
823
|
-
|
|
822
|
+
function isSessionOwnedBySubagent(ctx, session, agent) {
|
|
823
|
+
if (session.header.origin === "subagent") return true;
|
|
824
|
+
const parentId = session.header.parentSession;
|
|
825
|
+
if (parentId === void 0 || agent === void 0) return false;
|
|
826
|
+
const parent = ctx.agents.get(parentId);
|
|
827
|
+
return parent !== void 0 && ctx.agents.isOwnedBy(agent.id, parent);
|
|
828
|
+
}
|
|
829
|
+
function assertNever(value) {
|
|
830
|
+
throw new Error(`interconnect: unhandled variant ${JSON.stringify(value)}`);
|
|
831
|
+
}
|
|
832
|
+
var INTERCONNECT_CHANNEL = "/interconnect";
|
|
824
833
|
var INTERCONNECT_TOKEN_REF = "DSH_INTERCONNECT_TOKEN";
|
|
825
|
-
|
|
826
|
-
// src/interconnect/index.ts
|
|
827
834
|
var PLUGIN_SOURCE = "dsh-interconnect";
|
|
835
|
+
var MAX_LINK_FRAME_BYTES = 1024 * 1024;
|
|
836
|
+
var LINK_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
828
837
|
var LINK_CHANNEL = "/interconnect/link";
|
|
829
838
|
var notificationSchema = Schema.union([
|
|
830
|
-
Schema.object({ kind: Schema.const("agent/created"), sessionId: Schema.string() }),
|
|
831
|
-
Schema.object({ kind: Schema.const("agent/disposed"), sessionId: Schema.string() }),
|
|
832
|
-
Schema.object({ kind: Schema.const("agent/status"), sessionId: Schema.string(), status: Schema.union([Schema.const("idle"), Schema.const("running")]) }),
|
|
833
|
-
Schema.object({ kind: Schema.const("session/created"), sessionId: Schema.string(), parentSessionId: Schema.string() }),
|
|
834
|
-
Schema.object({ kind: Schema.const("session/disposed"), sessionId: Schema.string() }),
|
|
835
|
-
Schema.object({ kind: Schema.const("subagent/end"), provider: Schema.string(), childSessionId: Schema.string(), stopReason: Schema.string() })
|
|
839
|
+
Schema.object({ kind: Schema.const("agent/created").required(), sessionId: Schema.string().required() }),
|
|
840
|
+
Schema.object({ kind: Schema.const("agent/disposed").required(), sessionId: Schema.string().required() }),
|
|
841
|
+
Schema.object({ kind: Schema.const("agent/status").required(), sessionId: Schema.string().required(), status: Schema.union([Schema.const("idle"), Schema.const("running")]).required() }),
|
|
842
|
+
Schema.object({ kind: Schema.const("session/created").required(), sessionId: Schema.string().required(), parentSessionId: Schema.string() }),
|
|
843
|
+
Schema.object({ kind: Schema.const("session/disposed").required(), sessionId: Schema.string().required() }),
|
|
844
|
+
Schema.object({ kind: Schema.const("subagent/end").required(), provider: Schema.string().required(), childSessionId: Schema.string().required(), stopReason: Schema.string().required() })
|
|
836
845
|
]);
|
|
837
846
|
var senderSchema = Schema.object({ instanceId: Schema.string(), sessionId: Schema.string() });
|
|
838
|
-
var
|
|
847
|
+
var queryResultSchema = Schema.union([
|
|
848
|
+
Schema.object({ pong: Schema.const(true).required(), instance: Schema.string().required() }),
|
|
839
849
|
Schema.object({
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
850
|
+
instance: Schema.string().required(),
|
|
851
|
+
sessions: Schema.array(Schema.object({
|
|
852
|
+
sessionId: Schema.string().required(),
|
|
853
|
+
title: Schema.string().required(false),
|
|
854
|
+
status: Schema.string().required(false)
|
|
855
|
+
})).required()
|
|
846
856
|
}),
|
|
847
|
-
Schema.object({
|
|
848
|
-
kind: Schema.const("reply"),
|
|
849
|
-
sessionId: Schema.string(),
|
|
850
|
-
text: Schema.string(),
|
|
851
|
-
sender: senderSchema,
|
|
852
|
-
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
|
|
853
|
-
resume: Schema.boolean()
|
|
854
|
-
})
|
|
857
|
+
Schema.object({ accepted: Schema.const(true).required() })
|
|
855
858
|
]);
|
|
859
|
+
var messageSchema = Schema.object({
|
|
860
|
+
kind: Schema.const("send").required(),
|
|
861
|
+
sessionId: Schema.string().required(),
|
|
862
|
+
text: Schema.string().required(),
|
|
863
|
+
sender: senderSchema,
|
|
864
|
+
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
|
|
865
|
+
resume: Schema.boolean()
|
|
866
|
+
});
|
|
856
867
|
var querySchema = Schema.union([
|
|
857
|
-
Schema.object({ kind: Schema.const("ping") }),
|
|
858
|
-
Schema.object({ kind: Schema.const("list") }),
|
|
859
|
-
Schema.object({ kind: Schema.const("event"), notification: notificationSchema })
|
|
868
|
+
Schema.object({ kind: Schema.const("ping").required() }),
|
|
869
|
+
Schema.object({ kind: Schema.const("list").required() }),
|
|
870
|
+
Schema.object({ kind: Schema.const("event").required(), notification: notificationSchema.required() })
|
|
871
|
+
]);
|
|
872
|
+
var msgResultSchema = Schema.union([
|
|
873
|
+
Schema.object({ delivered: Schema.const(true).required(), instance: Schema.string().required(), delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]) }),
|
|
874
|
+
Schema.object({ delivered: Schema.const(false).required(), instance: Schema.string().required(), reason: Schema.union([
|
|
875
|
+
Schema.const("session-not-live"),
|
|
876
|
+
Schema.const("unreachable"),
|
|
877
|
+
Schema.const("resume-refused"),
|
|
878
|
+
Schema.const("resume-failed"),
|
|
879
|
+
Schema.const("session-owned-by-subagent"),
|
|
880
|
+
Schema.const("no-sender-known")
|
|
881
|
+
]).required() })
|
|
860
882
|
]);
|
|
861
883
|
var linkFrameSchema = Schema.union([
|
|
862
|
-
Schema.object({ type: Schema.const("hello"), sender: Schema.string() }),
|
|
863
|
-
Schema.object({ type: Schema.const("event"), notification: notificationSchema }),
|
|
864
|
-
Schema.object({ type: Schema.const("msg"), reqId: Schema.string(), message: messageSchema }),
|
|
865
|
-
Schema.object({ type: Schema.const("msg-result"), reqId: Schema.string(), result:
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
|
|
869
|
-
reason: Schema.string()
|
|
870
|
-
}) }),
|
|
871
|
-
Schema.object({ type: Schema.const("query"), reqId: Schema.string(), query: querySchema }),
|
|
872
|
-
Schema.object({ type: Schema.const("query-result"), reqId: Schema.string(), result: Schema.any() })
|
|
884
|
+
Schema.object({ type: Schema.const("hello").required(), sender: Schema.string().required() }),
|
|
885
|
+
Schema.object({ type: Schema.const("event").required(), notification: notificationSchema.required() }),
|
|
886
|
+
Schema.object({ type: Schema.const("msg").required(), reqId: Schema.string().required(), message: messageSchema.required() }),
|
|
887
|
+
Schema.object({ type: Schema.const("msg-result").required(), reqId: Schema.string().required(), result: msgResultSchema.required() }),
|
|
888
|
+
Schema.object({ type: Schema.const("query").required(), reqId: Schema.string().required(), query: querySchema.required() }),
|
|
889
|
+
Schema.object({ type: Schema.const("query-result").required(), reqId: Schema.string().required(), result: queryResultSchema.required() })
|
|
873
890
|
]);
|
|
874
891
|
var InterconnectService = class extends Service {
|
|
875
|
-
static inject = ["
|
|
892
|
+
static inject = ["agents", "credentials"];
|
|
876
893
|
static Config = Schema.object({
|
|
877
894
|
instanceId: Schema.string().default("dsh"),
|
|
878
895
|
requestTimeoutMs: Schema.natural().max(6e4).default(1e4),
|
|
879
|
-
peers: Schema.
|
|
896
|
+
peers: Schema.dict(Schema.string()).default({}),
|
|
880
897
|
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]).default("followup"),
|
|
881
898
|
allowResume: Schema.boolean().default(true)
|
|
882
899
|
});
|
|
@@ -885,7 +902,7 @@ var InterconnectService = class extends Service {
|
|
|
885
902
|
delivery;
|
|
886
903
|
allowResume;
|
|
887
904
|
subscriptions = [];
|
|
888
|
-
server = new WebSocketServer({ noServer: true });
|
|
905
|
+
server = new WebSocketServer({ noServer: true, maxPayload: MAX_LINK_FRAME_BYTES });
|
|
889
906
|
sockets = /* @__PURE__ */ new Set();
|
|
890
907
|
/** Outbound peer links keyed by the peer's `instanceId`. */
|
|
891
908
|
linkStates = /* @__PURE__ */ new Map();
|
|
@@ -897,12 +914,17 @@ var InterconnectService = class extends Service {
|
|
|
897
914
|
/** In-flight frames sent over a peer link, keyed by `reqId`, awaiting a correlated result. */
|
|
898
915
|
pendingMessages = /* @__PURE__ */ new Map();
|
|
899
916
|
reqIdCounter = 0;
|
|
917
|
+
/** Set when the service fiber unwinds; guards late inbound deliveries against driving disposed agents. */
|
|
918
|
+
disposed = false;
|
|
900
919
|
constructor(ctx, config) {
|
|
901
920
|
super(ctx, "interconnect");
|
|
902
921
|
this.instanceId = config.instanceId;
|
|
903
922
|
this.requestTimeoutMs = config.requestTimeoutMs;
|
|
904
923
|
this.delivery = config.delivery ?? "followup";
|
|
905
924
|
this.allowResume = config.allowResume ?? true;
|
|
925
|
+
for (const origin of Object.values(config.peers ?? {})) {
|
|
926
|
+
linkUrl(trimBase(origin));
|
|
927
|
+
}
|
|
906
928
|
for (const [peerInstanceId, origin] of Object.entries(config.peers ?? {})) {
|
|
907
929
|
this.link(peerInstanceId, origin);
|
|
908
930
|
}
|
|
@@ -912,9 +934,15 @@ var InterconnectService = class extends Service {
|
|
|
912
934
|
void this.handleUpgrade(req, socket, head);
|
|
913
935
|
}
|
|
914
936
|
};
|
|
915
|
-
ctx.
|
|
937
|
+
ctx.inject(["webServer"], (webCtx) => {
|
|
938
|
+
webCtx.effect(() => webCtx.webServer.registerUpgrade(upgrade), "interconnect: /interconnect/link websocket");
|
|
939
|
+
});
|
|
916
940
|
this.heartbeatTimer = setInterval(() => {
|
|
917
941
|
for (const socket of this.sockets) {
|
|
942
|
+
if (socket.readyState !== WebSocket.OPEN) {
|
|
943
|
+
this.sockets.delete(socket);
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
918
946
|
if (socket.isAlive === false) {
|
|
919
947
|
socket.terminate();
|
|
920
948
|
this.sockets.delete(socket);
|
|
@@ -943,6 +971,7 @@ var InterconnectService = class extends Service {
|
|
|
943
971
|
});
|
|
944
972
|
}));
|
|
945
973
|
this.subscriptions.push(ctx.on("session/disposed", (session) => {
|
|
974
|
+
this.senders.delete(String(session.id));
|
|
946
975
|
this.fanout({ kind: "session/disposed", sessionId: String(session.id) });
|
|
947
976
|
}));
|
|
948
977
|
this.subscriptions.push(ctx.on("subagent/end", (info) => {
|
|
@@ -964,16 +993,28 @@ var InterconnectService = class extends Service {
|
|
|
964
993
|
for (const socket of this.sockets) socket.terminate();
|
|
965
994
|
this.sockets.clear();
|
|
966
995
|
this.server.close();
|
|
996
|
+
for (const pending of this.pendingMessages.values()) {
|
|
997
|
+
clearTimeout(pending.timer);
|
|
998
|
+
pending.resolve?.(void 0);
|
|
999
|
+
}
|
|
1000
|
+
this.pendingMessages.clear();
|
|
1001
|
+
this.disposed = true;
|
|
967
1002
|
}, "interconnect: websocket teardown");
|
|
968
1003
|
}
|
|
969
1004
|
/**
|
|
970
1005
|
* This instance's identity to attach to outbound messages. Always present:
|
|
971
1006
|
* it needs no address, only this instance's id and the calling session.
|
|
972
1007
|
* @param sessionId - the local session that is sending, used as the reply target.
|
|
1008
|
+
* @returns this instance's address-free identity for the calling session.
|
|
973
1009
|
*/
|
|
974
1010
|
selfSender(sessionId) {
|
|
975
1011
|
return { instanceId: this.instanceId, sessionId };
|
|
976
1012
|
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Deliver one text message to a live session on a peer instance.
|
|
1015
|
+
* @param request - the peer instance id, target session, text, and optional delivery/resume overrides.
|
|
1016
|
+
* @returns the peer's answer, or an unreachable result when no link answers.
|
|
1017
|
+
*/
|
|
977
1018
|
async send(request) {
|
|
978
1019
|
const payload = {
|
|
979
1020
|
sessionId: request.sessionId,
|
|
@@ -982,14 +1023,20 @@ var InterconnectService = class extends Service {
|
|
|
982
1023
|
...request.delivery === void 0 ? {} : { delivery: request.delivery },
|
|
983
1024
|
...request.resume === void 0 ? {} : { resume: request.resume }
|
|
984
1025
|
};
|
|
985
|
-
|
|
986
|
-
|
|
1026
|
+
try {
|
|
1027
|
+
const result = await this.msgRequest(request.instanceId, "send", payload);
|
|
1028
|
+
return result ?? { delivered: false, instance: request.instanceId, reason: "unreachable" };
|
|
1029
|
+
} catch {
|
|
1030
|
+
return { delivered: false, instance: request.instanceId, reason: "unreachable" };
|
|
1031
|
+
}
|
|
987
1032
|
}
|
|
988
1033
|
/**
|
|
989
1034
|
* Deliver one text message back to the peer that a local session last
|
|
990
1035
|
* received a send from. `sessionId` names the LOCAL replying session; the
|
|
991
1036
|
* outbound target is the `sender` that session recorded, addressed through
|
|
992
1037
|
* this instance's own link to the sender's instance.
|
|
1038
|
+
* @param request - the LOCAL replying session id, the reply text, and optional delivery/resume overrides.
|
|
1039
|
+
* @returns the recalled sender's answer, or an unreachable result when no link answers.
|
|
993
1040
|
*/
|
|
994
1041
|
async reply(request) {
|
|
995
1042
|
const sender = this.senders.get(request.sessionId);
|
|
@@ -1005,13 +1052,19 @@ var InterconnectService = class extends Service {
|
|
|
1005
1052
|
// back — chaining the conversation.
|
|
1006
1053
|
sender: this.selfSender(request.sessionId)
|
|
1007
1054
|
};
|
|
1008
|
-
|
|
1009
|
-
|
|
1055
|
+
try {
|
|
1056
|
+
const result = await this.msgRequest(sender.instanceId, "send", payload);
|
|
1057
|
+
return result ?? { delivered: false, instance: sender.instanceId, reason: "unreachable" };
|
|
1058
|
+
} catch {
|
|
1059
|
+
return { delivered: false, instance: sender.instanceId, reason: "unreachable" };
|
|
1060
|
+
}
|
|
1010
1061
|
}
|
|
1011
1062
|
/**
|
|
1012
1063
|
* Probe a peer instance for liveness and identity over its persistent link.
|
|
1013
1064
|
* Returns the peer identity when reachable, or undefined when the link is
|
|
1014
1065
|
* not up or no answer arrives.
|
|
1066
|
+
* @param instanceId - the peer's `instanceId` as configured under {@link Config.peers}.
|
|
1067
|
+
* @returns the peer's identity, or undefined when the link is not up or no answer arrives.
|
|
1015
1068
|
*/
|
|
1016
1069
|
async ping(instanceId) {
|
|
1017
1070
|
return this.queryRequest(instanceId, { kind: "ping" });
|
|
@@ -1019,15 +1072,12 @@ var InterconnectService = class extends Service {
|
|
|
1019
1072
|
/**
|
|
1020
1073
|
* List a peer instance's live sessions over its persistent link. Undefined on
|
|
1021
1074
|
* transport failure, matching `ping`.
|
|
1075
|
+
* @param instanceId - the peer's `instanceId` as configured under {@link Config.peers}.
|
|
1076
|
+
* @returns the peer's live session rows, or undefined on transport failure.
|
|
1022
1077
|
*/
|
|
1023
1078
|
async list(instanceId) {
|
|
1024
1079
|
return this.queryRequest(instanceId, { kind: "list" });
|
|
1025
1080
|
}
|
|
1026
|
-
/** The origin this instance dials to reach a configured peer, or undefined. */
|
|
1027
|
-
originOf(instanceId) {
|
|
1028
|
-
const state = this.linkStates.get(instanceId);
|
|
1029
|
-
return state?.peer;
|
|
1030
|
-
}
|
|
1031
1081
|
/**
|
|
1032
1082
|
* Send a `msg` frame over the live link to a peer and resolve with its
|
|
1033
1083
|
* correlated `msg-result`. Undefined when the peer has no live link here
|
|
@@ -1037,7 +1087,7 @@ var InterconnectService = class extends Service {
|
|
|
1037
1087
|
async msgRequest(instanceId, kind, payload) {
|
|
1038
1088
|
const state = this.linkStates.get(instanceId);
|
|
1039
1089
|
if (state === void 0 || !state.writable()) return void 0;
|
|
1040
|
-
const reqId = `m${++this.reqIdCounter}-${
|
|
1090
|
+
const reqId = `m${++this.reqIdCounter}-${randomUUID()}`;
|
|
1041
1091
|
const message = {
|
|
1042
1092
|
kind,
|
|
1043
1093
|
sessionId: payload.sessionId,
|
|
@@ -1046,7 +1096,7 @@ var InterconnectService = class extends Service {
|
|
|
1046
1096
|
...payload.delivery === void 0 ? {} : { delivery: payload.delivery },
|
|
1047
1097
|
...payload.resume === void 0 ? {} : { resume: payload.resume }
|
|
1048
1098
|
};
|
|
1049
|
-
return this.waitForResult(reqId, (failure) => {
|
|
1099
|
+
return this.waitForResult(reqId, "msg", (failure) => {
|
|
1050
1100
|
const wrote = state.sendFrame({ type: "msg", reqId, message });
|
|
1051
1101
|
if (!wrote) failure(new Error(`interconnect: peer link to ${instanceId} closed while sending`));
|
|
1052
1102
|
});
|
|
@@ -1055,32 +1105,34 @@ var InterconnectService = class extends Service {
|
|
|
1055
1105
|
async queryRequest(instanceId, query) {
|
|
1056
1106
|
const state = this.linkStates.get(instanceId);
|
|
1057
1107
|
if (state === void 0 || !state.writable()) return void 0;
|
|
1058
|
-
const reqId = `q${++this.reqIdCounter}-${
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1108
|
+
const reqId = `q${++this.reqIdCounter}-${randomUUID()}`;
|
|
1109
|
+
const accept = query.kind === "ping" ? (result) => typeof result === "object" && result !== null && result.pong === true && typeof result.instance === "string" : (result) => typeof result === "object" && result !== null && Array.isArray(result.sessions) && typeof result.instance === "string";
|
|
1110
|
+
try {
|
|
1111
|
+
return await this.waitForResult(reqId, "query", (failure) => {
|
|
1112
|
+
const wrote = state.sendFrame({ type: "query", reqId, query });
|
|
1113
|
+
if (!wrote) failure(new Error(`interconnect: peer link to ${instanceId} closed while querying`));
|
|
1114
|
+
}, accept);
|
|
1115
|
+
} catch {
|
|
1116
|
+
return void 0;
|
|
1117
|
+
}
|
|
1063
1118
|
}
|
|
1064
1119
|
/** Settle a `reqId` frame against its `*-result`, timing out at `requestTimeoutMs`. */
|
|
1065
|
-
waitForResult(reqId, send) {
|
|
1120
|
+
waitForResult(reqId, kind, send, accept) {
|
|
1066
1121
|
return new Promise((resolve2, reject) => {
|
|
1067
1122
|
const timer = setTimeout(() => {
|
|
1068
1123
|
this.pendingMessages.delete(reqId);
|
|
1069
1124
|
reject(new Error(`interconnect: no result for ${reqId} within ${this.requestTimeoutMs}ms`));
|
|
1070
1125
|
}, this.requestTimeoutMs);
|
|
1071
|
-
this.pendingMessages.set(reqId, {
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
this.pendingMessages.delete(reqId);
|
|
1082
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1083
|
-
};
|
|
1126
|
+
this.pendingMessages.set(reqId, {
|
|
1127
|
+
timer,
|
|
1128
|
+
kind,
|
|
1129
|
+
...accept === void 0 ? {} : { accept },
|
|
1130
|
+
resolve: (result) => {
|
|
1131
|
+
clearTimeout(timer);
|
|
1132
|
+
this.pendingMessages.delete(reqId);
|
|
1133
|
+
resolve2(result);
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1084
1136
|
send((error) => {
|
|
1085
1137
|
clearTimeout(timer);
|
|
1086
1138
|
this.pendingMessages.delete(reqId);
|
|
@@ -1096,12 +1148,20 @@ var InterconnectService = class extends Service {
|
|
|
1096
1148
|
* @returns disposer removing the peer route.
|
|
1097
1149
|
*/
|
|
1098
1150
|
subscribe(instanceId, origin) {
|
|
1099
|
-
this.link(instanceId, origin);
|
|
1151
|
+
const linked = this.link(instanceId, origin);
|
|
1152
|
+
const dialedOrigin = trimBase(origin);
|
|
1100
1153
|
return () => {
|
|
1101
|
-
this.
|
|
1154
|
+
const state = this.linkStates.get(instanceId);
|
|
1155
|
+
if (state === linked && state.peer === dialedOrigin) {
|
|
1156
|
+
state.close();
|
|
1157
|
+
this.linkStates.delete(instanceId);
|
|
1158
|
+
}
|
|
1102
1159
|
};
|
|
1103
1160
|
}
|
|
1104
|
-
/**
|
|
1161
|
+
/**
|
|
1162
|
+
* Remove a peer route, closing its outbound link.
|
|
1163
|
+
* @param instanceId - the peer route to remove.
|
|
1164
|
+
*/
|
|
1105
1165
|
unsubscribe(instanceId) {
|
|
1106
1166
|
const state = this.linkStates.get(instanceId);
|
|
1107
1167
|
if (state !== void 0) state.close();
|
|
@@ -1118,19 +1178,23 @@ var InterconnectService = class extends Service {
|
|
|
1118
1178
|
*/
|
|
1119
1179
|
link(instanceId, origin) {
|
|
1120
1180
|
const existing = this.linkStates.get(instanceId);
|
|
1121
|
-
if (existing !== void 0) {
|
|
1181
|
+
if (existing !== void 0 && !existing.isClosed()) {
|
|
1122
1182
|
existing.reroute(trimBase(origin));
|
|
1123
1183
|
return existing;
|
|
1124
1184
|
}
|
|
1125
|
-
const state = new LinkState(
|
|
1185
|
+
const state = new LinkState(
|
|
1186
|
+
(socket) => {
|
|
1187
|
+
this.attachSocket(socket);
|
|
1188
|
+
},
|
|
1189
|
+
this.ctx.logger,
|
|
1190
|
+
() => this.resolveTokenForDial(),
|
|
1191
|
+
instanceId,
|
|
1192
|
+
trimBase(origin)
|
|
1193
|
+
);
|
|
1126
1194
|
this.linkStates.set(instanceId, state);
|
|
1127
1195
|
state.dial();
|
|
1128
1196
|
return state;
|
|
1129
1197
|
}
|
|
1130
|
-
/** Remove a closed outbound link's state so a later `link` re-dials fresh. */
|
|
1131
|
-
forgetLink(instanceId) {
|
|
1132
|
-
this.linkStates.delete(instanceId);
|
|
1133
|
-
}
|
|
1134
1198
|
/** Push one serialized lifecycle fact out to every linked peer over WS. */
|
|
1135
1199
|
fanout(notification) {
|
|
1136
1200
|
this.broadcast(notification);
|
|
@@ -1140,11 +1204,15 @@ var InterconnectService = class extends Service {
|
|
|
1140
1204
|
if (this.sockets.size === 0) return;
|
|
1141
1205
|
const frame = { type: "event", notification };
|
|
1142
1206
|
const encoded = JSON.stringify(frame);
|
|
1207
|
+
const sent = /* @__PURE__ */ new Set();
|
|
1143
1208
|
for (const socket of this.sockets) {
|
|
1144
1209
|
if (socket.readyState !== WebSocket.OPEN) {
|
|
1145
1210
|
this.sockets.delete(socket);
|
|
1146
1211
|
continue;
|
|
1147
1212
|
}
|
|
1213
|
+
const peer = this.peerOf.get(socket);
|
|
1214
|
+
if (peer !== void 0 && sent.has(peer)) continue;
|
|
1215
|
+
if (peer !== void 0) sent.add(peer);
|
|
1148
1216
|
socket.send(encoded);
|
|
1149
1217
|
}
|
|
1150
1218
|
}
|
|
@@ -1154,27 +1222,18 @@ var InterconnectService = class extends Service {
|
|
|
1154
1222
|
* result so a throwing handler cannot escape the socket's message callback.
|
|
1155
1223
|
*/
|
|
1156
1224
|
async handleMsgFrame(socket, reqId, message) {
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
} else {
|
|
1166
|
-
result = await this.deliver({
|
|
1167
|
-
sessionId: message.sessionId,
|
|
1168
|
-
text: message.text,
|
|
1169
|
-
...message.sender === void 0 ? {} : { sender: message.sender },
|
|
1170
|
-
...message.delivery === void 0 ? {} : { delivery: message.delivery },
|
|
1171
|
-
...message.resume === void 0 ? {} : { resume: message.resume }
|
|
1172
|
-
});
|
|
1173
|
-
}
|
|
1225
|
+
const result = await this.deliver({
|
|
1226
|
+
sessionId: message.sessionId,
|
|
1227
|
+
text: message.text,
|
|
1228
|
+
/* v8 ignore next 1 -- schemastery resolves an absent optional `sender` to an empty object, never undefined. */
|
|
1229
|
+
...message.sender === void 0 ? {} : { sender: message.sender },
|
|
1230
|
+
...message.delivery === void 0 ? {} : { delivery: message.delivery },
|
|
1231
|
+
...message.resume === void 0 ? {} : { resume: message.resume }
|
|
1232
|
+
});
|
|
1174
1233
|
this.sendFrame(socket, { type: "msg-result", reqId, result });
|
|
1175
1234
|
}
|
|
1176
1235
|
/** Dispatch one inbound `query` frame (ping/list/event) and answer on the socket. */
|
|
1177
|
-
|
|
1236
|
+
handleQueryFrame(socket, reqId, query) {
|
|
1178
1237
|
let result;
|
|
1179
1238
|
if (query.kind === "ping") {
|
|
1180
1239
|
result = { pong: true, instance: this.instanceId };
|
|
@@ -1206,7 +1265,7 @@ var InterconnectService = class extends Service {
|
|
|
1206
1265
|
* matches how the Host's own session listing treats its projection column.
|
|
1207
1266
|
*/
|
|
1208
1267
|
listSessions() {
|
|
1209
|
-
const reachable = this.ctx.agents.list().filter((agent) => !
|
|
1268
|
+
const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(this.ctx, agent.session, agent));
|
|
1210
1269
|
const sessions = reachable.map((agent) => {
|
|
1211
1270
|
let title;
|
|
1212
1271
|
try {
|
|
@@ -1247,7 +1306,9 @@ var InterconnectService = class extends Service {
|
|
|
1247
1306
|
if (resolved === void 0 || resolved === null) return { reason: "session-not-live" };
|
|
1248
1307
|
return { agent: resolved };
|
|
1249
1308
|
} catch (error) {
|
|
1250
|
-
this.ctx.logger.info(
|
|
1309
|
+
this.ctx.logger.info(
|
|
1310
|
+
`interconnect: resume refused for ${payload.sessionId}: ${error instanceof Error ? error.message : String(error)}`
|
|
1311
|
+
);
|
|
1251
1312
|
return { reason: "resume-failed" };
|
|
1252
1313
|
}
|
|
1253
1314
|
}
|
|
@@ -1256,24 +1317,26 @@ var InterconnectService = class extends Service {
|
|
|
1256
1317
|
* when the sender asked and this receiver allows it.
|
|
1257
1318
|
*/
|
|
1258
1319
|
async deliver(payload) {
|
|
1320
|
+
if (this.disposed) return { delivered: false, instance: this.instanceId, reason: "unreachable" };
|
|
1259
1321
|
let agent = this.ctx.agents.get(payload.sessionId);
|
|
1260
1322
|
if (agent === void 0) {
|
|
1261
1323
|
const woken = await this.wake(payload);
|
|
1324
|
+
if (this.disposed) return { delivered: false, instance: this.instanceId, reason: "unreachable" };
|
|
1262
1325
|
if ("reason" in woken) {
|
|
1263
1326
|
return { delivered: false, instance: this.instanceId, reason: woken.reason };
|
|
1264
1327
|
}
|
|
1265
1328
|
agent = woken.agent;
|
|
1266
1329
|
}
|
|
1267
|
-
if (
|
|
1330
|
+
if (isSessionOwnedBySubagent(this.ctx, agent.session, agent)) {
|
|
1268
1331
|
return { delivered: false, instance: this.instanceId, reason: "session-owned-by-subagent" };
|
|
1269
1332
|
}
|
|
1270
1333
|
const sender = (() => {
|
|
1271
1334
|
const candidate = payload.sender;
|
|
1272
|
-
|
|
1335
|
+
if (typeof candidate !== "object" || candidate === null) return void 0;
|
|
1336
|
+
const record = candidate;
|
|
1337
|
+
if (typeof record.instanceId !== "string" || typeof record.sessionId !== "string") return void 0;
|
|
1338
|
+
return { instanceId: record.instanceId, sessionId: record.sessionId };
|
|
1273
1339
|
})();
|
|
1274
|
-
if (sender !== void 0) {
|
|
1275
|
-
this.senders.set(payload.sessionId, sender);
|
|
1276
|
-
}
|
|
1277
1340
|
const message = createUserMessage({
|
|
1278
1341
|
source: {
|
|
1279
1342
|
kind: "plugin",
|
|
@@ -1296,27 +1359,21 @@ var InterconnectService = class extends Service {
|
|
|
1296
1359
|
case "followup":
|
|
1297
1360
|
agent.followup(message);
|
|
1298
1361
|
break;
|
|
1362
|
+
/* v8 ignore next -- closed-union exhaustiveness guard */
|
|
1363
|
+
default:
|
|
1364
|
+
assertNever(mode);
|
|
1365
|
+
}
|
|
1366
|
+
if (sender !== void 0) {
|
|
1367
|
+
this.senders.set(payload.sessionId, sender);
|
|
1368
|
+
} else {
|
|
1369
|
+
this.senders.delete(payload.sessionId);
|
|
1299
1370
|
}
|
|
1300
1371
|
return { delivered: true, instance: this.instanceId, delivery: mode };
|
|
1301
1372
|
}
|
|
1302
|
-
/**
|
|
1303
|
-
* Inbound `reply` dispatch: forward a request to {link reply}, resolving the
|
|
1304
|
-
* outbound sender for the named local session. Kept separate from {link
|
|
1305
|
-
* deliver} so the two wire endpoints stay distinct in the request pipeline.
|
|
1306
|
-
*/
|
|
1307
|
-
async replyForSession(payload) {
|
|
1308
|
-
return this.reply({
|
|
1309
|
-
sessionId: payload.sessionId,
|
|
1310
|
-
text: payload.text,
|
|
1311
|
-
...payload.delivery === void 0 ? {} : { delivery: payload.delivery },
|
|
1312
|
-
...payload.resume === void 0 ? {} : { resume: payload.resume }
|
|
1313
|
-
});
|
|
1314
|
-
}
|
|
1315
1373
|
async resolveToken() {
|
|
1316
1374
|
const credential = await this.ctx.credentials.resolve(credentialRef(INTERCONNECT_TOKEN_REF));
|
|
1317
1375
|
return credential === void 0 || credential.value.length === 0 ? void 0 : credential.value;
|
|
1318
1376
|
}
|
|
1319
|
-
/** Resolve the shared token for an outbound dial (same source as inbound). */
|
|
1320
1377
|
resolveTokenForDial() {
|
|
1321
1378
|
return this.resolveToken();
|
|
1322
1379
|
}
|
|
@@ -1347,7 +1404,10 @@ var InterconnectService = class extends Service {
|
|
|
1347
1404
|
/**
|
|
1348
1405
|
* Install frame + liveness handling on one socket and add it to the live
|
|
1349
1406
|
* pool. Used by both the server half (accepted upgrade) and the client half
|
|
1350
|
-
* (outbound dial), so a single socket carries events both directions.
|
|
1407
|
+
* (outbound dial), so a single socket carries events both directions. Not a
|
|
1408
|
+
* model-facing entry point; the outbound dial reaches it through a private
|
|
1409
|
+
* closure.
|
|
1410
|
+
* @param websocket - the accepted or dialed socket to install handlers on.
|
|
1351
1411
|
*/
|
|
1352
1412
|
attachSocket(websocket) {
|
|
1353
1413
|
this.sockets.add(websocket);
|
|
@@ -1371,30 +1431,42 @@ var InterconnectService = class extends Service {
|
|
|
1371
1431
|
/** Parse and route one inbound link frame, attributing events to the socket's announced peer. */
|
|
1372
1432
|
handleFrame(socket, data) {
|
|
1373
1433
|
if (Array.isArray(data)) return;
|
|
1434
|
+
if (data.byteLength > MAX_LINK_FRAME_BYTES) {
|
|
1435
|
+
this.ctx.logger.warn("interconnect: dropping oversized link frame");
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1374
1438
|
const text = Buffer.isBuffer(data) ? data.toString("utf8") : Buffer.from(data).toString("utf8");
|
|
1375
|
-
let
|
|
1439
|
+
let parsed;
|
|
1376
1440
|
try {
|
|
1377
|
-
|
|
1441
|
+
parsed = Schema.resolve(JSON.parse(text), linkFrameSchema, {})[0];
|
|
1378
1442
|
} catch {
|
|
1379
1443
|
this.ctx.logger.warn("interconnect: dropping malformed link frame");
|
|
1380
1444
|
return;
|
|
1381
1445
|
}
|
|
1446
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1447
|
+
this.ctx.logger.warn("interconnect: dropping malformed link frame");
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
const frame = parsed;
|
|
1382
1451
|
if (frame.type === "hello") {
|
|
1383
1452
|
this.peerOf.set(socket, frame.sender);
|
|
1384
1453
|
return;
|
|
1385
1454
|
}
|
|
1386
1455
|
if (frame.type === "event") {
|
|
1387
1456
|
const sender = this.peerOf.get(socket) ?? "unknown-peer";
|
|
1457
|
+
const kind = frame.notification.kind;
|
|
1388
1458
|
try {
|
|
1389
1459
|
this.receiveEvent({ sender, notification: frame.notification });
|
|
1390
1460
|
} catch (error) {
|
|
1391
|
-
this.ctx.logger.warn(
|
|
1461
|
+
this.ctx.logger.warn(
|
|
1462
|
+
`interconnect: listener for a ${kind} event from ${sender} threw: ` + (error instanceof Error ? error.message : String(error))
|
|
1463
|
+
);
|
|
1392
1464
|
}
|
|
1393
1465
|
return;
|
|
1394
1466
|
}
|
|
1395
1467
|
if (frame.type === "msg-result") {
|
|
1396
1468
|
const pending = this.pendingMessages.get(frame.reqId);
|
|
1397
|
-
if (pending !== void 0) {
|
|
1469
|
+
if (pending !== void 0 && pending.kind === "msg") {
|
|
1398
1470
|
;
|
|
1399
1471
|
pending.resolve?.(frame.result);
|
|
1400
1472
|
}
|
|
@@ -1402,7 +1474,7 @@ var InterconnectService = class extends Service {
|
|
|
1402
1474
|
}
|
|
1403
1475
|
if (frame.type === "query-result") {
|
|
1404
1476
|
const pending = this.pendingMessages.get(frame.reqId);
|
|
1405
|
-
if (pending !== void 0) {
|
|
1477
|
+
if (pending !== void 0 && pending.kind === "query" && (pending.accept === void 0 || pending.accept(frame.result))) {
|
|
1406
1478
|
;
|
|
1407
1479
|
pending.resolve?.(frame.result);
|
|
1408
1480
|
}
|
|
@@ -1421,8 +1493,8 @@ var InterconnectService = class extends Service {
|
|
|
1421
1493
|
});
|
|
1422
1494
|
return;
|
|
1423
1495
|
}
|
|
1424
|
-
void Promise.resolve().then(
|
|
1425
|
-
|
|
1496
|
+
void Promise.resolve().then(() => {
|
|
1497
|
+
this.handleQueryFrame(socket, frame.reqId, frame.query);
|
|
1426
1498
|
}).catch((error) => {
|
|
1427
1499
|
this.ctx.logger.warn(`interconnect: query ${frame.reqId} handler threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
1428
1500
|
});
|
|
@@ -1435,15 +1507,20 @@ var InterconnectService = class extends Service {
|
|
|
1435
1507
|
}
|
|
1436
1508
|
};
|
|
1437
1509
|
var LinkState = class {
|
|
1438
|
-
constructor(
|
|
1439
|
-
this.
|
|
1510
|
+
constructor(attachSocket, logger, resolveToken, instanceId, origin) {
|
|
1511
|
+
this.attachSocket = attachSocket;
|
|
1512
|
+
this.logger = logger;
|
|
1513
|
+
this.resolveToken = resolveToken;
|
|
1440
1514
|
this.instanceId = instanceId;
|
|
1441
1515
|
this.peer = origin;
|
|
1442
1516
|
}
|
|
1443
1517
|
socket;
|
|
1518
|
+
dialEpoch = 0;
|
|
1444
1519
|
closed = false;
|
|
1445
1520
|
retry = 0;
|
|
1446
1521
|
reconnectTimer;
|
|
1522
|
+
/** Dial-registered handlers, removed by reference on close/reroute so the pool-cleanup handlers attachSocket added stay alive. */
|
|
1523
|
+
dialListeners;
|
|
1447
1524
|
// `peer` is mutable so `reroute` can point the link at a new origin.
|
|
1448
1525
|
peer;
|
|
1449
1526
|
instanceId;
|
|
@@ -1451,9 +1528,16 @@ var LinkState = class {
|
|
|
1451
1528
|
reroute(origin) {
|
|
1452
1529
|
if (origin === this.peer) return;
|
|
1453
1530
|
this.peer = origin;
|
|
1531
|
+
this.dialEpoch += 1;
|
|
1532
|
+
if (this.reconnectTimer !== void 0) {
|
|
1533
|
+
clearTimeout(this.reconnectTimer);
|
|
1534
|
+
this.reconnectTimer = void 0;
|
|
1535
|
+
}
|
|
1454
1536
|
const socket = this.socket;
|
|
1455
1537
|
if (socket !== void 0) {
|
|
1456
|
-
|
|
1538
|
+
this.removeDialListeners(socket);
|
|
1539
|
+
socket.on("error", () => {
|
|
1540
|
+
});
|
|
1457
1541
|
socket.terminate();
|
|
1458
1542
|
this.socket = void 0;
|
|
1459
1543
|
}
|
|
@@ -1462,29 +1546,54 @@ var LinkState = class {
|
|
|
1462
1546
|
/** Open the socket; reconnect is scheduled by the close handler. */
|
|
1463
1547
|
dial() {
|
|
1464
1548
|
if (this.closed) return;
|
|
1465
|
-
const
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1549
|
+
const epoch = this.dialEpoch;
|
|
1550
|
+
let url;
|
|
1551
|
+
try {
|
|
1552
|
+
url = linkUrl(this.peer);
|
|
1553
|
+
} catch {
|
|
1554
|
+
this.logger.warn(`interconnect: invalid peer origin ${this.peer}; link to ${this.instanceId} stays down`);
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
void this.resolveToken().then((token) => {
|
|
1558
|
+
if (token === void 0) {
|
|
1559
|
+
this.logger.warn(`interconnect: no shared token configured; peer ${this.instanceId} link stays down until one is set`);
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
if (this.closed || epoch !== this.dialEpoch) return;
|
|
1469
1563
|
const socket = new WebSocket(url, {
|
|
1470
|
-
headers: { authorization: `Bearer ${token}` }
|
|
1564
|
+
headers: { authorization: `Bearer ${token}` },
|
|
1565
|
+
maxPayload: MAX_LINK_FRAME_BYTES,
|
|
1566
|
+
// A peer that accepts TCP but never completes the upgrade would leave
|
|
1567
|
+
// the socket in CONNECTING forever: no open (so no heartbeat) and no
|
|
1568
|
+
// close (so no reconnect). ws aborts the connection past the handshake
|
|
1569
|
+
// window, so its own error+close fire and the close handler schedules
|
|
1570
|
+
// a reconnect — and ws owns the timer, so nothing leaks on teardown.
|
|
1571
|
+
handshakeTimeout: LINK_HANDSHAKE_TIMEOUT_MS
|
|
1471
1572
|
});
|
|
1472
1573
|
this.socket = socket;
|
|
1473
|
-
|
|
1574
|
+
const onOpen = () => {
|
|
1474
1575
|
this.retry = 0;
|
|
1475
|
-
this.
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1576
|
+
this.attachSocket(socket);
|
|
1577
|
+
};
|
|
1578
|
+
const onClose = () => {
|
|
1478
1579
|
if (this.closed) return;
|
|
1479
1580
|
this.scheduleReconnect();
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
}
|
|
1581
|
+
};
|
|
1582
|
+
const onError = () => {
|
|
1583
|
+
};
|
|
1584
|
+
this.dialListeners = { open: onOpen, close: onClose, error: onError };
|
|
1585
|
+
socket.once("open", onOpen);
|
|
1586
|
+
socket.once("close", onClose);
|
|
1587
|
+
socket.on("error", onError);
|
|
1483
1588
|
}).catch(() => {
|
|
1484
|
-
if (this.closed) return;
|
|
1589
|
+
if (this.closed || epoch !== this.dialEpoch) return;
|
|
1485
1590
|
this.scheduleReconnect();
|
|
1486
1591
|
});
|
|
1487
1592
|
}
|
|
1593
|
+
/** Whether this link has been closed; a closed state is never reused by `link()`. */
|
|
1594
|
+
isClosed() {
|
|
1595
|
+
return this.closed;
|
|
1596
|
+
}
|
|
1488
1597
|
/** Whether this link currently holds an open socket that can carry frames. */
|
|
1489
1598
|
writable() {
|
|
1490
1599
|
return this.socket !== void 0 && this.socket.readyState === WebSocket.OPEN;
|
|
@@ -1498,14 +1607,24 @@ var LinkState = class {
|
|
|
1498
1607
|
}
|
|
1499
1608
|
close() {
|
|
1500
1609
|
this.closed = true;
|
|
1501
|
-
this.owner.forgetLink(this.peer);
|
|
1502
1610
|
if (this.reconnectTimer !== void 0) clearTimeout(this.reconnectTimer);
|
|
1503
1611
|
const socket = this.socket;
|
|
1504
1612
|
if (socket !== void 0) {
|
|
1505
|
-
|
|
1613
|
+
this.removeDialListeners(socket);
|
|
1614
|
+
socket.on("error", () => {
|
|
1615
|
+
});
|
|
1506
1616
|
socket.terminate();
|
|
1507
1617
|
}
|
|
1508
1618
|
}
|
|
1619
|
+
/** Detach the dial-registered handlers from a socket before terminating it. */
|
|
1620
|
+
removeDialListeners(socket) {
|
|
1621
|
+
const listeners = this.dialListeners;
|
|
1622
|
+
if (listeners === void 0) return;
|
|
1623
|
+
socket.removeListener("open", listeners.open);
|
|
1624
|
+
socket.removeListener("close", listeners.close);
|
|
1625
|
+
socket.removeListener("error", listeners.error);
|
|
1626
|
+
this.dialListeners = void 0;
|
|
1627
|
+
}
|
|
1509
1628
|
scheduleReconnect() {
|
|
1510
1629
|
const delay = Math.min(3e4, 1e3 * 2 ** this.retry);
|
|
1511
1630
|
this.retry += 1;
|
|
@@ -1516,20 +1635,26 @@ var LinkState = class {
|
|
|
1516
1635
|
}
|
|
1517
1636
|
};
|
|
1518
1637
|
function timingSafeEqual(a, b) {
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1638
|
+
return constantTimeEqual(createHash("sha256").update(a, "utf8").digest(), createHash("sha256").update(b, "utf8").digest());
|
|
1639
|
+
}
|
|
1640
|
+
function linkUrl(origin) {
|
|
1641
|
+
const url = new URL("/interconnect/link", origin);
|
|
1642
|
+
if (url.protocol === "http:") url.protocol = "ws:";
|
|
1643
|
+
else if (url.protocol === "https:") url.protocol = "wss:";
|
|
1644
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") {
|
|
1645
|
+
throw new TypeError(`interconnect: unsupported peer origin protocol ${url.protocol}`);
|
|
1646
|
+
}
|
|
1647
|
+
return url;
|
|
1525
1648
|
}
|
|
1526
1649
|
function trimBase(baseUrl) {
|
|
1527
1650
|
return baseUrl.replace(/\/+$/, "");
|
|
1528
1651
|
}
|
|
1529
1652
|
var index_default = InterconnectService;
|
|
1530
1653
|
export {
|
|
1654
|
+
INTERCONNECT_CHANNEL,
|
|
1531
1655
|
INTERCONNECT_TOKEN_REF,
|
|
1532
1656
|
InterconnectService,
|
|
1533
|
-
index_default as default
|
|
1657
|
+
index_default as default,
|
|
1658
|
+
linkUrl
|
|
1534
1659
|
};
|
|
1535
1660
|
//# sourceMappingURL=index.js.map
|