@derec-alliance/react-native 0.0.7 → 0.0.9

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/lib/errors.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  // Copyright (c) 2026 DeRec Alliance. All rights reserved.
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
- exports.DEREC_ERROR_CATEGORIES = void 0;
5
+ exports.DEREC_ERROR_CODES = exports.DEREC_ERROR_CATEGORIES = void 0;
6
6
  /**
7
7
  * Every category name `derec_error_category_name` can return, and no others.
8
8
  *
@@ -33,3 +33,65 @@ exports.DEREC_ERROR_CATEGORIES = [
33
33
  "invariant",
34
34
  "state_store",
35
35
  ];
36
+ /**
37
+ * Every code `derec_error_code_name` can return, and no others.
38
+ *
39
+ * Held to the shared enum fixture by a test, like {@link DEREC_ERROR_CATEGORIES}.
40
+ * Unlike the WASM bindings, this SDK raises no marshalling-level codes of its
41
+ * own before a value reaches the library, so this list is exactly the Rust
42
+ * `Code` table in `library/src/interop/error_codes.rs`.
43
+ */
44
+ exports.DEREC_ERROR_CODES = [
45
+ "ok",
46
+ "non_ok_status",
47
+ "version_mismatch",
48
+ "invariant",
49
+ "invalid_input",
50
+ "protobuf_decode",
51
+ "protobuf_encode",
52
+ "protocol_violation",
53
+ "store_error",
54
+ "builder_error",
55
+ "missing_shared_key",
56
+ "role_mismatch",
57
+ "replica_id_not_configured",
58
+ "channel_already_paired",
59
+ "already_restored",
60
+ "restore_conflict",
61
+ "replica_id_conflict",
62
+ "encryption",
63
+ "keygen",
64
+ "finish_pairing_initiator",
65
+ "finish_pairing_responder",
66
+ "empty_transport_uri",
67
+ "invalid_contact_message",
68
+ "invalid_pair_request_message",
69
+ "invalid_pair_response_message",
70
+ "prepair_hash_mismatch",
71
+ "missing_replica_id",
72
+ "unexpected_replica_id",
73
+ "incompatible_parameter_range",
74
+ "empty_channels",
75
+ "duplicate_channel_id",
76
+ "invalid_threshold",
77
+ "empty_secret_data",
78
+ "vss_share_failed",
79
+ "invalid_weight",
80
+ "too_many_subshares",
81
+ "empty_responses",
82
+ "empty_committed_derec_share",
83
+ "decode_committed_derec_share",
84
+ "decode_derec_share",
85
+ "secret_id_mismatch",
86
+ "reconstruction_failed",
87
+ "malformed_recovered_secret",
88
+ "ffi_null_ptr",
89
+ "ffi_bad_length",
90
+ "ffi_bad_utf8",
91
+ "ffi_bad_proto",
92
+ "ffi_invalid_enum",
93
+ "ffi_bad_shared_key",
94
+ "ffi_nul_in_string",
95
+ "transport_invalid",
96
+ "no_usable_endpoint",
97
+ ];
@@ -1,4 +1,4 @@
1
- import type { CommittedDeRecShare, CommunicationInfo, ContactMessage, ContactMode, CreateContactResult, DiscoveryProcessResult, GetSecretIdsVersionsRequestMessage, GetSecretIdsVersionsResponseMessage, GetShareRequestMessage, GetShareResponseMessage, PairRequestMessage, PairResponseMessage, PairingProcessResult, PairingRequestProduceResult, PairingResponseProduceResult, ParameterRange, PrePairRequestExtractResult, PrePairRequestMessage, PrePairResponseExtractResult, PrePairResponseMessage, ProcessPrePairResult, ProducePrePairNoKeysResult, ProducePrePairResult, ProduceResult, RecoverResult, SecretVersionEntry, SenderKind, SharingResponseProduceResult, SplitResult, StoreShareRequestMessage, StoreShareResponseMessage, TransportProtocol, UnpairRequestMessage, UnpairResponseMessage, UnpairingProcessResult, VerifyShareRequestMessage, VerifyShareResponseMessage } from './types';
1
+ import type { CommittedDeRecShare, CommunicationInfo, ContactMessage, ContactMode, CreateContactResult, DiscoveryProcessResult, GetSecretIdsVersionsRequestMessage, GetSecretIdsVersionsResponseMessage, GetShareRequestMessage, GetShareResponseMessage, PairRequestMessage, PairResponseMessage, PairingProcessResult, PairingRequestProduceResult, PairingResponseProduceResult, ParameterRange, PrePairRequestExtractResult, PrePairRequestMessage, PrePairResponseExtractResult, PrePairResponseMessage, ProcessPrePairResult, ProducePrePairNoKeysResult, ProducePrePairResult, ProduceResult, RecoverResult, SecretVersionEntry, SenderKind, SharingResponseProduceResult, SplitResult, StoreShareRequestMessage, StoreShareResponseMessage, TransportProtocol, UnpairRequestMessage, UnpairResponseMessage, UnpairingProcessResult, VerifyShareRequestMessage, VerifyShareResponseMessage, WeightedChannel } from './types';
2
2
  export declare const primitives: {
3
3
  discovery: {
4
4
  request: {
@@ -67,7 +67,7 @@ export declare const primitives: {
67
67
  };
68
68
  sharing: {
69
69
  request: {
70
- split(channels: bigint[], secret_id: bigint, version: number, secret_data: Uint8Array, threshold: number): SplitResult;
70
+ split(channels: WeightedChannel[], secret_id: bigint, version: number, secret_data: Uint8Array, threshold: number): SplitResult;
71
71
  produce(channel_id: bigint, version: number, secret_id: bigint, committed_share: CommittedDeRecShare, keep_list: number[], description: string, shared_key: Uint8Array, reply_to?: TransportProtocol[]): ProduceResult;
72
72
  extract(envelope_bytes: Uint8Array, shared_key: Uint8Array): {
73
73
  request: StoreShareRequestMessage;
package/lib/primitives.js CHANGED
@@ -270,7 +270,9 @@ const recovery = {
270
270
  const sharing = {
271
271
  request: {
272
272
  split(channels, secret_id, version, secret_data, threshold) {
273
- const packed = call('protect_secret', secret_id, secret_data, channels, threshold, version);
273
+ const ids = channels.map(([channelId]) => BigInt(channelId));
274
+ const weights = channels.map(([, weight]) => weight);
275
+ const packed = call('protect_secret', secret_id, secret_data, ids, weights, threshold, version);
274
276
  const shares = new Map();
275
277
  for (const entry of (0, binary_1.decodeCommittedShareEntries)(packed)) {
276
278
  shares.set(entry.channelId, (0, messages_1.decodeMessage)(messages_1.MessageKind.CommittedDeRecShare, entry.shareProto));
package/lib/protocol.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { FlowKind } from './types';
2
- import type { AutoAcceptPolicy, ChannelStore, ContactMessage, ContactMode, DeRecEvent, Endpoint, DiscoveryParams, PairingParams, ParameterRange, ProtectSecretParams, RecoverSecretParams, UnpairReplicaParams, SecretStore, ShareStore, StateStore, StatusEnum, ReplicaDiscoveryParams, Timeouts, Transport, UnpairAck, UnpairParams, UpdateChannelInfoParams, UserSecretStore, VerifySharesParams } from './types';
2
+ import type { AutoAcceptPolicy, ChannelStore, ContactMessage, ContactMode, DeRecEvent, DeRecProtocolConfig, Endpoint, DiscoveryParams, PairingParams, ParameterRange, ProtectSecretParams, RecoverSecretParams, UnpairReplicaParams, SecretStore, ShareStore, StateStore, StatusEnum, ReplicaDiscoveryParams, Timeouts, Transport, UnpairAck, UnpairParams, UpdateChannelInfoParams, UserSecretStore, VerifySharesParams, StoresInit } from './types';
3
3
  /** Shape of the `__DeRec` host object as seen from this file: every member is
4
4
  * a plain callable, since the JSI layer exposes host functions this way and
5
5
  * TypeScript has no static knowledge of them (see `primitives.ts`'s `call`
@@ -20,9 +20,12 @@ export declare function buildProtectSecretParams(params: ProtectSecretParams): R
20
20
  * `SecretWire`'s own shape: `channel_id`/`replica_id` are the decimal `u64`
21
21
  * strings the `SecretRecovered` event already carries, forwarded verbatim;
22
22
  * `communication_info` is omitted per-entry (not sent as `{}`) whenever a
23
- * helper or replica member has none, matching the Go SDK's `omitempty` and
24
- * the shared golden fixture byte-for-byte; `replicas` is omitted entirely
25
- * when the recovered secret has no replica group.
23
+ * helper or replica member has none — `undefined`, `null`, or an empty map
24
+ * are all treated the same — matching the Go SDK's `omitempty` and the
25
+ * shared golden fixture byte-for-byte; `weight` is forwarded as given
26
+ * whenever a helper carries one; `replicas` is omitted entirely when the
27
+ * recovered secret has no replica group, and `threshold` when the roster
28
+ * records none.
26
29
  *
27
30
  * Exported so it can be golden-tested against the shared wire fixture
28
31
  * without a native module.
@@ -55,6 +58,13 @@ export declare class DeRecProtocolBuilder {
55
58
  withSecretStore(store: SecretStore): this;
56
59
  withUserSecretStore(store: UserSecretStore): this;
57
60
  withStateStore(store: StateStore): this;
61
+ /**
62
+ * Set every store in one call. Forwards to {@link withChannelStore},
63
+ * {@link withShareStore}, {@link withSecretStore},
64
+ * {@link withUserSecretStore} and {@link withStateStore} in turn, so
65
+ * calling one of them again afterwards replaces just that store.
66
+ */
67
+ withStores(stores: StoresInit): this;
58
68
  withTransport(transport: Transport): this;
59
69
  /**
60
70
  * Set every transport endpoint this application serves, in preference
@@ -72,7 +82,22 @@ export declare class DeRecProtocolBuilder {
72
82
  * since a node with no endpoint cannot be reached by any peer.
73
83
  */
74
84
  withOwnTransports(transports: Endpoint[]): this;
75
- /** Default: 3. */
85
+ /**
86
+ * The starting threshold: the minimum number of subshares required to
87
+ * reconstruct the secret, for a secret with no threshold stored. Every
88
+ * publish stores the threshold it used (`UserSecrets.threshold`), the
89
+ * roster carries it, and `restore` and replica hydration put it back. A
90
+ * round splits at `ProtectSecretParams.threshold`, otherwise at the stored
91
+ * threshold, and only otherwise at this value, so changing it later does
92
+ * not change a stored threshold.
93
+ *
94
+ * A Helper holds one subshare unless `ProtectSecretParams.weights` has
95
+ * given it a weight, so with no weights the threshold is a number of
96
+ * Helpers, as before. Every weight must be below the round's threshold, so
97
+ * no single Helper can recover alone. When this value applies and is
98
+ * lowered to or below a stored weight, every publish fails with
99
+ * `invalid_weight` until `ProtectSecret` assigns valid weights. Default: 3.
100
+ */
76
101
  withThreshold(threshold: number): this;
77
102
  /**
78
103
  * Every field is optional; not calling this at all leaves all four at the
@@ -92,7 +117,12 @@ export declare class DeRecProtocolBuilder {
92
117
  * map is part of the protocol from construction.
93
118
  */
94
119
  withCommunicationInfo(info: Record<string, string>): this;
95
- /** Default: false. */
120
+ /**
121
+ * Who answers an inbound request whose handling failed. `true`: the
122
+ * library sends the response itself. `false`: nothing is sent, and the
123
+ * `RequestFailed` event carries a token to pass to `reject`. Default:
124
+ * false.
125
+ */
96
126
  withAutoRespondOnFailure(enabled: boolean): this;
97
127
  /** Default: "required". */
98
128
  withUnpairAck(ack: UnpairAck): this;
@@ -117,8 +147,53 @@ export declare class DeRecProtocolBuilder {
117
147
  * applies its default.
118
148
  *
119
149
  * Default: unset — no constraints advertised, every peer range accepted.
150
+ *
151
+ * Share size is enforced by the helper; frequency limits are enforced by
152
+ * the app's share store (refuse from `save`/`recordVerification`); the
153
+ * deletion and deactivation timeouts are checked at pairing only.
120
154
  */
121
155
  withParameterRange(range: Partial<ParameterRange>): this;
156
+ /**
157
+ * Cap the total number of subshares one sharing round may split the
158
+ * secret into — the sum of every helper's weight. Crosses to the library
159
+ * as a decimal string. Default: unset, no cap.
160
+ *
161
+ * Checked only by `start(ProtectSecret)`, including the publishes the
162
+ * library starts itself: a round over the cap fails with
163
+ * `too_many_subshares` before anything is sent. Recovery applies no cap.
164
+ * Recovering costs time that grows quadratically with the total number of
165
+ * subshares, so the cap bounds what a later recovery must do. A cap below
166
+ * the threshold makes `build()` throw.
167
+ */
168
+ withMaxSubshares(max: bigint | number): this;
169
+ /**
170
+ * Layer a {@link DeRecProtocolConfig} onto the builder: a field the config
171
+ * sets (present and not `null`) replaces what the builder currently
172
+ * holds, a field it leaves unset is untouched, and `timeouts` merges per
173
+ * field against the builder's current timeouts rather than resetting the
174
+ * other three. Every other nested object replaces as a whole. Apply a
175
+ * general config, then a per-user one, then any individual `with*` call —
176
+ * the last write wins per field, same as two `withConfig` calls in a row.
177
+ *
178
+ * Every key is assigned verbatim into the same `snake_case` config JSON
179
+ * `build()` sends to `protocol_new`, whatever it is spelled as: nothing
180
+ * here validates, defaults, or recognizes field names. Rust does both —
181
+ * `deny_unknown_fields` is what turns a misspelled key into an error.
182
+ *
183
+ * `secret_id` is not a config field — it is the constructor argument —
184
+ * and `build()` always sends the constructor's value last, so a config
185
+ * (e.g. loaded from a per-user record) can never redirect which secret
186
+ * this instance manages even if it carries a `secret_id` key.
187
+ *
188
+ * A config parsed from JSON can carry an own `__proto__` key. Assigning
189
+ * it through plain bracket notation would hit the accessor
190
+ * `Object.prototype` defines for that name and replace this builder's
191
+ * config object's prototype instead of setting a field on it — the value
192
+ * would then vanish from the JSON payload instead of reaching Rust to be
193
+ * rejected as an unknown key. `Object.defineProperty` makes it a genuine
194
+ * own property, the same as every other key here.
195
+ */
196
+ withConfig(config: DeRecProtocolConfig): this;
122
197
  build(): DeRecProtocol;
123
198
  }
124
199
  /**
@@ -186,6 +261,17 @@ export declare class DeRecProtocol {
186
261
  start(flowKind: FlowKind.UnpairReplica, params: UnpairReplicaParams): Promise<DeRecEvent[]>;
187
262
  process(message: Uint8Array): Promise<DeRecEvent[]>;
188
263
  tick(): Promise<DeRecEvent[]>;
264
+ /**
265
+ * Accept a pending action from an `ActionRequired` event, returning the
266
+ * events it produces.
267
+ *
268
+ * When the action completes a pairing, the publish that follows reuses the
269
+ * stored helper weights. If one is no longer valid under the round's
270
+ * threshold, that publish fails with `invalid_weight` before anything is
271
+ * sent and this throws: the pairing has completed and its response was
272
+ * sent, but the `PairingCompleted` event is not returned. Call `start`
273
+ * with `ProtectSecret` and valid weights to publish.
274
+ */
189
275
  accept(actionBytes: Uint8Array): Promise<DeRecEvent[]>;
190
276
  reject(actionBytes: Uint8Array, status: StatusEnum, memo: string): Promise<void>;
191
277
  getFingerprint(channelId: bigint | number): Promise<string>;
@@ -198,6 +284,13 @@ export declare class DeRecProtocol {
198
284
  * group's vault: the source's publish is then installed as it arrives,
199
285
  * with no further prompt. Ask the user before calling this; to decline,
200
286
  * never confirm.
287
+ *
288
+ * On a promotion the current snapshot is published to the promoted peer,
289
+ * reusing the stored helper weights. If one is no longer valid under the
290
+ * current threshold, that publish fails with `invalid_weight` before
291
+ * anything is sent and this throws. The promotion stands: calling this
292
+ * again resolves `true` and publishes nothing, so call `start` with
293
+ * `ProtectSecret` and valid weights to reach the promoted peer.
201
294
  */
202
295
  verifyFingerprint(channelId: bigint | number, fingerprint: string): Promise<boolean>;
203
296
  removeExpiredChannels(olderThanSecs: number): Promise<string[]>;
@@ -211,6 +304,9 @@ export declare class DeRecProtocol {
211
304
  * reported as a `PeerNotRestored` event in the returned array and the rest
212
305
  * of the roster is restored.
213
306
  *
307
+ * A helper's or member's `communication_info` may be absent or `null`;
308
+ * both mean the same thing — no entries.
309
+ *
214
310
  * Rejects with a `DeRecError`; among its codes:
215
311
  *
216
312
  * | code | meaning |
package/lib/protocol.js CHANGED
@@ -78,19 +78,32 @@ function buildProtectSecretParams(params) {
78
78
  if (params.description !== undefined) {
79
79
  out.description = params.description;
80
80
  }
81
+ if (params.weights != null && params.weights.size > 0) {
82
+ const weights = {};
83
+ for (const [id, weight] of params.weights) {
84
+ weights[id.toString()] = weight;
85
+ }
86
+ out.weights = weights;
87
+ }
88
+ if (params.threshold != null) {
89
+ out.threshold = params.threshold;
90
+ }
81
91
  return out;
82
92
  }
83
93
  function communicationInfoOrOmit(info) {
84
- return info !== undefined && Object.keys(info).length > 0 ? info : undefined;
94
+ return info != null && Object.keys(info).length > 0 ? info : undefined;
85
95
  }
86
96
  /**
87
97
  * `RestoreParamsJson` (`library/src/interop/ffi/protocol/handle/flow.rs`). Mirrors
88
98
  * `SecretWire`'s own shape: `channel_id`/`replica_id` are the decimal `u64`
89
99
  * strings the `SecretRecovered` event already carries, forwarded verbatim;
90
100
  * `communication_info` is omitted per-entry (not sent as `{}`) whenever a
91
- * helper or replica member has none, matching the Go SDK's `omitempty` and
92
- * the shared golden fixture byte-for-byte; `replicas` is omitted entirely
93
- * when the recovered secret has no replica group.
101
+ * helper or replica member has none — `undefined`, `null`, or an empty map
102
+ * are all treated the same — matching the Go SDK's `omitempty` and the
103
+ * shared golden fixture byte-for-byte; `weight` is forwarded as given
104
+ * whenever a helper carries one; `replicas` is omitted entirely when the
105
+ * recovered secret has no replica group, and `threshold` when the roster
106
+ * records none.
94
107
  *
95
108
  * Exported so it can be golden-tested against the shared wire fixture
96
109
  * without a native module.
@@ -107,6 +120,9 @@ function buildRestoreParams(recoveredSecret, version) {
107
120
  if (info !== undefined) {
108
121
  out.communication_info = info;
109
122
  }
123
+ if (helper.weight !== undefined) {
124
+ out.weight = helper.weight;
125
+ }
110
126
  return out;
111
127
  }),
112
128
  secrets: recoveredSecret.secrets.map((secret) => ({
@@ -133,6 +149,9 @@ function buildRestoreParams(recoveredSecret, version) {
133
149
  shared_key: Array.from(recoveredSecret.replicas.shared_key),
134
150
  };
135
151
  }
152
+ if (recoveredSecret.threshold != null) {
153
+ recovered.threshold = recoveredSecret.threshold;
154
+ }
136
155
  return { version, recovered_secret: recovered };
137
156
  }
138
157
  /**
@@ -267,6 +286,19 @@ class DeRecProtocolBuilder {
267
286
  this.stores.stateStore = store;
268
287
  return this;
269
288
  }
289
+ /**
290
+ * Set every store in one call. Forwards to {@link withChannelStore},
291
+ * {@link withShareStore}, {@link withSecretStore},
292
+ * {@link withUserSecretStore} and {@link withStateStore} in turn, so
293
+ * calling one of them again afterwards replaces just that store.
294
+ */
295
+ withStores(stores) {
296
+ return this.withChannelStore(stores.channelStore)
297
+ .withShareStore(stores.shareStore)
298
+ .withSecretStore(stores.secretStore)
299
+ .withUserSecretStore(stores.userSecretStore)
300
+ .withStateStore(stores.stateStore);
301
+ }
270
302
  withTransport(transport) {
271
303
  this.stores.transport = transport;
272
304
  return this;
@@ -290,7 +322,22 @@ class DeRecProtocolBuilder {
290
322
  this.config.own_transports = transports.map(({ uri, protocol }) => ({ uri, protocol }));
291
323
  return this;
292
324
  }
293
- /** Default: 3. */
325
+ /**
326
+ * The starting threshold: the minimum number of subshares required to
327
+ * reconstruct the secret, for a secret with no threshold stored. Every
328
+ * publish stores the threshold it used (`UserSecrets.threshold`), the
329
+ * roster carries it, and `restore` and replica hydration put it back. A
330
+ * round splits at `ProtectSecretParams.threshold`, otherwise at the stored
331
+ * threshold, and only otherwise at this value, so changing it later does
332
+ * not change a stored threshold.
333
+ *
334
+ * A Helper holds one subshare unless `ProtectSecretParams.weights` has
335
+ * given it a weight, so with no weights the threshold is a number of
336
+ * Helpers, as before. Every weight must be below the round's threshold, so
337
+ * no single Helper can recover alone. When this value applies and is
338
+ * lowered to or below a stored weight, every publish fails with
339
+ * `invalid_weight` until `ProtectSecret` assigns valid weights. Default: 3.
340
+ */
294
341
  withThreshold(threshold) {
295
342
  this.config.threshold = threshold;
296
343
  return this;
@@ -322,7 +369,12 @@ class DeRecProtocolBuilder {
322
369
  this.config.communication_info = info;
323
370
  return this;
324
371
  }
325
- /** Default: false. */
372
+ /**
373
+ * Who answers an inbound request whose handling failed. `true`: the
374
+ * library sends the response itself. `false`: nothing is sent, and the
375
+ * `RequestFailed` event carries a token to pass to `reject`. Default:
376
+ * false.
377
+ */
326
378
  withAutoRespondOnFailure(enabled) {
327
379
  this.config.auto_respond_on_failure = enabled;
328
380
  return this;
@@ -371,6 +423,10 @@ class DeRecProtocolBuilder {
371
423
  * applies its default.
372
424
  *
373
425
  * Default: unset — no constraints advertised, every peer range accepted.
426
+ *
427
+ * Share size is enforced by the helper; frequency limits are enforced by
428
+ * the app's share store (refuse from `save`/`recordVerification`); the
429
+ * deletion and deactivation timeouts are checked at pairing only.
374
430
  */
375
431
  withParameterRange(range) {
376
432
  this.config.parameter_range = {
@@ -387,11 +443,85 @@ class DeRecProtocolBuilder {
387
443
  };
388
444
  return this;
389
445
  }
446
+ /**
447
+ * Cap the total number of subshares one sharing round may split the
448
+ * secret into — the sum of every helper's weight. Crosses to the library
449
+ * as a decimal string. Default: unset, no cap.
450
+ *
451
+ * Checked only by `start(ProtectSecret)`, including the publishes the
452
+ * library starts itself: a round over the cap fails with
453
+ * `too_many_subshares` before anything is sent. Recovery applies no cap.
454
+ * Recovering costs time that grows quadratically with the total number of
455
+ * subshares, so the cap bounds what a later recovery must do. A cap below
456
+ * the threshold makes `build()` throw.
457
+ */
458
+ withMaxSubshares(max) {
459
+ this.config.max_subshares = BigInt(max).toString();
460
+ return this;
461
+ }
462
+ /**
463
+ * Layer a {@link DeRecProtocolConfig} onto the builder: a field the config
464
+ * sets (present and not `null`) replaces what the builder currently
465
+ * holds, a field it leaves unset is untouched, and `timeouts` merges per
466
+ * field against the builder's current timeouts rather than resetting the
467
+ * other three. Every other nested object replaces as a whole. Apply a
468
+ * general config, then a per-user one, then any individual `with*` call —
469
+ * the last write wins per field, same as two `withConfig` calls in a row.
470
+ *
471
+ * Every key is assigned verbatim into the same `snake_case` config JSON
472
+ * `build()` sends to `protocol_new`, whatever it is spelled as: nothing
473
+ * here validates, defaults, or recognizes field names. Rust does both —
474
+ * `deny_unknown_fields` is what turns a misspelled key into an error.
475
+ *
476
+ * `secret_id` is not a config field — it is the constructor argument —
477
+ * and `build()` always sends the constructor's value last, so a config
478
+ * (e.g. loaded from a per-user record) can never redirect which secret
479
+ * this instance manages even if it carries a `secret_id` key.
480
+ *
481
+ * A config parsed from JSON can carry an own `__proto__` key. Assigning
482
+ * it through plain bracket notation would hit the accessor
483
+ * `Object.prototype` defines for that name and replace this builder's
484
+ * config object's prototype instead of setting a field on it — the value
485
+ * would then vanish from the JSON payload instead of reaching Rust to be
486
+ * rejected as an unknown key. `Object.defineProperty` makes it a genuine
487
+ * own property, the same as every other key here.
488
+ */
489
+ withConfig(config) {
490
+ for (const [key, value] of Object.entries(config)) {
491
+ if (value === undefined || value === null) {
492
+ continue;
493
+ }
494
+ if (key === 'timeouts') {
495
+ const held = this.config.timeouts ?? {};
496
+ const merged = { ...held };
497
+ for (const [timeoutKey, timeoutValue] of Object.entries(value)) {
498
+ if (timeoutValue !== undefined && timeoutValue !== null) {
499
+ merged[timeoutKey] = timeoutValue;
500
+ }
501
+ }
502
+ this.config.timeouts = merged;
503
+ continue;
504
+ }
505
+ if (key === '__proto__') {
506
+ Object.defineProperty(this.config, key, {
507
+ value,
508
+ writable: true,
509
+ enumerable: true,
510
+ configurable: true,
511
+ });
512
+ continue;
513
+ }
514
+ this.config[key] = value;
515
+ }
516
+ return this;
517
+ }
390
518
  build() {
391
519
  const config = {
392
- secret_id: this.secretIdValue.toString(),
393
520
  replica_id: null,
394
521
  ...this.config,
522
+ // Last, so a config's own `secret_id` key (it is not a config field;
523
+ // see `withConfig`) can never override the constructor's.
524
+ secret_id: this.secretIdValue.toString(),
395
525
  };
396
526
  const host = (0, native_1.getNative)().protocol_new(JSON.stringify(config), new Uint8Array(0), this.stores);
397
527
  return DeRecProtocol.fromHost(host);
@@ -490,6 +620,17 @@ class DeRecProtocol {
490
620
  const buffer = (await this.host.tick());
491
621
  return decodeEvents(buffer);
492
622
  }
623
+ /**
624
+ * Accept a pending action from an `ActionRequired` event, returning the
625
+ * events it produces.
626
+ *
627
+ * When the action completes a pairing, the publish that follows reuses the
628
+ * stored helper weights. If one is no longer valid under the round's
629
+ * threshold, that publish fails with `invalid_weight` before anything is
630
+ * sent and this throws: the pairing has completed and its response was
631
+ * sent, but the `PairingCompleted` event is not returned. Call `start`
632
+ * with `ProtectSecret` and valid weights to publish.
633
+ */
493
634
  async accept(actionBytes) {
494
635
  const buffer = (await this.host.accept(actionBytes));
495
636
  return decodeEvents(buffer);
@@ -509,6 +650,13 @@ class DeRecProtocol {
509
650
  * group's vault: the source's publish is then installed as it arrives,
510
651
  * with no further prompt. Ask the user before calling this; to decline,
511
652
  * never confirm.
653
+ *
654
+ * On a promotion the current snapshot is published to the promoted peer,
655
+ * reusing the stored helper weights. If one is no longer valid under the
656
+ * current threshold, that publish fails with `invalid_weight` before
657
+ * anything is sent and this throws. The promotion stands: calling this
658
+ * again resolves `true` and publishes nothing, so call `start` with
659
+ * `ProtectSecret` and valid weights to reach the promoted peer.
512
660
  */
513
661
  async verifyFingerprint(channelId, fingerprint) {
514
662
  return (await this.host.verifyFingerprint(toBigInt(channelId), fingerprint));
@@ -527,6 +675,9 @@ class DeRecProtocol {
527
675
  * reported as a `PeerNotRestored` event in the returned array and the rest
528
676
  * of the roster is restored.
529
677
  *
678
+ * A helper's or member's `communication_info` may be absent or `null`;
679
+ * both mean the same thing — no entries.
680
+ *
530
681
  * Rejects with a `DeRecError`; among its codes:
531
682
  *
532
683
  * | code | meaning |