@reticulum/dacar 1.1.1 → 1.2.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.
package/src/crdt.js CHANGED
@@ -305,7 +305,7 @@ export class StateVector {
305
305
  }
306
306
 
307
307
  /**
308
- * @param {unknown} value
308
+ * @param {Uint8Array} value
309
309
  * @param {number} len
310
310
  * @param {string} name
311
311
  * @returns {Uint8Array}
@@ -317,7 +317,7 @@ function expectBytes(value, len, name) {
317
317
  return value;
318
318
  }
319
319
 
320
- /** @param {unknown} value @param {string} name @returns {boolean} */
320
+ /** @param {boolean} value @param {string} name @returns {boolean} */
321
321
  function expectBool(value, name) {
322
322
  if (typeof value !== "boolean") throw new Error(`${name} must be a boolean`);
323
323
  return value;
package/src/operation.js CHANGED
@@ -232,7 +232,7 @@ export class Operation {
232
232
  }
233
233
 
234
234
  /**
235
- * @param {unknown} value
235
+ * @param {Uint8Array} value
236
236
  * @param {number} len
237
237
  * @param {string} name
238
238
  * @returns {Uint8Array}
@@ -245,7 +245,7 @@ function expectBytes(value, len, name) {
245
245
  }
246
246
 
247
247
  /**
248
- * @param {unknown} value
248
+ * @param {boolean} value
249
249
  * @param {string} name
250
250
  * @returns {boolean}
251
251
  */
@@ -18,7 +18,7 @@
18
18
  * never pulls it in. It depends only on `@reticulum/core`, which the core
19
19
  * already depends on, so no new dependency is added.
20
20
  *
21
- * Typical use (receiver, wired to a {@link import("@reticulum/core").LXMRouter
21
+ * Typical use (receiver, wired to a {@link import("@reticulum/core/src/lxmf/index.js").LXMRouter
22
22
  * LXMRouter}):
23
23
  *
24
24
  * ```js
@@ -38,7 +38,7 @@
38
38
  * await delivery.handleMessage(event.detail.message);
39
39
  */
40
40
 
41
- import { LXMessage as LXMFMessage } from "@reticulum/core";
41
+ import { LXMessage as LXMFMessage } from "@reticulum/core/src/lxmf/index.js";
42
42
  import { LXMF_DELIVERY_TITLE } from "../naming.js";
43
43
 
44
44
  /**
@@ -99,13 +99,13 @@ export class LxmfDeltaDelivery {
99
99
  * @param {import("../delta.js").DeltaReceiver | null} [opts.receiver]
100
100
  * The shared DeltaReceiver (state + key resolver). May be omitted on a
101
101
  * send-only node (then `handleMessage` throws if called).
102
- * @param {import("@reticulum/core").LXMRouter | null} [opts.router]
102
+ * @param {import("@reticulum/core/src/lxmf/index.js").LXMRouter | null} [opts.router]
103
103
  * Optional bound `LXMRouter` for `deliver` / `ingestPaperUri`.
104
104
  */
105
105
  constructor({ receiver = null, router = null } = {}) {
106
106
  /** @type {import("../delta.js").DeltaReceiver | null} */
107
107
  this._receiver = receiver;
108
- /** @type {import("@reticulum/core").LXMRouter | null} */
108
+ /** @type {import("@reticulum/core/src/lxmf/index.js").LXMRouter | null} */
109
109
  this._router = router;
110
110
  }
111
111
 
@@ -121,7 +121,7 @@ export class LxmfDeltaDelivery {
121
121
  * @param {Uint8Array} deltaPayload
122
122
  * @param {Uint8Array} destinationHash The recipient `lxmf.delivery` hash.
123
123
  * @param {Uint8Array} sourceHash The sender's `lxmf.delivery` hash.
124
- * @returns {import("@reticulum/core").LXMessage}
124
+ * @returns {import("@reticulum/core/src/lxmf/index.js").LXMessage}
125
125
  */
126
126
  makeMessage(deltaPayload, destinationHash, sourceHash) {
127
127
  if (!(deltaPayload instanceof Uint8Array)) {
@@ -145,7 +145,7 @@ export class LxmfDeltaDelivery {
145
145
  * @param {Uint8Array} destinationHash The recipient `lxmf.delivery` hash.
146
146
  * @param {Object} [opts]
147
147
  * @param {Uint8Array | null} [opts.linkId] Reuse an existing DIRECT link id.
148
- * @returns {Promise<import("@reticulum/core").LXMessage>}
148
+ * @returns {Promise<import("@reticulum/core/src/lxmf/index.js").LXMessage>}
149
149
  */
150
150
  async deliver(deltaPayload, destinationHash, { linkId = null } = {}) {
151
151
  if (!this._router) {
@@ -210,7 +210,7 @@ export class LxmfDeltaDelivery {
210
210
  * ingest result (the reconstructed message, or `null` if it was not for this
211
211
  * node / already ingested).
212
212
  * @param {string} uri
213
- * @returns {Promise<import("@reticulum/core").LXMessage | null>}
213
+ * @returns {Promise<import("@reticulum/core/src/lxmf/index.js").LXMessage | null>}
214
214
  */
215
215
  async ingestPaperUri(uri) {
216
216
  if (!this._router) {
@@ -10,60 +10,80 @@
10
10
  * delivery — **never** through the unauthenticated `StateVector.merge()` path.
11
11
  * A forged or stale Delta is simply dropped before it can mutate state.
12
12
  *
13
- * `RfedDeltaSync` wraps a `@reticulum/core` `RFedClient`. A Delta is wrapped as
14
- * the LXMF *content* of a channel message under the fixed `dacar/sync/delta`
15
- * title; on receipt the channel is the feed discriminator (every message on it
16
- * is a Dacar Delta), and the content bytes are fed to `DeltaReceiver`.
13
+ * `RfedDeltaSync` wraps a `@reticulum/core` `RFedClient`. A Delta travels in
14
+ * Dacar's **compact inner format** (§11.1.1): the raw §5.3 payload is placed
15
+ * straight after the RTID source-identity prelude and EC-encrypted to the
16
+ * derived channel identity no LXMF envelope, which would only duplicate the
17
+ * Delta's own destination/source/signature/timestamp and push a typical
18
+ * 170-byte Delta past the 500-byte RNS MTU. On receipt the channel is the feed
19
+ * discriminator (every message on it is a Dacar Delta), and the recovered Delta
20
+ * bytes are fed to `DeltaReceiver`.
17
21
  *
18
- * §11.3 air-gapped/optical transport is served by `./lxmfSync.js` (Paper
19
- * Messages); RFed is the online many-to-many path.
22
+ * The compact format is built on `@reticulum/core`'s raw RFed primitives —
23
+ * `subscribeRaw` / `publishRaw` / `unwrapRawChannelMessage` which carry an
24
+ * arbitrary self-authenticating payload in place of the LXMF tail. The Delta's
25
+ * own Ed25519 signature (§5.3 field [7]) is the authenticity check at
26
+ * verify-on-ingest; the prelude's `sender_identity_pub` only identifies the
27
+ * transport sender.
28
+ *
29
+ * §11.2 targeted delivery and §11.3 air-gapped/optical transport still use full
30
+ * LXMF framing (`./lxmfSync.js`, Paper Messages); only the RFed broadcast
31
+ * channel uses the compact format.
20
32
  *
21
33
  * This module is part of the optional transport layer: importing the pure core
22
34
  * never pulls it in. It depends only on `@reticulum/core`, which the core
23
- * already depends on, so no new dependency is added.
35
+ * already depends on, so no new dependency is added. It mirrors
36
+ * `python/dacar/transport/rfed_sync.py` (the canonical implementation).
24
37
  *
25
38
  * Typical use:
26
39
  *
27
40
  * ```js
28
41
  * const client = new RFedClient({ identity, rns });
29
42
  * const sync = new RfedDeltaSync({ receiver: new DeltaReceiver(state, resolver), client });
30
- * await sync.subscribe(nodeHash); // cache the channel's stamp cost
31
- * await sync.listen(); // receive live fanout Deltas
43
+ * await sync.subscribe(nodeHash); // subscribeRaw: cache stamp cost + mark raw
44
+ * await sync.listen(); // receive live fanout Deltas (kind: "raw")
32
45
  * await sync.publish(deltaPayload, nodeHash);
33
46
  * ```
34
47
  */
35
48
 
36
49
  import {
37
- LXMessage,
38
- deliveryHashFor,
39
50
  deriveChannel,
40
- unwrapChannelMessage,
41
- } from "@reticulum/core";
42
- import { LXMF_DELIVERY_TITLE, RFED_TOPIC } from "../naming.js";
43
- import { messageContent } from "./lxmfSync.js";
44
-
45
- /**
46
- * The shape of the decoded fanout callback argument from `RFedClient.listen`.
47
- * Only `message` is consumed here.
48
- * @typedef {Object} RfedDecoded
49
- * @property {import("@reticulum/core").LXMessage} message
50
- * @property {unknown} [senderIdentity]
51
- * @property {Uint8Array} [senderPub]
52
- * @property {Uint8Array} [sourceHash]
53
- * @property {boolean} [signatureValid]
54
- * @property {Uint8Array} [channelHash]
55
- * @property {string | null} [channelName]
56
- */
51
+ unwrapRawChannelMessage,
52
+ } from "@reticulum/core/src/rfed/index.js";
53
+ import { RFED_TOPIC } from "../naming.js";
57
54
 
58
55
  /**
59
56
  * The minimal `RFedClient` surface this adapter relies on. The real client from
60
- * `@reticulum/core` satisfies it; tests inject a fake.
57
+ * `@reticulum/core` satisfies it; tests inject a fake. The raw-publish API
58
+ * (`subscribeRaw` / `publishRaw`) carries a self-authenticating payload in the
59
+ * RTID prelude instead of an LXMF envelope (§11.1.1).
60
+ *
61
61
  * @typedef {Object} RFedClientLike
62
- * @property {(nodeHash: Uint8Array, channelName: string) => Promise<unknown>} subscribe
63
- * @property {(nodeHash: Uint8Array, channelName: string) => Promise<unknown>} [unsubscribe]
64
- * @property {(nodeHash: Uint8Array, channelName: string, lxmMessage: import("@reticulum/core").LXMessage) => Promise<unknown>} publish
62
+ * @property {(nodeHash: Uint8Array, channelName: string) => Promise<{ ok: boolean, stampCost: number | null }>} subscribeRaw
63
+ * Subscribes and marks the channel raw so fanout is decoded via
64
+ * `unwrapRawChannelMessage` (not the LXMF path).
65
+ * @property {(nodeHash: Uint8Array, channelName: string) => Promise<{ ok: boolean }>} [unsubscribe]
66
+ * @property {(nodeHash: Uint8Array, channelName: string, payload: Uint8Array) => Promise<void>} publishRaw
67
+ * Fire-and-forget SEND of a raw application payload (wrapped in the RTID
68
+ * prelude + EC-encrypted to the channel identity by the client).
65
69
  * @property {(nodeHash: Uint8Array, channelName: string) => Promise<{ items: Array<{ channelHash: Uint8Array, blob: Uint8Array }>, morePending: boolean }>} pull
66
- * @property {(onMessage: (decoded: RfedDecoded) => void) => Promise<Uint8Array>} listen
70
+ * @property {(onMessage: (decoded: RfedDecodedRaw) => void) => Promise<Uint8Array>} listen
71
+ * Delivers a decoded fanout object; channels subscribed via `subscribeRaw`
72
+ * carry `kind: "raw"` with a `payload` field (the unwrapped application
73
+ * bytes). The client performs the EC-decrypt + RTID-prelude unwrap.
74
+ */
75
+
76
+ /**
77
+ * The shape of the decoded fanout callback argument from `RFedClient.listen` for
78
+ * a raw channel. Only `kind` and `payload` are consumed here.
79
+ *
80
+ * @typedef {Object} RfedDecodedRaw
81
+ * @property {"raw"|"lxmf"} kind
82
+ * @property {Uint8Array} [payload] Raw application payload (`kind === "raw"`).
83
+ * @property {import("@reticulum/core/src/core/identity.js").Identity} [senderIdentity]
84
+ * @property {Uint8Array} [senderPub]
85
+ * @property {Uint8Array} [channelHash]
86
+ * @property {string} [channelName]
67
87
  */
68
88
 
69
89
  /**
@@ -97,42 +117,25 @@ export class RfedDeltaSync {
97
117
  }
98
118
 
99
119
  /**
100
- * Builds the LXMF channel message wrapping one §5.3 Delta payload.
120
+ * Subscribes to the channel on a node (raw mode) and caches its advertised
121
+ * stamp cost. Call at least once per session and after any publish seems
122
+ * dropped.
101
123
  *
102
- * The message's `sourceHash`/`destinationHash` are placeholders: the rfed
103
- * Phase-0 codec (`wrapChannelMessage`) overwrites them with the channel's
104
- * `lxmf.delivery` hashes before serialization, so the classic "source_hash
105
- * is the bare identity hash" bug cannot occur.
106
- * @param {Uint8Array} deltaPayload
107
- * @returns {import("@reticulum/core").LXMessage}
108
- */
109
- makeMessage(deltaPayload) {
110
- if (!(deltaPayload instanceof Uint8Array)) {
111
- throw new TypeError("deltaPayload must be a Uint8Array");
112
- }
113
- return new LXMessage({
114
- // Overwritten by the rfed codec before going on the wire.
115
- destinationHash: new Uint8Array(16),
116
- sourceHash: new Uint8Array(16),
117
- content: new Uint8Array(deltaPayload),
118
- title: LXMF_DELIVERY_TITLE,
119
- });
120
- }
121
-
122
- /**
123
- * Subscribes to the channel on a node, caching its advertised stamp cost.
124
- * Call at least once per session and after any publish seems dropped.
124
+ * Uses `subscribeRaw` so incoming fanout is decoded via
125
+ * `unwrapRawChannelMessage` (the Dacar compact inner format), not the LXMF
126
+ * path. The wire protocol is identical to `subscribe` — the node never
127
+ * inspects the `inner_blob` only this client's local decode changes.
125
128
  * @param {Uint8Array} nodeHash Any `rfed.*` destination hash of the node.
126
- * @returns {Promise<unknown>} The client's `{ ok, stampCost }` result.
129
+ * @returns {Promise<{ ok: boolean, stampCost: number | null }>}
127
130
  */
128
131
  async subscribe(nodeHash) {
129
- return this._client.subscribe(nodeHash, this._topic);
132
+ return this._client.subscribeRaw(nodeHash, this._topic);
130
133
  }
131
134
 
132
135
  /**
133
136
  * Removes the subscription.
134
137
  * @param {Uint8Array} nodeHash
135
- * @returns {Promise<unknown>}
138
+ * @returns {Promise<{ ok: boolean }>}
136
139
  */
137
140
  async unsubscribe(nodeHash) {
138
141
  if (!this._client.unsubscribe) {
@@ -144,26 +147,34 @@ export class RfedDeltaSync {
144
147
  /**
145
148
  * Publishes a Delta to the channel (fire-and-forget, §11.1).
146
149
  *
147
- * Call {@link subscribe} first so the channel's stamp cost is cached; an
148
- * unstamped publish may be silently dropped by a cost-enforcing node.
150
+ * The client wraps the raw Delta in the RTID prelude + EC-encrypts it to the
151
+ * channel identity (`publishRaw` `wrapRawChannelMessage`) and sends it as a
152
+ * fire-and-forget DATA packet. Call {@link subscribe} first so the channel's
153
+ * stamp cost is cached; an unstamped publish may be silently dropped by a
154
+ * cost-enforcing node. Returns `true` if the transport accepted the outbound
155
+ * packet (no throw) — transport acceptance ≠ node storage (fire-and-forget).
149
156
  * @param {Uint8Array} deltaPayload
150
157
  * @param {Uint8Array} nodeHash
151
- * @returns {Promise<import("@reticulum/core").LXMessage>} The published message.
158
+ * @returns {Promise<boolean>}
152
159
  */
153
160
  async publish(deltaPayload, nodeHash) {
154
- const message = this.makeMessage(deltaPayload);
155
- await this._client.publish(nodeHash, this._topic, message);
156
- return message;
161
+ if (!(deltaPayload instanceof Uint8Array)) {
162
+ throw new TypeError("deltaPayload must be a Uint8Array");
163
+ }
164
+ await this._client.publishRaw(nodeHash, this._topic, new Uint8Array(deltaPayload));
165
+ return true; // fire-and-forget: no throw ⇒ transport accepted the packet
157
166
  }
158
167
 
159
168
  /**
160
169
  * Starts listening for live fanout Deltas and routes each through
161
- * verify-on-ingest (§11.1, §11.2.4).
170
+ * verify-on-ingest (§11.1, §11.2).
162
171
  *
163
- * The channel is the feed discriminator, so every received message is a
164
- * Dacar Delta; `DeltaReceiver.applyPayload()` authenticates it by signature
165
- * and swallows any malformed/forged payload so a bad message can never crash
166
- * the transport or mutate state.
172
+ * Because the channel was subscribed via `subscribeRaw`, the client decodes
173
+ * each fanout delivery with `unwrapRawChannelMessage` and the callback
174
+ * receives `{ kind: "raw", payload }`. The `payload` is the carried §5.3
175
+ * Delta, fed to `DeltaReceiver.applyPayload()`, which authenticates it by
176
+ * signature and swallows any malformed/forged payload so a bad message can
177
+ * never crash the transport or mutate state.
167
178
  * @returns {Promise<Uint8Array>} The local `rfed.delivery` destination hash.
168
179
  */
169
180
  async listen() {
@@ -172,11 +183,10 @@ export class RfedDeltaSync {
172
183
  }
173
184
  const receiver = this._receiver;
174
185
  return this._client.listen((decoded) => {
186
+ if (decoded?.kind !== "raw") return; // not a raw channel payload
175
187
  // RFedClient.invoke does not await the callback; run applyPayload without
176
188
  // leaving an unhandled rejection (it swallows malformed payloads itself).
177
- Promise.resolve(receiver.applyPayload(messageContent(decoded.message))).catch(
178
- () => {},
179
- );
189
+ Promise.resolve(receiver.applyPayload(decoded.payload)).catch(() => {});
180
190
  });
181
191
  }
182
192
 
@@ -184,10 +194,12 @@ export class RfedDeltaSync {
184
194
  * Drains the node's deferred queue (offline catch-up) and routes each blob
185
195
  * through verify-on-ingest (§11.1).
186
196
  *
187
- * Each blob is EC-decrypted with the derived channel identity and the
188
- * recovered LXMF message's content is applied. Foreign/undecryptable blobs
189
- * are dropped, not fatal. Repeats until the node reports no more pending
190
- * pages. Returns the count of Deltas newly applied to the CRDT.
197
+ * Each blob is the EC-encrypted `inner_blob` (the node serves it verbatim, as
198
+ * stored); it is EC-decrypted with the derived channel identity and the
199
+ * recovered Dacar Delta (`unwrapRawChannelMessage`) is applied. Foreign/
200
+ * undecryptable blobs are dropped, not fatal. Repeats until the node reports
201
+ * no more pending pages. Returns the count of Deltas newly applied to the
202
+ * CRDT.
191
203
  *
192
204
  * > **Assumption:** the node serves each deferred entry's `blob` as the rfed
193
205
  * > `inner_blob` (the EC-encrypted channel message), matching the fanout
@@ -201,7 +213,6 @@ export class RfedDeltaSync {
201
213
  throw new Error("RfedDeltaSync.pull requires a receiver");
202
214
  }
203
215
  const { identity: channelIdentity } = await deriveChannel(this._topic);
204
- const channelDeliveryHash = await deliveryHashFor(channelIdentity);
205
216
  const receiver = this._receiver;
206
217
  let applied = 0;
207
218
  let morePending = true;
@@ -209,19 +220,18 @@ export class RfedDeltaSync {
209
220
  const page = await this._client.pull(nodeHash, this._topic);
210
221
  for (const item of page.items) {
211
222
  try {
212
- const decoded = await unwrapChannelMessage({
223
+ const decoded = await unwrapRawChannelMessage({
213
224
  innerBlob: item.blob,
214
225
  channelIdentity,
215
- channelDeliveryHash,
216
226
  });
217
- if (await receiver.applyPayload(messageContent(decoded.message))) {
227
+ if (await receiver.applyPayload(decoded.payload)) {
218
228
  applied++;
219
229
  }
220
230
  } catch {
221
231
  // a foreign/undecryptable blob is dropped, never fatal
222
232
  }
223
233
  }
224
- morePending = page.morePending;
234
+ morePending = !!page.morePending;
225
235
  }
226
236
  return applied;
227
237
  }
package/src/tuple.js CHANGED
@@ -105,7 +105,7 @@ export class Tuple {
105
105
  return toHex(this.preimage);
106
106
  }
107
107
 
108
- /** Structural equality with another Tuple. @param {unknown} other @returns {boolean} */
108
+ /** Structural equality with another Tuple. @param {Tuple} other @returns {boolean} */
109
109
  equals(other) {
110
110
  if (!(other instanceof Tuple)) return false;
111
111
  return (