wire-mesh-core 1.23.0 → 1.25.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 (59) hide show
  1. package/dist/adapters/frame-codec.d.cts +1 -1
  2. package/dist/adapters/frame-codec.d.mts +1 -1
  3. package/dist/adapters/node-identity.d.cts +2 -2
  4. package/dist/adapters/node-identity.d.mts +2 -2
  5. package/dist/adapters/tcp-transport.d.cts +1 -1
  6. package/dist/adapters/tcp-transport.d.mts +1 -1
  7. package/dist/adapters/tls-transport.d.cts +1 -1
  8. package/dist/adapters/tls-transport.d.mts +1 -1
  9. package/dist/domain/bulk.cjs +400 -0
  10. package/dist/domain/bulk.d.cts +97 -0
  11. package/dist/domain/bulk.d.mts +97 -0
  12. package/dist/domain/bulk.mjs +393 -0
  13. package/dist/domain/capability-grant.d.cts +3 -3
  14. package/dist/domain/capability-grant.d.mts +3 -3
  15. package/dist/domain/capability-request.d.cts +2 -2
  16. package/dist/domain/capability-request.d.mts +2 -2
  17. package/dist/domain/device-id.d.cts +1 -1
  18. package/dist/domain/device-id.d.mts +1 -1
  19. package/dist/domain/grant-candidates.d.cts +1 -1
  20. package/dist/domain/grant-candidates.d.mts +1 -1
  21. package/dist/domain/handshake.d.cts +1 -1
  22. package/dist/domain/handshake.d.mts +1 -1
  23. package/dist/domain/mesh-session.d.cts +3 -3
  24. package/dist/domain/mesh-session.d.mts +3 -3
  25. package/dist/domain/relay-hub.d.cts +2 -2
  26. package/dist/domain/relay-hub.d.mts +2 -2
  27. package/dist/domain/relay-use-gate.d.cts +2 -2
  28. package/dist/domain/relay-use-gate.d.mts +2 -2
  29. package/dist/domain/revocation-view.cjs +1 -1
  30. package/dist/domain/revocation-view.d.cts +2 -2
  31. package/dist/domain/revocation-view.d.mts +2 -2
  32. package/dist/domain/revocation-view.mjs +1 -1
  33. package/dist/domain/room-token-verification.cjs +1 -1
  34. package/dist/domain/room-token-verification.d.cts +2 -2
  35. package/dist/domain/room-token-verification.d.mts +2 -2
  36. package/dist/domain/room-token-verification.mjs +1 -1
  37. package/dist/domain/room.cjs +108 -0
  38. package/dist/domain/room.d.cts +36 -0
  39. package/dist/domain/room.d.mts +36 -0
  40. package/dist/domain/room.mjs +106 -0
  41. package/dist/domain/tokens.d.cts +1 -1
  42. package/dist/domain/tokens.d.mts +1 -1
  43. package/dist/generated/protocol.cjs +78 -28
  44. package/dist/generated/protocol.d.cts +2 -2
  45. package/dist/generated/protocol.d.mts +2 -2
  46. package/dist/generated/protocol.mjs +72 -29
  47. package/dist/{identity-5LJ_B8go.d.mts → identity-C5N86BPz.d.mts} +1 -1
  48. package/dist/{identity-bg6IVGsH.d.cts → identity-DJ06jn4k.d.cts} +1 -1
  49. package/dist/ports/identity.d.cts +1 -1
  50. package/dist/ports/identity.d.mts +1 -1
  51. package/dist/ports/transport.d.cts +1 -1
  52. package/dist/ports/transport.d.mts +1 -1
  53. package/dist/{protocol-l9RMR3s5.d.cts → protocol-sW8d6Og-.d.cts} +177 -43
  54. package/dist/{protocol-l9RMR3s5.d.mts → protocol-sW8d6Og-.d.mts} +177 -43
  55. package/dist/{tokens-CBI54oF-.d.mts → tokens-CpLDI5Fo.d.mts} +2 -2
  56. package/dist/{tokens-H_mV_JCC.d.cts → tokens-DfVfE00W.d.cts} +2 -2
  57. package/dist/{transport-Baipikk7.d.cts → transport-B5E896PN.d.mts} +1 -1
  58. package/dist/{transport-DWbvUc3L.d.mts → transport-DxZ4JWZc.d.cts} +1 -1
  59. package/package.json +9 -1
@@ -0,0 +1,97 @@
1
+ import { D as DeviceId, N as Frame, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-sW8d6Og-.mjs";
2
+ import { KeyValueStorage } from "../ports/storage.mjs";
3
+ import { t as Connection } from "../transport-B5E896PN.mjs";
4
+ import { IncomingManageRequest, MeshSession } from "./mesh-session.mjs";
5
+ //#region src/domain/bulk.d.ts
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. */
7
+ export declare const BULK_WRITE_CAPABILITY = "bulk:write";
8
+ export declare const BULK_READ_CAPABILITY = "bulk:read";
9
+ export declare function buildBulkOpenCommand(transferId: Uint8Array<ArrayBuffer>, options?: Readonly<{
10
+ totalSize?: number;
11
+ contentType?: string;
12
+ }>): ManageCommand;
13
+ export declare function buildBulkResumeCommand(transferId: Uint8Array<ArrayBuffer>, ackSeq: number): ManageCommand;
14
+ export declare function buildBulkCancelCommand(transferId: Uint8Array<ArrayBuffer>, capability: typeof BULK_WRITE_CAPABILITY | typeof BULK_READ_CAPABILITY, reason?: string): ManageCommand;
15
+ export interface BulkSendOutcome {
16
+ status: "complete" | "incomplete" | "rejected";
17
+ /** Count of chunks the receiver had acked as durably persisted when this call settled. */
18
+ ackedCount: number;
19
+ /** Present only when status is "rejected": the manage-error code the receiver responded with. */
20
+ code?: string;
21
+ }
22
+ export interface CreateBulkSenderOptions {
23
+ /**
24
+ * Yields every chunk of the transfer starting at `fromSeq` (0 for a fresh transfer) -- MUST be callable more than once, from any previously-reached seq, and yield the identical bytes each time (spec/bulk.cddl's own "a file is inherently durable/re-readable" design point): this is what makes both mid-transfer resume and the post-transfer digest re-derivation below correct without this module buffering the whole transfer in memory during the send itself.
25
+ */
26
+ source: (fromSeq: number) => AsyncIterable<Uint8Array<ArrayBuffer>>;
27
+ }
28
+ export interface BulkSenderOpenOptions {
29
+ transferId?: Uint8Array<ArrayBuffer>;
30
+ totalSize?: number;
31
+ contentType?: string;
32
+ targetDevice?: DeviceId;
33
+ token?: CapabilityToken;
34
+ timeoutMs?: number;
35
+ }
36
+ export interface BulkSender {
37
+ /** Observes bulk-ack frames for transfers this sender currently owns. Wire as (part of) the onFrame passed to acceptMeshSession/createSessionCore, alongside anything else already observing the same connection. */
38
+ onFrame: (connection: Readonly<Connection>, frame: Frame) => void;
39
+ /** Opens a brand-new transfer and streams it from seq 0 until the source is exhausted (returns "complete") or this connection ends (returns "incomplete", i.e. a caller with a fresh connection may retry via continueFrom once the receiver's own bulk.resume arrives). */
40
+ open: (connection: Readonly<Connection>, session: Readonly<MeshSession>, scope: Readonly<CapabilityScope>, options?: Readonly<BulkSenderOpenOptions>) => Promise<BulkSendOutcome>;
41
+ /** Resumes an already-open transfer on a fresh connection, streaming from wherever the receiver's own post-accept bulk-ack says to (its ack-seq, a count of chunks already durably persisted) with no bulk.open sent again -- the receiver already confirmed via a prior bulk.resume that it wants this. */
42
+ continueFrom: (connection: Readonly<Connection>, transferId: Uint8Array<ArrayBuffer>) => Promise<BulkSendOutcome>;
43
+ /** Builds a manage-request handler for incoming bulk.resume asks, bound to one connection (wire as this session's own bulk:read-scoped handler). Refuses a request naming any transfer other than the one this sender currently has open. onResumed, if given, is called with the resulting BulkSendOutcome once the resumed stream settles -- the handler itself does not await it, so a caller not needing to observe it may omit onResumed entirely without leaking an unhandled rejection (errors inside stream() are already caught and folded into an "incomplete" outcome, never thrown). */
44
+ createResumeHandler: (connection: Readonly<Connection>, onResumed?: (outcome: Readonly<BulkSendOutcome>) => void) => (incoming: Readonly<IncomingManageRequest>) => Promise<void>;
45
+ /** Builds a manage-request handler for incoming bulk.cancel asks (wire as this session's own bulk:write-scoped handler). Marks the named transfer cancelled so the next iteration of an in-progress stream() loop stops rather than sending further chunks -- a currently-blocked wait for an ack is not itself interrupted, since nothing here has a way to abort an in-flight Promise, only to stop starting new work once it resolves. */
46
+ createCancelHandler: () => (incoming: Readonly<IncomingManageRequest>) => Promise<void>;
47
+ /** Tells this sender that a connection it was streaming over has ended (its own receive() stream reached done:true) -- without this, an in-progress stream() call blocked waiting for the next ack would hang forever rather than settling as "incomplete", since nothing else here observes connection lifecycle (open()/continueFrom() only ever learn about the connection through frames arriving on it). A caller's own pump/onFrame loop should call this once its receive() iteration ends, mirroring the onSessionEnd hook acceptMeshSession already exposes for the identical reason. */
48
+ onConnectionEnd: (connection: Readonly<Connection>) => void;
49
+ }
50
+ /**
51
+ * Builds a sender coordinator over one `source`. A caller sending several independent transfers from different sources constructs one sender per source, mirroring createCapabilityRequestHandler's own one-instance-per-capability convention.
52
+ */
53
+ export declare function createBulkSender(options: Readonly<CreateBulkSenderOptions>): BulkSender;
54
+ export interface BulkReceiveResult {
55
+ ok: boolean;
56
+ /** Count of chunks durably persisted for this transfer once bulk-end arrived -- meaningful even when ok is false (digest mismatch), since the chunks themselves are still exactly what was received. */
57
+ chunkCount: number;
58
+ }
59
+ export type BulkOfferDecision = {
60
+ kind: "accept";
61
+ /** Initial credit window, in bytes, this receiver is willing to accept past its own ack-seq -- a resource-limit choice for the caller to make (per spec/bulk.cddl's own "receiver resource limits are deployment policy, not protocol" design point), not a value this module invents a default for. */
62
+ window: number;
63
+ /** Called once, when bulk-end arrives and the digest has been checked against every durably-persisted chunk -- the only signal a caller gets that this transfer finished (successfully or corrupt), since createOpenHandler's own handler returns long before that. */
64
+ onComplete?: (result: Readonly<BulkReceiveResult>) => void;
65
+ } | {
66
+ kind: "reject";
67
+ reason?: string;
68
+ };
69
+ export interface BulkOfferEvent {
70
+ transferId: Uint8Array<ArrayBuffer>;
71
+ totalSize?: number;
72
+ contentType?: string;
73
+ /** The peer device-id authenticated on the connection this bulk.open arrived over -- supplied by the caller (createOpenHandler's own parameter), the same "MeshSession exposes no way to learn this independently" reason capability-request.ts's own bearerDevice parameter exists. */
74
+ openerDevice: DeviceId;
75
+ decide: (decision: Readonly<BulkOfferDecision>) => Promise<void>;
76
+ }
77
+ export interface BulkReceiver {
78
+ /** Handles bulk-data/bulk-end frames for transfers this receiver has accepted, durably persisting each contiguous chunk and acking before advertising it (spec/bulk.cddl's own obligation 1). Ignores a frame naming a transfer this receiver hasn't accepted, or arriving on a connection other than the one currently registered for it (an old, since-superseded connection racing a fresh bulk.resume). Wire as (part of) the onFrame passed to acceptMeshSession, alongside anything else observing the same connection. */
79
+ onFrame: (connection: Readonly<Connection>, frame: Frame) => Promise<void>;
80
+ /** Builds a manage-request handler for incoming bulk.open asks, bound to one connection and its already-authenticated peer device-id (mirroring createCapabilityRequestHandler's own per-connection factory shape). Refuses a transfer-id already known to this receiver as "already_exists" -- resuming an existing transfer is requestResume's job, not a second bulk.open. */
81
+ createOpenHandler: (openerDevice: DeviceId, connection: Readonly<Connection>, onOffer: (event: Readonly<BulkOfferEvent>) => void) => (incoming: Readonly<IncomingManageRequest>) => Promise<void>;
82
+ /** Actively sends bulk.resume on a fresh connection for an already-accepted transfer, reading this receiver's own durably-persisted ack-seq as the resume point (spec/bulk.cddl's own "resumption is receiver-driven" design point) -- there is no separate bulk.open to send again. On success, re-associates the transfer with this connection so onFrame accepts its subsequent bulk-data/bulk-end frames. */
83
+ requestResume: (connection: Readonly<Connection>, session: Readonly<MeshSession>, transferId: Uint8Array<ArrayBuffer>, scope: Readonly<CapabilityScope>, targetDevice?: DeviceId, token?: CapabilityToken, timeoutMs?: number) => Promise<{
84
+ status: "ok" | "rejected";
85
+ code?: string;
86
+ }>;
87
+ /** Builds a manage-request handler for incoming bulk.cancel asks (wire as this session's own bulk:read-scoped handler, mirroring the sender's own bulk:write-scoped one) -- deregisters the transfer's connection association so a stray already-in-flight bulk-data/bulk-end for it is ignored by onFrame from then on. Durably-persisted chunks are left in storage rather than purged: a cancelled transfer is not proof the caller has lost interest in what was already received, only that no more is coming, and purging is the caller's own decision to make with the KeyValueStorage it supplied. */
88
+ createCancelHandler: () => (incoming: Readonly<IncomingManageRequest>) => Promise<void>;
89
+ }
90
+ export interface CreateBulkReceiverOptions {
91
+ storage: KeyValueStorage;
92
+ }
93
+ /**
94
+ * Builds a receiver coordinator over one KeyValueStorage. Constructed once and shared across every connection this side accepts -- durable per-transfer state (which chunks are persisted, who opened it) and the in-memory registry of which connection currently owns each transfer must both outlive any one connection for resumption to mean anything at all.
95
+ */
96
+ export declare function createBulkReceiver(options: Readonly<CreateBulkReceiverOptions>): BulkReceiver;
97
+ //#endregion
@@ -0,0 +1,393 @@
1
+ import { bulkCancelSchema, bulkOpenSchema, bulkResumeSchema } from "../generated/protocol.mjs";
2
+ import { bytesToHex } from "./device-id.mjs";
3
+ //#region src/domain/bulk.ts
4
+ /**
5
+ * 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).
6
+ *
7
+ * Two long-lived, connection-independent coordinators, not a single monolithic object: createBulkReceiver's own state (which transfer-id maps to which connection, and every durably-persisted chunk) must outlive any one connection for resumption to mean anything, so it is constructed once and its onFrame/createOpenHandler/createResumeHandler are wired into every connection a caller accepts, not recreated per connection. createBulkSender is scoped to what it can honestly promise: MeshSession itself has no mechanism that hands a caller a fresh Connection across a reconnect (acceptMeshSession's own doc comment says so explicitly), so open()/continueFrom() each operate over one connection and settle once that connection ends: the caller -- who already owns whatever reconnection policy it uses -- decides when to call continueFrom() again on a fresh connection, typically from its own createResumeHandler once the receiver's bulk.resume names a transfer this sender still remembers.
8
+ */
9
+ const TRANSFER_ID_BYTE_LENGTH = 16;
10
+ /** 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. */
11
+ const BULK_WRITE_CAPABILITY = "bulk:write";
12
+ const BULK_READ_CAPABILITY = "bulk:read";
13
+ function randomTransferId() {
14
+ const bytes = new Uint8Array(TRANSFER_ID_BYTE_LENGTH);
15
+ crypto.getRandomValues(bytes);
16
+ return bytes;
17
+ }
18
+ function transferIdHex(transferId) {
19
+ return bytesToHex(transferId);
20
+ }
21
+ async function digestOf(bytes) {
22
+ const buffer = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes).buffer);
23
+ return new Uint8Array(buffer);
24
+ }
25
+ function bytesEqual(a, b) {
26
+ if (a.length !== b.length) return false;
27
+ for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
28
+ return true;
29
+ }
30
+ function concatChunks(chunks) {
31
+ const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
32
+ const out = new Uint8Array(total);
33
+ let offset = 0;
34
+ for (const chunk of chunks) {
35
+ out.set(chunk, offset);
36
+ offset += chunk.length;
37
+ }
38
+ return out;
39
+ }
40
+ function buildBulkOpenCommand(transferId, options = {}) {
41
+ return {
42
+ verb: BULK_WRITE_CAPABILITY,
43
+ params: {
44
+ verb: "bulk.open",
45
+ "transfer-id": transferId,
46
+ ...options.totalSize !== void 0 ? { "total-size": options.totalSize } : {},
47
+ ...options.contentType !== void 0 ? { "content-type": options.contentType } : {}
48
+ }
49
+ };
50
+ }
51
+ function buildBulkResumeCommand(transferId, ackSeq) {
52
+ return {
53
+ verb: BULK_READ_CAPABILITY,
54
+ params: {
55
+ verb: "bulk.resume",
56
+ "transfer-id": transferId,
57
+ "ack-seq": ackSeq
58
+ }
59
+ };
60
+ }
61
+ function buildBulkCancelCommand(transferId, capability, reason) {
62
+ return {
63
+ verb: capability,
64
+ params: {
65
+ verb: "bulk.cancel",
66
+ "transfer-id": transferId,
67
+ ...reason !== void 0 ? { reason } : {}
68
+ }
69
+ };
70
+ }
71
+ /**
72
+ * Builds a sender coordinator over one `source`. A caller sending several independent transfers from different sources constructs one sender per source, mirroring createCapabilityRequestHandler's own one-instance-per-capability convention.
73
+ */
74
+ function createBulkSender(options) {
75
+ const pendingAcks = /* @__PURE__ */ new Map();
76
+ const cancelled = /* @__PURE__ */ new Set();
77
+ function resolveNextAck(id, frame) {
78
+ const queue = pendingAcks.get(id);
79
+ if (queue === void 0 || queue.length === 0) return;
80
+ queue.shift()?.resolve(frame);
81
+ }
82
+ function rejectAllAcks(id, error) {
83
+ const queue = pendingAcks.get(id);
84
+ if (queue === void 0) return;
85
+ pendingAcks.set(id, []);
86
+ for (const waiter of queue) waiter.reject(error);
87
+ }
88
+ async function waitForAck(id) {
89
+ return new Promise((resolve, reject) => {
90
+ const queue = pendingAcks.get(id) ?? [];
91
+ queue.push({
92
+ resolve,
93
+ reject
94
+ });
95
+ pendingAcks.set(id, queue);
96
+ });
97
+ }
98
+ async function stream(connection, transferId, fromSeq, initialWindow) {
99
+ const id = transferIdHex(transferId);
100
+ let seq = fromSeq;
101
+ let ackedCount = fromSeq;
102
+ let window = initialWindow;
103
+ let bytesInFlight = 0;
104
+ const inFlight = [];
105
+ try {
106
+ for await (const chunk of options.source(fromSeq)) {
107
+ if (cancelled.has(id)) return {
108
+ status: "incomplete",
109
+ ackedCount
110
+ };
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
+ }
121
+ }
122
+ const dataFrame = {
123
+ type: "bulk-data",
124
+ "transfer-id": transferId,
125
+ seq,
126
+ bytes: chunk
127
+ };
128
+ await connection.send(dataFrame);
129
+ inFlight.push({
130
+ seq,
131
+ length: chunk.length
132
+ });
133
+ bytesInFlight += chunk.length;
134
+ seq += 1;
135
+ }
136
+ } catch {
137
+ return {
138
+ status: "incomplete",
139
+ ackedCount
140
+ };
141
+ }
142
+ const chunks = [];
143
+ for await (const chunk of options.source(0)) chunks.push(chunk);
144
+ const endFrame = {
145
+ type: "bulk-end",
146
+ "transfer-id": transferId,
147
+ digest: await digestOf(concatChunks(chunks))
148
+ };
149
+ await connection.send(endFrame);
150
+ return {
151
+ status: "complete",
152
+ ackedCount: seq
153
+ };
154
+ }
155
+ /** Waits for the receiver's own post-accept bulk-ack -- ack-seq is a count of chunks durably persisted so far, so it doubles as the seq to resume streaming from with no adjustment -- and starts streaming from there. Shared by open() (after bulk.open is accepted, where the very first ack's own ack-seq is naturally 0 for a fresh transfer) and continueFrom() (after a bulk.resume is accepted), since a receiver sends this identical proactive ack in both cases per spec/bulk.cddl's own bulk-ack-frame comment. */
156
+ async function resumeStream(connection, transferId) {
157
+ const ack = await waitForAck(transferIdHex(transferId));
158
+ return stream(connection, transferId, ack["ack-seq"], ack.window);
159
+ }
160
+ let activeTransferId;
161
+ let activeConnection;
162
+ return {
163
+ onFrame(_connection, frame) {
164
+ if (frame.type !== "bulk-ack") return;
165
+ resolveNextAck(transferIdHex(frame["transfer-id"]), frame);
166
+ },
167
+ onConnectionEnd(connection) {
168
+ if (activeTransferId === void 0 || activeConnection !== connection) return;
169
+ rejectAllAcks(activeTransferId, /* @__PURE__ */ new Error("connection ended"));
170
+ },
171
+ createCancelHandler() {
172
+ return async function handleBulkCancel(incoming) {
173
+ const parsed = bulkCancelSchema.safeParse(incoming.command.params);
174
+ if (!parsed.success) {
175
+ await incoming.respond({
176
+ result: "error",
177
+ code: "malformed"
178
+ });
179
+ return;
180
+ }
181
+ cancelled.add(transferIdHex(parsed.data["transfer-id"]));
182
+ await incoming.respond({ result: "ok" });
183
+ };
184
+ },
185
+ async open(connection, session, scope, openOptions = {}) {
186
+ const transferId = openOptions.transferId ?? randomTransferId();
187
+ activeTransferId = transferIdHex(transferId);
188
+ activeConnection = connection;
189
+ const outcome = await session.sendManageRequest(buildBulkOpenCommand(transferId, {
190
+ ...openOptions.totalSize !== void 0 ? { totalSize: openOptions.totalSize } : {},
191
+ ...openOptions.contentType !== void 0 ? { contentType: openOptions.contentType } : {}
192
+ }), scope, openOptions.targetDevice, openOptions.token, openOptions.timeoutMs);
193
+ if (outcome.result !== "ok") return {
194
+ status: "rejected",
195
+ ackedCount: 0,
196
+ code: outcome.code
197
+ };
198
+ return resumeStream(connection, transferId);
199
+ },
200
+ async continueFrom(connection, transferId) {
201
+ activeTransferId = transferIdHex(transferId);
202
+ activeConnection = connection;
203
+ return resumeStream(connection, transferId);
204
+ },
205
+ createResumeHandler(connection, onResumed) {
206
+ return async function handleBulkResume(incoming) {
207
+ const parsed = bulkResumeSchema.safeParse(incoming.command.params);
208
+ if (!parsed.success) {
209
+ await incoming.respond({
210
+ result: "error",
211
+ code: "malformed"
212
+ });
213
+ return;
214
+ }
215
+ const { "transfer-id": transferId } = parsed.data;
216
+ if (transferIdHex(transferId) !== activeTransferId) {
217
+ await incoming.respond({
218
+ result: "error",
219
+ code: "unknown_transfer"
220
+ });
221
+ return;
222
+ }
223
+ await incoming.respond({ result: "ok" });
224
+ activeConnection = connection;
225
+ resumeStream(connection, transferId).then((outcome) => onResumed?.(outcome)).catch(() => void 0);
226
+ };
227
+ }
228
+ };
229
+ }
230
+ function openerKey(idHex) {
231
+ return `bulk/${idHex}/opener`;
232
+ }
233
+ function ackSeqKey(idHex) {
234
+ return `bulk/${idHex}/ack-seq`;
235
+ }
236
+ function windowKey(idHex) {
237
+ return `bulk/${idHex}/window`;
238
+ }
239
+ function chunkKey(idHex, seq) {
240
+ return `bulk/${idHex}/chunk/${String(seq)}`;
241
+ }
242
+ function encodeUint(n) {
243
+ return new TextEncoder().encode(String(n));
244
+ }
245
+ function decodeUint(bytes) {
246
+ return Number(new TextDecoder().decode(bytes));
247
+ }
248
+ /**
249
+ * Builds a receiver coordinator over one KeyValueStorage. Constructed once and shared across every connection this side accepts -- durable per-transfer state (which chunks are persisted, who opened it) and the in-memory registry of which connection currently owns each transfer must both outlive any one connection for resumption to mean anything at all.
250
+ */
251
+ function createBulkReceiver(options) {
252
+ const { storage } = options;
253
+ const registrations = /* @__PURE__ */ new Map();
254
+ async function readAckSeq(idHex) {
255
+ const bytes = await storage.get(ackSeqKey(idHex));
256
+ return bytes === void 0 ? 0 : decodeUint(bytes);
257
+ }
258
+ return {
259
+ async onFrame(connection, frame) {
260
+ if (frame.type !== "bulk-data" && frame.type !== "bulk-end") return;
261
+ const idHex = transferIdHex(frame["transfer-id"]);
262
+ const reg = registrations.get(idHex);
263
+ if (reg?.connection !== connection) return;
264
+ if (frame.type === "bulk-data") {
265
+ const ackedCount = await readAckSeq(idHex);
266
+ if (frame.seq !== ackedCount) return;
267
+ await storage.set(chunkKey(idHex, frame.seq), frame.bytes);
268
+ const newAckedCount = ackedCount + 1;
269
+ await storage.set(ackSeqKey(idHex), encodeUint(newAckedCount));
270
+ const windowBytes = await storage.get(windowKey(idHex));
271
+ const window = windowBytes === void 0 ? 0 : decodeUint(windowBytes);
272
+ const ackFrame = {
273
+ type: "bulk-ack",
274
+ "transfer-id": frame["transfer-id"],
275
+ "ack-seq": newAckedCount,
276
+ window
277
+ };
278
+ await connection.send(ackFrame);
279
+ return;
280
+ }
281
+ const ackedCount = await readAckSeq(idHex);
282
+ const chunks = [];
283
+ for (let seq = 0; seq < ackedCount; seq += 1) {
284
+ const chunk = await storage.get(chunkKey(idHex, seq));
285
+ if (chunk !== void 0) chunks.push(chunk);
286
+ }
287
+ const ok = bytesEqual(await digestOf(concatChunks(chunks)), frame.digest);
288
+ reg.onComplete?.({
289
+ ok,
290
+ chunkCount: ackedCount
291
+ });
292
+ },
293
+ createOpenHandler(openerDevice, connection, onOffer) {
294
+ return async function handleBulkOpen(incoming) {
295
+ const parsed = bulkOpenSchema.safeParse(incoming.command.params);
296
+ if (!parsed.success) {
297
+ await incoming.respond({
298
+ result: "error",
299
+ code: "malformed"
300
+ });
301
+ return;
302
+ }
303
+ const { "transfer-id": transferId } = parsed.data;
304
+ const idHex = transferIdHex(transferId);
305
+ if (await storage.get(openerKey(idHex)) !== void 0) {
306
+ await incoming.respond({
307
+ result: "error",
308
+ code: "already_exists"
309
+ });
310
+ return;
311
+ }
312
+ onOffer({
313
+ transferId,
314
+ ...parsed.data["total-size"] !== void 0 ? { totalSize: parsed.data["total-size"] } : {},
315
+ ...parsed.data["content-type"] !== void 0 ? { contentType: parsed.data["content-type"] } : {},
316
+ openerDevice,
317
+ async decide(decision) {
318
+ if (decision.kind === "reject") {
319
+ await incoming.respond({
320
+ result: "error",
321
+ code: "denied",
322
+ ...decision.reason !== void 0 ? { message: decision.reason } : {}
323
+ });
324
+ return;
325
+ }
326
+ await storage.set(openerKey(idHex), openerDevice);
327
+ await storage.set(ackSeqKey(idHex), encodeUint(0));
328
+ await storage.set(windowKey(idHex), encodeUint(decision.window));
329
+ registrations.set(idHex, {
330
+ openerDevice,
331
+ connection,
332
+ ...decision.onComplete !== void 0 ? { onComplete: decision.onComplete } : {}
333
+ });
334
+ await incoming.respond({ result: "ok" });
335
+ const ackFrame = {
336
+ type: "bulk-ack",
337
+ "transfer-id": transferId,
338
+ "ack-seq": 0,
339
+ window: decision.window
340
+ };
341
+ await connection.send(ackFrame);
342
+ }
343
+ });
344
+ };
345
+ },
346
+ async requestResume(connection, session, transferId, scope, targetDevice, token, timeoutMs) {
347
+ const idHex = transferIdHex(transferId);
348
+ const existing = registrations.get(idHex);
349
+ const storedOpener = await storage.get(openerKey(idHex));
350
+ const openerDevice = existing?.openerDevice ?? (storedOpener === void 0 ? void 0 : new Uint8Array(storedOpener));
351
+ if (openerDevice === void 0) return {
352
+ status: "rejected",
353
+ code: "unknown_transfer"
354
+ };
355
+ const ackedCount = await readAckSeq(idHex);
356
+ const outcome = await session.sendManageRequest(buildBulkResumeCommand(transferId, ackedCount), scope, targetDevice, token, timeoutMs);
357
+ if (outcome.result !== "ok") return {
358
+ status: "rejected",
359
+ code: outcome.code
360
+ };
361
+ registrations.set(idHex, {
362
+ openerDevice,
363
+ connection,
364
+ ...existing?.onComplete !== void 0 ? { onComplete: existing.onComplete } : {}
365
+ });
366
+ const windowBytes = await storage.get(windowKey(idHex));
367
+ const ackFrame = {
368
+ type: "bulk-ack",
369
+ "transfer-id": transferId,
370
+ "ack-seq": ackedCount,
371
+ window: windowBytes === void 0 ? 0 : decodeUint(windowBytes)
372
+ };
373
+ await connection.send(ackFrame);
374
+ return { status: "ok" };
375
+ },
376
+ createCancelHandler() {
377
+ return async function handleBulkCancel(incoming) {
378
+ const parsed = bulkCancelSchema.safeParse(incoming.command.params);
379
+ if (!parsed.success) {
380
+ await incoming.respond({
381
+ result: "error",
382
+ code: "malformed"
383
+ });
384
+ return;
385
+ }
386
+ registrations.delete(transferIdHex(parsed.data["transfer-id"]));
387
+ await incoming.respond({ result: "ok" });
388
+ };
389
+ }
390
+ };
391
+ }
392
+ //#endregion
393
+ export { BULK_READ_CAPABILITY, BULK_WRITE_CAPABILITY, buildBulkCancelCommand, buildBulkOpenCommand, buildBulkResumeCommand, createBulkReceiver, createBulkSender };
@@ -1,8 +1,8 @@
1
- import { F as ManageCommand, o as CapabilityScope, s as CapabilityToken, x as DeviceId } from "../protocol-l9RMR3s5.cjs";
2
- import { t as IdentityPort } from "../identity-bg6IVGsH.cjs";
1
+ import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-sW8d6Og-.cjs";
2
+ import { t as IdentityPort } from "../identity-DJ06jn4k.cjs";
3
3
  import { t as Clock } from "../clock-DiSx-WKM.cjs";
4
4
  import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.cjs";
5
- import { a as RevocationCheck } from "../tokens-H_mV_JCC.cjs";
5
+ import { a as RevocationCheck } from "../tokens-DfVfE00W.cjs";
6
6
  //#region src/domain/capability-grant.d.ts
7
7
  /**
8
8
  * Builds a capability-grant command per management.cddl. The outer `manage-command.verb` is the capability string itself, the identical convention capability-request.ts's own buildCapabilityRequestCommand already establishes -- a receiver's per-capability handler is how it knows which grant this push is even for. `params.verb` is the fixed "capability.grant" marker. No scope or invitee field: manage-request-frame's own top-level scope already carries the former, and the request's own destination already carries the latter -- naming either a second time inside params could only ever disagree with the fact it duplicates.
@@ -1,8 +1,8 @@
1
- import { F as ManageCommand, o as CapabilityScope, s as CapabilityToken, x as DeviceId } from "../protocol-l9RMR3s5.mjs";
2
- import { t as IdentityPort } from "../identity-5LJ_B8go.mjs";
1
+ import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-sW8d6Og-.mjs";
2
+ import { t as IdentityPort } from "../identity-C5N86BPz.mjs";
3
3
  import { t as Clock } from "../clock-DiSx-WKM.mjs";
4
4
  import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.mjs";
5
- import { a as RevocationCheck } from "../tokens-CBI54oF-.mjs";
5
+ import { a as RevocationCheck } from "../tokens-CpLDI5Fo.mjs";
6
6
  //#region src/domain/capability-grant.d.ts
7
7
  /**
8
8
  * Builds a capability-grant command per management.cddl. The outer `manage-command.verb` is the capability string itself, the identical convention capability-request.ts's own buildCapabilityRequestCommand already establishes -- a receiver's per-capability handler is how it knows which grant this push is even for. `params.verb` is the fixed "capability.grant" marker. No scope or invitee field: manage-request-frame's own top-level scope already carries the former, and the request's own destination already carries the latter -- naming either a second time inside params could only ever disagree with the fact it duplicates.
@@ -1,5 +1,5 @@
1
- import { F as ManageCommand, i as CapabilityGrantOk, o as CapabilityScope, x as DeviceId } from "../protocol-l9RMR3s5.cjs";
2
- import { t as IdentityPort } from "../identity-bg6IVGsH.cjs";
1
+ import { D as DeviceId, V as ManageCommand, f as CapabilityScope, u as CapabilityGrantOk } from "../protocol-sW8d6Og-.cjs";
2
+ import { t as IdentityPort } from "../identity-DJ06jn4k.cjs";
3
3
  import { t as Clock } from "../clock-DiSx-WKM.cjs";
4
4
  import { IncomingManageRequest, MeshSession } from "./mesh-session.cjs";
5
5
  //#region src/domain/capability-request.d.ts
@@ -1,5 +1,5 @@
1
- import { F as ManageCommand, i as CapabilityGrantOk, o as CapabilityScope, x as DeviceId } from "../protocol-l9RMR3s5.mjs";
2
- import { t as IdentityPort } from "../identity-5LJ_B8go.mjs";
1
+ import { D as DeviceId, V as ManageCommand, f as CapabilityScope, u as CapabilityGrantOk } from "../protocol-sW8d6Og-.mjs";
2
+ import { t as IdentityPort } from "../identity-C5N86BPz.mjs";
3
3
  import { t as Clock } from "../clock-DiSx-WKM.mjs";
4
4
  import { IncomingManageRequest, MeshSession } from "./mesh-session.mjs";
5
5
  //#region src/domain/capability-request.d.ts
@@ -1,4 +1,4 @@
1
- import { x as DeviceId } from "../protocol-l9RMR3s5.cjs";
1
+ import { D as DeviceId } from "../protocol-sW8d6Og-.cjs";
2
2
  //#region src/domain/device-id.d.ts
3
3
  /** Lowercase, byte-exact hex for an arbitrary-length byte string -- the same encoding convention deviceIdToHex uses for the fixed-length device-id case, extracted so any other byte string needing a stable, displayable, map-keyable text form (e.g. a token-id, which tokens.cddl defines as an arbitrary-length bstr rather than a 32-byte device-id) can use the identical convention without going through a device-id-shaped function. */
4
4
  export declare function bytesToHex(bytes: Uint8Array): string;
@@ -1,4 +1,4 @@
1
- import { x as DeviceId } from "../protocol-l9RMR3s5.mjs";
1
+ import { D as DeviceId } from "../protocol-sW8d6Og-.mjs";
2
2
  //#region src/domain/device-id.d.ts
3
3
  /** Lowercase, byte-exact hex for an arbitrary-length byte string -- the same encoding convention deviceIdToHex uses for the fixed-length device-id case, extracted so any other byte string needing a stable, displayable, map-keyable text form (e.g. a token-id, which tokens.cddl defines as an arbitrary-length bstr rather than a 32-byte device-id) can use the identical convention without going through a device-id-shaped function. */
4
4
  export declare function bytesToHex(bytes: Uint8Array): string;
@@ -1,4 +1,4 @@
1
- import { o as CapabilityScope, x as DeviceId } from "../protocol-l9RMR3s5.cjs";
1
+ import { D as DeviceId, f as CapabilityScope } from "../protocol-sW8d6Og-.cjs";
2
2
  import { DirectoryEntry, MeshSession } from "./mesh-session.cjs";
3
3
  import { z } from "zod";
4
4
  //#region src/domain/grant-candidates.d.ts
@@ -1,4 +1,4 @@
1
- import { o as CapabilityScope, x as DeviceId } from "../protocol-l9RMR3s5.mjs";
1
+ import { D as DeviceId, f as CapabilityScope } from "../protocol-sW8d6Og-.mjs";
2
2
  import { DirectoryEntry, MeshSession } from "./mesh-session.mjs";
3
3
  import { z } from "zod";
4
4
  //#region src/domain/grant-candidates.d.ts
@@ -1,4 +1,4 @@
1
- import { M as HandshakeFrame, et as ProtocolVersion, w as DomainId } from "../protocol-l9RMR3s5.cjs";
1
+ import { A as DomainId, R as HandshakeFrame, ot as ProtocolVersion } from "../protocol-sW8d6Og-.cjs";
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;
@@ -1,4 +1,4 @@
1
- import { M as HandshakeFrame, et as ProtocolVersion, w as DomainId } from "../protocol-l9RMR3s5.mjs";
1
+ import { A as DomainId, R as HandshakeFrame, ot as ProtocolVersion } from "../protocol-sW8d6Og-.mjs";
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;
@@ -1,7 +1,7 @@
1
- import { D as Frame, F as ManageCommand, K as PeerAdvert, L as ManageError, R as ManageOk, dt as RevocationEntry, et as ProtocolVersion, o as CapabilityScope, s as CapabilityToken, x as DeviceId } from "../protocol-l9RMR3s5.cjs";
2
- import { t as IdentityPort } from "../identity-bg6IVGsH.cjs";
1
+ import { D as DeviceId, N as Frame, Q as PeerAdvert, U as ManageError, V as ManageCommand, W as ManageOk, _t as RevocationEntry, f as CapabilityScope, ot as ProtocolVersion, p as CapabilityToken } from "../protocol-sW8d6Og-.cjs";
2
+ import { t as IdentityPort } from "../identity-DJ06jn4k.cjs";
3
3
  import { t as Clock } from "../clock-DiSx-WKM.cjs";
4
- import { r as Transport, t as Connection } from "../transport-Baipikk7.cjs";
4
+ import { r as Transport, t as Connection } from "../transport-DxZ4JWZc.cjs";
5
5
  //#region src/domain/mesh-session.d.ts
6
6
  /** How long to wait for the node's handshake before calling it unanswered. A relay-only node never sends one; that is a state to display, not an error. */
7
7
  export declare const HANDSHAKE_TIMEOUT_MS = 3000;
@@ -1,7 +1,7 @@
1
- import { D as Frame, F as ManageCommand, K as PeerAdvert, L as ManageError, R as ManageOk, dt as RevocationEntry, et as ProtocolVersion, o as CapabilityScope, s as CapabilityToken, x as DeviceId } from "../protocol-l9RMR3s5.mjs";
2
- import { t as IdentityPort } from "../identity-5LJ_B8go.mjs";
1
+ import { D as DeviceId, N as Frame, Q as PeerAdvert, U as ManageError, V as ManageCommand, W as ManageOk, _t as RevocationEntry, f as CapabilityScope, ot as ProtocolVersion, p as CapabilityToken } from "../protocol-sW8d6Og-.mjs";
2
+ import { t as IdentityPort } from "../identity-C5N86BPz.mjs";
3
3
  import { t as Clock } from "../clock-DiSx-WKM.mjs";
4
- import { r as Transport, t as Connection } from "../transport-DWbvUc3L.mjs";
4
+ import { r as Transport, t as Connection } from "../transport-B5E896PN.mjs";
5
5
  //#region src/domain/mesh-session.d.ts
6
6
  /** How long to wait for the node's handshake before calling it unanswered. A relay-only node never sends one; that is a state to display, not an error. */
7
7
  export declare const HANDSHAKE_TIMEOUT_MS = 3000;
@@ -1,5 +1,5 @@
1
- import { D as Frame } from "../protocol-l9RMR3s5.cjs";
2
- import { t as Connection } from "../transport-Baipikk7.cjs";
1
+ import { N as Frame } from "../protocol-sW8d6Og-.cjs";
2
+ import { t as Connection } from "../transport-DxZ4JWZc.cjs";
3
3
  //#region src/domain/relay-hub.d.ts
4
4
  export interface RelayHub {
5
5
  /** Drives one accepted connection until it closes: registers gossip-advertised devices, re-broadcasts each gossip frame to every other connected client and replies to the gossiping connection with a catch-up frame of every other already-known device, answers relay-connect by pairing and notifying the target, and forwards relay-data within established pairings. Resolves when the connection's frame stream ends. Internally just registerConnection followed by a loop of onFrame calls and a final onDisconnect -- kept as its own method since a dedicated hub deployment (wire-mesh-node/cloudflare-hub) has nothing else driving the connection and wants the whole lifecycle in one call. */
@@ -1,5 +1,5 @@
1
- import { D as Frame } from "../protocol-l9RMR3s5.mjs";
2
- import { t as Connection } from "../transport-DWbvUc3L.mjs";
1
+ import { N as Frame } from "../protocol-sW8d6Og-.mjs";
2
+ import { t as Connection } from "../transport-B5E896PN.mjs";
3
3
  //#region src/domain/relay-hub.d.ts
4
4
  export interface RelayHub {
5
5
  /** Drives one accepted connection until it closes: registers gossip-advertised devices, re-broadcasts each gossip frame to every other connected client and replies to the gossiping connection with a catch-up frame of every other already-known device, answers relay-connect by pairing and notifying the target, and forwards relay-data within established pairings. Resolves when the connection's frame stream ends. Internally just registerConnection followed by a loop of onFrame calls and a final onDisconnect -- kept as its own method since a dedicated hub deployment (wire-mesh-node/cloudflare-hub) has nothing else driving the connection and wants the whole lifecycle in one call. */
@@ -1,5 +1,5 @@
1
- import { D as Frame } from "../protocol-l9RMR3s5.cjs";
2
- import { t as Connection } from "../transport-Baipikk7.cjs";
1
+ import { N as Frame } from "../protocol-sW8d6Og-.cjs";
2
+ import { t as Connection } from "../transport-DxZ4JWZc.cjs";
3
3
  //#region src/domain/relay-use-gate.d.ts
4
4
  export interface RelayUseAuthorizationTracker {
5
5
  /** True once authorize() has been called for this exact connection and revoke() has not been called since. */