agent-comms 5.2.5 → 7.0.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +6 -2
- package/dist/bridges/user/controller.d.ts +1 -1
- package/dist/bridges/user/controller.d.ts.map +1 -1
- package/dist/bridges/user/controller.js +1 -1
- package/dist/bridges/user/controller.js.map +1 -1
- package/dist/core/bridge-mesh.d.ts +2 -2
- package/dist/core/bridge-mesh.d.ts.map +1 -1
- package/dist/core/bridge-mesh.js +3 -2
- package/dist/core/bridge-mesh.js.map +1 -1
- package/dist/core/gateway-trust.d.ts +15 -9
- package/dist/core/gateway-trust.d.ts.map +1 -1
- package/dist/core/gateway-trust.js +49 -21
- package/dist/core/gateway-trust.js.map +1 -1
- package/dist/core/gossip-directory.d.ts +1 -1
- package/dist/core/gossip-directory.d.ts.map +1 -1
- package/dist/core/gossip-directory.js +4 -2
- package/dist/core/gossip-directory.js.map +1 -1
- package/dist/core/hub-forwarding.d.ts +4 -18
- package/dist/core/hub-forwarding.d.ts.map +1 -1
- package/dist/core/hub-forwarding.js +2 -41
- package/dist/core/hub-forwarding.js.map +1 -1
- package/dist/core/hub-link.d.ts +58 -0
- package/dist/core/hub-link.d.ts.map +1 -0
- package/dist/core/hub-link.js +118 -0
- package/dist/core/hub-link.js.map +1 -0
- package/dist/core/hub-session.d.ts +14 -15
- package/dist/core/hub-session.d.ts.map +1 -1
- package/dist/core/hub-session.js +30 -51
- package/dist/core/hub-session.js.map +1 -1
- package/dist/core/identity-store.d.ts +8 -4
- package/dist/core/identity-store.d.ts.map +1 -1
- package/dist/core/identity-store.js +29 -15
- package/dist/core/identity-store.js.map +1 -1
- package/dist/core/mesh-graph.d.ts +1 -1
- package/dist/core/mesh-graph.js +1 -1
- package/dist/core/mesh-store.d.ts +11 -6
- package/dist/core/mesh-store.d.ts.map +1 -1
- package/dist/core/mesh-store.js +33 -22
- package/dist/core/mesh-store.js.map +1 -1
- package/dist/core/peer-lifecycle.d.ts +2 -5
- package/dist/core/peer-lifecycle.d.ts.map +1 -1
- package/dist/core/peer-lifecycle.js +0 -1
- package/dist/core/peer-lifecycle.js.map +1 -1
- package/dist/core/room-router.d.ts +2 -2
- package/dist/core/room-router.d.ts.map +1 -1
- package/dist/core/room-router.js +2 -32
- package/dist/core/room-router.js.map +1 -1
- package/dist/core/shutdown-signals.d.ts +1 -1
- package/dist/core/shutdown-signals.js +1 -1
- package/dist/core/transport.d.ts +7 -7
- package/dist/core/transport.d.ts.map +1 -1
- package/dist/core/wire-mesh-transport.d.ts +7 -3
- package/dist/core/wire-mesh-transport.d.ts.map +1 -1
- package/dist/core/wire-mesh-transport.js +39 -22
- package/dist/core/wire-mesh-transport.js.map +1 -1
- package/dist/test/bridge-mesh.integration.test.js +15 -1
- package/dist/test/bridge-mesh.integration.test.js.map +1 -1
- package/dist/test/chat-controller-mesh-errors.integration.test.js +7 -1
- package/dist/test/chat-controller-mesh-errors.integration.test.js.map +1 -1
- package/dist/test/coordinator-failover-hub.integration.test.d.ts +5 -0
- package/dist/test/coordinator-failover-hub.integration.test.d.ts.map +1 -0
- package/dist/test/coordinator-failover-hub.integration.test.js +78 -0
- package/dist/test/coordinator-failover-hub.integration.test.js.map +1 -0
- package/dist/test/coordinator-failover.integration.test.js +3 -6
- package/dist/test/coordinator-failover.integration.test.js.map +1 -1
- package/dist/test/coordinator-failover.unit.test.js +0 -7
- package/dist/test/coordinator-failover.unit.test.js.map +1 -1
- package/dist/test/gateway-trust.integration.test.d.ts +1 -1
- package/dist/test/gateway-trust.integration.test.js +26 -2
- package/dist/test/gateway-trust.integration.test.js.map +1 -1
- package/dist/test/gateway-trust.unit.test.js +43 -5
- package/dist/test/gateway-trust.unit.test.js.map +1 -1
- package/dist/test/hub-exposure.integration.test.d.ts +5 -0
- package/dist/test/hub-exposure.integration.test.d.ts.map +1 -0
- package/dist/test/hub-exposure.integration.test.js +121 -0
- package/dist/test/hub-exposure.integration.test.js.map +1 -0
- package/dist/test/hub-helpers.d.ts +18 -3
- package/dist/test/hub-helpers.d.ts.map +1 -1
- package/dist/test/hub-helpers.js +24 -3
- package/dist/test/hub-helpers.js.map +1 -1
- package/dist/test/hub-link.unit.test.d.ts +2 -0
- package/dist/test/hub-link.unit.test.d.ts.map +1 -0
- package/dist/test/hub-link.unit.test.js +210 -0
- package/dist/test/hub-link.unit.test.js.map +1 -0
- package/dist/test/hub-peer-per-store.integration.test.d.ts +9 -0
- package/dist/test/hub-peer-per-store.integration.test.d.ts.map +1 -0
- package/dist/test/hub-peer-per-store.integration.test.js +211 -0
- package/dist/test/hub-peer-per-store.integration.test.js.map +1 -0
- package/dist/test/hub-room-domain-verb-trust-bypass.integration.test.js +2 -2
- package/dist/test/hub-room-domain-verb-trust-bypass.integration.test.js.map +1 -1
- package/dist/test/hub-session-close.integration.test.js +1 -1
- package/dist/test/hub-session-close.integration.test.js.map +1 -1
- package/dist/test/hub-session-dispatch.unit.test.js +1 -66
- package/dist/test/hub-session-dispatch.unit.test.js.map +1 -1
- package/dist/test/identity-store.integration.test.js +24 -32
- package/dist/test/identity-store.integration.test.js.map +1 -1
- package/dist/test/mesh-graph-trace.integration.test.js +2 -2
- package/dist/test/mesh-graph-trace.integration.test.js.map +1 -1
- package/dist/test/mesh-smoke.runner.js +1 -3
- package/dist/test/mesh-smoke.runner.js.map +1 -1
- package/dist/test/mesh-store-orchestration.unit.test.js +11 -41
- package/dist/test/mesh-store-orchestration.unit.test.js.map +1 -1
- package/dist/test/peer-lifecycle.unit.test.js +0 -8
- package/dist/test/peer-lifecycle.unit.test.js.map +1 -1
- package/dist/test/principal-membership-trust.integration.test.js +8 -1
- package/dist/test/principal-membership-trust.integration.test.js.map +1 -1
- package/dist/test/room-join-by-bare-name.integration.test.js +1 -1
- package/dist/test/room-join-by-bare-name.integration.test.js.map +1 -1
- package/dist/test/test-transport.d.ts +1 -1
- package/dist/test/test-transport.d.ts.map +1 -1
- package/dist/test/test-transport.js +1 -1
- package/dist/test/test-transport.js.map +1 -1
- package/dist/test/wire-mesh-transport-hub.integration.test.d.ts +1 -1
- package/dist/test/wire-mesh-transport-hub.integration.test.js +96 -9
- package/dist/test/wire-mesh-transport-hub.integration.test.js.map +1 -1
- package/package.json +1 -1
- package/dist/core/coordinator-gateway.d.ts +0 -25
- package/dist/core/coordinator-gateway.d.ts.map +0 -1
- package/dist/core/coordinator-gateway.js +0 -34
- package/dist/core/coordinator-gateway.js.map +0 -1
- package/dist/test/coordinator-gateway.unit.test.d.ts +0 -2
- package/dist/test/coordinator-gateway.unit.test.d.ts.map +0 -1
- package/dist/test/coordinator-gateway.unit.test.js +0 -88
- package/dist/test/coordinator-gateway.unit.test.js.map +0 -1
- package/dist/test/gateway-forwarding.integration.test.d.ts +0 -9
- package/dist/test/gateway-forwarding.integration.test.d.ts.map +0 -1
- package/dist/test/gateway-forwarding.integration.test.js +0 -226
- package/dist/test/gateway-forwarding.integration.test.js.map +0 -1
- package/dist/test/hub-forwarding.unit.test.d.ts +0 -2
- package/dist/test/hub-forwarding.unit.test.d.ts.map +0 -1
- package/dist/test/hub-forwarding.unit.test.js +0 -102
- package/dist/test/hub-forwarding.unit.test.js.map +0 -1
- package/dist/test/hub-todevice-routing.integration.test.d.ts +0 -7
- package/dist/test/hub-todevice-routing.integration.test.d.ts.map +0 -1
- package/dist/test/hub-todevice-routing.integration.test.js +0 -121
- package/dist/test/hub-todevice-routing.integration.test.js.map +0 -1
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* hub-forwarding --
|
|
2
|
+
* hub-forwarding -- local-to-remote routing, split from wire-mesh-transport.ts under the repo's max-lines cap: falls a room-domain request back through the hub's own relay-connect/relay-data pairing when its target isn't a local peer session.
|
|
3
3
|
*/
|
|
4
|
-
import type { CapabilityScope, CapabilityToken, ManageCommand
|
|
5
|
-
import type {
|
|
4
|
+
import type { CapabilityScope, CapabilityToken, ManageCommand } from "wire-mesh-core/generated/protocol";
|
|
5
|
+
import type { ManageOutcome } from "wire-mesh-core/domain/mesh-session";
|
|
6
6
|
import type { HubSession } from "./hub-session.js";
|
|
7
|
-
/**
|
|
8
|
-
export declare function forwardAdvertsToHub(hub: Readonly<Pick<HubSession, "isConnected" | "advertiseDevices">>, directory: readonly DirectoryEntry[], onError: ((error: Error) => void) | undefined, hasAnyTrustedGateway: () => boolean): void;
|
|
9
|
-
/** Catches the hub up with every local device already known at the moment this side becomes the gateway (connectHub's own trailing call, right after hub.connect resolves) -- without this, a device whose own last gossip arrived before this coordinator took over the gateway role would never be (re-)advertised until its own next periodic gossip tick (hub-side state is rebuilt from scratch on every takeover, per coordinator-gateway.ts's own class doc). Reuses forwardAdvertsToHub's own eligibility filter and trust gate, so only ever a visible, agent/self-bearing device is pushed, and only once a remote gateway is trusted, exactly as an ordinary directory-change forward would. */
|
|
10
|
-
export declare function pushHubCatchUp(hub: Readonly<Pick<HubSession, "isConnected" | "advertiseDevices" | "peers">>, knownDevices: ReadonlyMap<string, Readonly<PeerAdvert>>, onError: ((error: Error) => void) | undefined, hasAnyTrustedGateway: () => boolean): void;
|
|
11
|
-
/** Builds a forwardToLocalPeer callback closing over peerSessions -- a factory rather than a plain function taking peerSessions as its own first parameter, so the returned callback's shape matches HubSessionDeps.forwardToLocalPeer's own fixed external contract exactly (deviceHex, command, scope, token?) with no positional-binding tricks needed at the call site. Sends a room-domain manage-request to a LOCAL peer session only, never falling back to hub routing -- HubSession's own toDevice-forwarding leg (agent-comms#184: a hub-relayed request explicitly addressed to a non-gateway local peer this gateway also fronts). Returns undefined when no local session exists for that device-id, in which case HubSession falls back to dispatching the request against this gateway's own local state instead. */
|
|
12
|
-
export declare function makeSendToLocalPeer(peerSessions: ReadonlyMap<string, Readonly<Pick<AcceptedMeshSession, "sendManageRequest">>>): (memberId: string, command: ManageCommand, scope: Readonly<CapabilityScope>, token?: CapabilityToken) => Promise<ManageOutcome> | undefined;
|
|
13
|
-
/** Routes a room-domain request through the hub's relay-connect/relay-data pairing when memberId isn't a local peer session -- WireMeshTransport.sendRoomRequest's own fallback, since the member may be a remote agent reachable only via this machine's gateway connection (agent-comms#155's local-to-remote leg). WireMeshTransport.sendRoomRequest itself gates memberId against the gateway trust boundary (agent-comms#156) before ever calling this, so by the time this runs memberId is already known-trusted -- this function stays focused on the hub-connectivity outcome alone. Resolves the same not_connected outcome sendRoomRequest already returned before the hub existed at all when this side isn't currently the gateway. */
|
|
7
|
+
/** Routes a room-domain request through the hub's relay-connect/relay-data pairing when memberId isn't a local peer session -- WireMeshTransport.sendRoomRequest's own fallback, since the member may be a remote agent reachable only through the hub this store is connected to (agent-comms#155's local-to-remote leg). WireMeshTransport.sendRoomRequest itself gates memberId against the gateway trust boundary (agent-comms#156) before ever calling this, so by the time this runs memberId is already known-trusted -- this function stays focused on the hub-connectivity outcome alone. Resolves the same not_connected outcome sendRoomRequest already returned before the hub existed at all when this side holds no hub session, which is a store that has no agent yet, has a ghost agent, or whose machine trusts nobody. */
|
|
14
8
|
export declare function routeRoomRequestViaHub(options: {
|
|
15
9
|
hub: Readonly<Pick<HubSession, "isConnected" | "sendRoomRequest">>;
|
|
16
10
|
memberId: string;
|
|
@@ -18,12 +12,4 @@ export declare function routeRoomRequestViaHub(options: {
|
|
|
18
12
|
scope: Readonly<CapabilityScope>;
|
|
19
13
|
token?: CapabilityToken | undefined;
|
|
20
14
|
}): Promise<ManageOutcome>;
|
|
21
|
-
/** Dials the hub and immediately pushes a catch-up of every already-known local device onto it (agent-comms#154's own hub-connection-establishment sequence, kept together here rather than split across two call-site statements in WireMeshTransport.connectHub) -- without the trailing catch-up, a device whose own last gossip arrived before this coordinator took over the gateway role would stay invisible on the hub until its own next periodic gossip tick. */
|
|
22
|
-
export declare function connectHubGateway(options: {
|
|
23
|
-
hub: Readonly<Pick<HubSession, "isConnected" | "advertiseDevices" | "connect" | "peers">>;
|
|
24
|
-
url: string;
|
|
25
|
-
knownDevices: ReadonlyMap<string, Readonly<PeerAdvert>>;
|
|
26
|
-
onError: ((error: Error) => void) | undefined;
|
|
27
|
-
hasAnyTrustedGateway: () => boolean;
|
|
28
|
-
}): Promise<void>;
|
|
29
15
|
//# sourceMappingURL=hub-forwarding.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hub-forwarding.d.ts","sourceRoot":"","sources":["../../src/core/hub-forwarding.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,aAAa,
|
|
1
|
+
{"version":3,"file":"hub-forwarding.d.ts","sourceRoot":"","sources":["../../src/core/hub-forwarding.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,aAAa,EACd,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,6yBAA6yB;AAC7yB,wBAAsB,sBAAsB,CAAC,OAAO,EAAE;IACpD,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa,GAAG,iBAAiB,CAAC,CAAC,CAAC;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,aAAa,CAAC;IACvB,KAAK,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;CACrC,GAAG,OAAO,CAAC,aAAa,CAAC,CAIzB"}
|
|
@@ -1,50 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* hub-forwarding --
|
|
2
|
+
* hub-forwarding -- local-to-remote routing, split from wire-mesh-transport.ts under the repo's max-lines cap: falls a room-domain request back through the hub's own relay-connect/relay-data pairing when its target isn't a local peer session.
|
|
3
3
|
*/
|
|
4
|
-
|
|
5
|
-
/** Forwards every directory entry carrying an agent/self extension onto hub, if hub currently holds a live connection AND at least one remote gateway is currently trusted -- a no-op otherwise. The trust gate (agent-comms#156, GatewayTrust.hasAny's own doc) is coarse by necessity: wire-mesh-core's relay-hub broadcasts a gossip frame to every connected peer with no per-recipient targeting, so "advertise only to allowlisted remote gateways" can only be approximated as "advertise nothing at all until the operator has trusted someone" -- it is not a per-recipient filter. Called both from WireMeshTransport's own watchForDisconnect (as each local peer session's directory changes) and from connectHub's own initial catch-up push, so this side's already-known local devices reach the hub immediately on taking over the gateway role rather than waiting for their own next periodic gossip tick. Forwards unconditionally on every call once past the gates above (no caching or dedup against a previous call), mirroring relay-hub.ts's own "forward every gossip frame, as received" philosophy: an advert's own snapshot-seconds/addresses are meant to keep propagating as a liveness heartbeat, so suppressing a "duplicate" would silently stop legitimate freshness updates from reaching remote gateways. A forward that fails is reported via onError and swallowed, matching every other best-effort gossip send in this codebase. */
|
|
6
|
-
export function forwardAdvertsToHub(hub, directory, onError, hasAnyTrustedGateway) {
|
|
7
|
-
if (!hub.isConnected)
|
|
8
|
-
return;
|
|
9
|
-
if (!hasAnyTrustedGateway())
|
|
10
|
-
return;
|
|
11
|
-
const eligible = directory.filter((entry) => entry.advert[AGENT_SELF_GOSSIP_KEY] !== undefined);
|
|
12
|
-
if (eligible.length === 0)
|
|
13
|
-
return;
|
|
14
|
-
hub.advertiseDevices(eligible).catch((error) => {
|
|
15
|
-
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
/** Catches the hub up with every local device already known at the moment this side becomes the gateway (connectHub's own trailing call, right after hub.connect resolves) -- without this, a device whose own last gossip arrived before this coordinator took over the gateway role would never be (re-)advertised until its own next periodic gossip tick (hub-side state is rebuilt from scratch on every takeover, per coordinator-gateway.ts's own class doc). Reuses forwardAdvertsToHub's own eligibility filter and trust gate, so only ever a visible, agent/self-bearing device is pushed, and only once a remote gateway is trusted, exactly as an ordinary directory-change forward would. */
|
|
19
|
-
export function pushHubCatchUp(hub, knownDevices, onError, hasAnyTrustedGateway) {
|
|
20
|
-
// knownDevices also holds every remote device learned from this same hub, and the hub already has those adverts from their own senders. Handing one back would register it against this gateway's connection if it were ever fresher than the hub's copy, and is otherwise dropped by the hub's freshness rule, so only the devices this side fronts are advertised.
|
|
21
|
-
const learnedFromHub = new Set(hub.peers());
|
|
22
|
-
const catchUp = [...knownDevices.entries()]
|
|
23
|
-
.filter(([deviceHex]) => !learnedFromHub.has(deviceHex))
|
|
24
|
-
.map(([, advert]) => ({ device: advert.device, advert }));
|
|
25
|
-
forwardAdvertsToHub(hub, catchUp, onError, hasAnyTrustedGateway);
|
|
26
|
-
}
|
|
27
|
-
/** Builds a forwardToLocalPeer callback closing over peerSessions -- a factory rather than a plain function taking peerSessions as its own first parameter, so the returned callback's shape matches HubSessionDeps.forwardToLocalPeer's own fixed external contract exactly (deviceHex, command, scope, token?) with no positional-binding tricks needed at the call site. Sends a room-domain manage-request to a LOCAL peer session only, never falling back to hub routing -- HubSession's own toDevice-forwarding leg (agent-comms#184: a hub-relayed request explicitly addressed to a non-gateway local peer this gateway also fronts). Returns undefined when no local session exists for that device-id, in which case HubSession falls back to dispatching the request against this gateway's own local state instead. */
|
|
28
|
-
export function makeSendToLocalPeer(peerSessions) {
|
|
29
|
-
// Deliberately not `async`: the "no local session" signal is a synchronous `undefined` (HubSession branches on `forwarded !== undefined` to decide whether to fall back to local dispatch), and an async wrapper would turn it into a Promise that is always defined.
|
|
30
|
-
return (memberId, command, scope, token) => {
|
|
31
|
-
const session = peerSessions.get(memberId);
|
|
32
|
-
if (session === undefined)
|
|
33
|
-
return undefined;
|
|
34
|
-
return session.sendManageRequest(command, scope, undefined, token);
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
/** Routes a room-domain request through the hub's relay-connect/relay-data pairing when memberId isn't a local peer session -- WireMeshTransport.sendRoomRequest's own fallback, since the member may be a remote agent reachable only via this machine's gateway connection (agent-comms#155's local-to-remote leg). WireMeshTransport.sendRoomRequest itself gates memberId against the gateway trust boundary (agent-comms#156) before ever calling this, so by the time this runs memberId is already known-trusted -- this function stays focused on the hub-connectivity outcome alone. Resolves the same not_connected outcome sendRoomRequest already returned before the hub existed at all when this side isn't currently the gateway. */
|
|
4
|
+
/** Routes a room-domain request through the hub's relay-connect/relay-data pairing when memberId isn't a local peer session -- WireMeshTransport.sendRoomRequest's own fallback, since the member may be a remote agent reachable only through the hub this store is connected to (agent-comms#155's local-to-remote leg). WireMeshTransport.sendRoomRequest itself gates memberId against the gateway trust boundary (agent-comms#156) before ever calling this, so by the time this runs memberId is already known-trusted -- this function stays focused on the hub-connectivity outcome alone. Resolves the same not_connected outcome sendRoomRequest already returned before the hub existed at all when this side holds no hub session, which is a store that has no agent yet, has a ghost agent, or whose machine trusts nobody. */
|
|
38
5
|
export async function routeRoomRequestViaHub(options) {
|
|
39
6
|
const { hub, memberId, command, scope, token } = options;
|
|
40
7
|
if (!hub.isConnected)
|
|
41
8
|
return { result: "error", code: "not_connected" };
|
|
42
9
|
return hub.sendRoomRequest(memberId, command, scope, token);
|
|
43
10
|
}
|
|
44
|
-
/** Dials the hub and immediately pushes a catch-up of every already-known local device onto it (agent-comms#154's own hub-connection-establishment sequence, kept together here rather than split across two call-site statements in WireMeshTransport.connectHub) -- without the trailing catch-up, a device whose own last gossip arrived before this coordinator took over the gateway role would stay invisible on the hub until its own next periodic gossip tick. */
|
|
45
|
-
export async function connectHubGateway(options) {
|
|
46
|
-
const { hub, url, knownDevices, onError, hasAnyTrustedGateway } = options;
|
|
47
|
-
await hub.connect(url);
|
|
48
|
-
pushHubCatchUp(hub, knownDevices, onError, hasAnyTrustedGateway);
|
|
49
|
-
}
|
|
50
11
|
//# sourceMappingURL=hub-forwarding.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hub-forwarding.js","sourceRoot":"","sources":["../../src/core/hub-forwarding.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"hub-forwarding.js","sourceRoot":"","sources":["../../src/core/hub-forwarding.ts"],"names":[],"mappings":"AAAA;;GAEG;AAUH,6yBAA6yB;AAC7yB,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,OAM5C;IACC,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IACzD,IAAI,CAAC,GAAG,CAAC,WAAW;QAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC;IACxE,OAAO,GAAG,CAAC,eAAe,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;AAC9D,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HubLink keeps one store's own session on the relay hub alive while the store should have one: it dials in the background, waits for the session to end, and dials again after a delay that doubles up to a ceiling while the hub stays unreachable. Every store owns one (agent-comms#293), because reachability from another machine has to belong to the device itself and not to whichever store on the machine happens to hold the local coordinator role.
|
|
3
|
+
*
|
|
4
|
+
* The store decides through shouldConnect whether it wants a session at all, and a change of mind, made in this process or by another one editing the shared trust file, is picked up within a poll interval, or at once through reconsider(). A store with nothing to say to another machine (nothing trusted, no agent, or a ghost agent) holds no session, so it puts nothing on the hub, not even its device id.
|
|
5
|
+
*
|
|
6
|
+
* The dial never blocks the caller: a bridge starting up offline must not wait out a connect timeout before it can serve its tools, and a hub that is down or restarting is an ordinary, recoverable state. Failures reach onError and the loop carries on.
|
|
7
|
+
*/
|
|
8
|
+
import type { HubSession } from "./hub-session.js";
|
|
9
|
+
/** Delay before the first redial after a failed dial or a lost session. */
|
|
10
|
+
export declare const HUB_RECONNECT_INITIAL_DELAY_MS = 1000;
|
|
11
|
+
/** Ceiling the redial delay doubles up to while the hub stays unreachable, so an outage is retried about once a minute rather than backing off without bound. */
|
|
12
|
+
export declare const HUB_RECONNECT_MAX_DELAY_MS = 60000;
|
|
13
|
+
/** How often a link re-evaluates shouldConnect while it has nothing to react to: a store that wants no session notices it now wants one, and a store holding one notices it no longer should, without anyone calling reconsider(). Bounds how long a trust change made by another process on the machine takes to show. */
|
|
14
|
+
export declare const HUB_LINK_POLL_INTERVAL_MS = 5000;
|
|
15
|
+
/** The parts of a HubSession a HubLink drives. */
|
|
16
|
+
export type HubLinkSession = Pick<HubSession, "connect" | "disconnect" | "isConnected" | "whenDisconnected">;
|
|
17
|
+
export interface HubLinkOptions {
|
|
18
|
+
hub: Readonly<HubLinkSession>;
|
|
19
|
+
/** The hub to hold a session on. */
|
|
20
|
+
url: string;
|
|
21
|
+
/** Whether the store wants a hub session right now. Read every poll interval and on reconsider(); a session held when this turns false is dropped, and none is dialled while it is false. */
|
|
22
|
+
shouldConnect: () => boolean;
|
|
23
|
+
/** Called each time a session is established, after the dial resolves. WireMeshTransport uses it to advertise this store's presence at once instead of waiting for the next presence tick. */
|
|
24
|
+
onConnected: () => void;
|
|
25
|
+
/** Reports a failed dial. Never called for anything else. */
|
|
26
|
+
onError: (error: Error) => void;
|
|
27
|
+
/** Overrides HUB_RECONNECT_INITIAL_DELAY_MS, for a test that must not wait a second per redial. */
|
|
28
|
+
initialDelayMs?: number;
|
|
29
|
+
/** Overrides HUB_RECONNECT_MAX_DELAY_MS. */
|
|
30
|
+
maxDelayMs?: number;
|
|
31
|
+
/** Overrides HUB_LINK_POLL_INTERVAL_MS. */
|
|
32
|
+
pollIntervalMs?: number;
|
|
33
|
+
}
|
|
34
|
+
export declare class HubLink {
|
|
35
|
+
private readonly options;
|
|
36
|
+
private readonly initialDelayMs;
|
|
37
|
+
private readonly maxDelayMs;
|
|
38
|
+
private readonly pollIntervalMs;
|
|
39
|
+
private stopped;
|
|
40
|
+
private running;
|
|
41
|
+
/** Cancels the wait between dials, so stop() does not sit out a delay. */
|
|
42
|
+
private wake;
|
|
43
|
+
constructor(options: Readonly<HubLinkOptions>);
|
|
44
|
+
/** Begins running in the background and returns at once. A second call while running does nothing. */
|
|
45
|
+
start(): void;
|
|
46
|
+
/** Re-evaluates shouldConnect now instead of at the next poll, and cuts short any wait, including the delay before a redial. Call it when something shouldConnect reads has changed. */
|
|
47
|
+
reconsider(): void;
|
|
48
|
+
/** Stops, drops the live session, and resolves once the loop has ended. */
|
|
49
|
+
stop(): Promise<void>;
|
|
50
|
+
/** A method call rather than a direct property read, so TypeScript's control-flow narrowing does not treat the flag as staying false across the awaits in run(), during which stop() can set it. */
|
|
51
|
+
private isStopped;
|
|
52
|
+
private run;
|
|
53
|
+
/** Holds the live session until the hub ends it, resolving true, or until the store stops wanting it, when it drops the session itself and resolves false. */
|
|
54
|
+
private holdWhileWanted;
|
|
55
|
+
/** Waits ms, or until reconsider() or stop() cuts it short. Each wait clears the wake handle only if it is still its own, since holdWhileWanted leaves a losing wait pending and its late end must not wipe the handle of the wait that replaced it. */
|
|
56
|
+
private sleep;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=hub-link.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hub-link.d.ts","sourceRoot":"","sources":["../../src/core/hub-link.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,2EAA2E;AAC3E,eAAO,MAAM,8BAA8B,OAAO,CAAC;AAEnD,iKAAiK;AACjK,eAAO,MAAM,0BAA0B,QAAS,CAAC;AAEjD,2TAA2T;AAC3T,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAK9C,kDAAkD;AAClD,MAAM,MAAM,cAAc,GAAG,IAAI,CAC/B,UAAU,EACV,SAAS,GAAG,YAAY,GAAG,aAAa,GAAG,kBAAkB,CAC9D,CAAC;AAEF,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC;IAC9B,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,6LAA6L;IAC7L,aAAa,EAAE,MAAM,OAAO,CAAC;IAC7B,8LAA8L;IAC9L,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,6DAA6D;IAC7D,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAChC,mGAAmG;IACnG,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,OAAO;IASN,OAAO,CAAC,QAAQ,CAAC,OAAO;IARpC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAA4B;IAC3C,0EAA0E;IAC1E,OAAO,CAAC,IAAI,CAA2B;gBAEV,OAAO,EAAE,QAAQ,CAAC,cAAc,CAAC;IAO9D,sGAAsG;IACtG,KAAK,IAAI,IAAI;IAKb,wLAAwL;IACxL,UAAU,IAAI,IAAI;IAIlB,2EAA2E;IACrE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B,oMAAoM;IACpM,OAAO,CAAC,SAAS;YAIH,GAAG;IA+BjB,8JAA8J;YAChJ,eAAe;IAmB7B,wPAAwP;YAC1O,KAAK;CAapB"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HubLink keeps one store's own session on the relay hub alive while the store should have one: it dials in the background, waits for the session to end, and dials again after a delay that doubles up to a ceiling while the hub stays unreachable. Every store owns one (agent-comms#293), because reachability from another machine has to belong to the device itself and not to whichever store on the machine happens to hold the local coordinator role.
|
|
3
|
+
*
|
|
4
|
+
* The store decides through shouldConnect whether it wants a session at all, and a change of mind, made in this process or by another one editing the shared trust file, is picked up within a poll interval, or at once through reconsider(). A store with nothing to say to another machine (nothing trusted, no agent, or a ghost agent) holds no session, so it puts nothing on the hub, not even its device id.
|
|
5
|
+
*
|
|
6
|
+
* The dial never blocks the caller: a bridge starting up offline must not wait out a connect timeout before it can serve its tools, and a hub that is down or restarting is an ordinary, recoverable state. Failures reach onError and the loop carries on.
|
|
7
|
+
*/
|
|
8
|
+
/** Delay before the first redial after a failed dial or a lost session. */
|
|
9
|
+
export const HUB_RECONNECT_INITIAL_DELAY_MS = 1000;
|
|
10
|
+
/** Ceiling the redial delay doubles up to while the hub stays unreachable, so an outage is retried about once a minute rather than backing off without bound. */
|
|
11
|
+
export const HUB_RECONNECT_MAX_DELAY_MS = 60_000;
|
|
12
|
+
/** How often a link re-evaluates shouldConnect while it has nothing to react to: a store that wants no session notices it now wants one, and a store holding one notices it no longer should, without anyone calling reconsider(). Bounds how long a trust change made by another process on the machine takes to show. */
|
|
13
|
+
export const HUB_LINK_POLL_INTERVAL_MS = 5000;
|
|
14
|
+
/** Factor the redial delay grows by after each failed dial. */
|
|
15
|
+
const RECONNECT_DELAY_GROWTH_FACTOR = 2;
|
|
16
|
+
export class HubLink {
|
|
17
|
+
options;
|
|
18
|
+
initialDelayMs;
|
|
19
|
+
maxDelayMs;
|
|
20
|
+
pollIntervalMs;
|
|
21
|
+
stopped = false;
|
|
22
|
+
running;
|
|
23
|
+
/** Cancels the wait between dials, so stop() does not sit out a delay. */
|
|
24
|
+
wake;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.options = options;
|
|
27
|
+
this.initialDelayMs =
|
|
28
|
+
options.initialDelayMs ?? HUB_RECONNECT_INITIAL_DELAY_MS;
|
|
29
|
+
this.maxDelayMs = options.maxDelayMs ?? HUB_RECONNECT_MAX_DELAY_MS;
|
|
30
|
+
this.pollIntervalMs = options.pollIntervalMs ?? HUB_LINK_POLL_INTERVAL_MS;
|
|
31
|
+
}
|
|
32
|
+
/** Begins running in the background and returns at once. A second call while running does nothing. */
|
|
33
|
+
start() {
|
|
34
|
+
if (this.running !== undefined || this.stopped)
|
|
35
|
+
return;
|
|
36
|
+
this.running = this.run();
|
|
37
|
+
}
|
|
38
|
+
/** Re-evaluates shouldConnect now instead of at the next poll, and cuts short any wait, including the delay before a redial. Call it when something shouldConnect reads has changed. */
|
|
39
|
+
reconsider() {
|
|
40
|
+
this.wake?.();
|
|
41
|
+
}
|
|
42
|
+
/** Stops, drops the live session, and resolves once the loop has ended. */
|
|
43
|
+
async stop() {
|
|
44
|
+
this.stopped = true;
|
|
45
|
+
this.wake?.();
|
|
46
|
+
await this.options.hub.disconnect();
|
|
47
|
+
await this.running;
|
|
48
|
+
}
|
|
49
|
+
/** A method call rather than a direct property read, so TypeScript's control-flow narrowing does not treat the flag as staying false across the awaits in run(), during which stop() can set it. */
|
|
50
|
+
isStopped() {
|
|
51
|
+
return this.stopped;
|
|
52
|
+
}
|
|
53
|
+
async run() {
|
|
54
|
+
let delayMs = this.initialDelayMs;
|
|
55
|
+
while (!this.isStopped()) {
|
|
56
|
+
if (!this.options.shouldConnect()) {
|
|
57
|
+
await this.sleep(this.pollIntervalMs);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
let sessionEnded = false;
|
|
61
|
+
try {
|
|
62
|
+
await this.options.hub.connect(this.options.url);
|
|
63
|
+
if (this.options.hub.isConnected) {
|
|
64
|
+
delayMs = this.initialDelayMs;
|
|
65
|
+
this.options.onConnected();
|
|
66
|
+
sessionEnded = await this.holdWhileWanted();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
this.options.onError(error instanceof Error ? error : new Error(String(error)));
|
|
71
|
+
sessionEnded = true;
|
|
72
|
+
}
|
|
73
|
+
if (this.isStopped())
|
|
74
|
+
return;
|
|
75
|
+
if (!sessionEnded)
|
|
76
|
+
continue;
|
|
77
|
+
await this.sleep(delayMs);
|
|
78
|
+
delayMs = Math.min(delayMs * RECONNECT_DELAY_GROWTH_FACTOR, this.maxDelayMs);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Holds the live session until the hub ends it, resolving true, or until the store stops wanting it, when it drops the session itself and resolves false. */
|
|
82
|
+
async holdWhileWanted() {
|
|
83
|
+
const hubEnded = this.options.hub
|
|
84
|
+
.whenDisconnected()
|
|
85
|
+
.then(() => "ended");
|
|
86
|
+
while (!this.isStopped()) {
|
|
87
|
+
const outcome = await Promise.race([
|
|
88
|
+
hubEnded,
|
|
89
|
+
this.sleep(this.pollIntervalMs).then(() => "woke"),
|
|
90
|
+
]);
|
|
91
|
+
if (outcome === "ended")
|
|
92
|
+
return true;
|
|
93
|
+
if (this.isStopped())
|
|
94
|
+
return false;
|
|
95
|
+
if (!this.options.shouldConnect()) {
|
|
96
|
+
await this.options.hub.disconnect();
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
/** Waits ms, or until reconsider() or stop() cuts it short. Each wait clears the wake handle only if it is still its own, since holdWhileWanted leaves a losing wait pending and its late end must not wipe the handle of the wait that replaced it. */
|
|
103
|
+
async sleep(ms) {
|
|
104
|
+
let wakeThis;
|
|
105
|
+
await new Promise((resolve) => {
|
|
106
|
+
const timer = setTimeout(resolve, ms);
|
|
107
|
+
timer.unref();
|
|
108
|
+
wakeThis = () => {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
resolve();
|
|
111
|
+
};
|
|
112
|
+
this.wake = wakeThis;
|
|
113
|
+
});
|
|
114
|
+
if (this.wake === wakeThis)
|
|
115
|
+
this.wake = undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=hub-link.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hub-link.js","sourceRoot":"","sources":["../../src/core/hub-link.ts"],"names":[],"mappings":"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"}
|
|
@@ -22,6 +22,8 @@ export interface HubSessionDeps {
|
|
|
22
22
|
onFrame: (connection: Readonly<Connection>, frame: Readonly<Frame>) => void | Promise<void>;
|
|
23
23
|
/** Tracks the session for shutdown -- every session the transport ever creates, always. */
|
|
24
24
|
trackForShutdown: (session: AcceptedMeshSession) => void;
|
|
25
|
+
/** Stops tracking a session that has ended, so a store that redials the hub for as long as it runs does not accumulate dead sessions the transport would keep gossiping to. */
|
|
26
|
+
untrack: (session: AcceptedMeshSession) => void;
|
|
25
27
|
/** Fires with every remote (non-self) directory entry the hub's own gossip/catch-up surfaces, every time the session's directory changes (agent-comms#155's own remote-directory-merge leg) -- the transport merges these into its own mesh-wide knownDevices the same way it already merges a local peer session's directory, so a hub-learned agent surfaces in listAgents/getAgent with no separate lookup path. */
|
|
26
28
|
onDirectory: (entries: readonly DirectoryEntry[]) => void;
|
|
27
29
|
/** Dispatches an already-received, non-legacy-frame request (a real core/room verb: room.send, room.notify, room.join, ...) to WireMeshTransport's own roomRouter through its relayed-request entry point, which shares the room-verb dispatch a local peer session's own drainSession uses but never routes a legacy frame -- the outbound half of agent-comms#155's "remote to local" routing leg. Deferred the same lazy-`this`-capture way DeliveryEngine's own sendRoomRequestToMember closure is, since roomRouter is constructed after this class's own instance in WireMeshTransport's constructor. Declared with the identical (request, handle, origin?) shape RoomRouter.handleRequest itself has (agent-comms#216) so WireMeshTransport can keep wiring this dep as a direct method reference rather than a wrapper -- dispatchHubRequest below is what actually supplies a non-empty origin, carrying this side's own dialled hub address. */
|
|
@@ -30,17 +32,15 @@ export interface HubSessionDeps {
|
|
|
30
32
|
isTrusted: (deviceHex: string) => boolean;
|
|
31
33
|
/** Whether a gossiped directory entry may be merged (agent-comms#156, widened by #187 and #266): its device is trusted by id, or is itself a trusted principal's own device (agent-comms#192), or carries a membership proof a trusted principal vouches for. A gossiped advert is self-asserted, so the proof is what makes trusting a principal cover its devices; verifying one is cryptography, hence asynchronous. */
|
|
32
34
|
admitEntries: (entries: readonly DirectoryEntry[]) => Promise<DirectoryEntry[]>;
|
|
33
|
-
/** Forwards a room-domain manage-request on to a specific LOCAL peer session (one this gateway is directly connected to over the ordinary local mesh, keyed by device-id hex) rather than dispatching it against this gateway's own local state -- consume()'s own toDevice disambiguation (agent-comms#184, wire-mesh-core 1.48.1's IncomingManageRequest.toDevice). Returns undefined when no local session exists for that device-id, in which case consume() falls back to handleRoomRequest exactly as it always has. */
|
|
34
|
-
forwardToLocalPeer: (deviceHex: string, command: ManageCommand, scope: Readonly<CapabilityScope>, token?: CapabilityToken) => Promise<ManageOutcome> | undefined;
|
|
35
35
|
}
|
|
36
36
|
export declare class HubSession {
|
|
37
37
|
private readonly deps;
|
|
38
38
|
private session;
|
|
39
|
-
/** The raw connection underlying `session` -- MeshSession's own sendGossipUpdate only ever advertises this side's own single device, so advertising OTHER (local mesh) devices onto the hub (agent-comms#155's outbound leg) needs a raw gossip frame sent directly, bypassing that per-session single-self-advert limit. */
|
|
40
|
-
private connection;
|
|
41
39
|
/** The URL this side itself dialled to reach the hub -- tracePath's own local.hubAddress (agent-comms#199), the equivalent of wire-mesh-core's own TracePathOptions.localHubAddress. */
|
|
42
40
|
private url;
|
|
43
41
|
private readonly hubPeersKnown;
|
|
42
|
+
/** Resolvers of every whenDisconnected() call still waiting for the live session to end. */
|
|
43
|
+
private readonly disconnectWaiters;
|
|
44
44
|
constructor(deps: Readonly<HubSessionDeps>);
|
|
45
45
|
/** This node's own device-id, hex-encoded -- the identifier hub peers address it by. */
|
|
46
46
|
ownDeviceHex(): Promise<string>;
|
|
@@ -48,6 +48,10 @@ export declare class HubSession {
|
|
|
48
48
|
get isConnected(): boolean;
|
|
49
49
|
/** Whether the given session object is this side's own currently-held hub session -- lets WireMeshTransport distinguish the hub's session from an ordinary local-peer session within its own allSessions bookkeeping (agent-comms#156's own readvertiseGossip gate needs this) without this class ever exposing the raw session object itself. */
|
|
50
50
|
ownsSession(session: AcceptedMeshSession): boolean;
|
|
51
|
+
/** Resolves once no hub session is live: at once when none is, otherwise when the current one ends, whether the hub closed it or disconnect() dropped it. HubLink waits on this to know when to dial again. */
|
|
52
|
+
whenDisconnected(): Promise<void>;
|
|
53
|
+
/** Forgets the held session and releases everyone waiting in whenDisconnected(). */
|
|
54
|
+
private clearSession;
|
|
51
55
|
/** Drops the held hub connection. A no-op if none is live (connect() was never called, disconnect() already ran, or the hub itself already closed the session). */
|
|
52
56
|
disconnect(): Promise<void>;
|
|
53
57
|
/** The device ids (hex) of peers discovered through the hub's gossiped directory. */
|
|
@@ -57,36 +61,31 @@ export declare class HubSession {
|
|
|
57
61
|
/**
|
|
58
62
|
* The single consumer of one hub session's own event stream: merges the hub's directory (its catch-up arrives as the first event, and it is refreshed on every subsequent one) and, once the session ends, drops the held connection so isConnected answers honestly.
|
|
59
63
|
*
|
|
60
|
-
* Single is the operative word. wire-mesh-core delivers each event to exactly one waiting reader, so two concurrent loops over the same stream split it between them
|
|
64
|
+
* Single is the operative word. wire-mesh-core delivers each event to exactly one waiting reader, so two concurrent loops over the same stream split it between them, and the closed event reaches whichever happened to be waiting first. If that is the directory loop, the connection stays recorded as live for the rest of the process.
|
|
61
65
|
*
|
|
62
66
|
* Every trusted remote entry is surfaced via onDirectory, so the transport can merge it into its own mesh-wide knownDevices (agent-comms#155's remote-directory-merge leg). Filtered to admitEntries (agent-comms#156, widened by agent-comms#187's principal allowlist per agent-comms#192 and by #266's membership proofs) before either hubPeersKnown tracking or onDirectory sees it: an untrusted device's gossiped presence is not merely withheld from listAgents, it is never even recorded as "known" here, so nothing downstream can act on it via any path this class exposes.
|
|
63
67
|
*/
|
|
64
68
|
private consumeEvents;
|
|
65
|
-
/** Consumes one hub session's inbound relayed manage-requests until it ends, dispatching each in arrival order to dispatchHubRequest -- see that function's own doc for the actual per-request trust decision. Split out purely so the decision itself is directly unit-testable against fake requests/deps (hub-session-dispatch.test.ts), the same reason hub-forwarding.ts's own
|
|
69
|
+
/** Consumes one hub session's inbound relayed manage-requests until it ends, dispatching each in arrival order to dispatchHubRequest -- see that function's own doc for the actual per-request trust decision. Split out purely so the decision itself is directly unit-testable against fake requests/deps (hub-session-dispatch.test.ts), the same reason hub-forwarding.ts's own routeRoomRequestViaHub is a standalone function rather than a private method. Passes this.url through as dispatchHubRequest's own hubAddress -- this side's own dialled address for the hub every request on this specific session arrived over (agent-comms#216), the answering-side equivalent of tracePath's own local.hubAddress above. */
|
|
66
70
|
private consume;
|
|
67
|
-
/** Gossips a raw `gossip` frame carrying every given entry's own advert onto the hub, over the raw connection rather than sendGossipUpdate (which can only ever advertise this side's own single device) -- the outbound leg of agent-comms#155's gateway forwarding. relay-hub.ts registers each advert's own `device` field against the CONNECTION it arrives on, so every entry passed here becomes reachable, cross-machine, as "via this gateway" -- callers are responsible for only ever passing entries that are actually meant to be advertised (WireMeshTransport filters to local, visible-agent-bearing entries before calling this). Throws if this side isn't currently connected to a hub, matching sendToPeer's own contract -- callers gate on isConnected first. */
|
|
68
|
-
advertiseDevices(entries: readonly DirectoryEntry[]): Promise<void>;
|
|
69
71
|
/** Sends a real core/room manage-request to a specific hub-reachable peer, through the hub's relay-connect/relay-data pairing -- the local-to-remote leg of agent-comms#155's routing, mirroring MeshTransport.sendRoomRequest's own not_connected/outcome contract so WireMeshTransport can fall back to this uniformly when memberId isn't a local peer session. Bounded by HUB_ROOM_REQUEST_TIMEOUT_MS (see its own doc) since an unreachable target-device is silently dropped by the hub with no error frame, unlike a local session's own connection-level failure. */
|
|
70
72
|
sendRoomRequest(peerDeviceHex: string, command: ManageCommand, scope: Readonly<CapabilityScope>, token?: CapabilityToken): Promise<ManageOutcome>;
|
|
71
73
|
/** Sends path.trace to a specific hub-reachable peer, through the hub's relay-connect/relay-data pairing -- WireMeshTransport.meshTrace's own hub-relayed fallback (agent-comms#199), mirroring sendRoomRequest's own not_connected contract and HUB_ROOM_REQUEST_TIMEOUT_MS default for the identical reason (an unreachable target-device is silently dropped by the hub, not answered with an error). Delegates the actual send/RTT-measurement/response-parsing to wire-mesh-core's own tracePath, reporting this.url (the address this side itself dialled to reach the hub) as local.hubAddress. */
|
|
72
74
|
tracePath(peerDeviceHex: string, timeoutMs?: number): Promise<MeshTraceResult>;
|
|
73
75
|
}
|
|
74
76
|
/** The slice of HubSessionDeps dispatchHubRequest actually needs. Named so its own tests, and consume()'s call site, don't repeat the same Pick inline. */
|
|
75
|
-
export type HubRequestDispatchDeps = Pick<HubSessionDeps, "isTrusted" | "handleRoomRequest"
|
|
77
|
+
export type HubRequestDispatchDeps = Pick<HubSessionDeps, "isTrusted" | "handleRoomRequest">;
|
|
76
78
|
/**
|
|
77
|
-
* Decides what to do with one inbound relayed manage-request, keeping every downstream consumer's handle keyed by the SENDING device (request.fromDevice names it on relay-routed requests) rather than by
|
|
79
|
+
* Decides what to do with one inbound relayed manage-request, keeping every downstream consumer's handle keyed by the SENDING device (request.fromDevice names it on relay-routed requests) rather than by the hub.
|
|
78
80
|
*
|
|
79
|
-
* A request with no fromDevice at all is refused before anything below sees it. A room-domain verb's own downstream handler (
|
|
81
|
+
* A request with no fromDevice at all is refused before anything below sees it. A room-domain verb's own downstream handler (a capability-token check such as verifyRoomToken) calls deviceIdFromHex on whatever handle.id it's given, which throws on anything that isn't valid hex rather than answering with an ordinary unauthorized outcome. Answering unauthorized here, before that, is what keeps this a clean, fast failure for the request's own caller instead of an unhandled rejection that would kill this session's whole drain loop.
|
|
80
82
|
*
|
|
81
83
|
* Only room-domain verbs are accepted over a relay at all. The legacy FRAME_VERB carriage (an opaque MeshMessage, which local peer sessions still use for mesh-state gossip) is refused here with unsupported_verb whoever sent it and whether or not that sender is trusted: a relayed sender has passed neither connect_request approval nor the coordinator's own membership, so it has no business feeding state_sync or state_update into this side's mesh state, and nothing on the relay path has any other use for a bare frame. Refusing the verb wholesale is the allow-list; there is no per-message-type list to keep in step with new MeshMessage variants (agent-comms#169, agent-comms#268). The router's own handleRelayedRequest refuses a frame too, so the local branch below cannot deliver one by construction; the check here is what stops a frame being forwarded on to a local peer session, whose own drain loop would otherwise treat it as an ordinary local frame.
|
|
82
84
|
*
|
|
83
|
-
* A real room-domain verb (room.send, room.join, room.notify, ...) is never gated on isTrusted at all (agent-comms#192): it is independently gated by its own room:member capability token, verified regardless of which transport path it arrived over, and already principal-aware since agent-comms#187 (device-membership-verification.ts's own chain-walk). Stacking the coarse bare-device allowlist in front of that check would be redundant, not protective, and for room.join specifically (the one deliberately ungated verb, whose own security model is a human decision) it would actively defeat the protocol's own intended design by never letting an untrusted device reach that approval step at all. onKnownPeer is still only called here for a sender isTrusted already recognises: reaching this branch has not yet had its own token verified (that happens inside handleRoomRequest
|
|
84
|
-
*
|
|
85
|
-
* Multi-device gateway routing (agent-comms#184, wire-mesh-core 1.48.1's own IncomingManageRequest.toDevice, read directly from each relay-data frame's own to-device field rather than guessed from pairing state): when the request carries a toDevice naming a different device than ownDeviceHex, the command is first re-stamped with an on-behalf-of params field naming the true sender before forwardToLocalPeer forwards it on to that device's own local mesh session (one this gateway is directly connected to, never merely gossiped-about); room-router.ts's own resolveHandle reads that field back out on the receiving end, so the forwarded request is attributed to the true remote sender there, never to this gateway. Its outcome is relayed straight back. Only when forwardToLocalPeer finds no such local session (toDevice is absent, matches ownDeviceHex, or names a device this gateway doesn't actually front) does the request fall through to handleRoomRequest, dispatched against this side's own local mesh state, the same fallback a sender still on a pre-#184 wire-mesh-core (never stamping toDevice at all) already relied on -- and it's exactly that fallback call which carries hubAddress on to handleRoomRequest as origin.relayHubAddress (agent-comms#216), never the forwardToLocalPeer branch: a request forwarded on to a different local device is answered by that device's own session, not this gateway's hub session, so this gateway's own dialled hub address would be the wrong fact to attach to it.
|
|
85
|
+
* A real room-domain verb (room.send, room.join, room.notify, ...) is never gated on isTrusted at all (agent-comms#192): it is independently gated by its own room:member capability token, verified regardless of which transport path it arrived over, and already principal-aware since agent-comms#187 (device-membership-verification.ts's own chain-walk). Stacking the coarse bare-device allowlist in front of that check would be redundant, not protective, and for room.join specifically (the one deliberately ungated verb, whose own security model is a human decision) it would actively defeat the protocol's own intended design by never letting an untrusted device reach that approval step at all. onKnownPeer is still only called here for a sender isTrusted already recognises: reaching this branch has not yet had its own token verified (that happens inside handleRoomRequest), so hubPeersKnown stays "gateway-trusted hub peers", never "every device that has sent a syntactically valid room-domain request".
|
|
86
86
|
*/
|
|
87
87
|
export declare function dispatchHubRequest(options: {
|
|
88
88
|
request: IncomingManageRequest;
|
|
89
|
-
ownDeviceHex: string;
|
|
90
89
|
deps: Readonly<HubRequestDispatchDeps>;
|
|
91
90
|
onKnownPeer: (deviceHex: string) => void;
|
|
92
91
|
hubAddress?: string | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hub-session.d.ts","sourceRoot":"","sources":["../../src/core/hub-session.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EACnB,MAAM,oCAAoC,CAAC;AAG5C,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,KAAK,EACL,aAAa,EACd,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EACf,eAAe,EAChB,MAAM,gBAAgB,CAAC;AAKxB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAI1D,2fAA2f;AAE3f,MAAM,WAAW,cAAc;IAC7B,oIAAoI;IACpI,yBAAyB,EAAE,MAAM,CAAC;IAClC,yDAAyD;IACzD,aAAa,EAAE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;IAC/C,MAAM,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IAClC,yHAAyH;IACzH,cAAc,EAAE,MAAM,OAAO,CAAC;IAC9B,wEAAwE;IACxE,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,sIAAsI;IACtI,OAAO,EAAE,CACP,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,EAChC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,KACnB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,2FAA2F;IAC3F,gBAAgB,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACzD,uZAAuZ;IACvZ,WAAW,EAAE,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,KAAK,IAAI,CAAC;IAC1D,25BAA25B;IAC35B,iBAAiB,EAAE,CACjB,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE,QAAQ,CAAC,gBAAgB,CAAC,EAClC,MAAM,CAAC,EAAE,QAAQ,CAAC,iBAAiB,CAAC,KACjC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,oaAAoa;IACpa,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;IAC1C,2ZAA2Z;IAC3Z,YAAY,EAAE,CACZ,OAAO,EAAE,SAAS,cAAc,EAAE,KAC/B,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"hub-session.d.ts","sourceRoot":"","sources":["../../src/core/hub-session.ts"],"names":[],"mappings":"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"}
|