wire-mesh-core 1.54.0 → 1.56.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/dist/adapters/frame-codec.d.cts +1 -1
- package/dist/adapters/frame-codec.d.mts +1 -1
- package/dist/adapters/node-identity.d.cts +2 -2
- package/dist/adapters/node-identity.d.mts +2 -2
- package/dist/adapters/tcp-transport.d.cts +1 -1
- package/dist/adapters/tcp-transport.d.mts +1 -1
- package/dist/adapters/threshold-identity.d.cts +2 -2
- package/dist/adapters/threshold-identity.d.mts +2 -2
- package/dist/adapters/threshold-wasm.d.cts +1 -1
- package/dist/adapters/threshold-wasm.d.mts +1 -1
- package/dist/adapters/tls-transport.d.cts +1 -1
- package/dist/adapters/tls-transport.d.mts +1 -1
- package/dist/domain/bulk.d.cts +2 -2
- package/dist/domain/bulk.d.mts +2 -2
- package/dist/domain/capability-grant.d.cts +3 -3
- package/dist/domain/capability-grant.d.mts +3 -3
- package/dist/domain/capability-request.d.cts +2 -2
- package/dist/domain/capability-request.d.mts +2 -2
- package/dist/domain/data-sync.d.cts +2 -2
- package/dist/domain/data-sync.d.mts +2 -2
- package/dist/domain/device-id.d.cts +1 -1
- package/dist/domain/device-id.d.mts +1 -1
- package/dist/domain/direct-manage-request.cjs +64 -0
- package/dist/domain/direct-manage-request.d.cts +16 -0
- package/dist/domain/direct-manage-request.d.mts +16 -0
- package/dist/domain/direct-manage-request.mjs +62 -0
- package/dist/domain/gossip-expansion.cjs +44 -0
- package/dist/domain/gossip-expansion.d.cts +29 -0
- package/dist/domain/gossip-expansion.d.mts +29 -0
- package/dist/domain/gossip-expansion.mjs +43 -0
- package/dist/domain/grant-candidates.d.cts +1 -1
- package/dist/domain/grant-candidates.d.mts +1 -1
- package/dist/domain/handshake.d.cts +1 -1
- package/dist/domain/handshake.d.mts +1 -1
- package/dist/domain/mesh-session.cjs +40 -70
- package/dist/domain/mesh-session.d.cts +8 -15
- package/dist/domain/mesh-session.d.mts +8 -15
- package/dist/domain/mesh-session.mjs +39 -69
- package/dist/domain/notice-board.d.cts +3 -3
- package/dist/domain/notice-board.d.mts +3 -3
- package/dist/domain/relay-hub.d.cts +2 -2
- package/dist/domain/relay-hub.d.mts +2 -2
- package/dist/domain/relay-use-gate.d.cts +2 -2
- package/dist/domain/relay-use-gate.d.mts +2 -2
- package/dist/domain/revocation-view.d.cts +2 -2
- package/dist/domain/revocation-view.d.mts +2 -2
- package/dist/domain/room-rekey.d.cts +3 -3
- package/dist/domain/room-rekey.d.mts +3 -3
- package/dist/domain/room-token-verification.d.cts +1 -1
- package/dist/domain/room-token-verification.d.mts +1 -1
- package/dist/domain/room.d.cts +4 -4
- package/dist/domain/room.d.mts +4 -4
- package/dist/domain/shard-manifest.d.cts +1 -1
- package/dist/domain/shard-manifest.d.mts +1 -1
- package/dist/domain/tokens.d.cts +1 -1
- package/dist/domain/tokens.d.mts +1 -1
- package/dist/domain/webrtc-signaling.d.cts +2 -2
- package/dist/domain/webrtc-signaling.d.mts +2 -2
- package/dist/generated/protocol.cjs +5 -1
- package/dist/generated/protocol.d.cts +2 -2
- package/dist/generated/protocol.d.mts +2 -2
- package/dist/generated/protocol.mjs +5 -2
- package/dist/{identity-BXrJ6_YM.d.mts → identity-Br6e0Rx5.d.mts} +1 -1
- package/dist/{identity-CmD4l_Mp.d.cts → identity-pOt5KxJB.d.cts} +1 -1
- package/dist/ports/identity.d.cts +1 -1
- package/dist/ports/identity.d.mts +1 -1
- package/dist/ports/transport.d.cts +1 -1
- package/dist/ports/transport.d.mts +1 -1
- package/dist/{protocol-PhUVprXA.d.cts → protocol-D2Ue2nd2.d.cts} +25 -11
- package/dist/{protocol-PhUVprXA.d.mts → protocol-D2Ue2nd2.d.mts} +25 -11
- package/dist/{room-token-verification-D8HndWYM.d.cts → room-token-verification-CNXMjIQd.d.cts} +2 -2
- package/dist/{room-token-verification-Dv1O6-FV.d.mts → room-token-verification-ZHgJhnsu.d.mts} +2 -2
- package/dist/{tokens-DIPzZ9JJ.d.cts → tokens-BtczHfrc.d.cts} +2 -2
- package/dist/{tokens-BVrgBeqF.d.mts → tokens-Hnq9oURj.d.mts} +2 -2
- package/dist/{transport-CbXX1Xd-.d.cts → transport-C5wU0UtA.d.mts} +1 -1
- package/dist/{transport-DG9ouGe-.d.mts → transport-fUY3alfO.d.cts} +1 -1
- package/package.json +9 -1
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_rolldown_runtime = require("../rolldown-runtime-VH7oDXx4.cjs");
|
|
2
3
|
const require_domain_device_id = require("./device-id.cjs");
|
|
3
4
|
const require_domain_handshake = require("./handshake.cjs");
|
|
4
5
|
const require_adapters_frame_codec = require("../adapters/frame-codec.cjs");
|
|
6
|
+
let package_json = require("../../package.json");
|
|
7
|
+
package_json = require_rolldown_runtime.__toESM(package_json, 1);
|
|
5
8
|
//#region src/domain/relay-pairing.ts
|
|
6
9
|
function createRelayPairings() {
|
|
7
10
|
const established = /* @__PURE__ */ new Map();
|
|
@@ -49,6 +52,24 @@ function createPingRoundTrips() {
|
|
|
49
52
|
};
|
|
50
53
|
}
|
|
51
54
|
//#endregion
|
|
55
|
+
//#region src/domain/own-version.ts
|
|
56
|
+
/** This node's own actual, currently-running wire-mesh-core version -- read once from the installed package's own package.json, never from anything gossiped or cached, so buildSelfAdvert's own self-advert extension and version.get's manage-response both always answer with the truth for this exact running process. */
|
|
57
|
+
const OWN_VERSION = package_json.default.version;
|
|
58
|
+
/** The gossip extension key wire-mesh-core itself injects into every self-advert (mesh-session.ts's own buildSelfAdvert) to report OWN_VERSION -- reserved so a sendGossipUpdate caller can never also supply it, separately from peer-advert's own typed struct fields (RESERVED_PEER_ADVERT_KEYS in mesh-session.ts), since this is just an extension-tail key (transport.cddl), not a struct field. */
|
|
59
|
+
const CORE_VERSION_GOSSIP_KEY = "wire-mesh/version";
|
|
60
|
+
const VERSION_GET_VERB = "version.get";
|
|
61
|
+
/** True when params is the core/version domain's own version-get command (version.cddl) -- a receiver answers this unconditionally, bypassing incomingManageRequests entirely, since it is deliberately ungated: exactly as sensitive as what CORE_VERSION_GOSSIP_KEY already broadcasts unconditionally via gossip. */
|
|
62
|
+
function isVersionGetCommand(params) {
|
|
63
|
+
return "verb" in params && params.verb === VERSION_GET_VERB;
|
|
64
|
+
}
|
|
65
|
+
/** The manage-ok outcome mesh-session.ts answers a version.get request with -- OWN_VERSION on manage-ok's own open `* tstr => any` tail, the same pattern exec-list's own exec-session-info result already uses. */
|
|
66
|
+
function versionGetOutcome() {
|
|
67
|
+
return {
|
|
68
|
+
result: "ok",
|
|
69
|
+
version: OWN_VERSION
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
52
73
|
//#region src/domain/gossip-extensions.ts
|
|
53
74
|
/** peer-advert's own three typed fields -- reserved so a `sendGossipUpdate` caller can never override the session's own device-id, address list, or freshness timestamp by supplying an extension of the same name. */
|
|
54
75
|
const RESERVED_PEER_ADVERT_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -58,10 +79,11 @@ const RESERVED_PEER_ADVERT_KEYS = /* @__PURE__ */ new Set([
|
|
|
58
79
|
]);
|
|
59
80
|
/** A gossip extension key must be domain-qualified as `<domain>/<field>` (lowercase kebab-case each side), per `spec/CONVENTIONS.md`'s gossip-extension-namespacing convention -- this is what stops two independent applications sharing one gossip tail from silently colliding on a bare name like "status". */
|
|
60
81
|
const GOSSIP_EXTENSION_KEY_PATTERN = /^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/;
|
|
61
|
-
/** Rejects a `sendGossipUpdate` extension bag that would either shadow one of peer-advert's own mandatory fields or use a bare, non-domain-qualified key --
|
|
82
|
+
/** Rejects a `sendGossipUpdate` extension bag that would either shadow one of peer-advert's own mandatory fields, use the key wire-mesh-core itself reserves for its own self-reported version, or use a bare, non-domain-qualified key -- all are caller bugs that must fail loudly at the call site, not silently corrupt or ambiguously merge into the wire frame. */
|
|
62
83
|
function validateGossipExtensions(extensions) {
|
|
63
84
|
for (const key of Object.keys(extensions)) {
|
|
64
85
|
if (RESERVED_PEER_ADVERT_KEYS.has(key)) throw new Error(`sendGossipUpdate extension key "${key}" collides with a mandatory peer-advert field`);
|
|
86
|
+
if (key === "wire-mesh/version") throw new Error(`sendGossipUpdate extension key "${key}" is reserved for wire-mesh-core's own self-reported version`);
|
|
65
87
|
if (!GOSSIP_EXTENSION_KEY_PATTERN.test(key)) throw new Error(`sendGossipUpdate extension key "${key}" must be domain-qualified as "<domain>/<field>" (e.g. "presence/status")`);
|
|
66
88
|
}
|
|
67
89
|
}
|
|
@@ -162,12 +184,12 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
162
184
|
pending.resolve(frame.outcome);
|
|
163
185
|
}
|
|
164
186
|
}
|
|
165
|
-
/** relayFrame is present only for a manage-request that arrived wrapped in relay-data, and is that same outer relay-data-frame -- its own to-device/from-device fields carry whatever addressing it received. See IncomingManageRequest's own fromDevice/toDevice doc comments for what each means and why neither is ever guessed from pairing state. */
|
|
187
|
+
/** relayFrame is present only for a manage-request that arrived wrapped in relay-data, and is that same outer relay-data-frame -- its own to-device/from-device fields carry whatever addressing it received. See IncomingManageRequest's own fromDevice/toDevice doc comments for what each means and why neither is ever guessed from pairing state. A version.get request (VERSION_GET_VERB) is answered directly, right here, rather than ever reaching incomingManageRequests -- deliberately ungated (version.cddl), so no application needs to remember to register its own handler for it the way every other verb requires. */
|
|
166
188
|
function applyManageRequest(frame, relayFrame) {
|
|
167
189
|
const requestId = frame["request-id"];
|
|
168
190
|
const fromDevice = relayFrame?.["from-device"];
|
|
169
191
|
const toDevice = relayFrame?.["to-device"];
|
|
170
|
-
|
|
192
|
+
const incoming = {
|
|
171
193
|
requestId,
|
|
172
194
|
command: frame.command,
|
|
173
195
|
scope: frame.scope,
|
|
@@ -187,7 +209,12 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
187
209
|
await transmit(response, relayFrame !== void 0, fromDevice);
|
|
188
210
|
emit();
|
|
189
211
|
}
|
|
190
|
-
}
|
|
212
|
+
};
|
|
213
|
+
if (isVersionGetCommand(frame.command.params)) {
|
|
214
|
+
incoming.respond(versionGetOutcome());
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
emitIncomingManageRequest(incoming);
|
|
191
218
|
}
|
|
192
219
|
function applyFrame(frame) {
|
|
193
220
|
if (frame.type === "relay-data") {
|
|
@@ -301,13 +328,14 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
301
328
|
onSessionEnd?.(link);
|
|
302
329
|
if (state.status === "connected") handleDisconnect("node closed the connection", address, localDomains);
|
|
303
330
|
}
|
|
304
|
-
/** Builds this side's own self-advert: this node's own directly-reachable addresses (wire-mesh#38), or none for a caller with nothing to offer (a browser client, which cannot accept inbound connections) -- either is an honest advert, not a stopgap. extensions merge onto peer-advert's own open `* tstr => any` tail -- the mechanism sendGossipUpdate uses to keep a gossiped fact (presence status, an accept/refuse policy, or any future domain's own) live over the connection's lifetime. Extensions are spread before the
|
|
331
|
+
/** Builds this side's own self-advert: this node's own directly-reachable addresses (wire-mesh#38), or none for a caller with nothing to offer (a browser client, which cannot accept inbound connections) -- either is an honest advert, not a stopgap. extensions merge onto peer-advert's own open `* tstr => any` tail -- the mechanism sendGossipUpdate uses to keep a gossiped fact (presence status, an accept/refuse policy, or any future domain's own) live over the connection's lifetime. Extensions and CORE_VERSION_GOSSIP_KEY are spread before the mandatory fields (never after) so a caller-supplied key of the same name can never shadow them on the wire -- validateGossipExtensions already rejects both collisions loudly, but the field order is kept safe in its own right rather than relying solely on the guard staying in sync. */
|
|
305
332
|
function buildSelfAdvert(extensions) {
|
|
306
333
|
if (extensions !== void 0) validateGossipExtensions(extensions);
|
|
307
334
|
return {
|
|
308
335
|
type: "gossip",
|
|
309
336
|
peers: [{
|
|
310
337
|
...extensions,
|
|
338
|
+
[CORE_VERSION_GOSSIP_KEY]: OWN_VERSION,
|
|
311
339
|
device: identity.deviceId,
|
|
312
340
|
addresses: [...addresses],
|
|
313
341
|
"snapshot-seconds": Math.floor(clock.now() / MS_PER_SECOND)
|
|
@@ -530,8 +558,8 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
530
558
|
}
|
|
531
559
|
};
|
|
532
560
|
}
|
|
533
|
-
function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = []) {
|
|
534
|
-
const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address),
|
|
561
|
+
function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = [], onPeerAdvert) {
|
|
562
|
+
const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address), onPeerAdvert, addresses);
|
|
535
563
|
return session;
|
|
536
564
|
}
|
|
537
565
|
/** Wires an already-accepted Connection up as a full MeshSession, mirroring exactly what createMeshSession's own dial path does once a connection exists (send handshake, send self-advert, negotiate, consume frames) -- the wire-mesh#45 prerequisite agent-comms needs, since its peers both listen and dial rather than only ever dialing the way web-console's own console UI does. Reconnect does not apply here: if this connection drops, only the remote redialing and being accepted again produces a new connection, and therefore a new session -- there is nothing on this side to retry. */
|
|
@@ -543,9 +571,11 @@ async function acceptMeshSession(connection, identity, localDomains, options = {
|
|
|
543
571
|
});
|
|
544
572
|
let peerDeviceIdResolved = false;
|
|
545
573
|
const { session, wireUpConnection } = createSessionCore(identity, clock, null, null, (advert) => {
|
|
546
|
-
if (peerDeviceIdResolved)
|
|
547
|
-
|
|
548
|
-
|
|
574
|
+
if (!peerDeviceIdResolved) {
|
|
575
|
+
peerDeviceIdResolved = true;
|
|
576
|
+
resolvePeerDeviceId?.(advert.device);
|
|
577
|
+
}
|
|
578
|
+
options.onPeerAdvert?.(advert);
|
|
549
579
|
}, options.addresses, options.onFrame, options.onSessionEnd);
|
|
550
580
|
await wireUpConnection(connection, options.label ?? "accepted", localDomains);
|
|
551
581
|
return {
|
|
@@ -553,67 +583,7 @@ async function acceptMeshSession(connection, identity, localDomains, options = {
|
|
|
553
583
|
peerDeviceId
|
|
554
584
|
};
|
|
555
585
|
}
|
|
556
|
-
/** Reads exactly one frame off a freshly-accepted, bare connection and, if it's a manage-request, returns an IncomingManageRequest ready to hand to the same application-level dispatch logic session.incomingManageRequests already feeds elsewhere -- no handshake or gossip is ever read or sent on this connection (wire-mesh#38: the manage-request/manage-response exchange has no dependency on handshake state at the dispatch layer, confirmed against applyManageRequest/applyFrame above). respond() sends the manage-response directly and closes the connection, since a one-off request/response is this connection's entire purpose -- unlike a real MeshSession, there is nothing further to do with it afterwards. Resolves null, without closing the connection (that's the caller's call), for any other first frame or if the connection ends before one arrives: interpreting either case is a Transport.listen() caller's own business, e.g. peeking the first frame to route between this path and acceptMeshSession's own handshake path. */
|
|
557
|
-
async function acceptDirectManageRequest(connection) {
|
|
558
|
-
const result = await connection.receive()[Symbol.asyncIterator]().next();
|
|
559
|
-
if (result.done === true || result.value.type !== "manage-request") return null;
|
|
560
|
-
const frame = result.value;
|
|
561
|
-
return {
|
|
562
|
-
requestId: frame["request-id"],
|
|
563
|
-
command: frame.command,
|
|
564
|
-
scope: frame.scope,
|
|
565
|
-
...frame.token !== void 0 ? { token: frame.token } : {},
|
|
566
|
-
respond: async (outcome) => {
|
|
567
|
-
const response = {
|
|
568
|
-
type: "manage-response",
|
|
569
|
-
"request-id": frame["request-id"],
|
|
570
|
-
outcome
|
|
571
|
-
};
|
|
572
|
-
await connection.send(response);
|
|
573
|
-
await connection.close();
|
|
574
|
-
}
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
async function waitForDirectManageResponse(link, requestId) {
|
|
578
|
-
for await (const frame of link.receive()) if (frame.type === "manage-response" && frame["request-id"] === requestId) return frame.outcome;
|
|
579
|
-
throw new Error("connection closed before a response arrived");
|
|
580
|
-
}
|
|
581
|
-
/** Sends exactly one manage-request over a fresh, bare connection with no handshake, negotiation, or gossip exchanged, and no session left behind afterwards -- the connection closes once the correlated response arrives, the wait times out, or the peer-device-id check below refuses it. For attempting a direct connection to a peer whose reachable address is already known (wire-mesh#38), as an alternative to routing the same request through a relay via an established MeshSession's own sendManageRequest(targetDevice, ...). Resolves the same ManageOutcome shape sendManageRequest does -- including `{ result: "error", code: "timeout" }` on a timeout, never a rejection -- so a caller can fall back to the relay path uniformly regardless of which kind of failure this returns for the non-spoofing cases; a peer-device-id mismatch is the one case that rejects outright, since it is not an ordinary reachability failure a relay fallback should silently paper over. */
|
|
582
|
-
async function sendDirectManageRequest(transport, address, command, scope, options = {}) {
|
|
583
|
-
const link = await transport.connect(address);
|
|
584
|
-
if (options.expectedPeerDeviceId !== void 0 && link.peerDeviceId !== void 0 && require_domain_device_id.deviceIdToHex(link.peerDeviceId) !== require_domain_device_id.deviceIdToHex(options.expectedPeerDeviceId)) {
|
|
585
|
-
await link.close();
|
|
586
|
-
throw new Error("direct connection's authenticated peer-device-id does not match the expected target -- refusing as a possible spoofing attempt");
|
|
587
|
-
}
|
|
588
|
-
const requestId = 0;
|
|
589
|
-
const frame = {
|
|
590
|
-
type: "manage-request",
|
|
591
|
-
"request-id": requestId,
|
|
592
|
-
command,
|
|
593
|
-
scope,
|
|
594
|
-
...options.token !== void 0 ? { token: options.token } : {}
|
|
595
|
-
};
|
|
596
|
-
await link.send(frame);
|
|
597
|
-
const responsePromise = waitForDirectManageResponse(link, requestId);
|
|
598
|
-
let outcome;
|
|
599
|
-
if (options.timeoutMs === void 0) outcome = await responsePromise;
|
|
600
|
-
else {
|
|
601
|
-
outcome = await Promise.race([responsePromise, new Promise((resolve) => {
|
|
602
|
-
setTimeout(() => {
|
|
603
|
-
resolve({
|
|
604
|
-
result: "error",
|
|
605
|
-
code: "timeout"
|
|
606
|
-
});
|
|
607
|
-
}, options.timeoutMs);
|
|
608
|
-
})]);
|
|
609
|
-
responsePromise.catch(() => void 0);
|
|
610
|
-
}
|
|
611
|
-
await link.close();
|
|
612
|
-
return outcome;
|
|
613
|
-
}
|
|
614
586
|
//#endregion
|
|
615
587
|
exports.HANDSHAKE_TIMEOUT_MS = HANDSHAKE_TIMEOUT_MS;
|
|
616
|
-
exports.acceptDirectManageRequest = acceptDirectManageRequest;
|
|
617
588
|
exports.acceptMeshSession = acceptMeshSession;
|
|
618
589
|
exports.createMeshSession = createMeshSession;
|
|
619
|
-
exports.sendDirectManageRequest = sendDirectManageRequest;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, U as ManageError, V as ManageCommand, W as ManageOk, bt as RevocationEntry, et as PeerAdvert, f as CapabilityScope, lt as ProtocolVersion, p as CapabilityToken, w as DataEntriesFrame } from "../protocol-
|
|
2
|
-
import { t as IdentityPort } from "../identity-
|
|
1
|
+
import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, U as ManageError, V as ManageCommand, W as ManageOk, bt as RevocationEntry, et as PeerAdvert, f as CapabilityScope, lt as ProtocolVersion, p as CapabilityToken, w as DataEntriesFrame } from "../protocol-D2Ue2nd2.cjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-pOt5KxJB.cjs";
|
|
3
3
|
import { t as Clock } from "../clock-DiSx-WKM.cjs";
|
|
4
|
-
import { r as Transport, t as Connection } from "../transport-
|
|
4
|
+
import { r as Transport, t as Connection } from "../transport-fUY3alfO.cjs";
|
|
5
5
|
//#region src/domain/mesh-session.d.ts
|
|
6
6
|
/** How long to wait for the node's handshake before calling it unanswered. A relay-only node never sends one; that is a state to display, not an error. */
|
|
7
7
|
export declare const HANDSHAKE_TIMEOUT_MS = 3000;
|
|
@@ -95,7 +95,9 @@ export interface MeshSession {
|
|
|
95
95
|
}
|
|
96
96
|
export declare function createMeshSession(transport: Readonly<Transport>, identity: Readonly<IdentityPort>, clock?: Readonly<Clock>, reconnect?: ReconnectPolicy | null,
|
|
97
97
|
/** This node's own directly-reachable "host:port" candidates (wire-mesh#38), advertised in this session's self-advert so other peers can attempt a direct connection instead of always falling back to a relay. Omit (or pass none) for a caller with nothing to offer, e.g. a browser client. */
|
|
98
|
-
addresses?: readonly string[]
|
|
98
|
+
addresses?: readonly string[],
|
|
99
|
+
/** Fired for every peer-advert entry as it's applied to the directory, regardless of source -- the hook a gossip-expansion consumer (wire-mesh#187, gossip-expansion.ts's own createGossipExpansion) uses to observe newly-gossiped peers without becoming a second, competing consumer of this session's own single-reader events stream (each emitted SessionEvent wakes at most one waiter, so a second for-await loop over events would silently steal events from whichever consumer already reads it). Omit for a caller with no use for it, exactly today's behaviour. */
|
|
100
|
+
onPeerAdvert?: (advert: PeerAdvert) => void): MeshSession;
|
|
99
101
|
/** A MeshSession built over a connection that already exists (a Transport's own listen() handed it to onConnection), extended with the one thing a dial-side session can't offer: the device-id of the specific peer at the other end. Unlike createMeshSession, which may end up talking to a relay gossiping about many devices at once, an accepted connection is the agent-comms case -- exactly two peers, directly connected -- so "the peer" is well-defined here in a way it structurally isn't for the dial side. */
|
|
100
102
|
export interface AcceptedMeshSession extends MeshSession {
|
|
101
103
|
/** Resolves with the device-id carried by the first peer-advert this connection's remote sends -- the same self-advertisement mechanism createMeshSession's own directory already relies on for every peer, just narrowed to "the one peer this specific connection is with" rather than accumulated into a directory of possibly many. There is no transport-level authentication behind this yet (see wire-mesh#45's own createTlsTransport item): it is only as trustworthy as the remote's own gossip, exactly the same trust level the dial-side directory already has for every entry in it. */
|
|
@@ -111,18 +113,9 @@ export interface AcceptedMeshSessionOptions {
|
|
|
111
113
|
onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>;
|
|
112
114
|
/** Fired once this session's own connection.receive() stream ends -- the counterpart a RelayHub's own forgetConnection needs, since its registry is keyed by this exact Connection and must be cleaned up when it specifically ends. */
|
|
113
115
|
onSessionEnd?: (connection: Readonly<Connection>) => void;
|
|
116
|
+
/** Fired for every peer-advert entry as it's applied to the directory, regardless of source -- alongside (never instead of) this session's own internal peerDeviceId resolution, which uses this identical hook internally and keeps working unchanged. See createMeshSession's own onPeerAdvert doc comment for why this exists: a gossip-expansion consumer (wire-mesh#187) needs to observe every advert without becoming a second, competing consumer of this session's own single-reader events stream. */
|
|
117
|
+
onPeerAdvert?: (advert: PeerAdvert) => void;
|
|
114
118
|
}
|
|
115
119
|
/** Wires an already-accepted Connection up as a full MeshSession, mirroring exactly what createMeshSession's own dial path does once a connection exists (send handshake, send self-advert, negotiate, consume frames) -- the wire-mesh#45 prerequisite agent-comms needs, since its peers both listen and dial rather than only ever dialing the way web-console's own console UI does. Reconnect does not apply here: if this connection drops, only the remote redialing and being accepted again produces a new connection, and therefore a new session -- there is nothing on this side to retry. */
|
|
116
120
|
export declare function acceptMeshSession(connection: Readonly<Connection>, identity: Readonly<IdentityPort>, localDomains: readonly string[], options?: Readonly<AcceptedMeshSessionOptions>): Promise<AcceptedMeshSession>;
|
|
117
|
-
/** Reads exactly one frame off a freshly-accepted, bare connection and, if it's a manage-request, returns an IncomingManageRequest ready to hand to the same application-level dispatch logic session.incomingManageRequests already feeds elsewhere -- no handshake or gossip is ever read or sent on this connection (wire-mesh#38: the manage-request/manage-response exchange has no dependency on handshake state at the dispatch layer, confirmed against applyManageRequest/applyFrame above). respond() sends the manage-response directly and closes the connection, since a one-off request/response is this connection's entire purpose -- unlike a real MeshSession, there is nothing further to do with it afterwards. Resolves null, without closing the connection (that's the caller's call), for any other first frame or if the connection ends before one arrives: interpreting either case is a Transport.listen() caller's own business, e.g. peeking the first frame to route between this path and acceptMeshSession's own handshake path. */
|
|
118
|
-
export declare function acceptDirectManageRequest(connection: Readonly<Connection>): Promise<IncomingManageRequest | null>;
|
|
119
|
-
export interface DirectManageRequestOptions {
|
|
120
|
-
/** Refuse the connection as a possible spoofing attempt if the transport's own authenticated Connection.peerDeviceId (never a value read off the wire -- see the Transport port's own doc comment) doesn't match this. Left unchecked when the transport gives no authenticated peerDeviceId at all (an unauthenticated transport, or a peer that presented no credential) -- the caller proceeds at its own risk in that case, exactly the same trust boundary Connection.peerDeviceId already documents for every other consumer of it. */
|
|
121
|
-
expectedPeerDeviceId?: DeviceId;
|
|
122
|
-
token?: CapabilityToken;
|
|
123
|
-
/** Omit to wait indefinitely, matching sendManageRequest's own default. */
|
|
124
|
-
timeoutMs?: number;
|
|
125
|
-
}
|
|
126
|
-
/** Sends exactly one manage-request over a fresh, bare connection with no handshake, negotiation, or gossip exchanged, and no session left behind afterwards -- the connection closes once the correlated response arrives, the wait times out, or the peer-device-id check below refuses it. For attempting a direct connection to a peer whose reachable address is already known (wire-mesh#38), as an alternative to routing the same request through a relay via an established MeshSession's own sendManageRequest(targetDevice, ...). Resolves the same ManageOutcome shape sendManageRequest does -- including `{ result: "error", code: "timeout" }` on a timeout, never a rejection -- so a caller can fall back to the relay path uniformly regardless of which kind of failure this returns for the non-spoofing cases; a peer-device-id mismatch is the one case that rejects outright, since it is not an ordinary reachability failure a relay fallback should silently paper over. */
|
|
127
|
-
export declare function sendDirectManageRequest(transport: Readonly<Pick<Transport, "connect">>, address: string, command: ManageCommand, scope: Readonly<CapabilityScope>, options?: Readonly<DirectManageRequestOptions>): Promise<ManageOutcome>;
|
|
128
121
|
//#endregion
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, U as ManageError, V as ManageCommand, W as ManageOk, bt as RevocationEntry, et as PeerAdvert, f as CapabilityScope, lt as ProtocolVersion, p as CapabilityToken, w as DataEntriesFrame } from "../protocol-
|
|
2
|
-
import { t as IdentityPort } from "../identity-
|
|
1
|
+
import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, U as ManageError, V as ManageCommand, W as ManageOk, bt as RevocationEntry, et as PeerAdvert, f as CapabilityScope, lt as ProtocolVersion, p as CapabilityToken, w as DataEntriesFrame } from "../protocol-D2Ue2nd2.mjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-Br6e0Rx5.mjs";
|
|
3
3
|
import { t as Clock } from "../clock-DiSx-WKM.mjs";
|
|
4
|
-
import { r as Transport, t as Connection } from "../transport-
|
|
4
|
+
import { r as Transport, t as Connection } from "../transport-C5wU0UtA.mjs";
|
|
5
5
|
//#region src/domain/mesh-session.d.ts
|
|
6
6
|
/** How long to wait for the node's handshake before calling it unanswered. A relay-only node never sends one; that is a state to display, not an error. */
|
|
7
7
|
export declare const HANDSHAKE_TIMEOUT_MS = 3000;
|
|
@@ -95,7 +95,9 @@ export interface MeshSession {
|
|
|
95
95
|
}
|
|
96
96
|
export declare function createMeshSession(transport: Readonly<Transport>, identity: Readonly<IdentityPort>, clock?: Readonly<Clock>, reconnect?: ReconnectPolicy | null,
|
|
97
97
|
/** This node's own directly-reachable "host:port" candidates (wire-mesh#38), advertised in this session's self-advert so other peers can attempt a direct connection instead of always falling back to a relay. Omit (or pass none) for a caller with nothing to offer, e.g. a browser client. */
|
|
98
|
-
addresses?: readonly string[]
|
|
98
|
+
addresses?: readonly string[],
|
|
99
|
+
/** Fired for every peer-advert entry as it's applied to the directory, regardless of source -- the hook a gossip-expansion consumer (wire-mesh#187, gossip-expansion.ts's own createGossipExpansion) uses to observe newly-gossiped peers without becoming a second, competing consumer of this session's own single-reader events stream (each emitted SessionEvent wakes at most one waiter, so a second for-await loop over events would silently steal events from whichever consumer already reads it). Omit for a caller with no use for it, exactly today's behaviour. */
|
|
100
|
+
onPeerAdvert?: (advert: PeerAdvert) => void): MeshSession;
|
|
99
101
|
/** A MeshSession built over a connection that already exists (a Transport's own listen() handed it to onConnection), extended with the one thing a dial-side session can't offer: the device-id of the specific peer at the other end. Unlike createMeshSession, which may end up talking to a relay gossiping about many devices at once, an accepted connection is the agent-comms case -- exactly two peers, directly connected -- so "the peer" is well-defined here in a way it structurally isn't for the dial side. */
|
|
100
102
|
export interface AcceptedMeshSession extends MeshSession {
|
|
101
103
|
/** Resolves with the device-id carried by the first peer-advert this connection's remote sends -- the same self-advertisement mechanism createMeshSession's own directory already relies on for every peer, just narrowed to "the one peer this specific connection is with" rather than accumulated into a directory of possibly many. There is no transport-level authentication behind this yet (see wire-mesh#45's own createTlsTransport item): it is only as trustworthy as the remote's own gossip, exactly the same trust level the dial-side directory already has for every entry in it. */
|
|
@@ -111,18 +113,9 @@ export interface AcceptedMeshSessionOptions {
|
|
|
111
113
|
onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>;
|
|
112
114
|
/** Fired once this session's own connection.receive() stream ends -- the counterpart a RelayHub's own forgetConnection needs, since its registry is keyed by this exact Connection and must be cleaned up when it specifically ends. */
|
|
113
115
|
onSessionEnd?: (connection: Readonly<Connection>) => void;
|
|
116
|
+
/** Fired for every peer-advert entry as it's applied to the directory, regardless of source -- alongside (never instead of) this session's own internal peerDeviceId resolution, which uses this identical hook internally and keeps working unchanged. See createMeshSession's own onPeerAdvert doc comment for why this exists: a gossip-expansion consumer (wire-mesh#187) needs to observe every advert without becoming a second, competing consumer of this session's own single-reader events stream. */
|
|
117
|
+
onPeerAdvert?: (advert: PeerAdvert) => void;
|
|
114
118
|
}
|
|
115
119
|
/** Wires an already-accepted Connection up as a full MeshSession, mirroring exactly what createMeshSession's own dial path does once a connection exists (send handshake, send self-advert, negotiate, consume frames) -- the wire-mesh#45 prerequisite agent-comms needs, since its peers both listen and dial rather than only ever dialing the way web-console's own console UI does. Reconnect does not apply here: if this connection drops, only the remote redialing and being accepted again produces a new connection, and therefore a new session -- there is nothing on this side to retry. */
|
|
116
120
|
export declare function acceptMeshSession(connection: Readonly<Connection>, identity: Readonly<IdentityPort>, localDomains: readonly string[], options?: Readonly<AcceptedMeshSessionOptions>): Promise<AcceptedMeshSession>;
|
|
117
|
-
/** Reads exactly one frame off a freshly-accepted, bare connection and, if it's a manage-request, returns an IncomingManageRequest ready to hand to the same application-level dispatch logic session.incomingManageRequests already feeds elsewhere -- no handshake or gossip is ever read or sent on this connection (wire-mesh#38: the manage-request/manage-response exchange has no dependency on handshake state at the dispatch layer, confirmed against applyManageRequest/applyFrame above). respond() sends the manage-response directly and closes the connection, since a one-off request/response is this connection's entire purpose -- unlike a real MeshSession, there is nothing further to do with it afterwards. Resolves null, without closing the connection (that's the caller's call), for any other first frame or if the connection ends before one arrives: interpreting either case is a Transport.listen() caller's own business, e.g. peeking the first frame to route between this path and acceptMeshSession's own handshake path. */
|
|
118
|
-
export declare function acceptDirectManageRequest(connection: Readonly<Connection>): Promise<IncomingManageRequest | null>;
|
|
119
|
-
export interface DirectManageRequestOptions {
|
|
120
|
-
/** Refuse the connection as a possible spoofing attempt if the transport's own authenticated Connection.peerDeviceId (never a value read off the wire -- see the Transport port's own doc comment) doesn't match this. Left unchecked when the transport gives no authenticated peerDeviceId at all (an unauthenticated transport, or a peer that presented no credential) -- the caller proceeds at its own risk in that case, exactly the same trust boundary Connection.peerDeviceId already documents for every other consumer of it. */
|
|
121
|
-
expectedPeerDeviceId?: DeviceId;
|
|
122
|
-
token?: CapabilityToken;
|
|
123
|
-
/** Omit to wait indefinitely, matching sendManageRequest's own default. */
|
|
124
|
-
timeoutMs?: number;
|
|
125
|
-
}
|
|
126
|
-
/** Sends exactly one manage-request over a fresh, bare connection with no handshake, negotiation, or gossip exchanged, and no session left behind afterwards -- the connection closes once the correlated response arrives, the wait times out, or the peer-device-id check below refuses it. For attempting a direct connection to a peer whose reachable address is already known (wire-mesh#38), as an alternative to routing the same request through a relay via an established MeshSession's own sendManageRequest(targetDevice, ...). Resolves the same ManageOutcome shape sendManageRequest does -- including `{ result: "error", code: "timeout" }` on a timeout, never a rejection -- so a caller can fall back to the relay path uniformly regardless of which kind of failure this returns for the non-spoofing cases; a peer-device-id mismatch is the one case that rejects outright, since it is not an ordinary reachability failure a relay fallback should silently paper over. */
|
|
127
|
-
export declare function sendDirectManageRequest(transport: Readonly<Pick<Transport, "connect">>, address: string, command: ManageCommand, scope: Readonly<CapabilityScope>, options?: Readonly<DirectManageRequestOptions>): Promise<ManageOutcome>;
|
|
128
121
|
//#endregion
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { deviceIdToHex } from "./device-id.mjs";
|
|
2
2
|
import { negotiate } from "./handshake.mjs";
|
|
3
3
|
import { tryDecodeFrame, wrapRelayData } from "../adapters/frame-codec.mjs";
|
|
4
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
4
5
|
//#region src/domain/relay-pairing.ts
|
|
5
6
|
function createRelayPairings() {
|
|
6
7
|
const established = /* @__PURE__ */ new Map();
|
|
@@ -48,6 +49,24 @@ function createPingRoundTrips() {
|
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
//#endregion
|
|
52
|
+
//#region src/domain/own-version.ts
|
|
53
|
+
/** This node's own actual, currently-running wire-mesh-core version -- read once from the installed package's own package.json, never from anything gossiped or cached, so buildSelfAdvert's own self-advert extension and version.get's manage-response both always answer with the truth for this exact running process. */
|
|
54
|
+
const OWN_VERSION = packageJson.version;
|
|
55
|
+
/** The gossip extension key wire-mesh-core itself injects into every self-advert (mesh-session.ts's own buildSelfAdvert) to report OWN_VERSION -- reserved so a sendGossipUpdate caller can never also supply it, separately from peer-advert's own typed struct fields (RESERVED_PEER_ADVERT_KEYS in mesh-session.ts), since this is just an extension-tail key (transport.cddl), not a struct field. */
|
|
56
|
+
const CORE_VERSION_GOSSIP_KEY = "wire-mesh/version";
|
|
57
|
+
const VERSION_GET_VERB = "version.get";
|
|
58
|
+
/** True when params is the core/version domain's own version-get command (version.cddl) -- a receiver answers this unconditionally, bypassing incomingManageRequests entirely, since it is deliberately ungated: exactly as sensitive as what CORE_VERSION_GOSSIP_KEY already broadcasts unconditionally via gossip. */
|
|
59
|
+
function isVersionGetCommand(params) {
|
|
60
|
+
return "verb" in params && params.verb === VERSION_GET_VERB;
|
|
61
|
+
}
|
|
62
|
+
/** The manage-ok outcome mesh-session.ts answers a version.get request with -- OWN_VERSION on manage-ok's own open `* tstr => any` tail, the same pattern exec-list's own exec-session-info result already uses. */
|
|
63
|
+
function versionGetOutcome() {
|
|
64
|
+
return {
|
|
65
|
+
result: "ok",
|
|
66
|
+
version: OWN_VERSION
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
51
70
|
//#region src/domain/gossip-extensions.ts
|
|
52
71
|
/** peer-advert's own three typed fields -- reserved so a `sendGossipUpdate` caller can never override the session's own device-id, address list, or freshness timestamp by supplying an extension of the same name. */
|
|
53
72
|
const RESERVED_PEER_ADVERT_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -57,10 +76,11 @@ const RESERVED_PEER_ADVERT_KEYS = /* @__PURE__ */ new Set([
|
|
|
57
76
|
]);
|
|
58
77
|
/** A gossip extension key must be domain-qualified as `<domain>/<field>` (lowercase kebab-case each side), per `spec/CONVENTIONS.md`'s gossip-extension-namespacing convention -- this is what stops two independent applications sharing one gossip tail from silently colliding on a bare name like "status". */
|
|
59
78
|
const GOSSIP_EXTENSION_KEY_PATTERN = /^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/;
|
|
60
|
-
/** Rejects a `sendGossipUpdate` extension bag that would either shadow one of peer-advert's own mandatory fields or use a bare, non-domain-qualified key --
|
|
79
|
+
/** Rejects a `sendGossipUpdate` extension bag that would either shadow one of peer-advert's own mandatory fields, use the key wire-mesh-core itself reserves for its own self-reported version, or use a bare, non-domain-qualified key -- all are caller bugs that must fail loudly at the call site, not silently corrupt or ambiguously merge into the wire frame. */
|
|
61
80
|
function validateGossipExtensions(extensions) {
|
|
62
81
|
for (const key of Object.keys(extensions)) {
|
|
63
82
|
if (RESERVED_PEER_ADVERT_KEYS.has(key)) throw new Error(`sendGossipUpdate extension key "${key}" collides with a mandatory peer-advert field`);
|
|
83
|
+
if (key === "wire-mesh/version") throw new Error(`sendGossipUpdate extension key "${key}" is reserved for wire-mesh-core's own self-reported version`);
|
|
64
84
|
if (!GOSSIP_EXTENSION_KEY_PATTERN.test(key)) throw new Error(`sendGossipUpdate extension key "${key}" must be domain-qualified as "<domain>/<field>" (e.g. "presence/status")`);
|
|
65
85
|
}
|
|
66
86
|
}
|
|
@@ -161,12 +181,12 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
161
181
|
pending.resolve(frame.outcome);
|
|
162
182
|
}
|
|
163
183
|
}
|
|
164
|
-
/** relayFrame is present only for a manage-request that arrived wrapped in relay-data, and is that same outer relay-data-frame -- its own to-device/from-device fields carry whatever addressing it received. See IncomingManageRequest's own fromDevice/toDevice doc comments for what each means and why neither is ever guessed from pairing state. */
|
|
184
|
+
/** relayFrame is present only for a manage-request that arrived wrapped in relay-data, and is that same outer relay-data-frame -- its own to-device/from-device fields carry whatever addressing it received. See IncomingManageRequest's own fromDevice/toDevice doc comments for what each means and why neither is ever guessed from pairing state. A version.get request (VERSION_GET_VERB) is answered directly, right here, rather than ever reaching incomingManageRequests -- deliberately ungated (version.cddl), so no application needs to remember to register its own handler for it the way every other verb requires. */
|
|
165
185
|
function applyManageRequest(frame, relayFrame) {
|
|
166
186
|
const requestId = frame["request-id"];
|
|
167
187
|
const fromDevice = relayFrame?.["from-device"];
|
|
168
188
|
const toDevice = relayFrame?.["to-device"];
|
|
169
|
-
|
|
189
|
+
const incoming = {
|
|
170
190
|
requestId,
|
|
171
191
|
command: frame.command,
|
|
172
192
|
scope: frame.scope,
|
|
@@ -186,7 +206,12 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
186
206
|
await transmit(response, relayFrame !== void 0, fromDevice);
|
|
187
207
|
emit();
|
|
188
208
|
}
|
|
189
|
-
}
|
|
209
|
+
};
|
|
210
|
+
if (isVersionGetCommand(frame.command.params)) {
|
|
211
|
+
incoming.respond(versionGetOutcome());
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
emitIncomingManageRequest(incoming);
|
|
190
215
|
}
|
|
191
216
|
function applyFrame(frame) {
|
|
192
217
|
if (frame.type === "relay-data") {
|
|
@@ -300,13 +325,14 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
300
325
|
onSessionEnd?.(link);
|
|
301
326
|
if (state.status === "connected") handleDisconnect("node closed the connection", address, localDomains);
|
|
302
327
|
}
|
|
303
|
-
/** Builds this side's own self-advert: this node's own directly-reachable addresses (wire-mesh#38), or none for a caller with nothing to offer (a browser client, which cannot accept inbound connections) -- either is an honest advert, not a stopgap. extensions merge onto peer-advert's own open `* tstr => any` tail -- the mechanism sendGossipUpdate uses to keep a gossiped fact (presence status, an accept/refuse policy, or any future domain's own) live over the connection's lifetime. Extensions are spread before the
|
|
328
|
+
/** Builds this side's own self-advert: this node's own directly-reachable addresses (wire-mesh#38), or none for a caller with nothing to offer (a browser client, which cannot accept inbound connections) -- either is an honest advert, not a stopgap. extensions merge onto peer-advert's own open `* tstr => any` tail -- the mechanism sendGossipUpdate uses to keep a gossiped fact (presence status, an accept/refuse policy, or any future domain's own) live over the connection's lifetime. Extensions and CORE_VERSION_GOSSIP_KEY are spread before the mandatory fields (never after) so a caller-supplied key of the same name can never shadow them on the wire -- validateGossipExtensions already rejects both collisions loudly, but the field order is kept safe in its own right rather than relying solely on the guard staying in sync. */
|
|
304
329
|
function buildSelfAdvert(extensions) {
|
|
305
330
|
if (extensions !== void 0) validateGossipExtensions(extensions);
|
|
306
331
|
return {
|
|
307
332
|
type: "gossip",
|
|
308
333
|
peers: [{
|
|
309
334
|
...extensions,
|
|
335
|
+
[CORE_VERSION_GOSSIP_KEY]: OWN_VERSION,
|
|
310
336
|
device: identity.deviceId,
|
|
311
337
|
addresses: [...addresses],
|
|
312
338
|
"snapshot-seconds": Math.floor(clock.now() / MS_PER_SECOND)
|
|
@@ -529,8 +555,8 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
529
555
|
}
|
|
530
556
|
};
|
|
531
557
|
}
|
|
532
|
-
function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = []) {
|
|
533
|
-
const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address),
|
|
558
|
+
function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = [], onPeerAdvert) {
|
|
559
|
+
const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address), onPeerAdvert, addresses);
|
|
534
560
|
return session;
|
|
535
561
|
}
|
|
536
562
|
/** Wires an already-accepted Connection up as a full MeshSession, mirroring exactly what createMeshSession's own dial path does once a connection exists (send handshake, send self-advert, negotiate, consume frames) -- the wire-mesh#45 prerequisite agent-comms needs, since its peers both listen and dial rather than only ever dialing the way web-console's own console UI does. Reconnect does not apply here: if this connection drops, only the remote redialing and being accepted again produces a new connection, and therefore a new session -- there is nothing on this side to retry. */
|
|
@@ -542,9 +568,11 @@ async function acceptMeshSession(connection, identity, localDomains, options = {
|
|
|
542
568
|
});
|
|
543
569
|
let peerDeviceIdResolved = false;
|
|
544
570
|
const { session, wireUpConnection } = createSessionCore(identity, clock, null, null, (advert) => {
|
|
545
|
-
if (peerDeviceIdResolved)
|
|
546
|
-
|
|
547
|
-
|
|
571
|
+
if (!peerDeviceIdResolved) {
|
|
572
|
+
peerDeviceIdResolved = true;
|
|
573
|
+
resolvePeerDeviceId?.(advert.device);
|
|
574
|
+
}
|
|
575
|
+
options.onPeerAdvert?.(advert);
|
|
548
576
|
}, options.addresses, options.onFrame, options.onSessionEnd);
|
|
549
577
|
await wireUpConnection(connection, options.label ?? "accepted", localDomains);
|
|
550
578
|
return {
|
|
@@ -552,63 +580,5 @@ async function acceptMeshSession(connection, identity, localDomains, options = {
|
|
|
552
580
|
peerDeviceId
|
|
553
581
|
};
|
|
554
582
|
}
|
|
555
|
-
/** Reads exactly one frame off a freshly-accepted, bare connection and, if it's a manage-request, returns an IncomingManageRequest ready to hand to the same application-level dispatch logic session.incomingManageRequests already feeds elsewhere -- no handshake or gossip is ever read or sent on this connection (wire-mesh#38: the manage-request/manage-response exchange has no dependency on handshake state at the dispatch layer, confirmed against applyManageRequest/applyFrame above). respond() sends the manage-response directly and closes the connection, since a one-off request/response is this connection's entire purpose -- unlike a real MeshSession, there is nothing further to do with it afterwards. Resolves null, without closing the connection (that's the caller's call), for any other first frame or if the connection ends before one arrives: interpreting either case is a Transport.listen() caller's own business, e.g. peeking the first frame to route between this path and acceptMeshSession's own handshake path. */
|
|
556
|
-
async function acceptDirectManageRequest(connection) {
|
|
557
|
-
const result = await connection.receive()[Symbol.asyncIterator]().next();
|
|
558
|
-
if (result.done === true || result.value.type !== "manage-request") return null;
|
|
559
|
-
const frame = result.value;
|
|
560
|
-
return {
|
|
561
|
-
requestId: frame["request-id"],
|
|
562
|
-
command: frame.command,
|
|
563
|
-
scope: frame.scope,
|
|
564
|
-
...frame.token !== void 0 ? { token: frame.token } : {},
|
|
565
|
-
respond: async (outcome) => {
|
|
566
|
-
const response = {
|
|
567
|
-
type: "manage-response",
|
|
568
|
-
"request-id": frame["request-id"],
|
|
569
|
-
outcome
|
|
570
|
-
};
|
|
571
|
-
await connection.send(response);
|
|
572
|
-
await connection.close();
|
|
573
|
-
}
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
|
-
async function waitForDirectManageResponse(link, requestId) {
|
|
577
|
-
for await (const frame of link.receive()) if (frame.type === "manage-response" && frame["request-id"] === requestId) return frame.outcome;
|
|
578
|
-
throw new Error("connection closed before a response arrived");
|
|
579
|
-
}
|
|
580
|
-
/** Sends exactly one manage-request over a fresh, bare connection with no handshake, negotiation, or gossip exchanged, and no session left behind afterwards -- the connection closes once the correlated response arrives, the wait times out, or the peer-device-id check below refuses it. For attempting a direct connection to a peer whose reachable address is already known (wire-mesh#38), as an alternative to routing the same request through a relay via an established MeshSession's own sendManageRequest(targetDevice, ...). Resolves the same ManageOutcome shape sendManageRequest does -- including `{ result: "error", code: "timeout" }` on a timeout, never a rejection -- so a caller can fall back to the relay path uniformly regardless of which kind of failure this returns for the non-spoofing cases; a peer-device-id mismatch is the one case that rejects outright, since it is not an ordinary reachability failure a relay fallback should silently paper over. */
|
|
581
|
-
async function sendDirectManageRequest(transport, address, command, scope, options = {}) {
|
|
582
|
-
const link = await transport.connect(address);
|
|
583
|
-
if (options.expectedPeerDeviceId !== void 0 && link.peerDeviceId !== void 0 && deviceIdToHex(link.peerDeviceId) !== deviceIdToHex(options.expectedPeerDeviceId)) {
|
|
584
|
-
await link.close();
|
|
585
|
-
throw new Error("direct connection's authenticated peer-device-id does not match the expected target -- refusing as a possible spoofing attempt");
|
|
586
|
-
}
|
|
587
|
-
const requestId = 0;
|
|
588
|
-
const frame = {
|
|
589
|
-
type: "manage-request",
|
|
590
|
-
"request-id": requestId,
|
|
591
|
-
command,
|
|
592
|
-
scope,
|
|
593
|
-
...options.token !== void 0 ? { token: options.token } : {}
|
|
594
|
-
};
|
|
595
|
-
await link.send(frame);
|
|
596
|
-
const responsePromise = waitForDirectManageResponse(link, requestId);
|
|
597
|
-
let outcome;
|
|
598
|
-
if (options.timeoutMs === void 0) outcome = await responsePromise;
|
|
599
|
-
else {
|
|
600
|
-
outcome = await Promise.race([responsePromise, new Promise((resolve) => {
|
|
601
|
-
setTimeout(() => {
|
|
602
|
-
resolve({
|
|
603
|
-
result: "error",
|
|
604
|
-
code: "timeout"
|
|
605
|
-
});
|
|
606
|
-
}, options.timeoutMs);
|
|
607
|
-
})]);
|
|
608
|
-
responsePromise.catch(() => void 0);
|
|
609
|
-
}
|
|
610
|
-
await link.close();
|
|
611
|
-
return outcome;
|
|
612
|
-
}
|
|
613
583
|
//#endregion
|
|
614
|
-
export { HANDSHAKE_TIMEOUT_MS,
|
|
584
|
+
export { HANDSHAKE_TIMEOUT_MS, acceptMeshSession, createMeshSession };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { D as DeviceId, Ot as RoomNotice, T as DataHaveFrame, kt as RoomNoticeClaims, p as CapabilityToken } from "../protocol-
|
|
1
|
+
import { D as DeviceId, Ot as RoomNotice, T as DataHaveFrame, kt as RoomNoticeClaims, p as CapabilityToken } from "../protocol-D2Ue2nd2.cjs";
|
|
2
2
|
import { KeyValueStorage } from "../ports/storage.cjs";
|
|
3
|
-
import { t as IdentityPort } from "../identity-
|
|
3
|
+
import { t as IdentityPort } from "../identity-pOt5KxJB.cjs";
|
|
4
4
|
import { t as Clock } from "../clock-DiSx-WKM.cjs";
|
|
5
|
-
import { a as RevocationCheck } from "../tokens-
|
|
5
|
+
import { a as RevocationCheck } from "../tokens-BtczHfrc.cjs";
|
|
6
6
|
import { RoomNoticeVerdictReason } from "./room.cjs";
|
|
7
7
|
//#region src/domain/notice-board.d.ts
|
|
8
8
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { D as DeviceId, Ot as RoomNotice, T as DataHaveFrame, kt as RoomNoticeClaims, p as CapabilityToken } from "../protocol-
|
|
1
|
+
import { D as DeviceId, Ot as RoomNotice, T as DataHaveFrame, kt as RoomNoticeClaims, p as CapabilityToken } from "../protocol-D2Ue2nd2.mjs";
|
|
2
2
|
import { KeyValueStorage } from "../ports/storage.mjs";
|
|
3
|
-
import { t as IdentityPort } from "../identity-
|
|
3
|
+
import { t as IdentityPort } from "../identity-Br6e0Rx5.mjs";
|
|
4
4
|
import { t as Clock } from "../clock-DiSx-WKM.mjs";
|
|
5
|
-
import { a as RevocationCheck } from "../tokens-
|
|
5
|
+
import { a as RevocationCheck } from "../tokens-Hnq9oURj.mjs";
|
|
6
6
|
import { RoomNoticeVerdictReason } from "./room.mjs";
|
|
7
7
|
//#region src/domain/notice-board.d.ts
|
|
8
8
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { N as Frame } from "../protocol-
|
|
2
|
-
import { t as Connection } from "../transport-
|
|
1
|
+
import { N as Frame } from "../protocol-D2Ue2nd2.cjs";
|
|
2
|
+
import { t as Connection } from "../transport-fUY3alfO.cjs";
|
|
3
3
|
//#region src/domain/relay-hub.d.ts
|
|
4
4
|
export interface RelayHub {
|
|
5
5
|
/** Drives one accepted connection until it closes: registers gossip-advertised devices, re-broadcasts each gossip frame to every other connected client and replies to the gossiping connection with a catch-up frame of every other already-known device, answers relay-connect by pairing and notifying the target, forwards relay-data within established pairings, and replies to a bare ping with a bare pong (wire-mesh#181) so a client can time its own sender-to-hub leg independent of anything being relayed. Resolves when the connection's frame stream ends. Internally just registerConnection followed by a loop of onFrame calls and a final onDisconnect -- kept as its own method since a dedicated hub deployment (wire-mesh-node/cloudflare-hub) has nothing else driving the connection and wants the whole lifecycle in one call. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { N as Frame } from "../protocol-
|
|
2
|
-
import { t as Connection } from "../transport-
|
|
1
|
+
import { N as Frame } from "../protocol-D2Ue2nd2.mjs";
|
|
2
|
+
import { t as Connection } from "../transport-C5wU0UtA.mjs";
|
|
3
3
|
//#region src/domain/relay-hub.d.ts
|
|
4
4
|
export interface RelayHub {
|
|
5
5
|
/** Drives one accepted connection until it closes: registers gossip-advertised devices, re-broadcasts each gossip frame to every other connected client and replies to the gossiping connection with a catch-up frame of every other already-known device, answers relay-connect by pairing and notifying the target, forwards relay-data within established pairings, and replies to a bare ping with a bare pong (wire-mesh#181) so a client can time its own sender-to-hub leg independent of anything being relayed. Resolves when the connection's frame stream ends. Internally just registerConnection followed by a loop of onFrame calls and a final onDisconnect -- kept as its own method since a dedicated hub deployment (wire-mesh-node/cloudflare-hub) has nothing else driving the connection and wants the whole lifecycle in one call. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { N as Frame } from "../protocol-
|
|
2
|
-
import { t as Connection } from "../transport-
|
|
1
|
+
import { N as Frame } from "../protocol-D2Ue2nd2.cjs";
|
|
2
|
+
import { t as Connection } from "../transport-fUY3alfO.cjs";
|
|
3
3
|
//#region src/domain/relay-use-gate.d.ts
|
|
4
4
|
export interface RelayUseAuthorizationTracker {
|
|
5
5
|
/** True once authorize() has been called for this exact connection and revoke() has not been called since. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { N as Frame } from "../protocol-
|
|
2
|
-
import { t as Connection } from "../transport-
|
|
1
|
+
import { N as Frame } from "../protocol-D2Ue2nd2.mjs";
|
|
2
|
+
import { t as Connection } from "../transport-C5wU0UtA.mjs";
|
|
3
3
|
//#region src/domain/relay-use-gate.d.ts
|
|
4
4
|
export interface RelayUseAuthorizationTracker {
|
|
5
5
|
/** True once authorize() has been called for this exact connection and revoke() has not been called since. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { bt as RevocationEntry } from "../protocol-
|
|
2
|
-
import { a as RevocationCheck, d as VerifyRevocationEntryOptions, o as RevocationEntryVerdict } from "../tokens-
|
|
1
|
+
import { bt as RevocationEntry } from "../protocol-D2Ue2nd2.cjs";
|
|
2
|
+
import { a as RevocationCheck, d as VerifyRevocationEntryOptions, o as RevocationEntryVerdict } from "../tokens-BtczHfrc.cjs";
|
|
3
3
|
//#region src/domain/revocation-view.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* An in-memory RevocationCheck fed by ingested revocation-announce frames. Keyed by token-id alone, per management.cddl's contract: a token-id can legitimately carry multiple recorded entries from different issuers (the token's own issuer, and/or any number of parties holding a delegated manage:revoke authorization over it) -- entriesFor returns every one of them unfiltered, since checking which actually apply (issuer-match, or a verified authorization) is verifyTokenChain's own obligation, not this store's.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { bt as RevocationEntry } from "../protocol-
|
|
2
|
-
import { a as RevocationCheck, d as VerifyRevocationEntryOptions, o as RevocationEntryVerdict } from "../tokens-
|
|
1
|
+
import { bt as RevocationEntry } from "../protocol-D2Ue2nd2.mjs";
|
|
2
|
+
import { a as RevocationCheck, d as VerifyRevocationEntryOptions, o as RevocationEntryVerdict } from "../tokens-Hnq9oURj.mjs";
|
|
3
3
|
//#region src/domain/revocation-view.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* An in-memory RevocationCheck fed by ingested revocation-announce frames. Keyed by token-id alone, per management.cddl's contract: a token-id can legitimately carry multiple recorded entries from different issuers (the token's own issuer, and/or any number of parties holding a delegated manage:revoke authorization over it) -- entriesFor returns every one of them unfiltered, since checking which actually apply (issuer-match, or a verified authorization) is verifyTokenChain's own obligation, not this store's.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { D as DeviceId, V as ManageCommand, p as CapabilityToken } from "../protocol-
|
|
2
|
-
import { t as IdentityPort } from "../identity-
|
|
1
|
+
import { D as DeviceId, V as ManageCommand, p as CapabilityToken } from "../protocol-D2Ue2nd2.cjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-pOt5KxJB.cjs";
|
|
3
3
|
import { t as Clock } from "../clock-DiSx-WKM.cjs";
|
|
4
4
|
import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.cjs";
|
|
5
|
-
import { a as RevocationCheck } from "../tokens-
|
|
5
|
+
import { a as RevocationCheck } from "../tokens-BtczHfrc.cjs";
|
|
6
6
|
//#region src/domain/room-rekey.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* Builds a room.rekey command per room.cddl. The outer verb is room:member, the same "one resource, several inner verbs" pattern room.send/room.leave/room.members already establish -- not a new capability of its own.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { D as DeviceId, V as ManageCommand, p as CapabilityToken } from "../protocol-
|
|
2
|
-
import { t as IdentityPort } from "../identity-
|
|
1
|
+
import { D as DeviceId, V as ManageCommand, p as CapabilityToken } from "../protocol-D2Ue2nd2.mjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-Br6e0Rx5.mjs";
|
|
3
3
|
import { t as Clock } from "../clock-DiSx-WKM.mjs";
|
|
4
4
|
import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.mjs";
|
|
5
|
-
import { a as RevocationCheck } from "../tokens-
|
|
5
|
+
import { a as RevocationCheck } from "../tokens-Hnq9oURj.mjs";
|
|
6
6
|
//#region src/domain/room-rekey.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* Builds a room.rekey command per room.cddl. The outer verb is room:member, the same "one resource, several inner verbs" pattern room.send/room.leave/room.members already establish -- not a new capability of its own.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as verifyRoomToken, i as VerifyRoomTokenOptions, n as RoomTokenVerdict, r as RoomTokenVerdictReason, t as ROOM_MEMBER_CAPABILITY } from "../room-token-verification-
|
|
1
|
+
import { a as verifyRoomToken, i as VerifyRoomTokenOptions, n as RoomTokenVerdict, r as RoomTokenVerdictReason, t as ROOM_MEMBER_CAPABILITY } from "../room-token-verification-CNXMjIQd.cjs";
|
|
2
2
|
export { ROOM_MEMBER_CAPABILITY, RoomTokenVerdict, RoomTokenVerdictReason, VerifyRoomTokenOptions, verifyRoomToken };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as verifyRoomToken, i as VerifyRoomTokenOptions, n as RoomTokenVerdict, r as RoomTokenVerdictReason, t as ROOM_MEMBER_CAPABILITY } from "../room-token-verification-
|
|
1
|
+
import { a as verifyRoomToken, i as VerifyRoomTokenOptions, n as RoomTokenVerdict, r as RoomTokenVerdictReason, t as ROOM_MEMBER_CAPABILITY } from "../room-token-verification-ZHgJhnsu.mjs";
|
|
2
2
|
export { ROOM_MEMBER_CAPABILITY, RoomTokenVerdict, RoomTokenVerdictReason, VerifyRoomTokenOptions, verifyRoomToken };
|