wire-mesh-core 3.3.1 → 3.4.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.
@@ -1,5 +1,5 @@
1
1
  import { N as Frame } from "../protocol-DCDae3z4.cjs";
2
- import { t as Connection } from "../transport-CfW7fsGK.cjs";
2
+ import { t as Connection } from "../transport-DIXjoQKn.cjs";
3
3
  //#region src/adapters/byte-stream-connection.d.ts
4
4
  /** The largest frame body a peer may announce in a length prefix, matching the 100 MiB the `ws` package applies to one message on the same hub, so a WebSocket peer and a byte-stream peer are bounded alike. Without a bound a peer-controlled 4 GiB prefix makes the reader buffer until memory runs out. */
5
5
  export declare const MAX_FRAME_BYTES: number;
@@ -1,5 +1,5 @@
1
1
  import { N as Frame } from "../protocol-DCDae3z4.mjs";
2
- import { t as Connection } from "../transport-iLBxIkRS.mjs";
2
+ import { t as Connection } from "../transport-B0GywEzG.mjs";
3
3
  //#region src/adapters/byte-stream-connection.d.ts
4
4
  /** The largest frame body a peer may announce in a length prefix, matching the 100 MiB the `ws` package applies to one message on the same hub, so a WebSocket peer and a byte-stream peer are bounded alike. Without a bound a peer-controlled 4 GiB prefix makes the reader buffer until memory runs out. */
5
5
  export declare const MAX_FRAME_BYTES: number;
@@ -1,4 +1,4 @@
1
- import { r as Transport } from "../transport-CfW7fsGK.cjs";
1
+ import { r as Transport } from "../transport-DIXjoQKn.cjs";
2
2
  //#region src/adapters/tcp-transport.d.ts
3
3
  /** A Node net.Socket-based Transport: length-prefixed, CBOR-encoded frames over plain TCP -- matching Cascade's own transport shape, since interop with Cascade nodes is wire-mesh's stated goal. Framing (not TLS) is this adapter's own concern; a TLS-terminated variant is a separate adapter behind the same Transport contract. */
4
4
  export declare function createTcpTransport(): Transport;
@@ -1,4 +1,4 @@
1
- import { r as Transport } from "../transport-iLBxIkRS.mjs";
1
+ import { r as Transport } from "../transport-B0GywEzG.mjs";
2
2
  //#region src/adapters/tcp-transport.d.ts
3
3
  /** A Node net.Socket-based Transport: length-prefixed, CBOR-encoded frames over plain TCP -- matching Cascade's own transport shape, since interop with Cascade nodes is wire-mesh's stated goal. Framing (not TLS) is this adapter's own concern; a TLS-terminated variant is a separate adapter behind the same Transport contract. */
4
4
  export declare function createTcpTransport(): Transport;
@@ -1,4 +1,4 @@
1
- import { r as Transport } from "../transport-CfW7fsGK.cjs";
1
+ import { r as Transport } from "../transport-DIXjoQKn.cjs";
2
2
  //#region src/adapters/tls-transport.d.ts
3
3
  export interface TlsIdentity {
4
4
  certificatePem: string;
@@ -1,4 +1,4 @@
1
- import { r as Transport } from "../transport-iLBxIkRS.mjs";
1
+ import { r as Transport } from "../transport-B0GywEzG.mjs";
2
2
  //#region src/adapters/tls-transport.d.ts
3
3
  export interface TlsIdentity {
4
4
  certificatePem: string;
@@ -1,5 +1,5 @@
1
1
  import { D as DeviceId, N as Frame, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DCDae3z4.cjs";
2
- import { t as Connection } from "../transport-CfW7fsGK.cjs";
2
+ import { t as Connection } from "../transport-DIXjoQKn.cjs";
3
3
  import { t as KeyValueStorage } from "../storage-B403CRyu.cjs";
4
4
  import { IncomingManageRequest, MeshSession } from "./mesh-session.cjs";
5
5
  //#region src/domain/bulk.d.ts
@@ -1,5 +1,5 @@
1
1
  import { D as DeviceId, N as Frame, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DCDae3z4.mjs";
2
- import { t as Connection } from "../transport-iLBxIkRS.mjs";
2
+ import { t as Connection } from "../transport-B0GywEzG.mjs";
3
3
  import { t as KeyValueStorage } from "../storage-B403CRyu.mjs";
4
4
  import { IncomingManageRequest, MeshSession } from "./mesh-session.mjs";
5
5
  //#region src/domain/bulk.d.ts
@@ -1,5 +1,5 @@
1
1
  import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DCDae3z4.cjs";
2
- import { r as Transport, t as Connection } from "../transport-CfW7fsGK.cjs";
2
+ import { r as Transport, t as Connection } from "../transport-DIXjoQKn.cjs";
3
3
  import { IncomingManageRequest, ManageOutcome } from "./mesh-session.cjs";
4
4
  //#region src/domain/direct-manage-request.d.ts
5
5
  /** 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 mesh-session.ts's own applyManageRequest/applyFrame). 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. */
@@ -1,5 +1,5 @@
1
1
  import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DCDae3z4.mjs";
2
- import { r as Transport, t as Connection } from "../transport-iLBxIkRS.mjs";
2
+ import { r as Transport, t as Connection } from "../transport-B0GywEzG.mjs";
3
3
  import { IncomingManageRequest, ManageOutcome } from "./mesh-session.mjs";
4
4
  //#region src/domain/direct-manage-request.d.ts
5
5
  /** 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 mesh-session.ts's own applyManageRequest/applyFrame). 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. */
@@ -504,6 +504,10 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
504
504
  handshake
505
505
  };
506
506
  }
507
+ /** The address to dial after `link` ended: the one the adapter says reaches the same peer now, if it says anything, else the one this session dialled. */
508
+ function redialAddress(link, dialled) {
509
+ return link.redialAddress?.() ?? dialled;
510
+ }
507
511
  function handleDisconnect(reason, address, localDomains) {
508
512
  if (feedCancelled) return;
509
513
  rejectPendingManageRequests("disconnected before a response arrived");
@@ -534,14 +538,19 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
534
538
  emit();
535
539
  }
536
540
  async function consume(link, address, localDomains) {
541
+ let heardFromPeer = false;
537
542
  for await (const frame of link.receive()) {
538
543
  if (feedCancelled) return;
544
+ if (!heardFromPeer) {
545
+ heardFromPeer = true;
546
+ attempt = 0;
547
+ }
539
548
  await applyFrame(frame);
540
549
  await onFrame?.(link, frame);
541
550
  emit();
542
551
  }
543
552
  onSessionEnd?.(link);
544
- if (state.status === "connected") handleDisconnect("node closed the connection", address, localDomains);
553
+ if (state.status === "connected") handleDisconnect("node closed the connection", redialAddress(link, address), localDomains);
545
554
  }
546
555
  /** Builds this side's own self-advert and signs it with this node's own key (wire-mesh#225), which is what makes it usable by a receiver that learned of it through a hub or a gateway rather than directly from here. this node's own directly-reachable addresses (wire-mesh#38) are advertised, 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, and every such key is covered by the signature. Extensions, CORE_VERSION_GOSSIP_KEY, and topology/peers are all 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 every such collision loudly, but the field order is kept safe in its own right rather than relying solely on the guard staying in sync. topology/peers (wire-mesh#180) is recomputed fresh on every call, the same "always current, never cached" treatment snapshot-seconds already gets, since a session's own connection identity and relay pairings can change between one self-advert and the next. */
547
556
  async function buildSelfAdvert(extensions) {
@@ -596,7 +605,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
596
605
  }
597
606
  }, HANDSHAKE_TIMEOUT_MS);
598
607
  consume(connection, address, localDomains).catch((error) => {
599
- if (state.status === "connected") handleDisconnect(error instanceof Error ? error.message : String(error), address, localDomains);
608
+ if (state.status === "connected") handleDisconnect(error instanceof Error ? error.message : String(error), redialAddress(link, address), localDomains);
600
609
  });
601
610
  }
602
611
  async function doConnect(address, localDomains) {
@@ -1,5 +1,5 @@
1
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, nn as TopologyPeers, p as CapabilityToken, w as DataEntriesFrame } from "../protocol-DCDae3z4.cjs";
2
- import { r as Transport, t as Connection } from "../transport-CfW7fsGK.cjs";
2
+ import { r as Transport, t as Connection } from "../transport-DIXjoQKn.cjs";
3
3
  import { t as IdentityPort } from "../identity-DVCNuFLG.cjs";
4
4
  import { t as Clock } from "../clock-DiSx-WKM.cjs";
5
5
  //#region src/domain/mesh-session.d.ts
@@ -1,5 +1,5 @@
1
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, nn as TopologyPeers, p as CapabilityToken, w as DataEntriesFrame } from "../protocol-DCDae3z4.mjs";
2
- import { r as Transport, t as Connection } from "../transport-iLBxIkRS.mjs";
2
+ import { r as Transport, t as Connection } from "../transport-B0GywEzG.mjs";
3
3
  import { t as IdentityPort } from "../identity-DcaYgjXT.mjs";
4
4
  import { t as Clock } from "../clock-DiSx-WKM.mjs";
5
5
  //#region src/domain/mesh-session.d.ts
@@ -503,6 +503,10 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
503
503
  handshake
504
504
  };
505
505
  }
506
+ /** The address to dial after `link` ended: the one the adapter says reaches the same peer now, if it says anything, else the one this session dialled. */
507
+ function redialAddress(link, dialled) {
508
+ return link.redialAddress?.() ?? dialled;
509
+ }
506
510
  function handleDisconnect(reason, address, localDomains) {
507
511
  if (feedCancelled) return;
508
512
  rejectPendingManageRequests("disconnected before a response arrived");
@@ -533,14 +537,19 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
533
537
  emit();
534
538
  }
535
539
  async function consume(link, address, localDomains) {
540
+ let heardFromPeer = false;
536
541
  for await (const frame of link.receive()) {
537
542
  if (feedCancelled) return;
543
+ if (!heardFromPeer) {
544
+ heardFromPeer = true;
545
+ attempt = 0;
546
+ }
538
547
  await applyFrame(frame);
539
548
  await onFrame?.(link, frame);
540
549
  emit();
541
550
  }
542
551
  onSessionEnd?.(link);
543
- if (state.status === "connected") handleDisconnect("node closed the connection", address, localDomains);
552
+ if (state.status === "connected") handleDisconnect("node closed the connection", redialAddress(link, address), localDomains);
544
553
  }
545
554
  /** Builds this side's own self-advert and signs it with this node's own key (wire-mesh#225), which is what makes it usable by a receiver that learned of it through a hub or a gateway rather than directly from here. this node's own directly-reachable addresses (wire-mesh#38) are advertised, 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, and every such key is covered by the signature. Extensions, CORE_VERSION_GOSSIP_KEY, and topology/peers are all 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 every such collision loudly, but the field order is kept safe in its own right rather than relying solely on the guard staying in sync. topology/peers (wire-mesh#180) is recomputed fresh on every call, the same "always current, never cached" treatment snapshot-seconds already gets, since a session's own connection identity and relay pairings can change between one self-advert and the next. */
546
555
  async function buildSelfAdvert(extensions) {
@@ -595,7 +604,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
595
604
  }
596
605
  }, HANDSHAKE_TIMEOUT_MS);
597
606
  consume(connection, address, localDomains).catch((error) => {
598
- if (state.status === "connected") handleDisconnect(error instanceof Error ? error.message : String(error), address, localDomains);
607
+ if (state.status === "connected") handleDisconnect(error instanceof Error ? error.message : String(error), redialAddress(link, address), localDomains);
599
608
  });
600
609
  }
601
610
  async function doConnect(address, localDomains) {
@@ -1,8 +1,10 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let cbor2 = require("cbor2");
2
3
  //#region src/domain/pinned-address.ts
3
4
  const PIN_FRAGMENT = "#sha256=";
4
5
  const HASH_SEPARATOR = ",";
5
6
  const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
7
+ const HEX_RADIX = 16;
6
8
  function hexToBytes(hex) {
7
9
  const bytes = new Uint8Array(hex.length / 2);
8
10
  for (let index = 0; index < bytes.length; index++) bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
@@ -34,7 +36,56 @@ function parsePinnedAddress(address) {
34
36
  sha256: hashes.map(hexToBytes)
35
37
  };
36
38
  }
39
+ /** The bytes in a SHA-256 hash. */
40
+ const SHA256_BYTES = 32;
41
+ /** The most hashes a node may announce at once. A browser is asked to accept any of them, and a longer list is a node misbehaving rather than one advertising a schedule. */
42
+ const MAX_ANNOUNCED_HASHES = 8;
43
+ /**
44
+ * The message a node sends a client, on a stream it opens to that client, to say which certificates it serves now and will serve next: a CBOR map with one key, `sha256`, holding an array of 32-byte hashes in the order they take over. It is authentic because it arrives on the session the client pinned, so no signature is needed.
45
+ */
46
+ function encodeCertificateHashes(sha256Hex) {
47
+ return (0, cbor2.encode)({ sha256: sha256Hex.map(hexToBytes) }, cbor2.cdeEncodeOptions);
48
+ }
49
+ /**
50
+ * Reads a message made by `encodeCertificateHashes`.
51
+ * @throws Error when it is not a map with a `sha256` array of one to MAX_ANNOUNCED_HASHES byte strings of 32 bytes each.
52
+ */
53
+ function decodeCertificateHashes(bytes) {
54
+ const decoded = (0, cbor2.decode)(bytes, cbor2.cdeDecodeOptions);
55
+ if (typeof decoded !== "object" || decoded === null || !("sha256" in decoded) || !Array.isArray(decoded.sha256)) throw new Error("expected a map with a sha256 array");
56
+ const announced = decoded.sha256;
57
+ if (announced.length === 0 || announced.length > 8) throw new Error(`expected 1 to ${String(8)} hashes, got ${String(announced.length)}`);
58
+ const hashes = [];
59
+ for (const hash of announced) {
60
+ if (!(hash instanceof Uint8Array) || hash.length !== SHA256_BYTES) throw new Error(`each hash must be ${String(SHA256_BYTES)} bytes`);
61
+ hashes.push(new Uint8Array(hash));
62
+ }
63
+ return hashes;
64
+ }
65
+ /** `hashes` followed by those of `more` that are not already in it, so a client can pin what it was given and what it has learned since. */
66
+ function mergeHashes(hashes, more) {
67
+ const known = new Set(hashes.map(bytesToHex));
68
+ const merged = [...hashes];
69
+ for (const hash of more) if (!known.has(bytesToHex(hash))) {
70
+ known.add(bytesToHex(hash));
71
+ merged.push(hash);
72
+ }
73
+ return merged;
74
+ }
75
+ function bytesToHex(bytes) {
76
+ return Array.from(bytes, (byte) => byte.toString(HEX_RADIX).padStart(2, "0")).join("");
77
+ }
78
+ /** `address` with its pinned hashes replaced by `sha256`. */
79
+ function withPinnedHashes(address, sha256) {
80
+ const { url } = parsePinnedAddress(address);
81
+ return formatPinnedAddress(new URL(url).host, sha256.map(bytesToHex));
82
+ }
37
83
  //#endregion
84
+ exports.MAX_ANNOUNCED_HASHES = MAX_ANNOUNCED_HASHES;
85
+ exports.decodeCertificateHashes = decodeCertificateHashes;
86
+ exports.encodeCertificateHashes = encodeCertificateHashes;
38
87
  exports.formatPinnedAddress = formatPinnedAddress;
39
88
  exports.isPinnedAddress = isPinnedAddress;
89
+ exports.mergeHashes = mergeHashes;
40
90
  exports.parsePinnedAddress = parsePinnedAddress;
91
+ exports.withPinnedHashes = withPinnedHashes;
@@ -19,4 +19,19 @@ export declare function isPinnedAddress(address: string): boolean;
19
19
  * @throws Error when the address is not `https://` or does not carry one or more 64-digit lowercase hex hashes.
20
20
  */
21
21
  export declare function parsePinnedAddress(address: string): PinnedAddress;
22
+ /** The most hashes a node may announce at once. A browser is asked to accept any of them, and a longer list is a node misbehaving rather than one advertising a schedule. */
23
+ export declare const MAX_ANNOUNCED_HASHES = 8;
24
+ /**
25
+ * The message a node sends a client, on a stream it opens to that client, to say which certificates it serves now and will serve next: a CBOR map with one key, `sha256`, holding an array of 32-byte hashes in the order they take over. It is authentic because it arrives on the session the client pinned, so no signature is needed.
26
+ */
27
+ export declare function encodeCertificateHashes(sha256Hex: readonly string[]): Uint8Array;
28
+ /**
29
+ * Reads a message made by `encodeCertificateHashes`.
30
+ * @throws Error when it is not a map with a `sha256` array of one to MAX_ANNOUNCED_HASHES byte strings of 32 bytes each.
31
+ */
32
+ export declare function decodeCertificateHashes(bytes: Readonly<Uint8Array>): Uint8Array<ArrayBuffer>[];
33
+ /** `hashes` followed by those of `more` that are not already in it, so a client can pin what it was given and what it has learned since. */
34
+ export declare function mergeHashes(hashes: readonly Uint8Array<ArrayBuffer>[], more: readonly Uint8Array<ArrayBuffer>[]): Uint8Array<ArrayBuffer>[];
35
+ /** `address` with its pinned hashes replaced by `sha256`. */
36
+ export declare function withPinnedHashes(address: string, sha256: readonly Uint8Array<ArrayBuffer>[]): string;
22
37
  //#endregion
@@ -19,4 +19,19 @@ export declare function isPinnedAddress(address: string): boolean;
19
19
  * @throws Error when the address is not `https://` or does not carry one or more 64-digit lowercase hex hashes.
20
20
  */
21
21
  export declare function parsePinnedAddress(address: string): PinnedAddress;
22
+ /** The most hashes a node may announce at once. A browser is asked to accept any of them, and a longer list is a node misbehaving rather than one advertising a schedule. */
23
+ export declare const MAX_ANNOUNCED_HASHES = 8;
24
+ /**
25
+ * The message a node sends a client, on a stream it opens to that client, to say which certificates it serves now and will serve next: a CBOR map with one key, `sha256`, holding an array of 32-byte hashes in the order they take over. It is authentic because it arrives on the session the client pinned, so no signature is needed.
26
+ */
27
+ export declare function encodeCertificateHashes(sha256Hex: readonly string[]): Uint8Array;
28
+ /**
29
+ * Reads a message made by `encodeCertificateHashes`.
30
+ * @throws Error when it is not a map with a `sha256` array of one to MAX_ANNOUNCED_HASHES byte strings of 32 bytes each.
31
+ */
32
+ export declare function decodeCertificateHashes(bytes: Readonly<Uint8Array>): Uint8Array<ArrayBuffer>[];
33
+ /** `hashes` followed by those of `more` that are not already in it, so a client can pin what it was given and what it has learned since. */
34
+ export declare function mergeHashes(hashes: readonly Uint8Array<ArrayBuffer>[], more: readonly Uint8Array<ArrayBuffer>[]): Uint8Array<ArrayBuffer>[];
35
+ /** `address` with its pinned hashes replaced by `sha256`. */
36
+ export declare function withPinnedHashes(address: string, sha256: readonly Uint8Array<ArrayBuffer>[]): string;
22
37
  //#endregion
@@ -1,7 +1,9 @@
1
+ import { cdeDecodeOptions, cdeEncodeOptions, decode, encode } from "cbor2";
1
2
  //#region src/domain/pinned-address.ts
2
3
  const PIN_FRAGMENT = "#sha256=";
3
4
  const HASH_SEPARATOR = ",";
4
5
  const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
6
+ const HEX_RADIX = 16;
5
7
  function hexToBytes(hex) {
6
8
  const bytes = new Uint8Array(hex.length / 2);
7
9
  for (let index = 0; index < bytes.length; index++) bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
@@ -33,5 +35,49 @@ function parsePinnedAddress(address) {
33
35
  sha256: hashes.map(hexToBytes)
34
36
  };
35
37
  }
38
+ /** The bytes in a SHA-256 hash. */
39
+ const SHA256_BYTES = 32;
40
+ /** The most hashes a node may announce at once. A browser is asked to accept any of them, and a longer list is a node misbehaving rather than one advertising a schedule. */
41
+ const MAX_ANNOUNCED_HASHES = 8;
42
+ /**
43
+ * The message a node sends a client, on a stream it opens to that client, to say which certificates it serves now and will serve next: a CBOR map with one key, `sha256`, holding an array of 32-byte hashes in the order they take over. It is authentic because it arrives on the session the client pinned, so no signature is needed.
44
+ */
45
+ function encodeCertificateHashes(sha256Hex) {
46
+ return encode({ sha256: sha256Hex.map(hexToBytes) }, cdeEncodeOptions);
47
+ }
48
+ /**
49
+ * Reads a message made by `encodeCertificateHashes`.
50
+ * @throws Error when it is not a map with a `sha256` array of one to MAX_ANNOUNCED_HASHES byte strings of 32 bytes each.
51
+ */
52
+ function decodeCertificateHashes(bytes) {
53
+ const decoded = decode(bytes, cdeDecodeOptions);
54
+ if (typeof decoded !== "object" || decoded === null || !("sha256" in decoded) || !Array.isArray(decoded.sha256)) throw new Error("expected a map with a sha256 array");
55
+ const announced = decoded.sha256;
56
+ if (announced.length === 0 || announced.length > 8) throw new Error(`expected 1 to ${String(8)} hashes, got ${String(announced.length)}`);
57
+ const hashes = [];
58
+ for (const hash of announced) {
59
+ if (!(hash instanceof Uint8Array) || hash.length !== SHA256_BYTES) throw new Error(`each hash must be ${String(SHA256_BYTES)} bytes`);
60
+ hashes.push(new Uint8Array(hash));
61
+ }
62
+ return hashes;
63
+ }
64
+ /** `hashes` followed by those of `more` that are not already in it, so a client can pin what it was given and what it has learned since. */
65
+ function mergeHashes(hashes, more) {
66
+ const known = new Set(hashes.map(bytesToHex));
67
+ const merged = [...hashes];
68
+ for (const hash of more) if (!known.has(bytesToHex(hash))) {
69
+ known.add(bytesToHex(hash));
70
+ merged.push(hash);
71
+ }
72
+ return merged;
73
+ }
74
+ function bytesToHex(bytes) {
75
+ return Array.from(bytes, (byte) => byte.toString(HEX_RADIX).padStart(2, "0")).join("");
76
+ }
77
+ /** `address` with its pinned hashes replaced by `sha256`. */
78
+ function withPinnedHashes(address, sha256) {
79
+ const { url } = parsePinnedAddress(address);
80
+ return formatPinnedAddress(new URL(url).host, sha256.map(bytesToHex));
81
+ }
36
82
  //#endregion
37
- export { formatPinnedAddress, isPinnedAddress, parsePinnedAddress };
83
+ export { MAX_ANNOUNCED_HASHES, decodeCertificateHashes, encodeCertificateHashes, formatPinnedAddress, isPinnedAddress, mergeHashes, parsePinnedAddress, withPinnedHashes };
@@ -1,5 +1,5 @@
1
1
  import { D as DeviceId, N as Frame, et as PeerAdvert } from "../protocol-DCDae3z4.cjs";
2
- import { t as Connection } from "../transport-CfW7fsGK.cjs";
2
+ import { t as Connection } from "../transport-DIXjoQKn.cjs";
3
3
  import { AdvertExtensionPolicy } from "./advert-extension-policy.cjs";
4
4
  import { Mailbox, MailboxRefusalReason } from "./hub-mailbox.cjs";
5
5
  import { PeerAdvertVerifier } from "./peer-advert.cjs";
@@ -1,5 +1,5 @@
1
1
  import { D as DeviceId, N as Frame, et as PeerAdvert } from "../protocol-DCDae3z4.mjs";
2
- import { t as Connection } from "../transport-iLBxIkRS.mjs";
2
+ import { t as Connection } from "../transport-B0GywEzG.mjs";
3
3
  import { AdvertExtensionPolicy } from "./advert-extension-policy.mjs";
4
4
  import { Mailbox, MailboxRefusalReason } from "./hub-mailbox.mjs";
5
5
  import { PeerAdvertVerifier } from "./peer-advert.mjs";
@@ -1,5 +1,5 @@
1
1
  import { N as Frame } from "../protocol-DCDae3z4.cjs";
2
- import { t as Connection } from "../transport-CfW7fsGK.cjs";
2
+ import { t as Connection } from "../transport-DIXjoQKn.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
1
  import { N as Frame } from "../protocol-DCDae3z4.mjs";
2
- import { t as Connection } from "../transport-iLBxIkRS.mjs";
2
+ import { t as Connection } from "../transport-B0GywEzG.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,2 +1,2 @@
1
- import { n as Listener, r as Transport, t as Connection } from "../transport-CfW7fsGK.cjs";
1
+ import { n as Listener, r as Transport, t as Connection } from "../transport-DIXjoQKn.cjs";
2
2
  export { Connection, Listener, Transport };
@@ -1,2 +1,2 @@
1
- import { n as Listener, r as Transport, t as Connection } from "../transport-iLBxIkRS.mjs";
1
+ import { n as Listener, r as Transport, t as Connection } from "../transport-B0GywEzG.mjs";
2
2
  export { Connection, Listener, Transport };
@@ -10,6 +10,8 @@ interface Connection {
10
10
  close: () => Promise<void>;
11
11
  /** The peer's device-id, cryptographically authenticated by the adapter itself (never a value read off the wire) -- absent when the adapter has nothing to authenticate against (createTcpTransport) or the peer presented no credential to check (an accepting createTlsTransport connection whose peer sent no certificate). A caller that later receives an application-level claim of who the peer is (e.g. an `introduce`-style message) MUST treat a mismatch against this field as a spoofing attempt and refuse the claim, exactly the same "reject and destroy" obligation self-certifying claims elsewhere in this spec already carry. */
12
12
  readonly peerDeviceId?: DeviceId;
13
+ /** The address to dial to reach this same peer again, when the adapter has learned a better one than the address this connection was dialled with: a node that rotates the certificate a browser pins tells each connection the certificates it will serve next, so a dial made after the address the user was given has aged still succeeds. Absent when the adapter learned nothing, in which case the dialled address stands. */
14
+ readonly redialAddress?: () => string;
13
15
  /** Lets the event loop exit while this connection is still open and functioning -- a caller that wants I/O to keep working without keeping the process alive (matching Node's own `net.Socket.unref()`) calls this once. Adapters with no underlying handle to unref (an in-memory test double, a browser WebSocket) simply omit it. */
14
16
  unref?: () => void;
15
17
  }
@@ -10,6 +10,8 @@ interface Connection {
10
10
  close: () => Promise<void>;
11
11
  /** The peer's device-id, cryptographically authenticated by the adapter itself (never a value read off the wire) -- absent when the adapter has nothing to authenticate against (createTcpTransport) or the peer presented no credential to check (an accepting createTlsTransport connection whose peer sent no certificate). A caller that later receives an application-level claim of who the peer is (e.g. an `introduce`-style message) MUST treat a mismatch against this field as a spoofing attempt and refuse the claim, exactly the same "reject and destroy" obligation self-certifying claims elsewhere in this spec already carry. */
12
12
  readonly peerDeviceId?: DeviceId;
13
+ /** The address to dial to reach this same peer again, when the adapter has learned a better one than the address this connection was dialled with: a node that rotates the certificate a browser pins tells each connection the certificates it will serve next, so a dial made after the address the user was given has aged still succeeds. Absent when the adapter learned nothing, in which case the dialled address stands. */
14
+ readonly redialAddress?: () => string;
13
15
  /** Lets the event loop exit while this connection is still open and functioning -- a caller that wants I/O to keep working without keeping the process alive (matching Node's own `net.Socket.unref()`) calls this once. Adapters with no underlying handle to unref (an in-memory test double, a browser WebSocket) simply omit it. */
14
16
  unref?: () => void;
15
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wire-mesh-core",
3
- "version": "3.3.1",
3
+ "version": "3.4.0",
4
4
  "dependencies": {
5
5
  "cbor2": "2.3.0",
6
6
  "cddl.js": "1.0.1",