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.
Files changed (77) hide show
  1. package/dist/adapters/frame-codec.d.cts +1 -1
  2. package/dist/adapters/frame-codec.d.mts +1 -1
  3. package/dist/adapters/node-identity.d.cts +2 -2
  4. package/dist/adapters/node-identity.d.mts +2 -2
  5. package/dist/adapters/tcp-transport.d.cts +1 -1
  6. package/dist/adapters/tcp-transport.d.mts +1 -1
  7. package/dist/adapters/threshold-identity.d.cts +2 -2
  8. package/dist/adapters/threshold-identity.d.mts +2 -2
  9. package/dist/adapters/threshold-wasm.d.cts +1 -1
  10. package/dist/adapters/threshold-wasm.d.mts +1 -1
  11. package/dist/adapters/tls-transport.d.cts +1 -1
  12. package/dist/adapters/tls-transport.d.mts +1 -1
  13. package/dist/domain/bulk.d.cts +2 -2
  14. package/dist/domain/bulk.d.mts +2 -2
  15. package/dist/domain/capability-grant.d.cts +3 -3
  16. package/dist/domain/capability-grant.d.mts +3 -3
  17. package/dist/domain/capability-request.d.cts +2 -2
  18. package/dist/domain/capability-request.d.mts +2 -2
  19. package/dist/domain/data-sync.d.cts +2 -2
  20. package/dist/domain/data-sync.d.mts +2 -2
  21. package/dist/domain/device-id.d.cts +1 -1
  22. package/dist/domain/device-id.d.mts +1 -1
  23. package/dist/domain/direct-manage-request.cjs +64 -0
  24. package/dist/domain/direct-manage-request.d.cts +16 -0
  25. package/dist/domain/direct-manage-request.d.mts +16 -0
  26. package/dist/domain/direct-manage-request.mjs +62 -0
  27. package/dist/domain/gossip-expansion.cjs +44 -0
  28. package/dist/domain/gossip-expansion.d.cts +29 -0
  29. package/dist/domain/gossip-expansion.d.mts +29 -0
  30. package/dist/domain/gossip-expansion.mjs +43 -0
  31. package/dist/domain/grant-candidates.d.cts +1 -1
  32. package/dist/domain/grant-candidates.d.mts +1 -1
  33. package/dist/domain/handshake.d.cts +1 -1
  34. package/dist/domain/handshake.d.mts +1 -1
  35. package/dist/domain/mesh-session.cjs +40 -70
  36. package/dist/domain/mesh-session.d.cts +8 -15
  37. package/dist/domain/mesh-session.d.mts +8 -15
  38. package/dist/domain/mesh-session.mjs +39 -69
  39. package/dist/domain/notice-board.d.cts +3 -3
  40. package/dist/domain/notice-board.d.mts +3 -3
  41. package/dist/domain/relay-hub.d.cts +2 -2
  42. package/dist/domain/relay-hub.d.mts +2 -2
  43. package/dist/domain/relay-use-gate.d.cts +2 -2
  44. package/dist/domain/relay-use-gate.d.mts +2 -2
  45. package/dist/domain/revocation-view.d.cts +2 -2
  46. package/dist/domain/revocation-view.d.mts +2 -2
  47. package/dist/domain/room-rekey.d.cts +3 -3
  48. package/dist/domain/room-rekey.d.mts +3 -3
  49. package/dist/domain/room-token-verification.d.cts +1 -1
  50. package/dist/domain/room-token-verification.d.mts +1 -1
  51. package/dist/domain/room.d.cts +4 -4
  52. package/dist/domain/room.d.mts +4 -4
  53. package/dist/domain/shard-manifest.d.cts +1 -1
  54. package/dist/domain/shard-manifest.d.mts +1 -1
  55. package/dist/domain/tokens.d.cts +1 -1
  56. package/dist/domain/tokens.d.mts +1 -1
  57. package/dist/domain/webrtc-signaling.d.cts +2 -2
  58. package/dist/domain/webrtc-signaling.d.mts +2 -2
  59. package/dist/generated/protocol.cjs +5 -1
  60. package/dist/generated/protocol.d.cts +2 -2
  61. package/dist/generated/protocol.d.mts +2 -2
  62. package/dist/generated/protocol.mjs +5 -2
  63. package/dist/{identity-BXrJ6_YM.d.mts → identity-Br6e0Rx5.d.mts} +1 -1
  64. package/dist/{identity-CmD4l_Mp.d.cts → identity-pOt5KxJB.d.cts} +1 -1
  65. package/dist/ports/identity.d.cts +1 -1
  66. package/dist/ports/identity.d.mts +1 -1
  67. package/dist/ports/transport.d.cts +1 -1
  68. package/dist/ports/transport.d.mts +1 -1
  69. package/dist/{protocol-PhUVprXA.d.cts → protocol-D2Ue2nd2.d.cts} +25 -11
  70. package/dist/{protocol-PhUVprXA.d.mts → protocol-D2Ue2nd2.d.mts} +25 -11
  71. package/dist/{room-token-verification-D8HndWYM.d.cts → room-token-verification-CNXMjIQd.d.cts} +2 -2
  72. package/dist/{room-token-verification-Dv1O6-FV.d.mts → room-token-verification-ZHgJhnsu.d.mts} +2 -2
  73. package/dist/{tokens-DIPzZ9JJ.d.cts → tokens-BtczHfrc.d.cts} +2 -2
  74. package/dist/{tokens-BVrgBeqF.d.mts → tokens-Hnq9oURj.d.mts} +2 -2
  75. package/dist/{transport-CbXX1Xd-.d.cts → transport-C5wU0UtA.d.mts} +1 -1
  76. package/dist/{transport-DG9ouGe-.d.mts → transport-fUY3alfO.d.cts} +1 -1
  77. 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 -- both are caller bugs that must fail loudly at the call site, not silently corrupt or ambiguously merge into the wire frame. */
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
- emitIncomingManageRequest({
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 three mandatory fields (never after) so a caller-supplied key of the same name can never shadow them on the wire -- validateGossipExtensions already rejects that case loudly, but the field order is kept safe in its own right rather than relying solely on the guard staying in sync. */
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), void 0, addresses);
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) return;
547
- peerDeviceIdResolved = true;
548
- resolvePeerDeviceId?.(advert.device);
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-PhUVprXA.cjs";
2
- import { t as IdentityPort } from "../identity-CmD4l_Mp.cjs";
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-CbXX1Xd-.cjs";
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[]): MeshSession;
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-PhUVprXA.mjs";
2
- import { t as IdentityPort } from "../identity-BXrJ6_YM.mjs";
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-DG9ouGe-.mjs";
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[]): MeshSession;
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 -- both are caller bugs that must fail loudly at the call site, not silently corrupt or ambiguously merge into the wire frame. */
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
- emitIncomingManageRequest({
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 three mandatory fields (never after) so a caller-supplied key of the same name can never shadow them on the wire -- validateGossipExtensions already rejects that case loudly, but the field order is kept safe in its own right rather than relying solely on the guard staying in sync. */
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), void 0, addresses);
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) return;
546
- peerDeviceIdResolved = true;
547
- resolvePeerDeviceId?.(advert.device);
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, acceptDirectManageRequest, acceptMeshSession, createMeshSession, sendDirectManageRequest };
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-PhUVprXA.cjs";
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-CmD4l_Mp.cjs";
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-DIPzZ9JJ.cjs";
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-PhUVprXA.mjs";
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-BXrJ6_YM.mjs";
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-BVrgBeqF.mjs";
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-PhUVprXA.cjs";
2
- import { t as Connection } from "../transport-CbXX1Xd-.cjs";
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-PhUVprXA.mjs";
2
- import { t as Connection } from "../transport-DG9ouGe-.mjs";
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-PhUVprXA.cjs";
2
- import { t as Connection } from "../transport-CbXX1Xd-.cjs";
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-PhUVprXA.mjs";
2
- import { t as Connection } from "../transport-DG9ouGe-.mjs";
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-PhUVprXA.cjs";
2
- import { a as RevocationCheck, d as VerifyRevocationEntryOptions, o as RevocationEntryVerdict } from "../tokens-DIPzZ9JJ.cjs";
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-PhUVprXA.mjs";
2
- import { a as RevocationCheck, d as VerifyRevocationEntryOptions, o as RevocationEntryVerdict } from "../tokens-BVrgBeqF.mjs";
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-PhUVprXA.cjs";
2
- import { t as IdentityPort } from "../identity-CmD4l_Mp.cjs";
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-DIPzZ9JJ.cjs";
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-PhUVprXA.mjs";
2
- import { t as IdentityPort } from "../identity-BXrJ6_YM.mjs";
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-BVrgBeqF.mjs";
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-D8HndWYM.cjs";
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-Dv1O6-FV.mjs";
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 };