wire-mesh-core 4.0.2 → 4.0.4

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 (68) hide show
  1. package/README.md +11 -4
  2. package/dist/adapters/byte-stream-connection.cjs +15 -4
  3. package/dist/adapters/byte-stream-connection.mjs +15 -4
  4. package/dist/adapters/memory-storage.d.cts +1 -1
  5. package/dist/adapters/memory-storage.d.mts +1 -1
  6. package/dist/adapters/node-fs-storage.cjs +4 -3
  7. package/dist/adapters/node-fs-storage.d.cts +1 -1
  8. package/dist/adapters/node-fs-storage.d.mts +1 -1
  9. package/dist/adapters/node-fs-storage.mjs +4 -3
  10. package/dist/adapters/system-clock.d.cts +1 -1
  11. package/dist/adapters/system-clock.d.mts +1 -1
  12. package/dist/clock-DhUPRfLh.d.cts +10 -0
  13. package/dist/clock-DhUPRfLh.d.mts +10 -0
  14. package/dist/domain/bulk.cjs +21 -12
  15. package/dist/domain/bulk.d.cts +1 -1
  16. package/dist/domain/bulk.d.mts +1 -1
  17. package/dist/domain/bulk.mjs +21 -12
  18. package/dist/domain/capability-grant.d.cts +1 -1
  19. package/dist/domain/capability-grant.d.mts +1 -1
  20. package/dist/domain/capability-request.d.cts +2 -2
  21. package/dist/domain/capability-request.d.mts +2 -2
  22. package/dist/domain/data-sync.cjs +22 -19
  23. package/dist/domain/data-sync.d.cts +1 -1
  24. package/dist/domain/data-sync.d.mts +1 -1
  25. package/dist/domain/data-sync.mjs +22 -19
  26. package/dist/domain/gossip-expansion.cjs +11 -5
  27. package/dist/domain/gossip-expansion.mjs +11 -5
  28. package/dist/domain/hub-mailbox.d.cts +1 -1
  29. package/dist/domain/hub-mailbox.d.mts +1 -1
  30. package/dist/domain/mesh-session.cjs +27 -14
  31. package/dist/domain/mesh-session.d.cts +1 -1
  32. package/dist/domain/mesh-session.d.mts +1 -1
  33. package/dist/domain/mesh-session.mjs +27 -14
  34. package/dist/domain/notice-board.cjs +4 -1
  35. package/dist/domain/notice-board.d.cts +2 -2
  36. package/dist/domain/notice-board.d.mts +2 -2
  37. package/dist/domain/notice-board.mjs +4 -1
  38. package/dist/domain/path-trace.d.cts +1 -1
  39. package/dist/domain/path-trace.d.mts +1 -1
  40. package/dist/domain/relay-hub.cjs +15 -10
  41. package/dist/domain/relay-hub.mjs +15 -10
  42. package/dist/domain/room-rekey.d.cts +1 -1
  43. package/dist/domain/room-rekey.d.mts +1 -1
  44. package/dist/domain/room.d.cts +1 -1
  45. package/dist/domain/room.d.mts +1 -1
  46. package/dist/domain/token-predicates.cjs +10 -10
  47. package/dist/domain/token-predicates.d.cts +1 -1
  48. package/dist/domain/token-predicates.d.mts +1 -1
  49. package/dist/domain/token-predicates.mjs +10 -10
  50. package/dist/domain/tokens.cjs +16 -4
  51. package/dist/domain/tokens.d.cts +2 -2
  52. package/dist/domain/tokens.d.mts +2 -2
  53. package/dist/domain/tokens.mjs +16 -4
  54. package/dist/ports/clock.d.cts +1 -1
  55. package/dist/ports/clock.d.mts +1 -1
  56. package/dist/ports/storage.d.cts +1 -1
  57. package/dist/ports/storage.d.mts +1 -1
  58. package/dist/sequential-CtQVlW8M.cjs +31 -0
  59. package/dist/sequential-D_W3THly.mjs +20 -0
  60. package/dist/storage-jbGFAYIV.d.cts +13 -0
  61. package/dist/storage-jbGFAYIV.d.mts +13 -0
  62. package/dist/{token-predicates-DqpG8VZZ.d.mts → token-predicates-CrieP1WI.d.mts} +1 -1
  63. package/dist/{token-predicates-D4NNPLnp.d.cts → token-predicates-lg4TtUxm.d.cts} +1 -1
  64. package/package.json +2 -2
  65. package/dist/clock-DiSx-WKM.d.cts +0 -7
  66. package/dist/clock-DiSx-WKM.d.mts +0 -7
  67. package/dist/storage-B403CRyu.d.cts +0 -10
  68. package/dist/storage-B403CRyu.d.mts +0 -10
package/README.md CHANGED
@@ -4,8 +4,8 @@ The TypeScript implementation of wire-mesh's protocol, built ports/adapters: dom
4
4
 
5
5
  ## Ports
6
6
 
7
- - **Transport** (`src/ports/transport.ts`) -- an async contract for sending/receiving `Frame` values over a connection. Adapter: `src/adapters/tcp-transport.ts` (length-prefixed CBOR frames over plain `node:net`, matching Cascade's own transport shape).
8
- - **Storage** (`src/ports/storage.ts`) -- an async key/value contract. Adapter: `src/adapters/memory-storage.ts` (in-process, for tests and single-process nodes).
7
+ - **Transport** (`src/ports/transport.ts`): an async contract for sending/receiving `Frame` values over a connection. Adapters: `src/adapters/tcp-transport.ts` (length-prefixed CBOR frames over plain `node:net`, matching Cascade's own transport shape) and `src/adapters/tls-transport.ts` (the same framing over `node:tls`).
8
+ - **Storage** (`src/ports/storage.ts`): an async key/value contract. Adapters: `src/adapters/memory-storage.ts` (in-process, for tests and single-process nodes) and `src/adapters/node-fs-storage.ts` (filesystem).
9
9
  - **Identity** (`src/ports/identity.ts`) -- device-id derivation and COSE signing/verification. Adapter: `src/adapters/node-identity.ts` (Web Crypto, ECDSA P-256 and Ed25519 -- the two algorithms the spec's own conformance vectors use).
10
10
  - **Clock** (`src/ports/clock.ts`) -- injected time, never `Date.now()` pulled directly into domain logic. Adapter: `src/adapters/system-clock.ts`.
11
11
 
@@ -15,10 +15,17 @@ The TypeScript implementation of wire-mesh's protocol, built ports/adapters: dom
15
15
  - **Capability-token verification** (`src/domain/tokens.ts`) -- the full chain tokens.cddl documents: COSE_Sign1 signature verification, the self-certifying issuer-key check (`sha256(issuer-key.public-key) == issuer`), expiry/not-before, issuer-matched revocation (only a token's own issuer's signed revocation-entry counts, checked across every ancestor in the delegation chain, not just the leaf), and recursive delegation-chain narrowing across all three axes of authority: a delegated token's issuer must be its parent's bearer (the chain is unbroken), its expiry must not exceed its parent's, and its scope must narrow its parent's (identical kind, equal-or-descendant path when the parent carries one) with an identical capability verb (the verb grammar has no sub-verb relation, so a different verb is different authority, not narrower). Also exports `verifyRevocationEntry` for ingesting gossiped revocation-announce frames: each entry is itself a signed, self-certifying COSE_Sign1 over revocation-claims, verified before it may enter the revocation view.
16
16
  - **Peer-advert verification** (`src/domain/peer-advert.ts`): the two checks transport.cddl states for a gossiped advert, applied by the relay hub before it registers or forwards one and by every session before one reaches its directory: the embedded identity-key self-certifies (`sha256(identity-key.public-key) == device`), and the signature verifies under that same key over the domain-separated canonical encoding of the advert with its signature removed, which covers the open extension tail as well. Deliberately not tokens.cddl's COSE Sig_structure: an advert's open tail lets an attacker craft one whose encoding also satisfies token-claims, so the prefixed construction keeps the two byte strings disjoint. Reaches a verdict for every input rather than throwing, since an advert's algorithm and key bytes are attacker-chosen.
17
17
  - **Secure channel** (`src/domain/secure-channel.ts`, `src/domain/relay-channels.ts`): the end-to-end channel spec/secure-channel.cddl defines between two devices that reach each other through a relay. A session opens one with each peer it addresses through a relay pairing, seals every manage-request, manage-response and core/data frame in it, drops any that arrives as plain relay-data, and attributes a request to the identity the peer's handshake proved rather than to the `from-device` a hub stamps. There is no plaintext fallback, so a peer that does not speak it cannot be reached through a relay.
18
+ - **Relay hub** (`src/domain/relay-hub.ts`, `src/domain/relay-pairing.ts`, `src/domain/hub-mailbox.ts`): the hub's relay role against the Transport port alone. It verifies every gossiped advert before registering, forwarding or replaying it, pairs devices for relayed traffic, and, as a mailbox, replicates the `core/data` logs of devices that are offline.
19
+ - **Bulk transfer** (`src/domain/bulk.ts`): resumable, flow-controlled `core/bulk` transfer, with a byte-credit window that the receiver's acknowledgements move. `src/domain/sharded-delivery.ts` composes it with erasure coding (`src/domain/erasure-coding.ts`) and shard manifests (`src/domain/shard-manifest.ts`) to spread a payload across holder devices.
20
+ - **Data sync** (`src/domain/data-sync.ts`): the have, request and entries primitive over a storage-backed oplog, which never accepts a gap. What to broadcast, whose logs to track and which holder to ask are left to the application.
21
+ - **Coordinator election** (`src/domain/coordinator-election.ts`): the gossiped, term-based election over `coordinator-frame`, where a higher term supersedes a lower one and equal terms are broken by lowest device-id.
22
+ - **Rooms** (`src/domain/room.ts`, `src/domain/room-path.ts`, `src/domain/room-token-verification.ts`, `src/domain/room-rekey.ts`, `src/domain/group-key.ts`, `src/domain/notice-board.ts`): self-certifying room-notice verification, owner-named and direct-message room paths, the room verifier obligations on top of token verification, content-key distribution and rotation, and the encrypted noticeboard that composes them with data sync.
23
+ - **Threshold signing** (`src/domain/threshold-network.ts`, `src/adapters/threshold-*.ts`): the `exadev.io/threshold` manage-commands for FROST(Ed25519) key generation, resharing and two-round signing, with the cryptography supplied by the WebAssembly build of the Rust threshold crate.
24
+ - **Mesh session** (`src/domain/mesh-session.ts`): the connection session that owns the handshake exchange, the gossip-built peer directory, manage-request dispatch and reconnection, and that the hub, LAN node and web console build on.
18
25
 
19
- ## Deliberately deferred
26
+ ## Not implemented
20
27
 
21
- Every other frame family (management/exec, streaming, data-domain, discovery, coordinator election) is covered by schema validation only -- `conformance-check` proves the generated schemas decode and re-encode every golden vector byte-exactly, including these families, but no domain-level business logic (dispatch, session bookkeeping, PTY/proc lifecycle, oplog replication, coordinator term tracking) exists for them yet. This first pass deliberately scopes domain logic to transport + handshake + tokens (the families with real business rules worth pinning down before the others), because the conformance suite already covers every family's wire shape either way -- nothing here is unverified, only unimplemented. (The federation link protocol no longer exists in the spec at all: cross-scope sharing is ordinary capability-token delegation, and `src/domain/tokens.ts`'s verification chain is exactly the mechanism that governs it.)
28
+ Exec and streaming are schema only: `conformance-check` proves the generated schemas decode and re-encode their golden vectors byte-exactly, but no code spawns a process or PTY (`proc.*`, `pty.*`) or sends, receives or enforces the credit window of a stream (`stream-data` and its acknowledgements). Tracked in [#341](https://github.com/ExaDev/wire-mesh/issues/341). Direct-path NAT traversal is not implemented either, so a connection across a NAT goes through the relay; tracked in [#343](https://github.com/ExaDev/wire-mesh/issues/343). Nothing resolves DNS-anchored handles. The federation link protocol no longer exists in the spec: cross-scope sharing is ordinary capability-token delegation, and the verification chain in `src/domain/tokens.ts` is the mechanism that governs it.
22
29
 
23
30
  ## Building from a fresh checkout
24
31
 
@@ -22,16 +22,27 @@ function concat(first, second) {
22
22
  joined.set(second, first.length);
23
23
  return joined;
24
24
  }
25
+ /** A reader's chunks as an async iterable, which ends with the stream and leaves releasing or cancelling the reader to its owner. */
26
+ function chunksOf(reader) {
27
+ return { [Symbol.asyncIterator]: () => ({ next: async () => {
28
+ const chunk = await reader.read();
29
+ return chunk.done ? {
30
+ done: true,
31
+ value: void 0
32
+ } : {
33
+ done: false,
34
+ value: chunk.value
35
+ };
36
+ } }) };
37
+ }
25
38
  /**
26
39
  * Frames arriving on a byte stream, in order, until it ends. An empty body (STREAM_OPEN_MARKER) is skipped. A body that does not decode as CBOR, or a length prefix above MAX_FRAME_BYTES, rejects the iteration (a connection-level failure); a body that decodes but fails frame validation is dropped and the stream continues, since an unrecognised frame from a newer peer is what version negotiation exists to tolerate.
27
40
  */
28
41
  async function* readFrames(reader) {
29
42
  let buffered = /* @__PURE__ */ new Uint8Array(0);
30
43
  try {
31
- for (;;) {
32
- const chunk = await reader.read();
33
- if (chunk.done) return;
34
- buffered = concat(buffered, chunk.value);
44
+ for await (const chunk of chunksOf(reader)) {
45
+ buffered = concat(buffered, chunk);
35
46
  while (buffered.length >= LENGTH_PREFIX_BYTES) {
36
47
  const bodyLength = new DataView(buffered.buffer, buffered.byteOffset).getUint32(0);
37
48
  if (bodyLength > 104857600) throw new Error(`frame of ${String(bodyLength)} bytes exceeds the ${String(MAX_FRAME_BYTES)}-byte limit`);
@@ -21,16 +21,27 @@ function concat(first, second) {
21
21
  joined.set(second, first.length);
22
22
  return joined;
23
23
  }
24
+ /** A reader's chunks as an async iterable, which ends with the stream and leaves releasing or cancelling the reader to its owner. */
25
+ function chunksOf(reader) {
26
+ return { [Symbol.asyncIterator]: () => ({ next: async () => {
27
+ const chunk = await reader.read();
28
+ return chunk.done ? {
29
+ done: true,
30
+ value: void 0
31
+ } : {
32
+ done: false,
33
+ value: chunk.value
34
+ };
35
+ } }) };
36
+ }
24
37
  /**
25
38
  * Frames arriving on a byte stream, in order, until it ends. An empty body (STREAM_OPEN_MARKER) is skipped. A body that does not decode as CBOR, or a length prefix above MAX_FRAME_BYTES, rejects the iteration (a connection-level failure); a body that decodes but fails frame validation is dropped and the stream continues, since an unrecognised frame from a newer peer is what version negotiation exists to tolerate.
26
39
  */
27
40
  async function* readFrames(reader) {
28
41
  let buffered = /* @__PURE__ */ new Uint8Array(0);
29
42
  try {
30
- for (;;) {
31
- const chunk = await reader.read();
32
- if (chunk.done) return;
33
- buffered = concat(buffered, chunk.value);
43
+ for await (const chunk of chunksOf(reader)) {
44
+ buffered = concat(buffered, chunk);
34
45
  while (buffered.length >= LENGTH_PREFIX_BYTES) {
35
46
  const bodyLength = new DataView(buffered.buffer, buffered.byteOffset).getUint32(0);
36
47
  if (bodyLength > 104857600) throw new Error(`frame of ${String(bodyLength)} bytes exceeds the ${String(MAX_FRAME_BYTES)}-byte limit`);
@@ -1,4 +1,4 @@
1
- import { t as KeyValueStorage } from "../storage-B403CRyu.cjs";
1
+ import { t as KeyValueStorage } from "../storage-jbGFAYIV.cjs";
2
2
  //#region src/adapters/memory-storage.d.ts
3
3
  /** An in-process KeyValueStorage backed by a Map -- for tests and single-process nodes; a real deployment substitutes a persistent adapter behind the same contract without touching anything that depends on the port. Not declared `async`: every operation is genuinely synchronous under the hood, so the contract's Promise return is satisfied directly via Promise.resolve() rather than an async function with no await in its body. */
4
4
  export declare function createMemoryStorage(): KeyValueStorage;
@@ -1,4 +1,4 @@
1
- import { t as KeyValueStorage } from "../storage-B403CRyu.mjs";
1
+ import { t as KeyValueStorage } from "../storage-jbGFAYIV.mjs";
2
2
  //#region src/adapters/memory-storage.d.ts
3
3
  /** An in-process KeyValueStorage backed by a Map -- for tests and single-process nodes; a real deployment substitutes a persistent adapter behind the same contract without touching anything that depends on the port. Not declared `async`: every operation is genuinely synchronous under the hood, so the contract's Promise return is satisfied directly via Promise.resolve() rather than an async function with no await in its body. */
4
4
  export declare function createMemoryStorage(): KeyValueStorage;
@@ -23,9 +23,10 @@ async function listAllRelativePaths(baseDir) {
23
23
  if (isEnoent(error)) return [];
24
24
  throw error;
25
25
  }
26
- const files = [];
27
- for (const entry of entries) if ((await node_fs_promises.stat(node_path.join(baseDir, entry)).catch(() => null))?.isFile() === true) files.push(entry.split(node_path.sep).join("/"));
28
- return files;
26
+ return (await Promise.all(entries.map(async (entry) => ({
27
+ entry,
28
+ stat: await node_fs_promises.stat(node_path.join(baseDir, entry)).catch(() => null)
29
+ })))).filter(({ stat }) => stat?.isFile() === true).map(({ entry }) => entry.split(node_path.sep).join("/"));
29
30
  }
30
31
  function createNodeFsStorage(options) {
31
32
  const baseDir = options.dir;
@@ -1,4 +1,4 @@
1
- import { t as KeyValueStorage } from "../storage-B403CRyu.cjs";
1
+ import { t as KeyValueStorage } from "../storage-jbGFAYIV.cjs";
2
2
  //#region src/adapters/node-fs-storage.d.ts
3
3
  export interface NodeFsStorageOptions {
4
4
  /** Directory to store entries under. Created on first write if it doesn't already exist. */
@@ -1,4 +1,4 @@
1
- import { t as KeyValueStorage } from "../storage-B403CRyu.mjs";
1
+ import { t as KeyValueStorage } from "../storage-jbGFAYIV.mjs";
2
2
  //#region src/adapters/node-fs-storage.d.ts
3
3
  export interface NodeFsStorageOptions {
4
4
  /** Directory to store entries under. Created on first write if it doesn't already exist. */
@@ -19,9 +19,10 @@ async function listAllRelativePaths(baseDir) {
19
19
  if (isEnoent(error)) return [];
20
20
  throw error;
21
21
  }
22
- const files = [];
23
- for (const entry of entries) if ((await fs.stat(path.join(baseDir, entry)).catch(() => null))?.isFile() === true) files.push(entry.split(path.sep).join("/"));
24
- return files;
22
+ return (await Promise.all(entries.map(async (entry) => ({
23
+ entry,
24
+ stat: await fs.stat(path.join(baseDir, entry)).catch(() => null)
25
+ })))).filter(({ stat }) => stat?.isFile() === true).map(({ entry }) => entry.split(path.sep).join("/"));
25
26
  }
26
27
  function createNodeFsStorage(options) {
27
28
  const baseDir = options.dir;
@@ -1,4 +1,4 @@
1
- import { t as Clock } from "../clock-DiSx-WKM.cjs";
1
+ import { t as Clock } from "../clock-DhUPRfLh.cjs";
2
2
  //#region src/adapters/system-clock.d.ts
3
3
  /** Wraps the system wall clock -- the only place Date.now() appears in this package. */
4
4
  export declare function createSystemClock(): Clock;
@@ -1,4 +1,4 @@
1
- import { t as Clock } from "../clock-DiSx-WKM.mjs";
1
+ import { t as Clock } from "../clock-DhUPRfLh.mjs";
2
2
  //#region src/adapters/system-clock.d.ts
3
3
  /** Wraps the system wall clock -- the only place Date.now() appears in this package. */
4
4
  export declare function createSystemClock(): Clock;
@@ -0,0 +1,10 @@
1
+ //#region src/ports/clock.d.ts
2
+ /**
3
+ * Injected time source, never pulled from global Date.now() directly in domain logic -- lets token-expiry and revocation-timestamp checks be tested deterministically and lets a consumer substitute a synchronised network clock later without touching domain code.
4
+ */
5
+ interface Clock {
6
+ /** Current time as Unix milliseconds, matching every uint timestamp field in the protocol (token-claims.expires, revocation-entry.revoked-at, handshake params, etc.). */
7
+ now: () => number;
8
+ }
9
+ //#endregion
10
+ export { Clock as t };
@@ -0,0 +1,10 @@
1
+ //#region src/ports/clock.d.ts
2
+ /**
3
+ * Injected time source, never pulled from global Date.now() directly in domain logic -- lets token-expiry and revocation-timestamp checks be tested deterministically and lets a consumer substitute a synchronised network clock later without touching domain code.
4
+ */
5
+ interface Clock {
6
+ /** Current time as Unix milliseconds, matching every uint timestamp field in the protocol (token-claims.expires, revocation-entry.revoked-at, handshake params, etc.). */
7
+ now: () => number;
8
+ }
9
+ //#endregion
10
+ export { Clock as t };
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_generated_protocol = require("../generated/protocol.cjs");
3
3
  const require_domain_device_id = require("./device-id.cjs");
4
+ const require_sequential = require("../sequential-CtQVlW8M.cjs");
4
5
  //#region src/domain/bulk.ts
5
6
  /**
6
7
  * core/bulk (wire-mesh#34/#123): resumable, flow-controlled bulk transfer. `bulk.open`/`bulk.resume`/`bulk.cancel` ride manage-request-frame as ordinary manage-command params; `bulk-data`/`bulk-ack`/`bulk-end` are their own frame kind, riding the connection directly rather than a manage-response -- see spec/bulk.cddl's own header comment for the full design, including the three previously-open questions this module resolves in code (chunk-size negotiation is not a wire concept at all; transfer-id/issuer pairing is a verifier obligation, not a wire field; token expiry is checked once per manage-command, never per data frame).
@@ -103,22 +104,30 @@ function createBulkSender(options) {
103
104
  let window = initialWindow;
104
105
  let bytesInFlight = 0;
105
106
  const inFlight = [];
107
+ /** Takes one ack's credit in: it replaces the window, and every chunk it covers is retired. */
108
+ const applyAck = (ack) => {
109
+ ackedCount = ack["ack-seq"];
110
+ window = ack.window;
111
+ for (let head = inFlight[0]; head !== void 0 && head.seq < ackedCount; head = inFlight[0]) {
112
+ inFlight.shift();
113
+ bytesInFlight -= head.length;
114
+ }
115
+ };
116
+ const exceedsWindow = (length) => bytesInFlight + length > window;
117
+ /** Takes acks until `length` more bytes fit in the window. */
118
+ const awaitCredit = async (length) => {
119
+ applyAck(await waitForAck(id));
120
+ if (exceedsWindow(length)) await awaitCredit(length);
121
+ };
106
122
  try {
107
123
  for await (const chunk of options.source(fromSeq)) {
108
124
  if (cancelled.has(id)) return {
109
125
  status: "incomplete",
110
126
  ackedCount
111
127
  };
112
- while (bytesInFlight + chunk.length > window) {
113
- const ack = await waitForAck(id);
114
- ackedCount = ack["ack-seq"];
115
- window = ack.window;
116
- for (;;) {
117
- const head = inFlight[0];
118
- if (head === void 0 || head.seq >= ackedCount) break;
119
- inFlight.shift();
120
- bytesInFlight -= head.length;
121
- }
128
+ if (exceedsWindow(chunk.length)) {
129
+ applyAck(await waitForAck(id));
130
+ if (exceedsWindow(chunk.length)) await awaitCredit(chunk.length);
122
131
  }
123
132
  const dataFrame = {
124
133
  type: "bulk-data",
@@ -306,10 +315,10 @@ function createBulkReceiver(options) {
306
315
  }
307
316
  const ackedCount = await readAckSeq(idHex);
308
317
  const chunks = [];
309
- for (let seq = 0; seq < ackedCount; seq += 1) {
318
+ await require_sequential.eachInOrder(Array.from({ length: ackedCount }, (_, seq) => seq), async (seq) => {
310
319
  const chunk = await storage.get(chunkKey(idHex, seq));
311
320
  if (chunk !== void 0) chunks.push(chunk);
312
- }
321
+ });
313
322
  const ok = bytesEqual(await digestOf(concatChunks(chunks)), frame.digest);
314
323
  reg.onComplete?.({
315
324
  ok,
@@ -1,6 +1,6 @@
1
1
  import { D as DeviceId, N as Frame, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DzmpxxUr.cjs";
2
2
  import { t as Connection } from "../transport-gDbSHO-r.cjs";
3
- import { t as KeyValueStorage } from "../storage-B403CRyu.cjs";
3
+ import { t as KeyValueStorage } from "../storage-jbGFAYIV.cjs";
4
4
  import { IncomingManageRequest, MeshSession } from "./mesh-session.cjs";
5
5
  //#region src/domain/bulk.d.ts
6
6
  /** The two capabilities matching the direction each manage-command actually flows (spec/bulk.cddl): the sender proves write authority to open a transfer, the receiver proves read authority to ask for one to continue. */
@@ -1,6 +1,6 @@
1
1
  import { D as DeviceId, N as Frame, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DzmpxxUr.mjs";
2
2
  import { t as Connection } from "../transport-bD3p_TO_.mjs";
3
- import { t as KeyValueStorage } from "../storage-B403CRyu.mjs";
3
+ import { t as KeyValueStorage } from "../storage-jbGFAYIV.mjs";
4
4
  import { IncomingManageRequest, MeshSession } from "./mesh-session.mjs";
5
5
  //#region src/domain/bulk.d.ts
6
6
  /** The two capabilities matching the direction each manage-command actually flows (spec/bulk.cddl): the sender proves write authority to open a transfer, the receiver proves read authority to ask for one to continue. */
@@ -1,5 +1,6 @@
1
1
  import { bulkCancelSchema, bulkOpenSchema, bulkResumeSchema } from "../generated/protocol.mjs";
2
2
  import { bytesToHex } from "./device-id.mjs";
3
+ import { t as eachInOrder } from "../sequential-D_W3THly.mjs";
3
4
  //#region src/domain/bulk.ts
4
5
  /**
5
6
  * core/bulk (wire-mesh#34/#123): resumable, flow-controlled bulk transfer. `bulk.open`/`bulk.resume`/`bulk.cancel` ride manage-request-frame as ordinary manage-command params; `bulk-data`/`bulk-ack`/`bulk-end` are their own frame kind, riding the connection directly rather than a manage-response -- see spec/bulk.cddl's own header comment for the full design, including the three previously-open questions this module resolves in code (chunk-size negotiation is not a wire concept at all; transfer-id/issuer pairing is a verifier obligation, not a wire field; token expiry is checked once per manage-command, never per data frame).
@@ -102,22 +103,30 @@ function createBulkSender(options) {
102
103
  let window = initialWindow;
103
104
  let bytesInFlight = 0;
104
105
  const inFlight = [];
106
+ /** Takes one ack's credit in: it replaces the window, and every chunk it covers is retired. */
107
+ const applyAck = (ack) => {
108
+ ackedCount = ack["ack-seq"];
109
+ window = ack.window;
110
+ for (let head = inFlight[0]; head !== void 0 && head.seq < ackedCount; head = inFlight[0]) {
111
+ inFlight.shift();
112
+ bytesInFlight -= head.length;
113
+ }
114
+ };
115
+ const exceedsWindow = (length) => bytesInFlight + length > window;
116
+ /** Takes acks until `length` more bytes fit in the window. */
117
+ const awaitCredit = async (length) => {
118
+ applyAck(await waitForAck(id));
119
+ if (exceedsWindow(length)) await awaitCredit(length);
120
+ };
105
121
  try {
106
122
  for await (const chunk of options.source(fromSeq)) {
107
123
  if (cancelled.has(id)) return {
108
124
  status: "incomplete",
109
125
  ackedCount
110
126
  };
111
- while (bytesInFlight + chunk.length > window) {
112
- const ack = await waitForAck(id);
113
- ackedCount = ack["ack-seq"];
114
- window = ack.window;
115
- for (;;) {
116
- const head = inFlight[0];
117
- if (head === void 0 || head.seq >= ackedCount) break;
118
- inFlight.shift();
119
- bytesInFlight -= head.length;
120
- }
127
+ if (exceedsWindow(chunk.length)) {
128
+ applyAck(await waitForAck(id));
129
+ if (exceedsWindow(chunk.length)) await awaitCredit(chunk.length);
121
130
  }
122
131
  const dataFrame = {
123
132
  type: "bulk-data",
@@ -305,10 +314,10 @@ function createBulkReceiver(options) {
305
314
  }
306
315
  const ackedCount = await readAckSeq(idHex);
307
316
  const chunks = [];
308
- for (let seq = 0; seq < ackedCount; seq += 1) {
317
+ await eachInOrder(Array.from({ length: ackedCount }, (_, seq) => seq), async (seq) => {
309
318
  const chunk = await storage.get(chunkKey(idHex, seq));
310
319
  if (chunk !== void 0) chunks.push(chunk);
311
- }
320
+ });
312
321
  const ok = bytesEqual(await digestOf(concatChunks(chunks)), frame.digest);
313
322
  reg.onComplete?.({
314
323
  ok,
@@ -1,6 +1,6 @@
1
1
  import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DzmpxxUr.cjs";
2
2
  import { t as IdentityPort } from "../identity-B6nhlSOT.cjs";
3
- import { t as Clock } from "../clock-DiSx-WKM.cjs";
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";
6
6
  //#region src/domain/capability-grant.d.ts
@@ -1,6 +1,6 @@
1
1
  import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-DzmpxxUr.mjs";
2
2
  import { t as IdentityPort } from "../identity-EV-HHv_D.mjs";
3
- import { t as Clock } from "../clock-DiSx-WKM.mjs";
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";
6
6
  //#region src/domain/capability-grant.d.ts
@@ -1,8 +1,8 @@
1
1
  import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken, u as CapabilityGrantOk } from "../protocol-DzmpxxUr.cjs";
2
2
  import { t as IdentityPort } from "../identity-B6nhlSOT.cjs";
3
- import { t as Clock } from "../clock-DiSx-WKM.cjs";
3
+ import { t as Clock } from "../clock-DhUPRfLh.cjs";
4
4
  import { IncomingManageRequest, MeshSession } from "./mesh-session.cjs";
5
- import { m as TokenDelegateHandler, r as ConditionsContext } from "../token-predicates-D4NNPLnp.cjs";
5
+ import { m as TokenDelegateHandler, r as ConditionsContext } from "../token-predicates-lg4TtUxm.cjs";
6
6
  import { RevocationCheck } from "./tokens.cjs";
7
7
  //#region src/domain/capability-request.d.ts
8
8
  /**
@@ -1,8 +1,8 @@
1
1
  import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken, u as CapabilityGrantOk } from "../protocol-DzmpxxUr.mjs";
2
2
  import { t as IdentityPort } from "../identity-EV-HHv_D.mjs";
3
- import { t as Clock } from "../clock-DiSx-WKM.mjs";
3
+ import { t as Clock } from "../clock-DhUPRfLh.mjs";
4
4
  import { IncomingManageRequest, MeshSession } from "./mesh-session.mjs";
5
- import { m as TokenDelegateHandler, r as ConditionsContext } from "../token-predicates-DqpG8VZZ.mjs";
5
+ import { m as TokenDelegateHandler, r as ConditionsContext } from "../token-predicates-CrieP1WI.mjs";
6
6
  import { RevocationCheck } from "./tokens.mjs";
7
7
  //#region src/domain/capability-request.d.ts
8
8
  /**
@@ -1,5 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_domain_device_id = require("./device-id.cjs");
3
+ const require_sequential = require("../sequential-CtQVlW8M.cjs");
3
4
  //#region src/domain/data-sync.ts
4
5
  function headKey(peer) {
5
6
  return `data/${require_domain_device_id.deviceIdToHex(peer)}/head`;
@@ -50,18 +51,28 @@ async function handleDataHave(storage, frame) {
50
51
  };
51
52
  }
52
53
  /**
54
+ * The entries stored for `peer` from sequence `first` to `last` inclusive, read one after another and cut at the first hole: a hole is a record missing from a range the head claims is held, which should not happen while entryKey and headKey stay consistent, and stopping there is better than returning a run with a silent gap that a receiver's own check would only catch after the fact. Keys past a hole are never read.
55
+ */
56
+ async function readRun(storage, peer, first, last) {
57
+ const run = [];
58
+ const readFrom = async (seq) => {
59
+ if (seq > last) return;
60
+ const entry = await storage.get(entryKey(peer, seq));
61
+ if (entry === void 0) return;
62
+ run.push(entry);
63
+ await readFrom(seq + 1);
64
+ };
65
+ await readFrom(first);
66
+ return run;
67
+ }
68
+ /**
53
69
  * Handles an incoming data-request: the entries this store actually holds for `frame.peer` after `frame["from-seq"]`, capped at `limit`, or null if it holds none past that point. The responder need not be `frame.peer` itself -- any peer holding a replica may answer on the original author's behalf, per data-domain.cddl's own fan-out rule; this function makes no distinction.
54
70
  */
55
71
  async function handleDataRequest(storage, frame, limit) {
56
72
  const ourHead = await readHeadSeq(storage, frame.peer);
57
73
  if (ourHead <= frame["from-seq"]) return null;
58
74
  const lastSeq = Math.min(ourHead, frame["from-seq"] + limit);
59
- const entries = [];
60
- for (let seq = frame["from-seq"] + 1; seq <= lastSeq; seq += 1) {
61
- const entry = await storage.get(entryKey(frame.peer, seq));
62
- if (entry === void 0) break;
63
- entries.push(entry);
64
- }
75
+ const entries = await readRun(storage, frame.peer, frame["from-seq"] + 1, lastSeq);
65
76
  if (entries.length === 0) return null;
66
77
  return {
67
78
  type: "data-entries",
@@ -79,12 +90,10 @@ async function handleDataEntries(storage, frame) {
79
90
  ok: false,
80
91
  reason: "gap"
81
92
  };
82
- let seq = frame["from-seq"];
83
- for (const entry of frame.entries) {
84
- seq += 1;
85
- await storage.set(entryKey(frame.peer, seq), entry);
86
- }
87
- await writeHeadSeq(storage, frame.peer, seq);
93
+ await require_sequential.eachInOrder(frame.entries, async (entry, index) => {
94
+ await storage.set(entryKey(frame.peer, frame["from-seq"] + 1 + index), entry);
95
+ });
96
+ await writeHeadSeq(storage, frame.peer, frame["from-seq"] + frame.entries.length);
88
97
  return { ok: true };
89
98
  }
90
99
  /**
@@ -92,13 +101,7 @@ async function handleDataEntries(storage, frame) {
92
101
  */
93
102
  async function readEntries(storage, peer, fromSeqExclusive) {
94
103
  const head = await readHeadSeq(storage, peer);
95
- const entries = [];
96
- for (let seq = fromSeqExclusive + 1; seq <= head; seq += 1) {
97
- const entry = await storage.get(entryKey(peer, seq));
98
- if (entry === void 0) break;
99
- entries.push(entry);
100
- }
101
- return entries;
104
+ return readRun(storage, peer, fromSeqExclusive + 1, head);
102
105
  }
103
106
  //#endregion
104
107
  exports.appendOwnEntry = appendOwnEntry;
@@ -1,5 +1,5 @@
1
1
  import { D as DeviceId, E as DataRequestFrame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-DzmpxxUr.cjs";
2
- import { t as KeyValueStorage } from "../storage-B403CRyu.cjs";
2
+ import { t as KeyValueStorage } from "../storage-jbGFAYIV.cjs";
3
3
  import { t as IdentityPort } from "../identity-B6nhlSOT.cjs";
4
4
  //#region src/domain/data-sync.d.ts
5
5
  export interface DataSyncOptions {
@@ -1,5 +1,5 @@
1
1
  import { D as DeviceId, E as DataRequestFrame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-DzmpxxUr.mjs";
2
- import { t as KeyValueStorage } from "../storage-B403CRyu.mjs";
2
+ import { t as KeyValueStorage } from "../storage-jbGFAYIV.mjs";
3
3
  import { t as IdentityPort } from "../identity-EV-HHv_D.mjs";
4
4
  //#region src/domain/data-sync.d.ts
5
5
  export interface DataSyncOptions {
@@ -1,4 +1,5 @@
1
1
  import { deviceIdToHex } from "./device-id.mjs";
2
+ import { t as eachInOrder } from "../sequential-D_W3THly.mjs";
2
3
  //#region src/domain/data-sync.ts
3
4
  function headKey(peer) {
4
5
  return `data/${deviceIdToHex(peer)}/head`;
@@ -49,18 +50,28 @@ async function handleDataHave(storage, frame) {
49
50
  };
50
51
  }
51
52
  /**
53
+ * The entries stored for `peer` from sequence `first` to `last` inclusive, read one after another and cut at the first hole: a hole is a record missing from a range the head claims is held, which should not happen while entryKey and headKey stay consistent, and stopping there is better than returning a run with a silent gap that a receiver's own check would only catch after the fact. Keys past a hole are never read.
54
+ */
55
+ async function readRun(storage, peer, first, last) {
56
+ const run = [];
57
+ const readFrom = async (seq) => {
58
+ if (seq > last) return;
59
+ const entry = await storage.get(entryKey(peer, seq));
60
+ if (entry === void 0) return;
61
+ run.push(entry);
62
+ await readFrom(seq + 1);
63
+ };
64
+ await readFrom(first);
65
+ return run;
66
+ }
67
+ /**
52
68
  * Handles an incoming data-request: the entries this store actually holds for `frame.peer` after `frame["from-seq"]`, capped at `limit`, or null if it holds none past that point. The responder need not be `frame.peer` itself -- any peer holding a replica may answer on the original author's behalf, per data-domain.cddl's own fan-out rule; this function makes no distinction.
53
69
  */
54
70
  async function handleDataRequest(storage, frame, limit) {
55
71
  const ourHead = await readHeadSeq(storage, frame.peer);
56
72
  if (ourHead <= frame["from-seq"]) return null;
57
73
  const lastSeq = Math.min(ourHead, frame["from-seq"] + limit);
58
- const entries = [];
59
- for (let seq = frame["from-seq"] + 1; seq <= lastSeq; seq += 1) {
60
- const entry = await storage.get(entryKey(frame.peer, seq));
61
- if (entry === void 0) break;
62
- entries.push(entry);
63
- }
74
+ const entries = await readRun(storage, frame.peer, frame["from-seq"] + 1, lastSeq);
64
75
  if (entries.length === 0) return null;
65
76
  return {
66
77
  type: "data-entries",
@@ -78,12 +89,10 @@ async function handleDataEntries(storage, frame) {
78
89
  ok: false,
79
90
  reason: "gap"
80
91
  };
81
- let seq = frame["from-seq"];
82
- for (const entry of frame.entries) {
83
- seq += 1;
84
- await storage.set(entryKey(frame.peer, seq), entry);
85
- }
86
- await writeHeadSeq(storage, frame.peer, seq);
92
+ await eachInOrder(frame.entries, async (entry, index) => {
93
+ await storage.set(entryKey(frame.peer, frame["from-seq"] + 1 + index), entry);
94
+ });
95
+ await writeHeadSeq(storage, frame.peer, frame["from-seq"] + frame.entries.length);
87
96
  return { ok: true };
88
97
  }
89
98
  /**
@@ -91,13 +100,7 @@ async function handleDataEntries(storage, frame) {
91
100
  */
92
101
  async function readEntries(storage, peer, fromSeqExclusive) {
93
102
  const head = await readHeadSeq(storage, peer);
94
- const entries = [];
95
- for (let seq = fromSeqExclusive + 1; seq <= head; seq += 1) {
96
- const entry = await storage.get(entryKey(peer, seq));
97
- if (entry === void 0) break;
98
- entries.push(entry);
99
- }
100
- return entries;
103
+ return readRun(storage, peer, fromSeqExclusive + 1, head);
101
104
  }
102
105
  //#endregion
103
106
  export { appendOwnEntry, handleDataEntries, handleDataHave, handleDataRequest, headSeqFor, readEntries };
@@ -20,15 +20,21 @@ function createGossipExpansion(options) {
20
20
  options.onExpansionDeclined?.(candidate);
21
21
  return;
22
22
  }
23
- const errors = [];
24
- for (const address of candidate.addresses) try {
23
+ await dialInOrder(candidate, candidate.addresses, []);
24
+ }
25
+ /** Dials the addresses one at a time, in order, stopping at the first that connects: a later address is only tried once every earlier one has failed, and when none connects the failures, in order, are reported through `onExpansionFailed`. */
26
+ async function dialInOrder(candidate, addresses, failures) {
27
+ const [address, ...remaining] = addresses;
28
+ if (address === void 0) {
29
+ options.onExpansionFailed?.(candidate, failures);
30
+ return;
31
+ }
32
+ try {
25
33
  const session = await options.dial(address);
26
34
  options.onExpanded(candidate, address, session);
27
- return;
28
35
  } catch (error) {
29
- errors.push(toError(error));
36
+ await dialInOrder(candidate, remaining, [...failures, toError(error)]);
30
37
  }
31
- options.onExpansionFailed?.(candidate, errors);
32
38
  }
33
39
  return { considerAdvert(advert) {
34
40
  const hex = require_domain_device_id.deviceIdToHex(advert.device);
@@ -19,15 +19,21 @@ function createGossipExpansion(options) {
19
19
  options.onExpansionDeclined?.(candidate);
20
20
  return;
21
21
  }
22
- const errors = [];
23
- for (const address of candidate.addresses) try {
22
+ await dialInOrder(candidate, candidate.addresses, []);
23
+ }
24
+ /** Dials the addresses one at a time, in order, stopping at the first that connects: a later address is only tried once every earlier one has failed, and when none connects the failures, in order, are reported through `onExpansionFailed`. */
25
+ async function dialInOrder(candidate, addresses, failures) {
26
+ const [address, ...remaining] = addresses;
27
+ if (address === void 0) {
28
+ options.onExpansionFailed?.(candidate, failures);
29
+ return;
30
+ }
31
+ try {
24
32
  const session = await options.dial(address);
25
33
  options.onExpanded(candidate, address, session);
26
- return;
27
34
  } catch (error) {
28
- errors.push(toError(error));
35
+ await dialInOrder(candidate, remaining, [...failures, toError(error)]);
29
36
  }
30
- options.onExpansionFailed?.(candidate, errors);
31
37
  }
32
38
  return { considerAdvert(advert) {
33
39
  const hex = deviceIdToHex(advert.device);
@@ -1,5 +1,5 @@
1
1
  import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-DzmpxxUr.cjs";
2
- import { t as KeyValueStorage } from "../storage-B403CRyu.cjs";
2
+ import { t as KeyValueStorage } from "../storage-jbGFAYIV.cjs";
3
3
  //#region src/domain/hub-mailbox.d.ts
4
4
  export interface MailboxLimits {
5
5
  /** How many devices' logs the mailbox will hold at once. */
@@ -1,5 +1,5 @@
1
1
  import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-DzmpxxUr.mjs";
2
- import { t as KeyValueStorage } from "../storage-B403CRyu.mjs";
2
+ import { t as KeyValueStorage } from "../storage-jbGFAYIV.mjs";
3
3
  //#region src/domain/hub-mailbox.d.ts
4
4
  export interface MailboxLimits {
5
5
  /** How many devices' logs the mailbox will hold at once. */