@derec-alliance/nodejs 0.0.1-alpha.9 → 0.0.2

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/index.d.ts CHANGED
@@ -28,11 +28,76 @@ export interface SecretStore {
28
28
  remove(secretId: string, channelId: string, kind: 0 | 1 | 2): Promise<void>;
29
29
  }
30
30
 
31
+ /**
32
+ * Channel-record persistence.
33
+ *
34
+ * A record is addressed by `(channelId, replicaId)`. A `replicaId` of `"0"` —
35
+ * the value the protocol reserves as "absent" — addresses the helper channel
36
+ * at `channelId`.
37
+ *
38
+ * Any other value addresses that member of the replica group, and the member
39
+ * is keyed by **`replicaId` alone**. The accompanying `channelId` is context,
40
+ * not part of the key: a member moves between channels during an admission
41
+ * handover while remaining the same member, and a lookup that required both to
42
+ * match would miss it exactly when the move needs to be observed. Keep two
43
+ * maps — helpers by `channelId`, members by `replicaId` — not one keyed by the
44
+ * pair.
45
+ *
46
+ * `load`/`save` bytes are a JSON-encoded `ChannelRecord`: an externally
47
+ * tagged union carrying exactly one of `Helper` or `Replica`.
48
+ *
49
+ * `listHelpers` and `listReplicas` are **not** arrays of that union — they
50
+ * return a JSON array of the **inner** records with the tag stripped:
51
+ * `[{ channel_id, transport, ... }, ...]`, `HelperChannel` for the first and
52
+ * `ReplicaMember` for the second. Wrapping each element back in
53
+ * `{ "Helper": ... }` will not decode.
54
+ *
55
+ * Build that array by **splicing the stored bytes as text** — the payloads are
56
+ * opaque, so persist and re-emit them verbatim:
57
+ *
58
+ * ```js
59
+ * const inner = rows.map((r) => new TextDecoder().decode(r));
60
+ * return new TextEncoder().encode(`[${inner.join(",")}]`);
61
+ * ```
62
+ *
63
+ * Do not `JSON.parse` and re-serialise. Every id in these records is a `u64`,
64
+ * and `JSON.parse` silently rounds anything above 2^53 — the corruption only
65
+ * appears once a real id happens to be large.
66
+ */
31
67
  export interface ChannelStore {
32
- load(secretId: string, channelId: string): Promise<Uint8Array | null | undefined>;
33
- save(secretId: string, channelId: string, bytes: Uint8Array): Promise<void>;
34
- listChannels(secretId: string): Promise<string[]>;
35
- remove(secretId: string, channelId: string): Promise<boolean>;
68
+ load(
69
+ secretId: string,
70
+ channelId: string,
71
+ replicaId: string,
72
+ ): Promise<Uint8Array | null | undefined>;
73
+ save(
74
+ secretId: string,
75
+ channelId: string,
76
+ replicaId: string,
77
+ bytes: Uint8Array,
78
+ ): Promise<void>;
79
+ remove(secretId: string, channelId: string, replicaId: string): Promise<boolean>;
80
+ /** JSON array of the helper channels stored under `secretId`. */
81
+ listHelpers(secretId: string): Promise<Uint8Array | null | undefined>;
82
+ /**
83
+ * JSON array of the replica-group members stored under `secretId`,
84
+ * including this device's own row.
85
+ *
86
+ * The order is significant in exactly one situation. A group has one member
87
+ * holding the `Source` role; when it is removed, the protocol promotes the
88
+ * first element of this array that is neither the departing member nor
89
+ * itself leaving. Ordering this array is therefore how an application
90
+ * chooses its succession policy. The choice is read once, on the single
91
+ * device running the removal, and is then published in the roster, so
92
+ * implementations on different devices need not agree on order. Nothing else
93
+ * consults it.
94
+ *
95
+ * Returning an arbitrary order is correct and simply delegates the choice to
96
+ * the storage — note that a SQL `SELECT` without `ORDER BY` and `Map`
97
+ * insertion order after arbitrary edits are both effectively arbitrary.
98
+ * Order explicitly to make succession predictable.
99
+ */
100
+ listReplicas(secretId: string): Promise<Uint8Array | null | undefined>;
36
101
  linkChannel(
37
102
  secretId: string,
38
103
  channelId: string,
@@ -90,7 +155,7 @@ export interface UserSecretStore {
90
155
  * returns the exact blob it received, `remove` drops the row, and
91
156
  * `loadAll` returns every blob whose `kind` matches the requested
92
157
  * category (`0` = PendingVerification, `1` = PendingRecovery,
93
- * `2` = PendingUnpair, `3` = SharingRound).
158
+ * `2` = PendingUnpair, `3` = SharingRound, `4` = PendingSyncCheck).
94
159
  *
95
160
  * Rows are keyed by `(secretId, StateKey)` — the `keyJson` buffer is
96
161
  * a JSON object `{ kind, channel_id?, version? }` matching the `kind`
@@ -110,9 +175,27 @@ export interface StateStore {
110
175
  keyJson: Uint8Array,
111
176
  ): Promise<Uint8Array | null | undefined>;
112
177
  remove(secretId: string, keyJson: Uint8Array): Promise<boolean>;
113
- loadAll(secretId: string, kind: 0 | 1 | 2 | 3): Promise<Uint8Array[]>;
178
+ loadAll(secretId: string, kind: 0 | 1 | 2 | 3 | 4): Promise<Uint8Array[]>;
114
179
  }
115
180
 
181
+ /**
182
+ * Outbound message delivery.
183
+ *
184
+ * This is a mailbox, not a request/response channel: every peer has an
185
+ * address, and a reply is posted to that address rather than returned from
186
+ * `process`. Where both sides are reachable services, a one-way push is all
187
+ * that is needed.
188
+ *
189
+ * A peer that cannot be addressed — a phone, a browser, anything behind NAT —
190
+ * breaks that silently: the reply is handed to `send`, goes nowhere, and
191
+ * nothing reports an error. Such a service must answer on the connection the
192
+ * request arrived on, by building the protocol per request with a `Transport`
193
+ * that collects into a buffer instead of sending, then returning the collected
194
+ * message whose trace id matches the inbound envelope's
195
+ * (`envelope_read_trace_id`). One call can emit several messages, so the rest
196
+ * of the buffer is genuine fan-out and still has to be delivered. See "Serving
197
+ * DeRec over request/response transports" in the Rust SDK README.
198
+ */
116
199
  export interface Transport {
117
200
  send(endpoint: { protocol: string; uri: string }, message: Uint8Array): Promise<void>;
118
201
  }
@@ -141,6 +224,14 @@ export enum SenderKind {
141
224
  * already-KYC-authenticated institution). Applications MUST rate-limit
142
225
  * inbound `PrePairRequest`s per channel and expire outstanding NoKeys
143
226
  * contacts on a short timer.
227
+ *
228
+ * Because nothing binds the published keys to the contact, the channel is
229
+ * held `Pending` until `verifyFingerprint` succeeds on both sides: it is
230
+ * not a publish target, not a recovery source, and inbound messages on it
231
+ * are ignored. A man-in-the-middle on the plaintext `PrePair` leg leaves
232
+ * the two sides with different shared keys and so different fingerprints,
233
+ * which is what the comparison catches — the role `contact_binding_hash`
234
+ * plays for `HashedKeys`.
144
235
  */
145
236
  export enum ContactMode {
146
237
  InlineKeys = 0,
@@ -156,6 +247,15 @@ export enum FlowKind {
156
247
  RecoverSecret = 4,
157
248
  Unpair = 5,
158
249
  UpdateChannelInfo = 6,
250
+ /** Ask the replica group whether this device is behind, and catch up if it
251
+ * is. Replica-only, and takes no parameters — the group and this device's
252
+ * own version both come from the stores. */
253
+ SyncCheck = 7,
254
+ /** Remove a member from the replica group. Replica-only. Naming this device
255
+ * is a voluntary departure; naming another is an eviction. Params:
256
+ * `{ replica_id: string; memo?: string }` — `replica_id` is a decimal
257
+ * string so ids above 2^53 survive JS number handling. */
258
+ RemoveReplica = 8,
159
259
  }
160
260
 
161
261
  export type UnpairAck = "required" | "not_required";
@@ -223,6 +323,45 @@ export interface UpdateChannelInfoParams {
223
323
  transport_protocol?: { uri: string; protocol: number };
224
324
  }
225
325
 
326
+ /**
327
+ * How long the protocol waits on each thing that can keep it waiting. Every
328
+ * field is optional; omit one to keep the library's default for it.
329
+ */
330
+ export interface Timeouts {
331
+ /** Staleness boundary for inbound envelopes — the replay-defence window.
332
+ * Any message older than this is discarded on receipt, whatever the flow.
333
+ * Lowering it starts refusing legitimately old messages from slow
334
+ * transports or skewed clocks. Library default: 300. */
335
+ inbound_message_secs?: number;
336
+ /** How long a publishing round waits on a peer that has not answered.
337
+ * Bounds how long `SharingComplete` can be delayed by one unreachable
338
+ * peer. Library default: 60. */
339
+ sharing_round_secs?: number;
340
+ /** How long to wait for an unpair acknowledgement before dropping local
341
+ * channel state anyway. Library default: 60. */
342
+ unpair_ack_secs?: number;
343
+ /** Removal of channels still awaiting out-of-band fingerprint
344
+ * confirmation — every replica pairing, and every `NoKeys` pairing.
345
+ * Unlike the others this can be disabled, leaving the sweep to the
346
+ * application via `removeExpiredChannels`. The budget is a **human** one:
347
+ * someone comparing a fingerprint, possibly over the phone. Library
348
+ * default: `{ enabled: true, timeout_in_secs: 300 }`. */
349
+ expired_channels?: { enabled: boolean; timeout_in_secs: number };
350
+ }
351
+
352
+ /** `SyncCheck` takes no parameters: the group and this device's own version
353
+ * are both read from the stores. The argument may be omitted entirely. */
354
+ export type SyncCheckParams = Record<string, never>;
355
+
356
+ export interface RemoveReplicaParams {
357
+ /** The member to remove, as a **decimal** `u64` string — the same form
358
+ * `ReplicaPaired.peer_replica_id` hands back. A value naming no current
359
+ * member is rejected; it is not silently ignored. */
360
+ replica_id: string;
361
+
362
+ memo?: string;
363
+ }
364
+
226
365
  export type DeRecEvent =
227
366
  | {
228
367
  type: "PairingCompleted";
@@ -252,7 +391,62 @@ export type DeRecEvent =
252
391
  | { type: "ShareStored"; channel_id: string; version: number }
253
392
  | { type: "ShareConfirmed"; channel_id: string; version: number }
254
393
  | { type: "ShareRejected"; channel_id: string; version: number; status: number; memo: string }
394
+ /** A publishing round finished — every targeted helper confirmed,
395
+ * rejected, or timed out.
396
+ *
397
+ * **A mixed round waits for the replica leg.** The counts here describe
398
+ * helpers only and are known the instant the helpers answer, but the
399
+ * event is withheld until every replica member has also acknowledged,
400
+ * refused, or timed out. One unreachable member therefore delays it by
401
+ * up to the configured timeout, which is easy to mistake for a hang.
402
+ * Nothing is lost — the round always terminates and a silent member is
403
+ * reported in `ReplicaSyncComplete.behind` rather than failing it.
404
+ *
405
+ * Drive per-helper progress from `ShareConfirmed` instead: those land as
406
+ * each helper answers, with no cross-population wait. A helpers-only
407
+ * round is unaffected. */
255
408
  | { type: "SharingComplete"; version: number; confirmed_count: number; failed_count: number; threshold_met: boolean }
409
+ /** A group member refused a secret sync. Keyed by `replica_id`, not
410
+ * `channel_id`: every member answers on the one group channel. A
411
+ * `VERSION_CONFLICT` status means the round must be resolved and
412
+ * republished at a new version. */
413
+ | {
414
+ type: "ReplicaSyncRejected";
415
+ replica_id: string;
416
+ secret_id: string;
417
+ version: number;
418
+ status: number;
419
+ memo: string;
420
+ }
421
+ /** A secret sync could not be delivered to a member at all — distinct from
422
+ * `ReplicaSyncRejected`, which is the member answering "no". */
423
+ | { type: "ReplicaSyncFailed"; replica_id: string; version: number; reason: string }
424
+ /** A member left the group and its roster row was dropped. Fires on the
425
+ * members that remain. */
426
+ | { type: "ReplicaRemoved"; replica_id: string }
427
+ /** The group's source role moved to another member because the previous
428
+ * source is leaving. Fires on the device that chose the successor — which
429
+ * it does by the order its channel store returns members in — and on the
430
+ * successor itself when the roster promoting it arrives. */
431
+ | { type: "ReplicaSourceChanged"; replica_id: string }
432
+ /** This device left the group and dropped its whole `secret_id` partition —
433
+ * group channel, helper channels, shares, secrets and the snapshot. Fires
434
+ * only once it was told to leave *and* has since seen a roster excluding
435
+ * it; absence alone never destroys a copy of the secret. */
436
+ | { type: "SelfRemovedFromGroup"; version: number }
437
+ /** A replica catch-up finished. `fetched_from` is absent when this device
438
+ * was already current, in which case no hydration event follows. */
439
+ | {
440
+ type: "SyncCheckComplete";
441
+ local_version: number;
442
+ group_version: number;
443
+ fetched_from?: string;
444
+ }
445
+ /** The replica leg of a publishing round finished. Reported separately from
446
+ * `SharingComplete`: replicas are best-effort, so a member in `behind` does
447
+ * not fail the round. `behind` is the application's retry list — the
448
+ * library keeps no durable per-member sync state. */
449
+ | { type: "ReplicaSyncComplete"; version: number; synced: string[]; behind: string[] }
256
450
  | { type: "ShareVerified"; channel_id: string; version: number }
257
451
  | {
258
452
  type: "SecretsDiscovered";
@@ -265,9 +459,8 @@ export type DeRecEvent =
265
459
  /** Recovery completed — the typed `Secret` snapshot the owner
266
460
  * originally protected. Mirrors `ReplicaSecretReceived.secret`:
267
461
  * `secrets` is the user-facing `Vec<UserSecret>` the application
268
- * fed to `start(FlowKind.ProtectSecret)`; `helpers`, `replicas`
269
- * and `owner_replica_id` are the roster snapshot captured at
270
- * distribution time. The library handles the two-stage
462
+ * fed to `start(FlowKind.ProtectSecret)`; `helpers` and `replicas`
463
+ * are the roster snapshot captured at distribution time. The library handles the two-stage
271
464
  * `DeRecSecret` → `Secret` protobuf decode internally. */
272
465
  | {
273
466
  type: "SecretRecovered";
@@ -284,20 +477,23 @@ export type DeRecEvent =
284
477
  data: Uint8Array;
285
478
  }>;
286
479
  /** Replica composite. Absent when this `secret_id` has no
287
- * replica setup. Carries the destination roster, the
288
- * per-helper share map, and the 32-byte group key. Required
289
- * by `restore` to rebuild replica channels without re-pairing. */
480
+ * replica setup. Carries the full member roster, the one channel
481
+ * they share, and the 32-byte group key. Required by `restore` to
482
+ * rebuild replica state without re-pairing. */
290
483
  replicas?: {
291
- replicas: Array<{
292
- channel_id: string;
484
+ /** The one channel every member is addressed on. */
485
+ channel_id: string;
486
+ /** Every member of the group, including the writer. Exactly one
487
+ * carries `role: "Source"` — that member is where the secret
488
+ * originated, which is why no separate owner field is needed. */
489
+ members: Array<{
490
+ replica_id: string;
293
491
  transport_uri: string;
492
+ role: "Source" | "Destination";
294
493
  communication_info: Record<string, string>;
295
- replica_id: string;
296
- sender_kind: number;
297
494
  }>;
298
495
  shared_key: Uint8Array;
299
496
  };
300
- owner_replica_id: string;
301
497
  };
302
498
  }
303
499
 
@@ -311,8 +507,10 @@ export type DeRecEvent =
311
507
  | { type: "PrePairRejected"; channel_id: string; status: number; memo: string }
312
508
 
313
509
  /** Fires alongside `PairingCompleted` on replica-mode pair handshakes.
314
- * `peer_replica_id` is the peer's hex-encoded `u64` (matches the wire
315
- * `derec.replica_id` representation). The local side's role
510
+ * `peer_replica_id` is the peer's `u64` as a **decimal** string,
511
+ * matching the wire `derec.replica_id` representation and every other
512
+ * id across this boundary. Pass it back verbatim — `RemoveReplica`
513
+ * expects the same decimal form. The local side's role
316
514
  * (`ReplicaSource` vs `ReplicaDestination`) is on the persisted
317
515
  * channel record — replica pairings are unidirectional, so there is
318
516
  * no separate "role in pair" field. */
@@ -325,7 +523,8 @@ export type DeRecEvent =
325
523
  * `ReplicaDestination` channel. The library decoded the
326
524
  * `ReplicaSecretPayload`; the app installs `secret.secrets` and
327
525
  * optionally uses `shares` for recovery. `from_replica_id` and the
328
- * `replica_id` fields inside `secret` are hex-encoded `u64`. */
526
+ * `replica_id` fields inside `secret` are `u64` as **decimal**
527
+ * strings. */
329
528
  | {
330
529
  type: "ReplicaSecretReceived";
331
530
  channel_id: string;
@@ -347,16 +546,68 @@ export type DeRecEvent =
347
546
  /** Replica composite. Absent when this `secret_id` has no
348
547
  * replica setup. The same shape as `SecretRecovered.secret.replicas`. */
349
548
  replicas?: {
350
- replicas: Array<{
351
- channel_id: string;
549
+ /** The one channel every member is addressed on. */
550
+ channel_id: string;
551
+ /** Every member of the group, including the writer. Exactly one
552
+ * carries `role: "Source"` — that member is where the secret
553
+ * originated, which is why no separate owner field is needed. */
554
+ members: Array<{
555
+ replica_id: string;
352
556
  transport_uri: string;
557
+ role: "Source" | "Destination";
353
558
  communication_info: Record<string, string>;
559
+ }>;
560
+ shared_key: Uint8Array;
561
+ };
562
+ };
563
+ shares: Array<{
564
+ channel_id: string;
565
+ committed_share: Uint8Array;
566
+ }>;
567
+ }
568
+ /** The first sync for a `secret_id` this device had no snapshot for —
569
+ * the secret now exists here. Same payload as `ReplicaSecretReceived`,
570
+ * which reports a later version of a secret the device already held.
571
+ * Both are written to the stores by the library before the event is
572
+ * delivered; the distinct type is what tells an application the set of
573
+ * secrets on the device changed.
574
+ *
575
+ * This is not a recovery: recovery reconstructs a secret from helper
576
+ * shares and is driven by the application through `restore`. */
577
+ | {
578
+ type: "ReplicaSecretInstalled";
579
+ channel_id: string;
580
+ from_replica_id: string;
581
+ secret_id: string;
582
+ version: number;
583
+ secret: {
584
+ helpers: Array<{
585
+ channel_id: string;
586
+ transport_uri: string;
587
+ shared_key: Uint8Array;
588
+ communication_info: Record<string, string>;
589
+ }>;
590
+ secrets: Array<{
591
+ id: Uint8Array;
592
+ name: string;
593
+ data: Uint8Array;
594
+ }>;
595
+ /** Replica composite. Absent when this `secret_id` has no
596
+ * replica setup. The same shape as `SecretRecovered.secret.replicas`. */
597
+ replicas?: {
598
+ /** The one channel every member is addressed on. */
599
+ channel_id: string;
600
+ /** Every member of the group, including the writer. Exactly one
601
+ * carries `role: "Source"` — that member is where the secret
602
+ * originated, which is why no separate owner field is needed. */
603
+ members: Array<{
354
604
  replica_id: string;
355
- sender_kind: number;
605
+ transport_uri: string;
606
+ role: "Source" | "Destination";
607
+ communication_info: Record<string, string>;
356
608
  }>;
357
609
  shared_key: Uint8Array;
358
610
  };
359
- owner_replica_id: string;
360
611
  };
361
612
  shares: Array<{
362
613
  channel_id: string;
@@ -441,6 +692,7 @@ export type DeRecEvent =
441
692
  /** An unpair request was dispatched to `channel_id`. Followed by an
442
693
  * `Unpaired` event once the peer acknowledges (or in the same event
443
694
  * vec, under `UnpairAck.NotRequired`). */
695
+ | { type: "UnpairFailed"; channel_id: string; error: string }
444
696
  | { type: "UnpairStarted"; channel_id: string }
445
697
  /** An update-channel-info request was dispatched to `channel_id`. */
446
698
  | { type: "UpdateChannelInfoStarted"; channel_id: string }
@@ -463,6 +715,16 @@ export type DeRecEvent =
463
715
  * oracle. Anyone who knows a HashedKeys contact's nonce can elicit a
464
716
  * key-publish response. Keep off unless you control both ends of
465
717
  * the transport.
718
+ * - `storeShare` — the helper's only admission-control point for
719
+ * inbound shares. The protocol enforces no size, quota or rate limit
720
+ * of its own, and `maxShareSize` is checked for range overlap at
721
+ * pairing time only, never against an actual share. While this is
722
+ * `false`, `ActionRequired` carries the decoded request, so the
723
+ * application can inspect the share and call `reject()` with
724
+ * `StatusEnum.SizeLimitExceeded`. Setting it `true` removes that
725
+ * opportunity entirely: every share from every paired Owner is stored
726
+ * unconditionally, at whatever size it arrives. Keep off in any
727
+ * deployment with per-user storage limits.
466
728
  * - `unpair` — destructive. Accepting deletes the local channel
467
729
  * record before any UI confirmation.
468
730
  * - `updateChannelInfo` — silently overwrites the channel record with
@@ -510,8 +772,42 @@ export declare class DeRecProtocolBuilder {
510
772
  withThreshold(threshold: number): DeRecProtocolBuilder;
511
773
  /** Default: 3. */
512
774
  withKeepVersionsCount(count: number): DeRecProtocolBuilder;
513
- /** Seconds. Default: 300 (5 minutes). Clamped to at least 1. */
514
- withTimeout(timeoutInSecs: number): DeRecProtocolBuilder;
775
+ /**
776
+ * Configure how long the protocol waits on each thing that can keep it
777
+ * waiting. Every field is optional and **absent means "keep the library
778
+ * default"**; not calling this at all leaves all four at their defaults.
779
+ *
780
+ * These were one setting until it became clear they answer different
781
+ * questions. `inbound_message_secs` is a **security** boundary — how stale
782
+ * a message may be and still be accepted — so it has to tolerate transport
783
+ * latency and clock skew. The other three are **liveness** budgets: how
784
+ * long to keep hoping a peer will answer.
785
+ *
786
+ * Values are forwarded verbatim; clamping, and the meaning of a disabled
787
+ * `expired_channels`, are library decisions rather than this binding's.
788
+ */
789
+ withTimeouts(timeouts: Timeouts): DeRecProtocolBuilder;
790
+
791
+ /**
792
+ * Accept plaintext `http://` transport endpoints. **Development only.**
793
+ * Default: `false`.
794
+ *
795
+ * With `false`, plaintext is accepted in exactly one situation: an endpoint
796
+ * this device configured for **itself** that names loopback (`localhost`,
797
+ * `127.0.0.1`, `::1`). A local dev server therefore needs no configuration
798
+ * at all.
799
+ *
800
+ * With `true`, plaintext is accepted for **any host on any path**,
801
+ * including endpoints a peer supplies. That is what makes the LAN case
802
+ * work — a phone talking to a laptop, where neither side is loopback — and
803
+ * why the name is blunt.
804
+ *
805
+ * This is a guardrail, not transport security. The SDK opens no sockets;
806
+ * delivery is your `Transport`. Nothing here stops an application sending
807
+ * plaintext — it governs which endpoints the protocol will record,
808
+ * propagate to peers, and reply to.
809
+ */
810
+ withUnsafeHttp(allow: boolean): DeRecProtocolBuilder;
515
811
  /** Default: empty. */
516
812
  withCommunicationInfo(info: Record<string, string>): DeRecProtocolBuilder;
517
813
  /** Default: false. */
@@ -593,6 +889,16 @@ export declare class DeRecProtocol {
593
889
  start(flowKind: FlowKind.RecoverSecret, params: RecoverSecretParams): Promise<DeRecEvent[]>;
594
890
  start(flowKind: FlowKind.Unpair, params: UnpairParams): Promise<DeRecEvent[]>;
595
891
  start(flowKind: FlowKind.UpdateChannelInfo, params: UpdateChannelInfoParams): Promise<DeRecEvent[]>;
892
+ start(flowKind: FlowKind.SyncCheck, params?: SyncCheckParams): Promise<DeRecEvent[]>;
893
+
894
+ /** Announce a member's removal. This does **not** remove anything on its
895
+ * own and emits no `ReplicaRemoved`: it tells every member and flags the
896
+ * target locally. The removal completes only once the application
897
+ * publishes a roster omitting that member — an ordinary
898
+ * `start(FlowKind.ProtectSecret)` — at which point `ReplicaRemoved`
899
+ * fires. A group with no secret to publish therefore cannot complete a
900
+ * removal. */
901
+ start(flowKind: FlowKind.RemoveReplica, params: RemoveReplicaParams): Promise<DeRecEvent[]>;
596
902
 
597
903
  /**
598
904
  * Replace this node's local <c>communication_info</c> map. Does not
@@ -610,14 +916,31 @@ export declare class DeRecProtocol {
610
916
 
611
917
  process(message: Uint8Array): Promise<DeRecEvent[]>;
612
918
 
919
+ /**
920
+ * Advance time-driven state without an inbound message.
921
+ *
922
+ * Timeouts are otherwise only evaluated by `process`, so a publish whose
923
+ * helpers all go quiet has nothing left to close it: the round stays open
924
+ * and no `SharingComplete` is ever emitted. Call this from a timer —
925
+ * `setInterval`, a service-worker alarm, a job runner — at an interval
926
+ * shorter than the configured timeout.
927
+ *
928
+ * Safe to call at any time; with nothing in flight it resolves to an empty
929
+ * array. It mutates the same round state an inbound response does, so it
930
+ * must be serialized against `process` for the same `secretId`.
931
+ */
932
+ tick(): Promise<DeRecEvent[]>;
933
+
613
934
  accept(actionBytes: Uint8Array): Promise<DeRecEvent[]>;
614
935
 
615
936
  reject(actionBytes: Uint8Array, status: number, memo: string): Promise<void>;
616
937
 
617
938
  /**
618
939
  * Derive the human-readable fingerprint for a paired channel. Both sides
619
- * of a replica pair compute the same fingerprint from the shared key —
620
- * users compare them out of band before calling `verifyFingerprint`.
940
+ * compute the same value from the shared key — users compare them out of
941
+ * band before calling `verifyFingerprint`. Required for every replica
942
+ * pairing and every `NoKeys` pairing, which stay unusable until it
943
+ * succeeds.
621
944
  */
622
945
  getFingerprint(channelId: bigint | number): Promise<string>;
623
946
 
@@ -627,6 +950,16 @@ export declare class DeRecProtocol {
627
950
  * `true` on confirmation, `false` on mismatch.
628
951
  */
629
952
  verifyFingerprint(channelId: bigint | number, fingerprint: string): Promise<boolean>;
953
+ /**
954
+ * Remove `Pending` channels older than `olderThanSecs`, along with their
955
+ * pairing keys. Resolves to the removed channel ids as decimal strings.
956
+ *
957
+ * Independent of the configured cleanup policy — this sweeps at the
958
+ * threshold given even when that policy is disabled. The age comparison
959
+ * is strict, so a channel created within the current second survives
960
+ * even `0`.
961
+ */
962
+ removeExpiredChannels(olderThanSecs: number): Promise<string[]>;
630
963
 
631
964
  /**
632
965
  * Rebuild this protocol's `secret_id` namespace from a recovered
@@ -685,6 +1018,8 @@ export interface GetSecretIdsVersionsRequestMessage {
685
1018
  /** Ephemeral endpoint where the requester wants the response routed.
686
1019
  * Absent means "use the channel's stored peer endpoint". */
687
1020
  reply_to?: TransportProtocol;
1021
+ /** Replica-group member that sent this; see `replicaId` semantics. */
1022
+ replica_id?: bigint;
688
1023
  }
689
1024
 
690
1025
  export interface VersionList {
@@ -701,6 +1036,8 @@ export interface GetSecretIdsVersionsResponseMessage {
701
1036
  result?: DeRecResult;
702
1037
  secret_list: VersionList[];
703
1038
  timestamp?: Timestamp;
1039
+ /** Replica-group member that sent this; see `replicaId` semantics. */
1040
+ replica_id?: bigint;
704
1041
  }
705
1042
 
706
1043
  export interface VersionEntry {
@@ -795,6 +1132,8 @@ export interface GetShareRequestMessage {
795
1132
  timestamp?: Timestamp;
796
1133
  /** Ephemeral response endpoint; see `replyTo` semantics. */
797
1134
  reply_to?: TransportProtocol;
1135
+ /** Replica-group member that sent this; see `replicaId` semantics. */
1136
+ replica_id?: bigint;
798
1137
  }
799
1138
 
800
1139
  export interface GetShareResponseMessage {
@@ -808,6 +1147,8 @@ export interface GetShareResponseMessage {
808
1147
  secret_id: bigint;
809
1148
  /** Echoed from the request for the same correlation reasons as `secret_id`. */
810
1149
  version: number;
1150
+ /** Replica-group member that sent this; see `replicaId` semantics. */
1151
+ replica_id?: bigint;
811
1152
  }
812
1153
 
813
1154
  export interface SiblingHash {
@@ -833,6 +1174,8 @@ export interface StoreShareRequestMessage {
833
1174
  secret_id: bigint;
834
1175
  /** Ephemeral response endpoint; see `replyTo` semantics. */
835
1176
  reply_to?: TransportProtocol;
1177
+ /** Replica-group member that sent this; see `replicaId` semantics. */
1178
+ replica_id?: bigint;
836
1179
  }
837
1180
 
838
1181
  export interface StoreShareResponseMessage {
@@ -840,6 +1183,8 @@ export interface StoreShareResponseMessage {
840
1183
  version: number;
841
1184
  timestamp?: Timestamp;
842
1185
  secret_id: bigint;
1186
+ /** Replica-group member that sent this; see `replicaId` semantics. */
1187
+ replica_id?: bigint;
843
1188
  }
844
1189
 
845
1190
  export interface UnpairRequestMessage {
@@ -847,6 +1192,8 @@ export interface UnpairRequestMessage {
847
1192
  timestamp?: Timestamp;
848
1193
  /** Ephemeral response endpoint; see `replyTo` semantics. */
849
1194
  reply_to?: TransportProtocol;
1195
+ /** Replica-group member that sent this; see `replicaId` semantics. */
1196
+ replica_id?: bigint;
850
1197
  }
851
1198
 
852
1199
  export interface UnpairResponseMessage {
package/index.js CHANGED
@@ -8,9 +8,9 @@ const DeRecProtocolBuilder = wasm.DeRecProtocolBuilder;
8
8
 
9
9
  const SenderKind = Object.freeze({ Owner: 0, Helper: 1, ReplicaSource: 3, ReplicaDestination: 4 });
10
10
 
11
- const ContactMode = Object.freeze({ InlineKeys: 0, HashedKeys: 1 });
11
+ const ContactMode = Object.freeze({ InlineKeys: 0, HashedKeys: 1, NoKeys: 2 });
12
12
 
13
- const FlowKind = Object.freeze({ Pairing: 0, Discovery: 1, ProtectSecret: 2, VerifyShares: 3, RecoverSecret: 4, Unpair: 5, UpdateChannelInfo: 6 });
13
+ const FlowKind = Object.freeze({ Pairing: 0, Discovery: 1, ProtectSecret: 2, VerifyShares: 3, RecoverSecret: 4, Unpair: 5, UpdateChannelInfo: 6, SyncCheck: 7, RemoveReplica: 8 });
14
14
 
15
15
  const primitives = {
16
16
  discovery: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@derec-alliance/nodejs",
3
3
  "description": "Node.js WebAssembly bindings for derec-library, the Rust SDK for the DeRec protocol.",
4
- "version": "0.0.1-alpha.9",
4
+ "version": "0.0.2",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",