@derec-alliance/react-native 0.0.8 → 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/types.js CHANGED
@@ -7,6 +7,7 @@ exports.channelFilterMatches = channelFilterMatches;
7
7
  exports.advertisedEndpoints = advertisedEndpoints;
8
8
  exports.protocol_version = protocol_version;
9
9
  exports.generate_replica_id = generate_replica_id;
10
+ exports.shareStoreRefusal = shareStoreRefusal;
10
11
  exports.sequentialFailover = sequentialFailover;
11
12
  exports.singleEndpointTransport = singleEndpointTransport;
12
13
  const native_1 = require("./native");
@@ -65,6 +66,24 @@ function protocol_version() {
65
66
  function generate_replica_id() {
66
67
  return (0, native_1.getNative)().generate_replica_id();
67
68
  }
69
+ const SHARE_STORE_REFUSAL_REASONS = [
70
+ "too_frequent",
71
+ "size_limit_exceeded",
72
+ "rejected",
73
+ ];
74
+ /**
75
+ * Builds the `Error` a `ShareStore`'s `save`/`recordVerification` throws, or
76
+ * rejects its promise with, to refuse the call: `reason` tagged onto the
77
+ * error's `refusal` property.
78
+ */
79
+ function shareStoreRefusal(reason) {
80
+ if (!SHARE_STORE_REFUSAL_REASONS.includes(reason)) {
81
+ throw new Error(`unknown share store refusal reason ${JSON.stringify(reason)}; expected one of ${JSON.stringify(SHARE_STORE_REFUSAL_REASONS)}`);
82
+ }
83
+ const error = new Error(`share store refused: ${reason}`);
84
+ error.refusal = reason;
85
+ return error;
86
+ }
68
87
  /**
69
88
  * Builds a {@link Transport} that tries each endpoint in the order the peer
70
89
  * offered it and stops at the first success.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@derec-alliance/react-native",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "React Native bindings for derec-library, the Rust SDK for the DeRec protocol.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/src/errors.ts CHANGED
@@ -36,9 +36,74 @@ export const DEREC_ERROR_CATEGORIES = [
36
36
 
37
37
  export type DeRecErrorCategory = (typeof DEREC_ERROR_CATEGORIES)[number];
38
38
 
39
+ /**
40
+ * Every code `derec_error_code_name` can return, and no others.
41
+ *
42
+ * Held to the shared enum fixture by a test, like {@link DEREC_ERROR_CATEGORIES}.
43
+ * Unlike the WASM bindings, this SDK raises no marshalling-level codes of its
44
+ * own before a value reaches the library, so this list is exactly the Rust
45
+ * `Code` table in `library/src/interop/error_codes.rs`.
46
+ */
47
+ export const DEREC_ERROR_CODES = [
48
+ "ok",
49
+ "non_ok_status",
50
+ "version_mismatch",
51
+ "invariant",
52
+ "invalid_input",
53
+ "protobuf_decode",
54
+ "protobuf_encode",
55
+ "protocol_violation",
56
+ "store_error",
57
+ "builder_error",
58
+ "missing_shared_key",
59
+ "role_mismatch",
60
+ "replica_id_not_configured",
61
+ "channel_already_paired",
62
+ "already_restored",
63
+ "restore_conflict",
64
+ "replica_id_conflict",
65
+ "encryption",
66
+ "keygen",
67
+ "finish_pairing_initiator",
68
+ "finish_pairing_responder",
69
+ "empty_transport_uri",
70
+ "invalid_contact_message",
71
+ "invalid_pair_request_message",
72
+ "invalid_pair_response_message",
73
+ "prepair_hash_mismatch",
74
+ "missing_replica_id",
75
+ "unexpected_replica_id",
76
+ "incompatible_parameter_range",
77
+ "empty_channels",
78
+ "duplicate_channel_id",
79
+ "invalid_threshold",
80
+ "empty_secret_data",
81
+ "vss_share_failed",
82
+ "invalid_weight",
83
+ "too_many_subshares",
84
+ "empty_responses",
85
+ "empty_committed_derec_share",
86
+ "decode_committed_derec_share",
87
+ "decode_derec_share",
88
+ "secret_id_mismatch",
89
+ "reconstruction_failed",
90
+ "malformed_recovered_secret",
91
+ "ffi_null_ptr",
92
+ "ffi_bad_length",
93
+ "ffi_bad_utf8",
94
+ "ffi_bad_proto",
95
+ "ffi_invalid_enum",
96
+ "ffi_bad_shared_key",
97
+ "ffi_nul_in_string",
98
+ "transport_invalid",
99
+ "no_usable_endpoint",
100
+ ] as const;
101
+
102
+ export type DeRecErrorCode = (typeof DEREC_ERROR_CODES)[number];
103
+
39
104
  export interface DeRecError {
40
105
  category: DeRecErrorCategory;
41
- code: string;
106
+ code: DeRecErrorCode;
42
107
  message: string;
43
108
  status?: number;
44
109
  memo?: string;
package/src/primitives.ts CHANGED
@@ -71,6 +71,7 @@ import type {
71
71
  UnpairingProcessResult,
72
72
  VerifyShareRequestMessage,
73
73
  VerifyShareResponseMessage,
74
+ WeightedChannel,
74
75
  } from './types';
75
76
 
76
77
  /** Shape of the `__DeRec` host object: every member is a plain callable, since
@@ -593,13 +594,23 @@ const recovery = {
593
594
  const sharing = {
594
595
  request: {
595
596
  split(
596
- channels: bigint[],
597
+ channels: WeightedChannel[],
597
598
  secret_id: bigint,
598
599
  version: number,
599
600
  secret_data: Uint8Array,
600
601
  threshold: number,
601
602
  ): SplitResult {
602
- const packed = call('protect_secret', secret_id, secret_data, channels, threshold, version);
603
+ const ids = channels.map(([channelId]) => BigInt(channelId));
604
+ const weights = channels.map(([, weight]) => weight);
605
+ const packed = call(
606
+ 'protect_secret',
607
+ secret_id,
608
+ secret_data,
609
+ ids,
610
+ weights,
611
+ threshold,
612
+ version,
613
+ );
603
614
  const shares = new Map<bigint, CommittedDeRecShare>();
604
615
  for (const entry of decodeCommittedShareEntries(packed as ArrayBuffer)) {
605
616
  shares.set(
package/src/protocol.ts CHANGED
@@ -11,6 +11,7 @@ import type {
11
11
  ContactMessage,
12
12
  ContactMode,
13
13
  DeRecEvent,
14
+ DeRecProtocolConfig,
14
15
  Endpoint,
15
16
  DiscoveryParams,
16
17
  PairingParams,
@@ -31,6 +32,7 @@ import type {
31
32
  UpdateChannelInfoParams,
32
33
  UserSecretStore,
33
34
  VerifySharesParams,
35
+ StoresInit,
34
36
  } from './types';
35
37
 
36
38
  /** Shape of the `__DeRec` host object as seen from this file: every member is
@@ -113,13 +115,23 @@ export function buildProtectSecretParams(params: ProtectSecretParams): Record<st
113
115
  if (params.description !== undefined) {
114
116
  out.description = params.description;
115
117
  }
118
+ if (params.weights != null && params.weights.size > 0) {
119
+ const weights: Record<string, number> = {};
120
+ for (const [id, weight] of params.weights) {
121
+ weights[id.toString()] = weight;
122
+ }
123
+ out.weights = weights;
124
+ }
125
+ if (params.threshold != null) {
126
+ out.threshold = params.threshold;
127
+ }
116
128
  return out;
117
129
  }
118
130
 
119
131
  function communicationInfoOrOmit(
120
- info: Record<string, string> | undefined,
132
+ info: Record<string, string> | null | undefined,
121
133
  ): Record<string, string> | undefined {
122
- return info !== undefined && Object.keys(info).length > 0 ? info : undefined;
134
+ return info != null && Object.keys(info).length > 0 ? info : undefined;
123
135
  }
124
136
 
125
137
  /**
@@ -127,9 +139,12 @@ function communicationInfoOrOmit(
127
139
  * `SecretWire`'s own shape: `channel_id`/`replica_id` are the decimal `u64`
128
140
  * strings the `SecretRecovered` event already carries, forwarded verbatim;
129
141
  * `communication_info` is omitted per-entry (not sent as `{}`) whenever a
130
- * helper or replica member has none, matching the Go SDK's `omitempty` and
131
- * the shared golden fixture byte-for-byte; `replicas` is omitted entirely
132
- * when the recovered secret has no replica group.
142
+ * helper or replica member has none — `undefined`, `null`, or an empty map
143
+ * are all treated the same — matching the Go SDK's `omitempty` and the
144
+ * shared golden fixture byte-for-byte; `weight` is forwarded as given
145
+ * whenever a helper carries one; `replicas` is omitted entirely when the
146
+ * recovered secret has no replica group, and `threshold` when the roster
147
+ * records none.
133
148
  *
134
149
  * Exported so it can be golden-tested against the shared wire fixture
135
150
  * without a native module.
@@ -149,6 +164,9 @@ export function buildRestoreParams(
149
164
  if (info !== undefined) {
150
165
  out.communication_info = info;
151
166
  }
167
+ if (helper.weight !== undefined) {
168
+ out.weight = helper.weight;
169
+ }
152
170
  return out;
153
171
  }),
154
172
  secrets: recoveredSecret.secrets.map((secret) => ({
@@ -175,6 +193,9 @@ export function buildRestoreParams(
175
193
  shared_key: Array.from(recoveredSecret.replicas.shared_key),
176
194
  };
177
195
  }
196
+ if (recoveredSecret.threshold != null) {
197
+ recovered.threshold = recoveredSecret.threshold;
198
+ }
178
199
  return { version, recovered_secret: recovered };
179
200
  }
180
201
 
@@ -321,6 +342,20 @@ export class DeRecProtocolBuilder {
321
342
  return this;
322
343
  }
323
344
 
345
+ /**
346
+ * Set every store in one call. Forwards to {@link withChannelStore},
347
+ * {@link withShareStore}, {@link withSecretStore},
348
+ * {@link withUserSecretStore} and {@link withStateStore} in turn, so
349
+ * calling one of them again afterwards replaces just that store.
350
+ */
351
+ withStores(stores: StoresInit): this {
352
+ return this.withChannelStore(stores.channelStore)
353
+ .withShareStore(stores.shareStore)
354
+ .withSecretStore(stores.secretStore)
355
+ .withUserSecretStore(stores.userSecretStore)
356
+ .withStateStore(stores.stateStore);
357
+ }
358
+
324
359
  withTransport(transport: Transport): this {
325
360
  this.stores.transport = transport;
326
361
  return this;
@@ -346,7 +381,22 @@ export class DeRecProtocolBuilder {
346
381
  return this;
347
382
  }
348
383
 
349
- /** Default: 3. */
384
+ /**
385
+ * The starting threshold: the minimum number of subshares required to
386
+ * reconstruct the secret, for a secret with no threshold stored. Every
387
+ * publish stores the threshold it used (`UserSecrets.threshold`), the
388
+ * roster carries it, and `restore` and replica hydration put it back. A
389
+ * round splits at `ProtectSecretParams.threshold`, otherwise at the stored
390
+ * threshold, and only otherwise at this value, so changing it later does
391
+ * not change a stored threshold.
392
+ *
393
+ * A Helper holds one subshare unless `ProtectSecretParams.weights` has
394
+ * given it a weight, so with no weights the threshold is a number of
395
+ * Helpers, as before. Every weight must be below the round's threshold, so
396
+ * no single Helper can recover alone. When this value applies and is
397
+ * lowered to or below a stored weight, every publish fails with
398
+ * `invalid_weight` until `ProtectSecret` assigns valid weights. Default: 3.
399
+ */
350
400
  withThreshold(threshold: number): this {
351
401
  this.config.threshold = threshold;
352
402
  return this;
@@ -382,7 +432,12 @@ export class DeRecProtocolBuilder {
382
432
  return this;
383
433
  }
384
434
 
385
- /** Default: false. */
435
+ /**
436
+ * Who answers an inbound request whose handling failed. `true`: the
437
+ * library sends the response itself. `false`: nothing is sent, and the
438
+ * `RequestFailed` event carries a token to pass to `reject`. Default:
439
+ * false.
440
+ */
386
441
  withAutoRespondOnFailure(enabled: boolean): this {
387
442
  this.config.auto_respond_on_failure = enabled;
388
443
  return this;
@@ -436,6 +491,10 @@ export class DeRecProtocolBuilder {
436
491
  * applies its default.
437
492
  *
438
493
  * Default: unset — no constraints advertised, every peer range accepted.
494
+ *
495
+ * Share size is enforced by the helper; frequency limits are enforced by
496
+ * the app's share store (refuse from `save`/`recordVerification`); the
497
+ * deletion and deactivation timeouts are checked at pairing only.
439
498
  */
440
499
  withParameterRange(range: Partial<ParameterRange>): this {
441
500
  this.config.parameter_range = {
@@ -455,11 +514,89 @@ export class DeRecProtocolBuilder {
455
514
  return this;
456
515
  }
457
516
 
517
+ /**
518
+ * Cap the total number of subshares one sharing round may split the
519
+ * secret into — the sum of every helper's weight. Crosses to the library
520
+ * as a decimal string. Default: unset, no cap.
521
+ *
522
+ * Checked only by `start(ProtectSecret)`, including the publishes the
523
+ * library starts itself: a round over the cap fails with
524
+ * `too_many_subshares` before anything is sent. Recovery applies no cap.
525
+ * Recovering costs time that grows quadratically with the total number of
526
+ * subshares, so the cap bounds what a later recovery must do. A cap below
527
+ * the threshold makes `build()` throw.
528
+ */
529
+ withMaxSubshares(max: bigint | number): this {
530
+ this.config.max_subshares = BigInt(max).toString();
531
+ return this;
532
+ }
533
+
534
+ /**
535
+ * Layer a {@link DeRecProtocolConfig} onto the builder: a field the config
536
+ * sets (present and not `null`) replaces what the builder currently
537
+ * holds, a field it leaves unset is untouched, and `timeouts` merges per
538
+ * field against the builder's current timeouts rather than resetting the
539
+ * other three. Every other nested object replaces as a whole. Apply a
540
+ * general config, then a per-user one, then any individual `with*` call —
541
+ * the last write wins per field, same as two `withConfig` calls in a row.
542
+ *
543
+ * Every key is assigned verbatim into the same `snake_case` config JSON
544
+ * `build()` sends to `protocol_new`, whatever it is spelled as: nothing
545
+ * here validates, defaults, or recognizes field names. Rust does both —
546
+ * `deny_unknown_fields` is what turns a misspelled key into an error.
547
+ *
548
+ * `secret_id` is not a config field — it is the constructor argument —
549
+ * and `build()` always sends the constructor's value last, so a config
550
+ * (e.g. loaded from a per-user record) can never redirect which secret
551
+ * this instance manages even if it carries a `secret_id` key.
552
+ *
553
+ * A config parsed from JSON can carry an own `__proto__` key. Assigning
554
+ * it through plain bracket notation would hit the accessor
555
+ * `Object.prototype` defines for that name and replace this builder's
556
+ * config object's prototype instead of setting a field on it — the value
557
+ * would then vanish from the JSON payload instead of reaching Rust to be
558
+ * rejected as an unknown key. `Object.defineProperty` makes it a genuine
559
+ * own property, the same as every other key here.
560
+ */
561
+ withConfig(config: DeRecProtocolConfig): this {
562
+ for (const [key, value] of Object.entries(config as Record<string, unknown>)) {
563
+ if (value === undefined || value === null) {
564
+ continue;
565
+ }
566
+ if (key === 'timeouts') {
567
+ const held = (this.config.timeouts as Record<string, unknown> | undefined) ?? {};
568
+ const merged: Record<string, unknown> = { ...held };
569
+ for (const [timeoutKey, timeoutValue] of Object.entries(
570
+ value as Record<string, unknown>,
571
+ )) {
572
+ if (timeoutValue !== undefined && timeoutValue !== null) {
573
+ merged[timeoutKey] = timeoutValue;
574
+ }
575
+ }
576
+ this.config.timeouts = merged;
577
+ continue;
578
+ }
579
+ if (key === '__proto__') {
580
+ Object.defineProperty(this.config, key, {
581
+ value,
582
+ writable: true,
583
+ enumerable: true,
584
+ configurable: true,
585
+ });
586
+ continue;
587
+ }
588
+ this.config[key] = value;
589
+ }
590
+ return this;
591
+ }
592
+
458
593
  build(): DeRecProtocol {
459
594
  const config = {
460
- secret_id: this.secretIdValue.toString(),
461
595
  replica_id: null,
462
596
  ...this.config,
597
+ // Last, so a config's own `secret_id` key (it is not a config field;
598
+ // see `withConfig`) can never override the constructor's.
599
+ secret_id: this.secretIdValue.toString(),
463
600
  };
464
601
  const host = (getNative() as unknown as NativeHost).protocol_new(
465
602
  JSON.stringify(config),
@@ -591,6 +728,17 @@ export class DeRecProtocol {
591
728
  return decodeEvents(buffer);
592
729
  }
593
730
 
731
+ /**
732
+ * Accept a pending action from an `ActionRequired` event, returning the
733
+ * events it produces.
734
+ *
735
+ * When the action completes a pairing, the publish that follows reuses the
736
+ * stored helper weights. If one is no longer valid under the round's
737
+ * threshold, that publish fails with `invalid_weight` before anything is
738
+ * sent and this throws: the pairing has completed and its response was
739
+ * sent, but the `PairingCompleted` event is not returned. Call `start`
740
+ * with `ProtectSecret` and valid weights to publish.
741
+ */
594
742
  async accept(actionBytes: Uint8Array): Promise<DeRecEvent[]> {
595
743
  const buffer = (await this.host.accept(actionBytes)) as ArrayBuffer;
596
744
  return decodeEvents(buffer);
@@ -613,6 +761,13 @@ export class DeRecProtocol {
613
761
  * group's vault: the source's publish is then installed as it arrives,
614
762
  * with no further prompt. Ask the user before calling this; to decline,
615
763
  * never confirm.
764
+ *
765
+ * On a promotion the current snapshot is published to the promoted peer,
766
+ * reusing the stored helper weights. If one is no longer valid under the
767
+ * current threshold, that publish fails with `invalid_weight` before
768
+ * anything is sent and this throws. The promotion stands: calling this
769
+ * again resolves `true` and publishes nothing, so call `start` with
770
+ * `ProtectSecret` and valid weights to reach the promoted peer.
616
771
  */
617
772
  async verifyFingerprint(channelId: bigint | number, fingerprint: string): Promise<boolean> {
618
773
  return (await this.host.verifyFingerprint(toBigInt(channelId), fingerprint)) as boolean;
@@ -633,6 +788,9 @@ export class DeRecProtocol {
633
788
  * reported as a `PeerNotRestored` event in the returned array and the rest
634
789
  * of the roster is restored.
635
790
  *
791
+ * A helper's or member's `communication_info` may be absent or `null`;
792
+ * both mean the same thing — no entries.
793
+ *
636
794
  * Rejects with a `DeRecError`; among its codes:
637
795
  *
638
796
  * | code | meaning |