wire-mesh-core 4.0.8 → 4.1.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 (42) hide show
  1. package/dist/adapters/node-identity.cjs +16 -7
  2. package/dist/adapters/node-identity.d.cts +1 -1
  3. package/dist/adapters/node-identity.d.mts +1 -1
  4. package/dist/adapters/node-identity.mjs +16 -7
  5. package/dist/adapters/threshold-identity.d.cts +1 -1
  6. package/dist/adapters/threshold-identity.d.mts +1 -1
  7. package/dist/domain/capability-grant.d.cts +1 -1
  8. package/dist/domain/capability-grant.d.mts +1 -1
  9. package/dist/domain/capability-request.d.cts +1 -1
  10. package/dist/domain/capability-request.d.mts +1 -1
  11. package/dist/domain/channel-exhausted-error.cjs +11 -0
  12. package/dist/domain/channel-exhausted-error.d.cts +6 -0
  13. package/dist/domain/channel-exhausted-error.d.mts +6 -0
  14. package/dist/domain/channel-exhausted-error.mjs +10 -0
  15. package/dist/domain/data-sync.d.cts +1 -1
  16. package/dist/domain/data-sync.d.mts +1 -1
  17. package/dist/domain/handshake.cjs +9 -0
  18. package/dist/domain/handshake.d.cts +2 -0
  19. package/dist/domain/handshake.d.mts +2 -0
  20. package/dist/domain/handshake.mjs +9 -1
  21. package/dist/domain/mesh-session.cjs +112 -48
  22. package/dist/domain/mesh-session.d.cts +4 -2
  23. package/dist/domain/mesh-session.d.mts +4 -2
  24. package/dist/domain/mesh-session.mjs +111 -47
  25. package/dist/domain/notice-board.d.cts +1 -1
  26. package/dist/domain/notice-board.d.mts +1 -1
  27. package/dist/domain/peer-advert.d.cts +1 -1
  28. package/dist/domain/peer-advert.d.mts +1 -1
  29. package/dist/domain/room-rekey.d.cts +1 -1
  30. package/dist/domain/room-rekey.d.mts +1 -1
  31. package/dist/domain/room.d.cts +1 -1
  32. package/dist/domain/room.d.mts +1 -1
  33. package/dist/domain/tokens.d.cts +1 -1
  34. package/dist/domain/tokens.d.mts +1 -1
  35. package/dist/{identity-EV-HHv_D.d.mts → identity-BioGOV9M.d.mts} +10 -1
  36. package/dist/{identity-B6nhlSOT.d.cts → identity-C5jq53NV.d.cts} +10 -1
  37. package/dist/ports/identity.cjs +13 -0
  38. package/dist/ports/identity.d.cts +2 -2
  39. package/dist/ports/identity.d.mts +2 -2
  40. package/dist/ports/identity.mjs +12 -1
  41. package/package.json +5 -1
  42. package/wasm-dist/wire_mesh_threshold_wasm_bg.wasm +0 -0
@@ -1,4 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_ports_identity = require("../ports/identity.cjs");
2
3
  let node_crypto = require("node:crypto");
3
4
  //#region src/adapters/node-identity.ts
4
5
  const ES256 = -7;
@@ -11,7 +12,7 @@ const EDDSA_PUBLIC_KEY_BYTES = 32;
11
12
  function publicKeySize(alg) {
12
13
  if (alg === ES256) return ES256_PUBLIC_KEY_BYTES;
13
14
  if (alg === EDDSA) return EDDSA_PUBLIC_KEY_BYTES;
14
- throw new Error(`unsupported identity-key alg ${String(alg)}`);
15
+ throw new require_ports_identity.IdentityKeyError("unsupported_alg", `unsupported identity-key alg ${String(alg)}`);
15
16
  }
16
17
  function algParams(alg) {
17
18
  if (alg === ES256) return {
@@ -19,7 +20,7 @@ function algParams(alg) {
19
20
  hash: "SHA-256"
20
21
  };
21
22
  if (alg === EDDSA) return { name: "Ed25519" };
22
- throw new Error(`unsupported identity-key alg ${String(alg)}`);
23
+ throw new require_ports_identity.IdentityKeyError("unsupported_alg", `unsupported identity-key alg ${String(alg)}`);
23
24
  }
24
25
  /** Copies into a fresh, non-shared, whole-buffer Uint8Array -- Web Crypto's BufferSource parameters reject a view over a SharedArrayBuffer or a sub-range view, neither of which a caller-supplied Uint8Array is guaranteed not to be. */
25
26
  function toBufferSource(bytes) {
@@ -36,7 +37,7 @@ function importedKeyId(key) {
36
37
  return `${String(key.alg)}:${Buffer.from(key["public-key"]).toString("base64")}`;
37
38
  }
38
39
  async function importPublicKey(key) {
39
- if (key["public-key"].length !== publicKeySize(key.alg)) throw new Error(`identity-key public-key is ${String(key["public-key"].length)} bytes, not the ${String(publicKeySize(key.alg))} an alg ${String(key.alg)} key has`);
40
+ if (key["public-key"].length !== publicKeySize(key.alg)) throw new require_ports_identity.IdentityKeyError("invalid_key", `identity-key public-key is ${String(key["public-key"].length)} bytes, not the ${String(publicKeySize(key.alg))} an alg ${String(key.alg)} key has`);
40
41
  const id = importedKeyId(key);
41
42
  const held = importedKeys.get(id);
42
43
  if (held !== void 0) {
@@ -53,13 +54,21 @@ async function importPublicKey(key) {
53
54
  }
54
55
  return pending;
55
56
  }
57
+ /** Imports the raw public key, reporting a key the algorithm cannot import (not a point on the curve, for ES256) as an IdentityKeyError rather than the Web Crypto error it arrives as. An Ed25519 import does not check that the bytes are a curve point: such a key imports, and every signature under it fails to verify. */
58
+ async function importRawPublicKey(key, algorithm) {
59
+ try {
60
+ return await node_crypto.webcrypto.subtle.importKey("raw", toBufferSource(key["public-key"]), algorithm, false, ["verify"]);
61
+ } catch (cause) {
62
+ throw new require_ports_identity.IdentityKeyError("invalid_key", `identity-key public-key does not import as an alg ${String(key.alg)} key`, { cause });
63
+ }
64
+ }
56
65
  async function importPublicKeyUncached(key) {
57
- if (key.alg === ES256) return node_crypto.webcrypto.subtle.importKey("raw", toBufferSource(key["public-key"]), {
66
+ if (key.alg === ES256) return importRawPublicKey(key, {
58
67
  name: "ECDSA",
59
68
  namedCurve: "P-256"
60
- }, false, ["verify"]);
61
- if (key.alg === EDDSA) return node_crypto.webcrypto.subtle.importKey("raw", toBufferSource(key["public-key"]), { name: "Ed25519" }, false, ["verify"]);
62
- throw new Error(`unsupported identity-key alg ${String(key.alg)}`);
69
+ });
70
+ if (key.alg === EDDSA) return importRawPublicKey(key, { name: "Ed25519" });
71
+ throw new require_ports_identity.IdentityKeyError("unsupported_alg", `unsupported identity-key alg ${String(key.alg)}`);
63
72
  }
64
73
  async function deriveDeviceId(publicKey) {
65
74
  return new Uint8Array(await node_crypto.webcrypto.subtle.digest("SHA-256", toBufferSource(publicKey)));
@@ -1,5 +1,5 @@
1
1
  import { B as IdentityKey, D as DeviceId } from "../protocol-DzmpxxUr.cjs";
2
- import { t as IdentityPort } from "../identity-B6nhlSOT.cjs";
2
+ import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
3
3
  import { webcrypto } from "node:crypto";
4
4
  //#region src/adapters/node-identity.d.ts
5
5
  export declare function deriveDeviceId(publicKey: Uint8Array): Promise<DeviceId>;
@@ -1,5 +1,5 @@
1
1
  import { B as IdentityKey, D as DeviceId } from "../protocol-DzmpxxUr.mjs";
2
- import { t as IdentityPort } from "../identity-EV-HHv_D.mjs";
2
+ import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
3
3
  import { webcrypto } from "node:crypto";
4
4
  //#region src/adapters/node-identity.d.ts
5
5
  export declare function deriveDeviceId(publicKey: Uint8Array): Promise<DeviceId>;
@@ -1,3 +1,4 @@
1
+ import { IdentityKeyError } from "../ports/identity.mjs";
1
2
  import { webcrypto } from "node:crypto";
2
3
  //#region src/adapters/node-identity.ts
3
4
  const ES256 = -7;
@@ -10,7 +11,7 @@ const EDDSA_PUBLIC_KEY_BYTES = 32;
10
11
  function publicKeySize(alg) {
11
12
  if (alg === ES256) return ES256_PUBLIC_KEY_BYTES;
12
13
  if (alg === EDDSA) return EDDSA_PUBLIC_KEY_BYTES;
13
- throw new Error(`unsupported identity-key alg ${String(alg)}`);
14
+ throw new IdentityKeyError("unsupported_alg", `unsupported identity-key alg ${String(alg)}`);
14
15
  }
15
16
  function algParams(alg) {
16
17
  if (alg === ES256) return {
@@ -18,7 +19,7 @@ function algParams(alg) {
18
19
  hash: "SHA-256"
19
20
  };
20
21
  if (alg === EDDSA) return { name: "Ed25519" };
21
- throw new Error(`unsupported identity-key alg ${String(alg)}`);
22
+ throw new IdentityKeyError("unsupported_alg", `unsupported identity-key alg ${String(alg)}`);
22
23
  }
23
24
  /** Copies into a fresh, non-shared, whole-buffer Uint8Array -- Web Crypto's BufferSource parameters reject a view over a SharedArrayBuffer or a sub-range view, neither of which a caller-supplied Uint8Array is guaranteed not to be. */
24
25
  function toBufferSource(bytes) {
@@ -35,7 +36,7 @@ function importedKeyId(key) {
35
36
  return `${String(key.alg)}:${Buffer.from(key["public-key"]).toString("base64")}`;
36
37
  }
37
38
  async function importPublicKey(key) {
38
- if (key["public-key"].length !== publicKeySize(key.alg)) throw new Error(`identity-key public-key is ${String(key["public-key"].length)} bytes, not the ${String(publicKeySize(key.alg))} an alg ${String(key.alg)} key has`);
39
+ if (key["public-key"].length !== publicKeySize(key.alg)) throw new IdentityKeyError("invalid_key", `identity-key public-key is ${String(key["public-key"].length)} bytes, not the ${String(publicKeySize(key.alg))} an alg ${String(key.alg)} key has`);
39
40
  const id = importedKeyId(key);
40
41
  const held = importedKeys.get(id);
41
42
  if (held !== void 0) {
@@ -52,13 +53,21 @@ async function importPublicKey(key) {
52
53
  }
53
54
  return pending;
54
55
  }
56
+ /** Imports the raw public key, reporting a key the algorithm cannot import (not a point on the curve, for ES256) as an IdentityKeyError rather than the Web Crypto error it arrives as. An Ed25519 import does not check that the bytes are a curve point: such a key imports, and every signature under it fails to verify. */
57
+ async function importRawPublicKey(key, algorithm) {
58
+ try {
59
+ return await webcrypto.subtle.importKey("raw", toBufferSource(key["public-key"]), algorithm, false, ["verify"]);
60
+ } catch (cause) {
61
+ throw new IdentityKeyError("invalid_key", `identity-key public-key does not import as an alg ${String(key.alg)} key`, { cause });
62
+ }
63
+ }
55
64
  async function importPublicKeyUncached(key) {
56
- if (key.alg === ES256) return webcrypto.subtle.importKey("raw", toBufferSource(key["public-key"]), {
65
+ if (key.alg === ES256) return importRawPublicKey(key, {
57
66
  name: "ECDSA",
58
67
  namedCurve: "P-256"
59
- }, false, ["verify"]);
60
- if (key.alg === EDDSA) return webcrypto.subtle.importKey("raw", toBufferSource(key["public-key"]), { name: "Ed25519" }, false, ["verify"]);
61
- throw new Error(`unsupported identity-key alg ${String(key.alg)}`);
68
+ });
69
+ if (key.alg === EDDSA) return importRawPublicKey(key, { name: "Ed25519" });
70
+ throw new IdentityKeyError("unsupported_alg", `unsupported identity-key alg ${String(key.alg)}`);
62
71
  }
63
72
  async function deriveDeviceId(publicKey) {
64
73
  return new Uint8Array(await webcrypto.subtle.digest("SHA-256", toBufferSource(publicKey)));
@@ -1,5 +1,5 @@
1
1
  import { D as DeviceId } from "../protocol-DzmpxxUr.cjs";
2
- import { t as IdentityPort } from "../identity-B6nhlSOT.cjs";
2
+ import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
3
3
  import { DeviceKeyed } from "./threshold-wasm.cjs";
4
4
  import { ThresholdSubject } from "../domain/threshold-subject.cjs";
5
5
  //#region src/adapters/threshold-identity.d.ts
@@ -1,5 +1,5 @@
1
1
  import { D as DeviceId } from "../protocol-DzmpxxUr.mjs";
2
- import { t as IdentityPort } from "../identity-EV-HHv_D.mjs";
2
+ import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
3
3
  import { DeviceKeyed } from "./threshold-wasm.mjs";
4
4
  import { ThresholdSubject } from "../domain/threshold-subject.mjs";
5
5
  //#region src/adapters/threshold-identity.d.ts
@@ -1,5 +1,5 @@
1
1
  import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DzmpxxUr.cjs";
2
- import { t as IdentityPort } from "../identity-B6nhlSOT.cjs";
2
+ import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
3
3
  import { t as Clock } from "../clock-DhUPRfLh.cjs";
4
4
  import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.cjs";
5
5
  import { RevocationCheck } from "./tokens.cjs";
@@ -1,5 +1,5 @@
1
1
  import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DzmpxxUr.mjs";
2
- import { t as IdentityPort } from "../identity-EV-HHv_D.mjs";
2
+ import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
3
3
  import { t as Clock } from "../clock-DhUPRfLh.mjs";
4
4
  import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.mjs";
5
5
  import { RevocationCheck } from "./tokens.mjs";
@@ -1,5 +1,5 @@
1
1
  import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken, u as CapabilityGrantOk } from "../protocol-DzmpxxUr.cjs";
2
- import { t as IdentityPort } from "../identity-B6nhlSOT.cjs";
2
+ import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
3
3
  import { t as Clock } from "../clock-DhUPRfLh.cjs";
4
4
  import { IncomingManageRequest, MeshSession } from "./mesh-session.cjs";
5
5
  import { m as TokenDelegateHandler, r as ConditionsContext } from "../token-predicates-lg4TtUxm.cjs";
@@ -1,5 +1,5 @@
1
1
  import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken, u as CapabilityGrantOk } from "../protocol-DzmpxxUr.mjs";
2
- import { t as IdentityPort } from "../identity-EV-HHv_D.mjs";
2
+ import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
3
3
  import { t as Clock } from "../clock-DhUPRfLh.mjs";
4
4
  import { IncomingManageRequest, MeshSession } from "./mesh-session.mjs";
5
5
  import { m as TokenDelegateHandler, r as ConditionsContext } from "../token-predicates-CrieP1WI.mjs";
@@ -0,0 +1,11 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/domain/channel-exhausted-error.ts
3
+ /** Thrown when a secure channel's send counter is spent: nothing more can be sealed on that channel, and a new handshake gives a fresh counter. The session forgets such a channel, so the next send to the peer starts one; a caller that sees this error from a send can retry it. */
4
+ var ChannelExhaustedError = class extends Error {
5
+ constructor() {
6
+ super("the channel's send counter is exhausted");
7
+ this.name = "ChannelExhaustedError";
8
+ }
9
+ };
10
+ //#endregion
11
+ exports.ChannelExhaustedError = ChannelExhaustedError;
@@ -0,0 +1,6 @@
1
+ //#region src/domain/channel-exhausted-error.d.ts
2
+ /** Thrown when a secure channel's send counter is spent: nothing more can be sealed on that channel, and a new handshake gives a fresh counter. The session forgets such a channel, so the next send to the peer starts one; a caller that sees this error from a send can retry it. */
3
+ export declare class ChannelExhaustedError extends Error {
4
+ constructor();
5
+ }
6
+ //#endregion
@@ -0,0 +1,6 @@
1
+ //#region src/domain/channel-exhausted-error.d.ts
2
+ /** Thrown when a secure channel's send counter is spent: nothing more can be sealed on that channel, and a new handshake gives a fresh counter. The session forgets such a channel, so the next send to the peer starts one; a caller that sees this error from a send can retry it. */
3
+ export declare class ChannelExhaustedError extends Error {
4
+ constructor();
5
+ }
6
+ //#endregion
@@ -0,0 +1,10 @@
1
+ //#region src/domain/channel-exhausted-error.ts
2
+ /** Thrown when a secure channel's send counter is spent: nothing more can be sealed on that channel, and a new handshake gives a fresh counter. The session forgets such a channel, so the next send to the peer starts one; a caller that sees this error from a send can retry it. */
3
+ var ChannelExhaustedError = class extends Error {
4
+ constructor() {
5
+ super("the channel's send counter is exhausted");
6
+ this.name = "ChannelExhaustedError";
7
+ }
8
+ };
9
+ //#endregion
10
+ export { ChannelExhaustedError };
@@ -1,6 +1,6 @@
1
1
  import { D as DeviceId, E as DataRequestFrame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-DzmpxxUr.cjs";
2
2
  import { t as KeyValueStorage } from "../storage-jbGFAYIV.cjs";
3
- import { t as IdentityPort } from "../identity-B6nhlSOT.cjs";
3
+ import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
4
4
  //#region src/domain/data-sync.d.ts
5
5
  export interface DataSyncOptions {
6
6
  identity: IdentityPort;
@@ -1,6 +1,6 @@
1
1
  import { D as DeviceId, E as DataRequestFrame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-DzmpxxUr.mjs";
2
2
  import { t as KeyValueStorage } from "../storage-jbGFAYIV.mjs";
3
- import { t as IdentityPort } from "../identity-EV-HHv_D.mjs";
3
+ import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
4
4
  //#region src/domain/data-sync.d.ts
5
5
  export interface DataSyncOptions {
6
6
  identity: IdentityPort;
@@ -2,6 +2,14 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  //#region src/domain/handshake.ts
3
3
  /** The highest protocol version this build of core understands. */
4
4
  const SUPPORTED_PROTOCOL_VERSION = 1;
5
+ /** The handshake this side sends: the version it supports and the domains it offers. */
6
+ function localHandshake(domains) {
7
+ return {
8
+ type: "handshake",
9
+ version: 1,
10
+ domains: [...domains]
11
+ };
12
+ }
5
13
  /**
6
14
  * Domain names that must never be negotiated, per handshake.cddl: core/federation is retired (its string stays reserved, never reused) and "a peer must never advertise or negotiate it". Excluded here even if both peers advertise it -- two buggy advertisers must not end up speaking a dead domain.
7
15
  */
@@ -20,4 +28,5 @@ function negotiate(local, remote) {
20
28
  }
21
29
  //#endregion
22
30
  exports.SUPPORTED_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSION;
31
+ exports.localHandshake = localHandshake;
23
32
  exports.negotiate = negotiate;
@@ -2,6 +2,8 @@ import { A as DomainId, R as HandshakeFrame, lt as ProtocolVersion } from "../pr
2
2
  //#region src/domain/handshake.d.ts
3
3
  /** The highest protocol version this build of core understands. */
4
4
  export declare const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion;
5
+ /** The handshake this side sends: the version it supports and the domains it offers. */
6
+ export declare function localHandshake(domains: readonly string[]): HandshakeFrame;
5
7
  export interface NegotiationResult {
6
8
  ok: boolean;
7
9
  /** The version both peers will speak for the rest of the session -- the lower of the two offered versions, so a peer never has to understand a frame shape it didn't advertise. */
@@ -2,6 +2,8 @@ import { A as DomainId, R as HandshakeFrame, lt as ProtocolVersion } from "../pr
2
2
  //#region src/domain/handshake.d.ts
3
3
  /** The highest protocol version this build of core understands. */
4
4
  export declare const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion;
5
+ /** The handshake this side sends: the version it supports and the domains it offers. */
6
+ export declare function localHandshake(domains: readonly string[]): HandshakeFrame;
5
7
  export interface NegotiationResult {
6
8
  ok: boolean;
7
9
  /** The version both peers will speak for the rest of the session -- the lower of the two offered versions, so a peer never has to understand a frame shape it didn't advertise. */
@@ -1,6 +1,14 @@
1
1
  //#region src/domain/handshake.ts
2
2
  /** The highest protocol version this build of core understands. */
3
3
  const SUPPORTED_PROTOCOL_VERSION = 1;
4
+ /** The handshake this side sends: the version it supports and the domains it offers. */
5
+ function localHandshake(domains) {
6
+ return {
7
+ type: "handshake",
8
+ version: 1,
9
+ domains: [...domains]
10
+ };
11
+ }
4
12
  /**
5
13
  * Domain names that must never be negotiated, per handshake.cddl: core/federation is retired (its string stays reserved, never reused) and "a peer must never advertise or negotiate it". Excluded here even if both peers advertise it -- two buggy advertisers must not end up speaking a dead domain.
6
14
  */
@@ -18,4 +26,4 @@ function negotiate(local, remote) {
18
26
  };
19
27
  }
20
28
  //#endregion
21
- export { SUPPORTED_PROTOCOL_VERSION, negotiate };
29
+ export { SUPPORTED_PROTOCOL_VERSION, localHandshake, negotiate };
@@ -1,10 +1,12 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_adapters_frame_codec = require("../adapters/frame-codec.cjs");
3
+ const require_ports_identity = require("../ports/identity.cjs");
3
4
  const require_domain_device_id = require("./device-id.cjs");
4
5
  const require_domain_token_scope = require("./token-scope.cjs");
5
6
  const require_gossip_extensions = require("../gossip-extensions-BBYgMZ2N.cjs");
6
7
  const require_domain_peer_advert = require("./peer-advert.cjs");
7
8
  const require_domain_async_queue = require("./async-queue.cjs");
9
+ const require_domain_channel_exhausted_error = require("./channel-exhausted-error.cjs");
8
10
  const require_domain_handshake = require("./handshake.cjs");
9
11
  const require_domain_hub_mailbox = require("./hub-mailbox.cjs");
10
12
  const require_domain_ping_round_trips = require("./ping-round-trips.cjs");
@@ -27,6 +29,19 @@ function createRelayPairings() {
27
29
  };
28
30
  }
29
31
  //#endregion
32
+ //#region src/domain/bounded-log.ts
33
+ /** A log that keeps only its most recent `window` entries: newest at the end, the oldest dropped once the window is full. Pure and closure-free, split out of mesh-session.ts to keep that file under this repo's max-lines lint budget; nothing here is session-shaped. */
34
+ function createBoundedLog(window) {
35
+ const log = [];
36
+ return {
37
+ record: (entry) => {
38
+ log.push(entry);
39
+ if (log.length > window) log.splice(0, log.length - window);
40
+ },
41
+ entries: () => [...log]
42
+ };
43
+ }
44
+ //#endregion
30
45
  //#region src/domain/secure-channel.ts
31
46
  /**
32
47
  * The end-to-end channel two devices open between themselves inside a relay pairing (spec/secure-channel.cddl). A relay only forwards the bytes of a `relay-data` frame, and without this it could read them, alter them, and say who sent them. Here the two devices authenticate each other's identity keys over a fresh ECDH exchange, derive one AES-256-GCM key per direction, and seal every frame they exchange so the relay sees ciphertext and every receiver learns the sender's identity from the handshake and nothing a relay asserts.
@@ -96,16 +111,16 @@ async function beginHandshake(identity, peer) {
96
111
  };
97
112
  }
98
113
  /**
99
- * Checks a received hello: its identity key must hash to a device-id (which is then the peer), it must be addressed to this device, and its signature must verify under that key over the input spec/secure-channel.cddl defines. Never throws: a hostile hello gets a verdict.
114
+ * Checks a received hello: it must be addressed to this device, its identity key must be a usable key, and its signature must verify under that key over the input spec/secure-channel.cddl defines. A hostile hello gets a verdict. An error from the identity port that is not an IdentityKeyError is a failure of the port, not a verdict about the peer, and propagates.
100
115
  */
101
116
  async function verifyHello(identity, hello) {
102
117
  const key = hello["identity-key"];
118
+ const sender = await identity.deriveDeviceId(key["public-key"]);
119
+ if (!require_domain_token_scope.bytesEqual(hello["to-device"], identity.deviceId)) return {
120
+ ok: false,
121
+ reason: "wrong_recipient"
122
+ };
103
123
  try {
104
- const sender = await identity.deriveDeviceId(key["public-key"]);
105
- if (!require_domain_token_scope.bytesEqual(hello["to-device"], identity.deviceId)) return {
106
- ok: false,
107
- reason: "wrong_recipient"
108
- };
109
124
  return await identity.verify(key, helloSigningInput(hello["ephemeral-key"], hello.nonce, sender, identity.deviceId), hello.signature) ? {
110
125
  ok: true,
111
126
  peer: {
@@ -116,13 +131,30 @@ async function verifyHello(identity, hello) {
116
131
  ok: false,
117
132
  reason: "bad_signature"
118
133
  };
119
- } catch {
134
+ } catch (error) {
135
+ if (!(error instanceof require_ports_identity.IdentityKeyError)) throw error;
120
136
  return {
121
137
  ok: false,
122
- reason: "identity_mismatch"
138
+ reason: error.fault === "unsupported_alg" ? "unsupported_identity_alg" : "invalid_identity_key"
123
139
  };
124
140
  }
125
141
  }
142
+ /** The largest counter a channel uses: the counter is an unsigned 64-bit integer on the wire, and this implementation holds it as a JavaScript number, exact up to this value. A frame above it is refused at decoding, never truncated, and sealing refuses to go past it, never wraps. */
143
+ const MAX_COUNTER = Number.MAX_SAFE_INTEGER;
144
+ /** Throws a RangeError unless `counter` is a counter a channel can start from: a non-negative safe integer. */
145
+ function assertFirstSendCounter(counter) {
146
+ if (!Number.isSafeInteger(counter) || counter < 0) throw new RangeError("firstSendCounter must be a non-negative safe integer");
147
+ }
148
+ /**
149
+ * Takes the next send counter from `next`, the counter the next seal would use: returns it with the counter that follows. Throws once `next` is past MAX_COUNTER, since a counter that cannot be held exactly would repeat an IV under the same key.
150
+ */
151
+ function claimSendCounter(next) {
152
+ if (next > MAX_COUNTER) throw new require_domain_channel_exhausted_error.ChannelExhaustedError();
153
+ return {
154
+ counter: next,
155
+ next: next + 1
156
+ };
157
+ }
126
158
  function ivFor(counter) {
127
159
  const iv = new Uint8Array(GCM_IV_BYTE_LENGTH);
128
160
  iv.set(counterBytes(counter), IV_COUNTER_OFFSET);
@@ -144,11 +176,10 @@ async function deriveDirectionKey({ secret, salt, low, high, label }) {
144
176
  }, false, ["encrypt", "decrypt"]);
145
177
  }
146
178
  /**
147
- * Completes a handshake once the peer's hello has verified: derives the two directional keys from the ECDH result of the ephemeral keys and returns the channel. Returns undefined when the peer's ephemeral key is not a valid P-256 point, which only a hostile or broken peer sends.
148
- *
149
- * `peer` is the device verifyHello authenticated for `remote`, and `pending` is this side's own half, whether the handshake was begun here or in answer to that hello.
179
+ * establishChannel with its inputs in one object, and the one extra that tests need: where this end's send counter starts. This module is not a published entry (it is in tsdown.config.ts's INTERNAL_MODULES) and the session never passes `firstSendCounter`, so every real channel starts at 0; a test passes a counter near MAX_COUNTER to reach the bound without sealing 2^53 frames.
150
180
  */
151
- async function establishChannel(identity, pending, remote, peer) {
181
+ async function establishChannelFrom(identity, { pending, remote, peer, firstSendCounter = 0 }) {
182
+ assertFirstSendCounter(firstSendCounter);
152
183
  let secret;
153
184
  try {
154
185
  const remoteKey = await crypto.subtle.importKey("raw", Uint8Array.from(remote["ephemeral-key"]), ECDH_PARAMS, false, []);
@@ -179,7 +210,7 @@ async function establishChannel(identity, pending, remote, peer) {
179
210
  });
180
211
  const sendKey = localIsLow ? lowToHigh : highToLow;
181
212
  const receiveKey = localIsLow ? highToLow : lowToHigh;
182
- let nextSendCounter = 0;
213
+ let nextSendCounter = firstSendCounter;
183
214
  let highestReceived = -1;
184
215
  let sealing = Promise.resolve();
185
216
  let opening = Promise.resolve();
@@ -195,7 +226,10 @@ async function establishChannel(identity, pending, remote, peer) {
195
226
  };
196
227
  }
197
228
  async function openOne(data) {
198
- if (data.counter <= highestReceived) return void 0;
229
+ if (data.counter <= highestReceived) return {
230
+ ok: false,
231
+ reason: "replayed"
232
+ };
199
233
  let plaintext;
200
234
  try {
201
235
  plaintext = new Uint8Array(await crypto.subtle.decrypt({
@@ -204,30 +238,47 @@ async function establishChannel(identity, pending, remote, peer) {
204
238
  additionalData: associatedData(peer.deviceId, identity.deviceId, data.counter)
205
239
  }, receiveKey, Uint8Array.from(data.ciphertext)));
206
240
  } catch {
207
- return;
241
+ return {
242
+ ok: false,
243
+ reason: "unauthentic"
244
+ };
208
245
  }
209
246
  highestReceived = Math.max(highestReceived, data.counter);
210
- return require_adapters_frame_codec.tryDecodeFrame(plaintext) ?? void 0;
247
+ const frame = require_adapters_frame_codec.tryDecodeFrame(plaintext);
248
+ return frame === null ? {
249
+ ok: false,
250
+ reason: "undecodable"
251
+ } : {
252
+ ok: true,
253
+ frame
254
+ };
211
255
  }
256
+ const openOutcome = async (data) => {
257
+ const opened = opening.then(async () => openOne(data));
258
+ opening = opened.catch(() => void 0);
259
+ return opened;
260
+ };
212
261
  return {
213
262
  peer,
214
263
  seal: async (frame) => {
215
- const counter = nextSendCounter;
216
- nextSendCounter += 1;
264
+ const claimed = claimSendCounter(nextSendCounter);
265
+ const counter = claimed.counter;
266
+ nextSendCounter = claimed.next;
217
267
  const sealed = sealing.then(async () => sealAt(frame, counter));
218
268
  sealing = sealed.catch(() => void 0);
219
269
  return sealed;
220
270
  },
221
271
  open: async (data) => {
222
- const opened = opening.then(async () => openOne(data));
223
- opening = opened.catch(() => void 0);
224
- return opened;
225
- }
272
+ const outcome = await openOutcome(data);
273
+ return outcome.ok ? outcome.frame : void 0;
274
+ },
275
+ openOutcome
226
276
  };
227
277
  }
228
278
  //#endregion
229
279
  //#region src/domain/relay-channels.ts
230
- function createRelayChannels(identity, sendHello) {
280
+ function createRelayChannels(identity, sendHello, options = {}) {
281
+ if (options.firstSendCounter !== void 0) assertFirstSendCounter(options.firstSendCounter);
231
282
  const channels = /* @__PURE__ */ new Map();
232
283
  /** Registers the peer as awaiting at once, so a hello arriving meanwhile completes this handshake instead of starting a second, then sends this side's hello. */
233
284
  function start(peer) {
@@ -253,17 +304,40 @@ function createRelayChannels(identity, sendHello) {
253
304
  return (existing ?? start(peer)).settle.promise;
254
305
  },
255
306
  applyHello: async (hello) => {
256
- const verdict = await verifyHello(identity, hello);
307
+ let verdict;
308
+ try {
309
+ verdict = await verifyHello(identity, hello);
310
+ } catch (error) {
311
+ options.onFault?.(error);
312
+ return;
313
+ }
257
314
  if (!verdict.ok) return;
258
315
  const key = require_domain_device_id.deviceIdToHex(verdict.peer.deviceId);
259
316
  const current = channels.get(key);
260
317
  const entry = current?.status === "awaiting" ? current : start(verdict.peer.deviceId);
261
- const channel = await establishChannel(identity, await entry.pending, hello, verdict.peer);
262
- if (channel === void 0) {
318
+ const established = await establishChannelFrom(identity, {
319
+ pending: await entry.pending,
320
+ remote: hello,
321
+ peer: verdict.peer,
322
+ ...options.firstSendCounter === void 0 ? {} : { firstSendCounter: options.firstSendCounter }
323
+ });
324
+ if (established === void 0) {
263
325
  entry.settle.reject(/* @__PURE__ */ new Error("the peer sent an unusable ephemeral key"));
264
326
  if (channels.get(key) === entry) channels.delete(key);
265
327
  return;
266
328
  }
329
+ const channel = {
330
+ ...established,
331
+ seal: async (frame) => {
332
+ try {
333
+ return await established.seal(frame);
334
+ } catch (error) {
335
+ const held = channels.get(key);
336
+ if (error instanceof require_domain_channel_exhausted_error.ChannelExhaustedError && held?.status === "established" && held.channel === channel) channels.delete(key);
337
+ throw error;
338
+ }
339
+ }
340
+ };
267
341
  channels.set(key, {
268
342
  status: "established",
269
343
  channel
@@ -306,27 +380,16 @@ const HANDSHAKE_TIMEOUT_MS = 3e3;
306
380
  * always-on node grows without limit.
307
381
  */
308
382
  const FRAME_LOG_WINDOW = 256;
309
- function localHandshake(domains) {
310
- return {
311
- type: "handshake",
312
- version: 1,
313
- domains: [...domains]
314
- };
315
- }
316
383
  /**
317
384
  * Builds the connection-agnostic session state machine and its public MeshSession surface. dial is null for a session that can never (re)connect on its own -- acceptMeshSession's case, where the one connection it will ever have already exists by construction and reconnect therefore cannot apply (only the remote redialing, and being accepted again, produces a fresh connection). createMeshSession supplies dial as transport.connect so its own connect()/reconnect behaviour is unchanged from before this was factored out.
318
385
  */
319
- function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addresses = [], onFrame, onSessionEnd) {
386
+ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addresses = [], onFrame, onSessionEnd, onLocalFault) {
320
387
  let connection = null;
321
388
  let state = { status: "idle" };
322
389
  let handshake = { status: "pending" };
323
390
  const directory = /* @__PURE__ */ new Map();
324
- const frameLog = [];
325
- /** Keeps the frame log at its recent-activity window: newest entries at the end, the oldest dropped once the window is full. */
326
- function recordFrame(entry) {
327
- frameLog.push(entry);
328
- if (frameLog.length > 256) frameLog.splice(0, frameLog.length - 256);
329
- }
391
+ const frameLog = createBoundedLog(256);
392
+ const recordFrame = frameLog.record;
330
393
  let feedCancelled = false;
331
394
  const eventQueue = require_domain_async_queue.createAsyncQueue();
332
395
  let handshakeTimer = null;
@@ -341,7 +404,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
341
404
  getRelayedDevices: () => relayPairings.list()
342
405
  });
343
406
  const pendingManageRequests = /* @__PURE__ */ new Map();
344
- const relayChannels = createRelayChannels(identity, async (peer, hello) => {
407
+ const sendHello = async (peer, hello) => {
345
408
  if (connection === null) throw new Error("not connected");
346
409
  recordFrame({
347
410
  direction: "sent",
@@ -349,7 +412,8 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
349
412
  });
350
413
  await connection.send(require_adapters_frame_codec.wrapRelayData(hello, peer));
351
414
  emit();
352
- });
415
+ };
416
+ const relayChannels = createRelayChannels(identity, sendHello, { onFault: onLocalFault });
353
417
  const incomingQueue = require_domain_async_queue.createAsyncQueue();
354
418
  const revocationQueue = require_domain_async_queue.createAsyncQueue();
355
419
  const coordinatorQueue = require_domain_async_queue.createAsyncQueue();
@@ -359,7 +423,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
359
423
  return {
360
424
  state,
361
425
  directory: [...directory.values()],
362
- frameLog: [...frameLog]
426
+ frameLog: frameLog.entries()
363
427
  };
364
428
  }
365
429
  function emit() {
@@ -434,7 +498,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
434
498
  }
435
499
  };
436
500
  if (require_gossip_extensions.isVersionGetCommand(frame.command.params)) {
437
- incoming.respond(require_gossip_extensions.versionGetOutcome());
501
+ incoming.respond(require_gossip_extensions.versionGetOutcome()).catch(() => void 0);
438
502
  return;
439
503
  }
440
504
  incomingQueue.push(incoming);
@@ -528,7 +592,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
528
592
  relayPairings.add(targetDevice);
529
593
  emit();
530
594
  }
531
- let localHandshakeSent = localHandshake([]);
595
+ let localHandshakeSent = require_domain_handshake.localHandshake([]);
532
596
  function applyRemoteHandshake(remote) {
533
597
  if (handshake.status !== "pending") return;
534
598
  if (handshakeTimer !== null) {
@@ -618,7 +682,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
618
682
  connection = link;
619
683
  relayChannels.reset("the connection changed before the channel was ready");
620
684
  relayPairings.clear();
621
- localHandshakeSent = localHandshake(localDomains);
685
+ localHandshakeSent = require_domain_handshake.localHandshake(localDomains);
622
686
  handshake = { status: "pending" };
623
687
  state = {
624
688
  status: "connected",
@@ -828,8 +892,8 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
828
892
  }
829
893
  };
830
894
  }
831
- function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = [], onPeerAdvert, onFrame) {
832
- const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address), onPeerAdvert, addresses, onFrame);
895
+ function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = [], onPeerAdvert, onFrame, onLocalFault) {
896
+ const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address), onPeerAdvert, addresses, onFrame, void 0, onLocalFault);
833
897
  return session;
834
898
  }
835
899
  /** 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. */
@@ -1,6 +1,6 @@
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, g as CoordinatorFrame, lt as ProtocolVersion, nn as TopologyPeers, p as CapabilityToken, w as DataEntriesFrame } from "../protocol-DzmpxxUr.cjs";
2
2
  import { r as Transport, t as Connection } from "../transport-gDbSHO-r.cjs";
3
- import { t as IdentityPort } from "../identity-B6nhlSOT.cjs";
3
+ import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
4
4
  import { t as Clock } from "../clock-DhUPRfLh.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. */
@@ -136,7 +136,9 @@ addresses?: readonly string[],
136
136
  /** 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. */
137
137
  onPeerAdvert?: (advert: PeerAdvert) => void,
138
138
  /** Fired once per frame received on this session's connection, with the Connection it arrived on: a new Connection after a reconnect is how a caller notices it has to announce itself again, and a core/data reply is how it receives a log it asked a hub for. Omit for a caller with no use for it, exactly today's behaviour. */
139
- onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>): MeshSession;
139
+ onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>,
140
+ /** Told of a failure that is local to this session and did not end it: an identity port that failed while checking a hello from a peer. The hello is dropped and the peer is told nothing. Omit for a caller with no use for it: the failure is then not reported anywhere. */
141
+ onLocalFault?: (error: unknown) => void): MeshSession;
140
142
  /** 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. */
141
143
  export interface AcceptedMeshSession extends MeshSession {
142
144
  /** 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. */
@@ -1,6 +1,6 @@
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, g as CoordinatorFrame, lt as ProtocolVersion, nn as TopologyPeers, p as CapabilityToken, w as DataEntriesFrame } from "../protocol-DzmpxxUr.mjs";
2
2
  import { r as Transport, t as Connection } from "../transport-bD3p_TO_.mjs";
3
- import { t as IdentityPort } from "../identity-EV-HHv_D.mjs";
3
+ import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
4
4
  import { t as Clock } from "../clock-DhUPRfLh.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. */
@@ -136,7 +136,9 @@ addresses?: readonly string[],
136
136
  /** 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. */
137
137
  onPeerAdvert?: (advert: PeerAdvert) => void,
138
138
  /** Fired once per frame received on this session's connection, with the Connection it arrived on: a new Connection after a reconnect is how a caller notices it has to announce itself again, and a core/data reply is how it receives a log it asked a hub for. Omit for a caller with no use for it, exactly today's behaviour. */
139
- onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>): MeshSession;
139
+ onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>,
140
+ /** Told of a failure that is local to this session and did not end it: an identity port that failed while checking a hello from a peer. The hello is dropped and the peer is told nothing. Omit for a caller with no use for it: the failure is then not reported anywhere. */
141
+ onLocalFault?: (error: unknown) => void): MeshSession;
140
142
  /** 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. */
141
143
  export interface AcceptedMeshSession extends MeshSession {
142
144
  /** 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. */
@@ -1,10 +1,12 @@
1
1
  import { messageFromFrame, tryDecodeFrame, wrapRelayData } from "../adapters/frame-codec.mjs";
2
+ import { IdentityKeyError } from "../ports/identity.mjs";
2
3
  import { deviceIdToHex } from "./device-id.mjs";
3
4
  import { bytesEqual } from "./token-scope.mjs";
4
5
  import { a as OWN_VERSION, i as CORE_VERSION_GOSSIP_KEY, n as TOPOLOGY_PEERS_GOSSIP_KEY, o as isVersionGetCommand, r as validateGossipExtensions, s as versionGetOutcome } from "../gossip-extensions-B6WJjkAr.mjs";
5
6
  import { signPeerAdvert, verifyPeerAdvert } from "./peer-advert.mjs";
6
7
  import { createAsyncQueue } from "./async-queue.mjs";
7
- import { negotiate } from "./handshake.mjs";
8
+ import { ChannelExhaustedError } from "./channel-exhausted-error.mjs";
9
+ import { localHandshake, negotiate } from "./handshake.mjs";
8
10
  import { isDataFrame } from "./hub-mailbox.mjs";
9
11
  import { createPingRoundTrips } from "./ping-round-trips.mjs";
10
12
  import { createTopologySnapshotTracker } from "./topology-snapshot.mjs";
@@ -26,6 +28,19 @@ function createRelayPairings() {
26
28
  };
27
29
  }
28
30
  //#endregion
31
+ //#region src/domain/bounded-log.ts
32
+ /** A log that keeps only its most recent `window` entries: newest at the end, the oldest dropped once the window is full. Pure and closure-free, split out of mesh-session.ts to keep that file under this repo's max-lines lint budget; nothing here is session-shaped. */
33
+ function createBoundedLog(window) {
34
+ const log = [];
35
+ return {
36
+ record: (entry) => {
37
+ log.push(entry);
38
+ if (log.length > window) log.splice(0, log.length - window);
39
+ },
40
+ entries: () => [...log]
41
+ };
42
+ }
43
+ //#endregion
29
44
  //#region src/domain/secure-channel.ts
30
45
  /**
31
46
  * The end-to-end channel two devices open between themselves inside a relay pairing (spec/secure-channel.cddl). A relay only forwards the bytes of a `relay-data` frame, and without this it could read them, alter them, and say who sent them. Here the two devices authenticate each other's identity keys over a fresh ECDH exchange, derive one AES-256-GCM key per direction, and seal every frame they exchange so the relay sees ciphertext and every receiver learns the sender's identity from the handshake and nothing a relay asserts.
@@ -95,16 +110,16 @@ async function beginHandshake(identity, peer) {
95
110
  };
96
111
  }
97
112
  /**
98
- * Checks a received hello: its identity key must hash to a device-id (which is then the peer), it must be addressed to this device, and its signature must verify under that key over the input spec/secure-channel.cddl defines. Never throws: a hostile hello gets a verdict.
113
+ * Checks a received hello: it must be addressed to this device, its identity key must be a usable key, and its signature must verify under that key over the input spec/secure-channel.cddl defines. A hostile hello gets a verdict. An error from the identity port that is not an IdentityKeyError is a failure of the port, not a verdict about the peer, and propagates.
99
114
  */
100
115
  async function verifyHello(identity, hello) {
101
116
  const key = hello["identity-key"];
117
+ const sender = await identity.deriveDeviceId(key["public-key"]);
118
+ if (!bytesEqual(hello["to-device"], identity.deviceId)) return {
119
+ ok: false,
120
+ reason: "wrong_recipient"
121
+ };
102
122
  try {
103
- const sender = await identity.deriveDeviceId(key["public-key"]);
104
- if (!bytesEqual(hello["to-device"], identity.deviceId)) return {
105
- ok: false,
106
- reason: "wrong_recipient"
107
- };
108
123
  return await identity.verify(key, helloSigningInput(hello["ephemeral-key"], hello.nonce, sender, identity.deviceId), hello.signature) ? {
109
124
  ok: true,
110
125
  peer: {
@@ -115,13 +130,30 @@ async function verifyHello(identity, hello) {
115
130
  ok: false,
116
131
  reason: "bad_signature"
117
132
  };
118
- } catch {
133
+ } catch (error) {
134
+ if (!(error instanceof IdentityKeyError)) throw error;
119
135
  return {
120
136
  ok: false,
121
- reason: "identity_mismatch"
137
+ reason: error.fault === "unsupported_alg" ? "unsupported_identity_alg" : "invalid_identity_key"
122
138
  };
123
139
  }
124
140
  }
141
+ /** The largest counter a channel uses: the counter is an unsigned 64-bit integer on the wire, and this implementation holds it as a JavaScript number, exact up to this value. A frame above it is refused at decoding, never truncated, and sealing refuses to go past it, never wraps. */
142
+ const MAX_COUNTER = Number.MAX_SAFE_INTEGER;
143
+ /** Throws a RangeError unless `counter` is a counter a channel can start from: a non-negative safe integer. */
144
+ function assertFirstSendCounter(counter) {
145
+ if (!Number.isSafeInteger(counter) || counter < 0) throw new RangeError("firstSendCounter must be a non-negative safe integer");
146
+ }
147
+ /**
148
+ * Takes the next send counter from `next`, the counter the next seal would use: returns it with the counter that follows. Throws once `next` is past MAX_COUNTER, since a counter that cannot be held exactly would repeat an IV under the same key.
149
+ */
150
+ function claimSendCounter(next) {
151
+ if (next > MAX_COUNTER) throw new ChannelExhaustedError();
152
+ return {
153
+ counter: next,
154
+ next: next + 1
155
+ };
156
+ }
125
157
  function ivFor(counter) {
126
158
  const iv = new Uint8Array(GCM_IV_BYTE_LENGTH);
127
159
  iv.set(counterBytes(counter), IV_COUNTER_OFFSET);
@@ -143,11 +175,10 @@ async function deriveDirectionKey({ secret, salt, low, high, label }) {
143
175
  }, false, ["encrypt", "decrypt"]);
144
176
  }
145
177
  /**
146
- * Completes a handshake once the peer's hello has verified: derives the two directional keys from the ECDH result of the ephemeral keys and returns the channel. Returns undefined when the peer's ephemeral key is not a valid P-256 point, which only a hostile or broken peer sends.
147
- *
148
- * `peer` is the device verifyHello authenticated for `remote`, and `pending` is this side's own half, whether the handshake was begun here or in answer to that hello.
178
+ * establishChannel with its inputs in one object, and the one extra that tests need: where this end's send counter starts. This module is not a published entry (it is in tsdown.config.ts's INTERNAL_MODULES) and the session never passes `firstSendCounter`, so every real channel starts at 0; a test passes a counter near MAX_COUNTER to reach the bound without sealing 2^53 frames.
149
179
  */
150
- async function establishChannel(identity, pending, remote, peer) {
180
+ async function establishChannelFrom(identity, { pending, remote, peer, firstSendCounter = 0 }) {
181
+ assertFirstSendCounter(firstSendCounter);
151
182
  let secret;
152
183
  try {
153
184
  const remoteKey = await crypto.subtle.importKey("raw", Uint8Array.from(remote["ephemeral-key"]), ECDH_PARAMS, false, []);
@@ -178,7 +209,7 @@ async function establishChannel(identity, pending, remote, peer) {
178
209
  });
179
210
  const sendKey = localIsLow ? lowToHigh : highToLow;
180
211
  const receiveKey = localIsLow ? highToLow : lowToHigh;
181
- let nextSendCounter = 0;
212
+ let nextSendCounter = firstSendCounter;
182
213
  let highestReceived = -1;
183
214
  let sealing = Promise.resolve();
184
215
  let opening = Promise.resolve();
@@ -194,7 +225,10 @@ async function establishChannel(identity, pending, remote, peer) {
194
225
  };
195
226
  }
196
227
  async function openOne(data) {
197
- if (data.counter <= highestReceived) return void 0;
228
+ if (data.counter <= highestReceived) return {
229
+ ok: false,
230
+ reason: "replayed"
231
+ };
198
232
  let plaintext;
199
233
  try {
200
234
  plaintext = new Uint8Array(await crypto.subtle.decrypt({
@@ -203,30 +237,47 @@ async function establishChannel(identity, pending, remote, peer) {
203
237
  additionalData: associatedData(peer.deviceId, identity.deviceId, data.counter)
204
238
  }, receiveKey, Uint8Array.from(data.ciphertext)));
205
239
  } catch {
206
- return;
240
+ return {
241
+ ok: false,
242
+ reason: "unauthentic"
243
+ };
207
244
  }
208
245
  highestReceived = Math.max(highestReceived, data.counter);
209
- return tryDecodeFrame(plaintext) ?? void 0;
246
+ const frame = tryDecodeFrame(plaintext);
247
+ return frame === null ? {
248
+ ok: false,
249
+ reason: "undecodable"
250
+ } : {
251
+ ok: true,
252
+ frame
253
+ };
210
254
  }
255
+ const openOutcome = async (data) => {
256
+ const opened = opening.then(async () => openOne(data));
257
+ opening = opened.catch(() => void 0);
258
+ return opened;
259
+ };
211
260
  return {
212
261
  peer,
213
262
  seal: async (frame) => {
214
- const counter = nextSendCounter;
215
- nextSendCounter += 1;
263
+ const claimed = claimSendCounter(nextSendCounter);
264
+ const counter = claimed.counter;
265
+ nextSendCounter = claimed.next;
216
266
  const sealed = sealing.then(async () => sealAt(frame, counter));
217
267
  sealing = sealed.catch(() => void 0);
218
268
  return sealed;
219
269
  },
220
270
  open: async (data) => {
221
- const opened = opening.then(async () => openOne(data));
222
- opening = opened.catch(() => void 0);
223
- return opened;
224
- }
271
+ const outcome = await openOutcome(data);
272
+ return outcome.ok ? outcome.frame : void 0;
273
+ },
274
+ openOutcome
225
275
  };
226
276
  }
227
277
  //#endregion
228
278
  //#region src/domain/relay-channels.ts
229
- function createRelayChannels(identity, sendHello) {
279
+ function createRelayChannels(identity, sendHello, options = {}) {
280
+ if (options.firstSendCounter !== void 0) assertFirstSendCounter(options.firstSendCounter);
230
281
  const channels = /* @__PURE__ */ new Map();
231
282
  /** Registers the peer as awaiting at once, so a hello arriving meanwhile completes this handshake instead of starting a second, then sends this side's hello. */
232
283
  function start(peer) {
@@ -252,17 +303,40 @@ function createRelayChannels(identity, sendHello) {
252
303
  return (existing ?? start(peer)).settle.promise;
253
304
  },
254
305
  applyHello: async (hello) => {
255
- const verdict = await verifyHello(identity, hello);
306
+ let verdict;
307
+ try {
308
+ verdict = await verifyHello(identity, hello);
309
+ } catch (error) {
310
+ options.onFault?.(error);
311
+ return;
312
+ }
256
313
  if (!verdict.ok) return;
257
314
  const key = deviceIdToHex(verdict.peer.deviceId);
258
315
  const current = channels.get(key);
259
316
  const entry = current?.status === "awaiting" ? current : start(verdict.peer.deviceId);
260
- const channel = await establishChannel(identity, await entry.pending, hello, verdict.peer);
261
- if (channel === void 0) {
317
+ const established = await establishChannelFrom(identity, {
318
+ pending: await entry.pending,
319
+ remote: hello,
320
+ peer: verdict.peer,
321
+ ...options.firstSendCounter === void 0 ? {} : { firstSendCounter: options.firstSendCounter }
322
+ });
323
+ if (established === void 0) {
262
324
  entry.settle.reject(/* @__PURE__ */ new Error("the peer sent an unusable ephemeral key"));
263
325
  if (channels.get(key) === entry) channels.delete(key);
264
326
  return;
265
327
  }
328
+ const channel = {
329
+ ...established,
330
+ seal: async (frame) => {
331
+ try {
332
+ return await established.seal(frame);
333
+ } catch (error) {
334
+ const held = channels.get(key);
335
+ if (error instanceof ChannelExhaustedError && held?.status === "established" && held.channel === channel) channels.delete(key);
336
+ throw error;
337
+ }
338
+ }
339
+ };
266
340
  channels.set(key, {
267
341
  status: "established",
268
342
  channel
@@ -305,27 +379,16 @@ const HANDSHAKE_TIMEOUT_MS = 3e3;
305
379
  * always-on node grows without limit.
306
380
  */
307
381
  const FRAME_LOG_WINDOW = 256;
308
- function localHandshake(domains) {
309
- return {
310
- type: "handshake",
311
- version: 1,
312
- domains: [...domains]
313
- };
314
- }
315
382
  /**
316
383
  * Builds the connection-agnostic session state machine and its public MeshSession surface. dial is null for a session that can never (re)connect on its own -- acceptMeshSession's case, where the one connection it will ever have already exists by construction and reconnect therefore cannot apply (only the remote redialing, and being accepted again, produces a fresh connection). createMeshSession supplies dial as transport.connect so its own connect()/reconnect behaviour is unchanged from before this was factored out.
317
384
  */
318
- function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addresses = [], onFrame, onSessionEnd) {
385
+ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addresses = [], onFrame, onSessionEnd, onLocalFault) {
319
386
  let connection = null;
320
387
  let state = { status: "idle" };
321
388
  let handshake = { status: "pending" };
322
389
  const directory = /* @__PURE__ */ new Map();
323
- const frameLog = [];
324
- /** Keeps the frame log at its recent-activity window: newest entries at the end, the oldest dropped once the window is full. */
325
- function recordFrame(entry) {
326
- frameLog.push(entry);
327
- if (frameLog.length > 256) frameLog.splice(0, frameLog.length - 256);
328
- }
390
+ const frameLog = createBoundedLog(256);
391
+ const recordFrame = frameLog.record;
329
392
  let feedCancelled = false;
330
393
  const eventQueue = createAsyncQueue();
331
394
  let handshakeTimer = null;
@@ -340,7 +403,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
340
403
  getRelayedDevices: () => relayPairings.list()
341
404
  });
342
405
  const pendingManageRequests = /* @__PURE__ */ new Map();
343
- const relayChannels = createRelayChannels(identity, async (peer, hello) => {
406
+ const sendHello = async (peer, hello) => {
344
407
  if (connection === null) throw new Error("not connected");
345
408
  recordFrame({
346
409
  direction: "sent",
@@ -348,7 +411,8 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
348
411
  });
349
412
  await connection.send(wrapRelayData(hello, peer));
350
413
  emit();
351
- });
414
+ };
415
+ const relayChannels = createRelayChannels(identity, sendHello, { onFault: onLocalFault });
352
416
  const incomingQueue = createAsyncQueue();
353
417
  const revocationQueue = createAsyncQueue();
354
418
  const coordinatorQueue = createAsyncQueue();
@@ -358,7 +422,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
358
422
  return {
359
423
  state,
360
424
  directory: [...directory.values()],
361
- frameLog: [...frameLog]
425
+ frameLog: frameLog.entries()
362
426
  };
363
427
  }
364
428
  function emit() {
@@ -433,7 +497,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
433
497
  }
434
498
  };
435
499
  if (isVersionGetCommand(frame.command.params)) {
436
- incoming.respond(versionGetOutcome());
500
+ incoming.respond(versionGetOutcome()).catch(() => void 0);
437
501
  return;
438
502
  }
439
503
  incomingQueue.push(incoming);
@@ -827,8 +891,8 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
827
891
  }
828
892
  };
829
893
  }
830
- function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = [], onPeerAdvert, onFrame) {
831
- const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address), onPeerAdvert, addresses, onFrame);
894
+ function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = [], onPeerAdvert, onFrame, onLocalFault) {
895
+ const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address), onPeerAdvert, addresses, onFrame, void 0, onLocalFault);
832
896
  return session;
833
897
  }
834
898
  /** 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. */
@@ -1,6 +1,6 @@
1
1
  import { D as DeviceId, Ot as RoomNotice, T as DataHaveFrame, kt as RoomNoticeClaims, p as CapabilityToken } from "../protocol-DzmpxxUr.cjs";
2
2
  import { t as KeyValueStorage } from "../storage-jbGFAYIV.cjs";
3
- import { t as IdentityPort } from "../identity-B6nhlSOT.cjs";
3
+ import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
4
4
  import { t as Clock } from "../clock-DhUPRfLh.cjs";
5
5
  import { RevocationCheck } from "./tokens.cjs";
6
6
  import { RoomNoticeVerdictReason } from "./room.cjs";
@@ -1,6 +1,6 @@
1
1
  import { D as DeviceId, Ot as RoomNotice, T as DataHaveFrame, kt as RoomNoticeClaims, p as CapabilityToken } from "../protocol-DzmpxxUr.mjs";
2
2
  import { t as KeyValueStorage } from "../storage-jbGFAYIV.mjs";
3
- import { t as IdentityPort } from "../identity-EV-HHv_D.mjs";
3
+ import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
4
4
  import { t as Clock } from "../clock-DhUPRfLh.mjs";
5
5
  import { RevocationCheck } from "./tokens.mjs";
6
6
  import { RoomNoticeVerdictReason } from "./room.mjs";
@@ -1,5 +1,5 @@
1
1
  import { B as IdentityKey, D as DeviceId, et as PeerAdvert } from "../protocol-DzmpxxUr.cjs";
2
- import { t as IdentityPort } from "../identity-B6nhlSOT.cjs";
2
+ import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
3
3
  //#region src/domain/peer-advert.d.ts
4
4
  /**
5
5
  * The most a verifier's verdict memo may charge across all its entries, in bytes of key.
@@ -1,5 +1,5 @@
1
1
  import { B as IdentityKey, D as DeviceId, et as PeerAdvert } from "../protocol-DzmpxxUr.mjs";
2
- import { t as IdentityPort } from "../identity-EV-HHv_D.mjs";
2
+ import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
3
3
  //#region src/domain/peer-advert.d.ts
4
4
  /**
5
5
  * The most a verifier's verdict memo may charge across all its entries, in bytes of key.
@@ -1,5 +1,5 @@
1
1
  import { D as DeviceId, V as ManageCommand, p as CapabilityToken } from "../protocol-DzmpxxUr.cjs";
2
- import { t as IdentityPort } from "../identity-B6nhlSOT.cjs";
2
+ import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
3
3
  import { t as Clock } from "../clock-DhUPRfLh.cjs";
4
4
  import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.cjs";
5
5
  import { RevocationCheck } from "./tokens.cjs";
@@ -1,5 +1,5 @@
1
1
  import { D as DeviceId, V as ManageCommand, p as CapabilityToken } from "../protocol-DzmpxxUr.mjs";
2
- import { t as IdentityPort } from "../identity-EV-HHv_D.mjs";
2
+ import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
3
3
  import { t as Clock } from "../clock-DhUPRfLh.mjs";
4
4
  import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.mjs";
5
5
  import { RevocationCheck } from "./tokens.mjs";
@@ -1,5 +1,5 @@
1
1
  import { At as RoomPath, Ot as RoomNotice, kt as RoomNoticeClaims, p as CapabilityToken, q as MessageRef } from "../protocol-DzmpxxUr.cjs";
2
- import { t as IdentityPort } from "../identity-B6nhlSOT.cjs";
2
+ import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
3
3
  import { t as Clock } from "../clock-DhUPRfLh.cjs";
4
4
  import { RevocationCheck } from "./tokens.cjs";
5
5
  import { r as RoomTokenVerdictReason } from "../room-token-verification-Dh7SGJfK.cjs";
@@ -1,5 +1,5 @@
1
1
  import { At as RoomPath, Ot as RoomNotice, kt as RoomNoticeClaims, p as CapabilityToken, q as MessageRef } from "../protocol-DzmpxxUr.mjs";
2
- import { t as IdentityPort } from "../identity-EV-HHv_D.mjs";
2
+ import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
3
3
  import { t as Clock } from "../clock-DhUPRfLh.mjs";
4
4
  import { RevocationCheck } from "./tokens.mjs";
5
5
  import { r as RoomTokenVerdictReason } from "../room-token-verification-Hb1LkpFh.mjs";
@@ -1,5 +1,5 @@
1
1
  import { B as IdentityKey, D as DeviceId, bt as RevocationEntry, en as TokenClaims, p as CapabilityToken, yt as RevocationClaims } from "../protocol-DzmpxxUr.cjs";
2
- import { t as IdentityPort } from "../identity-B6nhlSOT.cjs";
2
+ import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
3
3
  import { t as Clock } from "../clock-DhUPRfLh.cjs";
4
4
  import { l as GrantSubject, m as TokenDelegateHandler, r as ConditionsContext, u as NarrowingCandidate } from "../token-predicates-lg4TtUxm.cjs";
5
5
  import { PredicateNode } from "trilean";
@@ -1,5 +1,5 @@
1
1
  import { B as IdentityKey, D as DeviceId, bt as RevocationEntry, en as TokenClaims, p as CapabilityToken, yt as RevocationClaims } from "../protocol-DzmpxxUr.mjs";
2
- import { t as IdentityPort } from "../identity-EV-HHv_D.mjs";
2
+ import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
3
3
  import { t as Clock } from "../clock-DhUPRfLh.mjs";
4
4
  import { l as GrantSubject, m as TokenDelegateHandler, r as ConditionsContext, u as NarrowingCandidate } from "../token-predicates-CrieP1WI.mjs";
5
5
  import { PredicateNode } from "trilean";
@@ -1,5 +1,14 @@
1
1
  import { B as IdentityKey, D as DeviceId } from "./protocol-DzmpxxUr.mjs";
2
2
  //#region src/ports/identity.d.ts
3
+ /** Why an identity-key could not be used: its algorithm is not one the implementation verifies, or its key bytes are not a valid key for that algorithm. */
4
+ type IdentityKeyFault = "unsupported_alg" | "invalid_key";
5
+ /** Thrown by an IdentityPort's verify for an identity-key it cannot use at all, as opposed to a signature that does not verify (which returns false). Any other error from a port is not a fault of the key: it is a failure of the port itself and is never to be read as a verdict about a peer. */
6
+ declare class IdentityKeyError extends Error {
7
+ readonly fault: IdentityKeyFault;
8
+ constructor(fault: IdentityKeyFault, message: string, options?: Readonly<{
9
+ cause: unknown;
10
+ }>);
11
+ }
3
12
  /**
4
13
  * Wraps the local node's own identity plus the signing/verification primitives capability-token handling needs -- never hardcoded to one crypto library's own API shape (Node's webcrypto, a WASM implementation, a hardware key, etc. can all satisfy this same contract).
5
14
  */
@@ -16,4 +25,4 @@ interface IdentityPort {
16
25
  deriveSharedSecret?: (peerKey: IdentityKey) => Promise<Uint8Array<ArrayBuffer>>;
17
26
  }
18
27
  //#endregion
19
- export { IdentityPort as t };
28
+ export { IdentityKeyFault as n, IdentityPort as r, IdentityKeyError as t };
@@ -1,5 +1,14 @@
1
1
  import { B as IdentityKey, D as DeviceId } from "./protocol-DzmpxxUr.cjs";
2
2
  //#region src/ports/identity.d.ts
3
+ /** Why an identity-key could not be used: its algorithm is not one the implementation verifies, or its key bytes are not a valid key for that algorithm. */
4
+ type IdentityKeyFault = "unsupported_alg" | "invalid_key";
5
+ /** Thrown by an IdentityPort's verify for an identity-key it cannot use at all, as opposed to a signature that does not verify (which returns false). Any other error from a port is not a fault of the key: it is a failure of the port itself and is never to be read as a verdict about a peer. */
6
+ declare class IdentityKeyError extends Error {
7
+ readonly fault: IdentityKeyFault;
8
+ constructor(fault: IdentityKeyFault, message: string, options?: Readonly<{
9
+ cause: unknown;
10
+ }>);
11
+ }
3
12
  /**
4
13
  * Wraps the local node's own identity plus the signing/verification primitives capability-token handling needs -- never hardcoded to one crypto library's own API shape (Node's webcrypto, a WASM implementation, a hardware key, etc. can all satisfy this same contract).
5
14
  */
@@ -16,4 +25,4 @@ interface IdentityPort {
16
25
  deriveSharedSecret?: (peerKey: IdentityKey) => Promise<Uint8Array<ArrayBuffer>>;
17
26
  }
18
27
  //#endregion
19
- export { IdentityPort as t };
28
+ export { IdentityKeyFault as n, IdentityPort as r, IdentityKeyError as t };
@@ -0,0 +1,13 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/ports/identity.ts
3
+ /** Thrown by an IdentityPort's verify for an identity-key it cannot use at all, as opposed to a signature that does not verify (which returns false). Any other error from a port is not a fault of the key: it is a failure of the port itself and is never to be read as a verdict about a peer. */
4
+ var IdentityKeyError = class extends Error {
5
+ fault;
6
+ constructor(fault, message, options) {
7
+ super(message, options);
8
+ this.name = "IdentityKeyError";
9
+ this.fault = fault;
10
+ }
11
+ };
12
+ //#endregion
13
+ exports.IdentityKeyError = IdentityKeyError;
@@ -1,2 +1,2 @@
1
- import { t as IdentityPort } from "../identity-B6nhlSOT.cjs";
2
- export { IdentityPort };
1
+ import { n as IdentityKeyFault, r as IdentityPort, t as IdentityKeyError } from "../identity-C5jq53NV.cjs";
2
+ export { IdentityKeyError, IdentityKeyFault, IdentityPort };
@@ -1,2 +1,2 @@
1
- import { t as IdentityPort } from "../identity-EV-HHv_D.mjs";
2
- export { IdentityPort };
1
+ import { n as IdentityKeyFault, r as IdentityPort, t as IdentityKeyError } from "../identity-BioGOV9M.mjs";
2
+ export { IdentityKeyError, IdentityKeyFault, IdentityPort };
@@ -1 +1,12 @@
1
- export {};
1
+ //#region src/ports/identity.ts
2
+ /** Thrown by an IdentityPort's verify for an identity-key it cannot use at all, as opposed to a signature that does not verify (which returns false). Any other error from a port is not a fault of the key: it is a failure of the port itself and is never to be read as a verdict about a peer. */
3
+ var IdentityKeyError = class extends Error {
4
+ fault;
5
+ constructor(fault, message, options) {
6
+ super(message, options);
7
+ this.name = "IdentityKeyError";
8
+ this.fault = fault;
9
+ }
10
+ };
11
+ //#endregion
12
+ export { IdentityKeyError };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wire-mesh-core",
3
- "version": "4.0.8",
3
+ "version": "4.1.0",
4
4
  "dependencies": {
5
5
  "cbor2": "2.3.0",
6
6
  "cddl.js": "1.0.1",
@@ -86,6 +86,10 @@
86
86
  "import": "./dist/domain/capability-request.mjs",
87
87
  "require": "./dist/domain/capability-request.cjs"
88
88
  },
89
+ "./domain/channel-exhausted-error": {
90
+ "import": "./dist/domain/channel-exhausted-error.mjs",
91
+ "require": "./dist/domain/channel-exhausted-error.cjs"
92
+ },
89
93
  "./domain/coordinator-election": {
90
94
  "import": "./dist/domain/coordinator-election.mjs",
91
95
  "require": "./dist/domain/coordinator-election.cjs"