@derec-alliance/react-native 0.0.6 → 0.0.7

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/README.md CHANGED
@@ -125,6 +125,27 @@ longer needed (safe to call more than once).
125
125
 
126
126
  ---
127
127
 
128
+ ## When `process()` fails
129
+
130
+ `process()` settles expired deadlines (sharing-round and unpair timeouts)
131
+ before it handles the message, and those are never reported again. So when
132
+ the message then fails, the thrown `DeRecError` carries them: `events` holds
133
+ every event produced before the failure, and `channel_id` names the channel
134
+ the message came from (absent when the bytes were not a decodable envelope).
135
+ Handle the events as you would a successful call's, then the error:
136
+
137
+ ```ts
138
+ try {
139
+ handle(await protocol.process(bytes));
140
+ } catch (error) {
141
+ const failure = error as DeRecError;
142
+ handle(failure.events ?? []);
143
+ report(failure.channel_id, failure);
144
+ }
145
+ ```
146
+
147
+ ---
148
+
128
149
  ## Store interfaces
129
150
 
130
151
  The application layer owns all persistence; the library never writes to disk
@@ -134,6 +155,9 @@ declared in `src/types.ts`:
134
155
  - **`ChannelStore`** — pairing state: helper channels and replica-group
135
156
  membership, keyed by `(channelId, replicaId)`.
136
157
  - **`ShareStore`** — versioned VSS shares this device holds as a helper.
158
+ On an owner, `keepList` chooses which versions helpers keep; returning
159
+ `null` sends no keepList, so helpers keep every version; see
160
+ [the owner's keep list](https://github.com/derecalliance/lib-derec/tree/main/library#share-store-the-owners-keep-list).
137
161
  - **`SecretStore`** — small per-channel key material (shared keys, etc.),
138
162
  distinct from share bytes.
139
163
  - **`UserSecretStore`** — the most recent user-facing secret snapshot per
@@ -20,6 +20,13 @@ void throwDeRecError(jsi::Runtime& rt, const DeRecError& error) {
20
20
  void throwDeRecError(jsi::Runtime& rt,
21
21
  const DeRecError& error,
22
22
  const std::vector<uint8_t>& conflictingChannelIdsJson) {
23
+ throwDeRecError(rt, error, conflictingChannelIdsJson, {});
24
+ }
25
+
26
+ void throwDeRecError(jsi::Runtime& rt,
27
+ const DeRecError& error,
28
+ const std::vector<uint8_t>& conflictingChannelIdsJson,
29
+ const ProcessFailureDetails& process) {
23
30
  auto names = errorName(error.category, error.code);
24
31
  // `message` and `peer_memo` are Rust-owned strings; copy before releasing.
25
32
  std::string message =
@@ -53,6 +60,15 @@ void throwDeRecError(jsi::Runtime& rt,
53
60
  jsi::Value::createFromJsonUtf8(rt, conflictingChannelIdsJson.data(),
54
61
  conflictingChannelIdsJson.size()));
55
62
  }
63
+ if (process.hasChannelId) {
64
+ payload.setProperty(rt, "channel_id",
65
+ jsi::String::createFromUtf8(rt, std::to_string(process.channelId)));
66
+ }
67
+ if (!process.eventsJson.empty()) {
68
+ payload.setProperty(rt, "events",
69
+ jsi::Value::createFromJsonUtf8(rt, process.eventsJson.data(),
70
+ process.eventsJson.size()));
71
+ }
56
72
  throw jsi::JSError(rt, jsi::Value(rt, payload));
57
73
  }
58
74
 
package/cpp/Primitives.h CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  #include <jsi/jsi.h>
7
7
 
8
+ #include <cstdint>
8
9
  #include <vector>
9
10
 
10
11
  extern "C" {
@@ -25,6 +26,22 @@ namespace derec {
25
26
  const DeRecError& error,
26
27
  const std::vector<uint8_t>& conflictingChannelIdsJson);
27
28
 
29
+ /// What a failed `derec_protocol_process` adds to its error: the events it
30
+ /// produced before failing, and the channel the failing message came from.
31
+ struct ProcessFailureDetails {
32
+ std::vector<uint8_t> eventsJson;
33
+ bool hasChannelId = false;
34
+ uint64_t channelId = 0;
35
+ };
36
+
37
+ /// As above, additionally attaching `channel_id` (a decimal string) when
38
+ /// `process.hasChannelId` is set, and `events` (the JSON array verbatim)
39
+ /// when `process.eventsJson` is non-empty. Only `process` produces these.
40
+ [[noreturn]] void throwDeRecError(facebook::jsi::Runtime& rt,
41
+ const DeRecError& error,
42
+ const std::vector<uint8_t>& conflictingChannelIdsJson,
43
+ const ProcessFailureDetails& process);
44
+
28
45
  /// Read an `ArrayBuffer` or typed-array argument as a byte range.
29
46
  /// Returns `{nullptr, 0}` for `null`/`undefined`.
30
47
  struct ByteView {
@@ -28,10 +28,14 @@ struct FfiFailure {
28
28
  explicit FfiFailure(DeRecError e) : error(e) {}
29
29
  FfiFailure(DeRecError e, std::vector<uint8_t> channelIdsJson)
30
30
  : error(e), conflictingChannelIdsJson(std::move(channelIdsJson)) {}
31
+ FfiFailure(DeRecError e, ProcessFailureDetails details)
32
+ : error(e), process(std::move(details)) {}
31
33
  DeRecError error;
32
34
  /// `restore`'s JSON array of colliding channel ids, empty for every other
33
35
  /// failure. Carried as raw bytes: parsing needs the runtime.
34
36
  std::vector<uint8_t> conflictingChannelIdsJson;
37
+ /// `process`'s events and failing channel, empty for every other failure.
38
+ ProcessFailureDetails process;
35
39
  };
36
40
 
37
41
  constexpr const char* kReentrancyMessage =
@@ -176,11 +180,13 @@ jsi::Value ProtocolHost::runAsync(
176
180
  bool unknownFailure = false;
177
181
  DeRecError capturedError{};
178
182
  std::vector<uint8_t> capturedChannelIdsJson;
183
+ ProcessFailureDetails capturedProcess;
179
184
  try {
180
185
  bytes = body();
181
186
  } catch (FfiFailure& failure) {
182
187
  capturedError = failure.error;
183
188
  capturedChannelIdsJson = std::move(failure.conflictingChannelIdsJson);
189
+ capturedProcess = std::move(failure.process);
184
190
  failed = true;
185
191
  } catch (...) {
186
192
  // Anything other than `FfiFailure` — `std::bad_alloc` from
@@ -207,7 +213,9 @@ jsi::Value ProtocolHost::runAsync(
207
213
  bytes = std::move(bytes), failed,
208
214
  unknownFailure, capturedError,
209
215
  capturedChannelIdsJson =
210
- std::move(capturedChannelIdsJson)]() mutable {
216
+ std::move(capturedChannelIdsJson),
217
+ capturedProcess =
218
+ std::move(capturedProcess)]() mutable {
211
219
  if (state->runtimeInvalidated.load()) {
212
220
  // Queued before the runtime was torn down, reached only
213
221
  // afterwards. Every JSI call below — including the ones the
@@ -221,7 +229,8 @@ jsi::Value ProtocolHost::runAsync(
221
229
  // Reuses the exact error shape every synchronous FFI
222
230
  // wrapper throws, rather than inventing a second one for
223
231
  // the asynchronous path.
224
- throwDeRecError(jsRt, capturedError, capturedChannelIdsJson);
232
+ throwDeRecError(jsRt, capturedError, capturedChannelIdsJson,
233
+ capturedProcess);
225
234
  }
226
235
  if (unknownFailure) {
227
236
  throw jsi::JSError(jsRt, "DeRec: an unexpected native error occurred");
@@ -407,12 +416,16 @@ jsi::Value ProtocolHost::get(jsi::Runtime& rt, const jsi::PropNameID& name) {
407
416
 
408
417
  auto body = [handle,
409
418
  message = std::move(message)]() -> std::vector<uint8_t> {
410
- DeRecProtocolEventsResult result =
419
+ DeRecProtocolProcessResult result =
411
420
  derec_protocol_process(handle, message.data(), message.size());
421
+ std::vector<uint8_t> events = takeBuffer(result.events_json);
412
422
  if (result.error.code != DEREC_CODE_OK) {
413
- throw FfiFailure(result.error);
423
+ throw FfiFailure(result.error,
424
+ ProcessFailureDetails{std::move(events),
425
+ result.has_channel_id,
426
+ result.channel_id});
414
427
  }
415
- return takeBuffer(result.events_json);
428
+ return events;
416
429
  };
417
430
  return runAsync(rt2, std::move(body), bytesToArrayBuffer);
418
431
  });
@@ -27,7 +27,7 @@ static_assert(sizeof(ChannelStoreCallbacks) == 9 * sizeof(void*),
27
27
  "ChannelStoreCallbacks layout changed; update the bindings");
28
28
  static_assert(sizeof(SecretStoreCallbacks) == 6 * sizeof(void*),
29
29
  "SecretStoreCallbacks layout changed; update the bindings");
30
- static_assert(sizeof(ShareStoreCallbacks) == 8 * sizeof(void*),
30
+ static_assert(sizeof(ShareStoreCallbacks) == 10 * sizeof(void*),
31
31
  "ShareStoreCallbacks layout changed; update the bindings");
32
32
  static_assert(sizeof(UserSecretStoreCallbacks) == 5 * sizeof(void*),
33
33
  "UserSecretStoreCallbacks layout changed; update the bindings");
@@ -320,6 +320,40 @@ CallResult toStateLoadAllResult(jsi::Runtime& rt, const jsi::Value& value) {
320
320
  return CallResult{0, std::vector<uint8_t>(text.begin(), text.end())};
321
321
  }
322
322
 
323
+ /// `ShareStore.keepList(): Promise<number[] | null | undefined>`. Encodes the
324
+ /// resolved value as the wire's `Option<Vec<u32>>` JSON: `null` for
325
+ /// `null`/`undefined`, otherwise the array of versions.
326
+ ///
327
+ /// Anything else, or an entry that is not an integer in `0..=4294967295`, is
328
+ /// a backend failure: reading it as `null` would silently send an empty
329
+ /// keepList, so helpers would keep every version instead of the list the
330
+ /// application meant to send.
331
+ CallResult toKeepListResult(jsi::Runtime& rt, const jsi::Value& value) {
332
+ if (value.isNull() || value.isUndefined()) {
333
+ return CallResult{0, {'n', 'u', 'l', 'l'}};
334
+ }
335
+ if (!value.isObject() || !value.asObject(rt).isArray(rt)) {
336
+ return CallResult{kBackendFailure, {}};
337
+ }
338
+ jsi::Array arr = value.asObject(rt).asArray(rt);
339
+ size_t n = arr.size(rt);
340
+ std::string text = "[";
341
+ for (size_t i = 0; i < n; ++i) {
342
+ jsi::Value entry = arr.getValueAtIndex(rt, i);
343
+ if (!entry.isNumber()) {
344
+ return CallResult{kBackendFailure, {}};
345
+ }
346
+ double version = entry.asNumber();
347
+ if (!(version >= 0 && version <= 4294967295.0) || version != static_cast<double>(static_cast<uint32_t>(version))) {
348
+ return CallResult{kBackendFailure, {}};
349
+ }
350
+ if (i != 0) text += ',';
351
+ text += std::to_string(static_cast<uint32_t>(version));
352
+ }
353
+ text += ']';
354
+ return CallResult{0, std::vector<uint8_t>(text.begin(), text.end())};
355
+ }
356
+
323
357
  /// `UserSecretStore.loadLatest(): Promise<UserSecrets | null | undefined>`.
324
358
  CallResult toUserSecretsResult(jsi::Runtime& rt, const jsi::Value& value) {
325
359
  if (value.isNull() || value.isUndefined()) {
@@ -802,6 +836,53 @@ extern "C" int32_t shareStoreRemoveChannel(void* userData, uint64_t secretId, ui
802
836
  return result.code;
803
837
  }
804
838
 
839
+ /// `ShareStore.removeVersions(secretId, channelId, versions) -> void`
840
+ extern "C" int32_t shareStoreRemoveVersions(void* userData, uint64_t secretId, uint64_t channelId,
841
+ const uint8_t* versionsJsonPtr, size_t versionsJsonLen) {
842
+ auto* self = static_cast<StoreBindings*>(userData);
843
+ // Kept alive past this call so the lambda handed to `callSync` — which the
844
+ // JavaScript CallInvoker's queue may still be holding after this object's
845
+ // owner has released its own reference — has somewhere safe to run.
846
+ auto keepAlive = self->shared_from_this();
847
+ std::vector<uint64_t> raw = parseUnsignedJsonArray(versionsJsonPtr, versionsJsonLen);
848
+ std::vector<uint32_t> versions(raw.begin(), raw.end());
849
+ CallResult result = keepAlive->bridge().callSync(
850
+ [keepAlive, secretId, channelId, versions](std::function<void(CallResult)> settle) {
851
+ jsi::Runtime& rt = keepAlive->runtime();
852
+ auto store = keepAlive->shareStore();
853
+ auto method = store->getPropertyAsFunction(rt, "removeVersions");
854
+ jsi::Value promise = method.callWithThis(rt, *store, idVal(rt, secretId), idVal(rt, channelId),
855
+ jsi::Value(rt, u32VectorToJsArray(rt, versions)));
856
+ keepAlive->settleFromPromise(rt, std::move(promise), std::move(settle), toVoidResult);
857
+ });
858
+ return result.code;
859
+ }
860
+
861
+ /// `ShareStore.keepList(secretId, version) -> number[] | null`
862
+ extern "C" int32_t shareStoreKeepList(void* userData, uint64_t secretId, uint32_t version, uint8_t** outPtr,
863
+ size_t* outLen) {
864
+ auto* self = static_cast<StoreBindings*>(userData);
865
+ auto keepAlive = self->shared_from_this();
866
+ CallResult result =
867
+ keepAlive->bridge().callSync([keepAlive, secretId, version](std::function<void(CallResult)> settle) {
868
+ jsi::Runtime& rt = keepAlive->runtime();
869
+ auto store = keepAlive->shareStore();
870
+ auto method = store->getPropertyAsFunction(rt, "keepList");
871
+ jsi::Value promise =
872
+ method.callWithThis(rt, *store, idVal(rt, secretId), jsi::Value(static_cast<double>(version)));
873
+ keepAlive->settleFromPromise(rt, std::move(promise), std::move(settle), toKeepListResult);
874
+ });
875
+ if (result.code == 0) {
876
+ if (!writeOut(result.bytes, outPtr, outLen)) {
877
+ return kBackendFailure;
878
+ }
879
+ } else {
880
+ *outPtr = nullptr;
881
+ *outLen = 0;
882
+ }
883
+ return result.code;
884
+ }
885
+
805
886
  /// `UserSecretStore.loadLatest(secretId) -> UserSecrets | null`
806
887
  extern "C" int32_t userSecretStoreLoadLatest(void* userData, uint64_t secretId, uint8_t** outPtr,
807
888
  size_t* outLen) {
@@ -1123,8 +1204,10 @@ std::shared_ptr<StoreBindings> StoreBindings::create(jsi::Runtime& rt, jsi::Obje
1123
1204
  shareStoreLoadAll, // load_all
1124
1205
  shareStoreLatestVersion, // latest_version
1125
1206
  shareStoreSave, // save
1126
- shareStoreRemoveChannel, // remove_channel
1127
- storeFreeBuffer, // free_buffer
1207
+ shareStoreRemoveChannel, // remove_channel
1208
+ shareStoreRemoveVersions, // remove_versions
1209
+ shareStoreKeepList, // keep_list
1210
+ storeFreeBuffer, // free_buffer
1128
1211
  };
1129
1212
  bindings->userSecretCallbacks_ = UserSecretStoreCallbacks{
1130
1213
  self, // user_data
@@ -1170,6 +1253,8 @@ std::shared_ptr<StoreBindings> StoreBindings::create(jsi::Runtime& rt, jsi::Obje
1170
1253
  requireField(rt, bindings->shareCallbacks_.latest_version, "ShareStoreCallbacks.latest_version");
1171
1254
  requireField(rt, bindings->shareCallbacks_.save, "ShareStoreCallbacks.save");
1172
1255
  requireField(rt, bindings->shareCallbacks_.remove_channel, "ShareStoreCallbacks.remove_channel");
1256
+ requireField(rt, bindings->shareCallbacks_.remove_versions, "ShareStoreCallbacks.remove_versions");
1257
+ requireField(rt, bindings->shareCallbacks_.keep_list, "ShareStoreCallbacks.keep_list");
1173
1258
  requireField(rt, bindings->shareCallbacks_.free_buffer, "ShareStoreCallbacks.free_buffer");
1174
1259
 
1175
1260
  requireField(rt, bindings->userSecretCallbacks_.user_data, "UserSecretStoreCallbacks.user_data");
package/cpp/derec_ffi.h CHANGED
@@ -65,13 +65,6 @@
65
65
  */
66
66
  #define DEFAULT_THRESHOLD 3
67
67
 
68
- /**
69
- * Number of recent share versions each helper retains, absent an explicit
70
- * [`DeRecProtocolBuilder::with_keep_versions_count`](crate::protocol::DeRecProtocolBuilder::with_keep_versions_count) call. Sole definition
71
- * of the value; see [`DEFAULT_THRESHOLD`].
72
- */
73
- #define DEFAULT_KEEP_VERSIONS_COUNT 3
74
-
75
68
  /**
76
69
  * Maximum accepted transport URI length, in bytes.
77
70
  *
@@ -844,6 +837,11 @@ typedef struct SecretStoreCallbacks {
844
837
  * arrays (`channel_ids[]`, `versions[]`) cross the FFI as JSON
845
838
  * strings, matching the `Vec<u8>` ↔ JSON-array convention used for
846
839
  * every other wire-format buffer in this module.
840
+ *
841
+ * `keep_list` returns the JSON encoding of the application's answer:
842
+ * `null` for no list (the owner sends an empty `keepList` and helpers keep
843
+ * every version) or an array of version numbers. An empty buffer or return code `1` also
844
+ * means `null`.
847
845
  */
848
846
  typedef struct ShareStoreCallbacks {
849
847
  void *user_data;
@@ -878,6 +876,16 @@ typedef struct ShareStoreCallbacks {
878
876
  const uint8_t *share_json_ptr,
879
877
  size_t share_json_len);
880
878
  int32_t (*remove_channel)(void *user_data, uint64_t secret_id, uint64_t channel_id);
879
+ int32_t (*remove_versions)(void *user_data,
880
+ uint64_t secret_id,
881
+ uint64_t channel_id,
882
+ const uint8_t *versions_json_ptr,
883
+ size_t versions_json_len);
884
+ int32_t (*keep_list)(void *user_data,
885
+ uint64_t secret_id,
886
+ uint32_t version,
887
+ uint8_t **out_ptr,
888
+ size_t *out_len);
881
889
  void (*free_buffer)(void *user_data, uint8_t *ptr, size_t len);
882
890
  } ShareStoreCallbacks;
883
891
 
@@ -1001,6 +1009,30 @@ typedef struct DeRecProtocolEventsResult {
1001
1009
  struct DeRecBuffer events_json;
1002
1010
  } DeRecProtocolEventsResult;
1003
1011
 
1012
+ /**
1013
+ * Result of [`derec_protocol_process`].
1014
+ *
1015
+ * A failed call still carries the events produced before the failure in
1016
+ * `events_json`, and names the channel the failing message came from.
1017
+ */
1018
+ typedef struct DeRecProtocolProcessResult {
1019
+ struct DeRecError error;
1020
+ /**
1021
+ * UTF-8 JSON array of events, as on [`DeRecProtocolEventsResult`].
1022
+ * Filled on success and on failure.
1023
+ */
1024
+ struct DeRecBuffer events_json;
1025
+ /**
1026
+ * Whether `channel_id` is set. False on success and when the bytes
1027
+ * were not a decodable envelope.
1028
+ */
1029
+ bool has_channel_id;
1030
+ /**
1031
+ * The channel the failing message came from.
1032
+ */
1033
+ uint64_t channel_id;
1034
+ } DeRecProtocolProcessResult;
1035
+
1004
1036
  /**
1005
1037
  * Result of [`derec_protocol_restore`].
1006
1038
  *
@@ -1685,7 +1717,6 @@ struct ProcessPrePairResponseMessageResult process_pre_pair_response_message(con
1685
1717
  * "secret_id": "12345678901234567890",
1686
1718
  * "own_transports": [{ "uri": "https://example.com/derec", "protocol": "https" }],
1687
1719
  * "threshold": 3,
1688
- * "keep_versions_count": 3,
1689
1720
  * "auto_respond_on_failure": false,
1690
1721
  * "unpair_ack": 0,
1691
1722
  * "auto_reply_to": false,
@@ -1730,10 +1761,8 @@ struct ProcessPrePairResponseMessageResult process_pre_pair_response_message(con
1730
1761
  * two entries of the same protocol are rejected. Required: an absent or
1731
1762
  * empty list is refused, since a node with no endpoint cannot be reached
1732
1763
  * by any peer.
1733
- * - `threshold` / `keep_versions_count`: omitted means
1734
- * [`crate::protocol::DEFAULT_THRESHOLD`] /
1735
- * [`crate::protocol::DEFAULT_KEEP_VERSIONS_COUNT`]. A `threshold` below
1736
- * `2` is rejected with `DEREC_CODE_INVALID_INPUT`.
1764
+ * - `threshold`: omitted means [`crate::protocol::DEFAULT_THRESHOLD`]. A
1765
+ * `threshold` below `2` is rejected with `DEREC_CODE_INVALID_INPUT`.
1737
1766
  * - `unpair_ack`: `0` / `"required"` = Required, `1` / `"not_required"` =
1738
1767
  * NotRequired; omitted means Required.
1739
1768
  * Any other value is rejected with `DEREC_CODE_FFI_INVALID_ENUM`.
@@ -1880,7 +1909,8 @@ struct DeRecProtocolEventsResult derec_protocol_start(struct DeRecProtocolHandle
1880
1909
 
1881
1910
  /**
1882
1911
  * Process an inbound `DeRecMessage` envelope. See
1883
- * [`crate::protocol::DeRecProtocol::process`].
1912
+ * [`crate::protocol::DeRecProtocol::process`]. On failure the result still
1913
+ * carries the events produced before it and the failing channel.
1884
1914
  *
1885
1915
  * # Safety
1886
1916
  *
@@ -1888,9 +1918,9 @@ struct DeRecProtocolEventsResult derec_protocol_start(struct DeRecProtocolHandle
1888
1918
  * [`super::derec_protocol_new`]. `message_ptr`/`message_len` must
1889
1919
  * describe a readable byte range.
1890
1920
  */
1891
- struct DeRecProtocolEventsResult derec_protocol_process(struct DeRecProtocolHandle *handle,
1892
- const uint8_t *message_ptr,
1893
- size_t message_len);
1921
+ struct DeRecProtocolProcessResult derec_protocol_process(struct DeRecProtocolHandle *handle,
1922
+ const uint8_t *message_ptr,
1923
+ size_t message_len);
1894
1924
 
1895
1925
  /**
1896
1926
  * Advance time-driven state without an inbound message. See
@@ -10,15 +10,18 @@
10
10
  <key>HeadersPath</key>
11
11
  <string>Headers</string>
12
12
  <key>LibraryIdentifier</key>
13
- <string>ios-arm64</string>
13
+ <string>ios-arm64_x86_64-simulator</string>
14
14
  <key>LibraryPath</key>
15
15
  <string>libderec_library.a</string>
16
16
  <key>SupportedArchitectures</key>
17
17
  <array>
18
18
  <string>arm64</string>
19
+ <string>x86_64</string>
19
20
  </array>
20
21
  <key>SupportedPlatform</key>
21
22
  <string>ios</string>
23
+ <key>SupportedPlatformVariant</key>
24
+ <string>simulator</string>
22
25
  </dict>
23
26
  <dict>
24
27
  <key>BinaryPath</key>
@@ -26,18 +29,15 @@
26
29
  <key>HeadersPath</key>
27
30
  <string>Headers</string>
28
31
  <key>LibraryIdentifier</key>
29
- <string>ios-arm64_x86_64-simulator</string>
32
+ <string>ios-arm64</string>
30
33
  <key>LibraryPath</key>
31
34
  <string>libderec_library.a</string>
32
35
  <key>SupportedArchitectures</key>
33
36
  <array>
34
37
  <string>arm64</string>
35
- <string>x86_64</string>
36
38
  </array>
37
39
  <key>SupportedPlatform</key>
38
40
  <string>ios</string>
39
- <key>SupportedPlatformVariant</key>
40
- <string>simulator</string>
41
41
  </dict>
42
42
  </array>
43
43
  <key>CFBundlePackageType</key>
@@ -65,13 +65,6 @@
65
65
  */
66
66
  #define DEFAULT_THRESHOLD 3
67
67
 
68
- /**
69
- * Number of recent share versions each helper retains, absent an explicit
70
- * [`DeRecProtocolBuilder::with_keep_versions_count`](crate::protocol::DeRecProtocolBuilder::with_keep_versions_count) call. Sole definition
71
- * of the value; see [`DEFAULT_THRESHOLD`].
72
- */
73
- #define DEFAULT_KEEP_VERSIONS_COUNT 3
74
-
75
68
  /**
76
69
  * Maximum accepted transport URI length, in bytes.
77
70
  *
@@ -844,6 +837,11 @@ typedef struct SecretStoreCallbacks {
844
837
  * arrays (`channel_ids[]`, `versions[]`) cross the FFI as JSON
845
838
  * strings, matching the `Vec<u8>` ↔ JSON-array convention used for
846
839
  * every other wire-format buffer in this module.
840
+ *
841
+ * `keep_list` returns the JSON encoding of the application's answer:
842
+ * `null` for no list (the owner sends an empty `keepList` and helpers keep
843
+ * every version) or an array of version numbers. An empty buffer or return code `1` also
844
+ * means `null`.
847
845
  */
848
846
  typedef struct ShareStoreCallbacks {
849
847
  void *user_data;
@@ -878,6 +876,16 @@ typedef struct ShareStoreCallbacks {
878
876
  const uint8_t *share_json_ptr,
879
877
  size_t share_json_len);
880
878
  int32_t (*remove_channel)(void *user_data, uint64_t secret_id, uint64_t channel_id);
879
+ int32_t (*remove_versions)(void *user_data,
880
+ uint64_t secret_id,
881
+ uint64_t channel_id,
882
+ const uint8_t *versions_json_ptr,
883
+ size_t versions_json_len);
884
+ int32_t (*keep_list)(void *user_data,
885
+ uint64_t secret_id,
886
+ uint32_t version,
887
+ uint8_t **out_ptr,
888
+ size_t *out_len);
881
889
  void (*free_buffer)(void *user_data, uint8_t *ptr, size_t len);
882
890
  } ShareStoreCallbacks;
883
891
 
@@ -1001,6 +1009,30 @@ typedef struct DeRecProtocolEventsResult {
1001
1009
  struct DeRecBuffer events_json;
1002
1010
  } DeRecProtocolEventsResult;
1003
1011
 
1012
+ /**
1013
+ * Result of [`derec_protocol_process`].
1014
+ *
1015
+ * A failed call still carries the events produced before the failure in
1016
+ * `events_json`, and names the channel the failing message came from.
1017
+ */
1018
+ typedef struct DeRecProtocolProcessResult {
1019
+ struct DeRecError error;
1020
+ /**
1021
+ * UTF-8 JSON array of events, as on [`DeRecProtocolEventsResult`].
1022
+ * Filled on success and on failure.
1023
+ */
1024
+ struct DeRecBuffer events_json;
1025
+ /**
1026
+ * Whether `channel_id` is set. False on success and when the bytes
1027
+ * were not a decodable envelope.
1028
+ */
1029
+ bool has_channel_id;
1030
+ /**
1031
+ * The channel the failing message came from.
1032
+ */
1033
+ uint64_t channel_id;
1034
+ } DeRecProtocolProcessResult;
1035
+
1004
1036
  /**
1005
1037
  * Result of [`derec_protocol_restore`].
1006
1038
  *
@@ -1685,7 +1717,6 @@ struct ProcessPrePairResponseMessageResult process_pre_pair_response_message(con
1685
1717
  * "secret_id": "12345678901234567890",
1686
1718
  * "own_transports": [{ "uri": "https://example.com/derec", "protocol": "https" }],
1687
1719
  * "threshold": 3,
1688
- * "keep_versions_count": 3,
1689
1720
  * "auto_respond_on_failure": false,
1690
1721
  * "unpair_ack": 0,
1691
1722
  * "auto_reply_to": false,
@@ -1730,10 +1761,8 @@ struct ProcessPrePairResponseMessageResult process_pre_pair_response_message(con
1730
1761
  * two entries of the same protocol are rejected. Required: an absent or
1731
1762
  * empty list is refused, since a node with no endpoint cannot be reached
1732
1763
  * by any peer.
1733
- * - `threshold` / `keep_versions_count`: omitted means
1734
- * [`crate::protocol::DEFAULT_THRESHOLD`] /
1735
- * [`crate::protocol::DEFAULT_KEEP_VERSIONS_COUNT`]. A `threshold` below
1736
- * `2` is rejected with `DEREC_CODE_INVALID_INPUT`.
1764
+ * - `threshold`: omitted means [`crate::protocol::DEFAULT_THRESHOLD`]. A
1765
+ * `threshold` below `2` is rejected with `DEREC_CODE_INVALID_INPUT`.
1737
1766
  * - `unpair_ack`: `0` / `"required"` = Required, `1` / `"not_required"` =
1738
1767
  * NotRequired; omitted means Required.
1739
1768
  * Any other value is rejected with `DEREC_CODE_FFI_INVALID_ENUM`.
@@ -1880,7 +1909,8 @@ struct DeRecProtocolEventsResult derec_protocol_start(struct DeRecProtocolHandle
1880
1909
 
1881
1910
  /**
1882
1911
  * Process an inbound `DeRecMessage` envelope. See
1883
- * [`crate::protocol::DeRecProtocol::process`].
1912
+ * [`crate::protocol::DeRecProtocol::process`]. On failure the result still
1913
+ * carries the events produced before it and the failing channel.
1884
1914
  *
1885
1915
  * # Safety
1886
1916
  *
@@ -1888,9 +1918,9 @@ struct DeRecProtocolEventsResult derec_protocol_start(struct DeRecProtocolHandle
1888
1918
  * [`super::derec_protocol_new`]. `message_ptr`/`message_len` must
1889
1919
  * describe a readable byte range.
1890
1920
  */
1891
- struct DeRecProtocolEventsResult derec_protocol_process(struct DeRecProtocolHandle *handle,
1892
- const uint8_t *message_ptr,
1893
- size_t message_len);
1921
+ struct DeRecProtocolProcessResult derec_protocol_process(struct DeRecProtocolHandle *handle,
1922
+ const uint8_t *message_ptr,
1923
+ size_t message_len);
1894
1924
 
1895
1925
  /**
1896
1926
  * Advance time-driven state without an inbound message. See
@@ -65,13 +65,6 @@
65
65
  */
66
66
  #define DEFAULT_THRESHOLD 3
67
67
 
68
- /**
69
- * Number of recent share versions each helper retains, absent an explicit
70
- * [`DeRecProtocolBuilder::with_keep_versions_count`](crate::protocol::DeRecProtocolBuilder::with_keep_versions_count) call. Sole definition
71
- * of the value; see [`DEFAULT_THRESHOLD`].
72
- */
73
- #define DEFAULT_KEEP_VERSIONS_COUNT 3
74
-
75
68
  /**
76
69
  * Maximum accepted transport URI length, in bytes.
77
70
  *
@@ -844,6 +837,11 @@ typedef struct SecretStoreCallbacks {
844
837
  * arrays (`channel_ids[]`, `versions[]`) cross the FFI as JSON
845
838
  * strings, matching the `Vec<u8>` ↔ JSON-array convention used for
846
839
  * every other wire-format buffer in this module.
840
+ *
841
+ * `keep_list` returns the JSON encoding of the application's answer:
842
+ * `null` for no list (the owner sends an empty `keepList` and helpers keep
843
+ * every version) or an array of version numbers. An empty buffer or return code `1` also
844
+ * means `null`.
847
845
  */
848
846
  typedef struct ShareStoreCallbacks {
849
847
  void *user_data;
@@ -878,6 +876,16 @@ typedef struct ShareStoreCallbacks {
878
876
  const uint8_t *share_json_ptr,
879
877
  size_t share_json_len);
880
878
  int32_t (*remove_channel)(void *user_data, uint64_t secret_id, uint64_t channel_id);
879
+ int32_t (*remove_versions)(void *user_data,
880
+ uint64_t secret_id,
881
+ uint64_t channel_id,
882
+ const uint8_t *versions_json_ptr,
883
+ size_t versions_json_len);
884
+ int32_t (*keep_list)(void *user_data,
885
+ uint64_t secret_id,
886
+ uint32_t version,
887
+ uint8_t **out_ptr,
888
+ size_t *out_len);
881
889
  void (*free_buffer)(void *user_data, uint8_t *ptr, size_t len);
882
890
  } ShareStoreCallbacks;
883
891
 
@@ -1001,6 +1009,30 @@ typedef struct DeRecProtocolEventsResult {
1001
1009
  struct DeRecBuffer events_json;
1002
1010
  } DeRecProtocolEventsResult;
1003
1011
 
1012
+ /**
1013
+ * Result of [`derec_protocol_process`].
1014
+ *
1015
+ * A failed call still carries the events produced before the failure in
1016
+ * `events_json`, and names the channel the failing message came from.
1017
+ */
1018
+ typedef struct DeRecProtocolProcessResult {
1019
+ struct DeRecError error;
1020
+ /**
1021
+ * UTF-8 JSON array of events, as on [`DeRecProtocolEventsResult`].
1022
+ * Filled on success and on failure.
1023
+ */
1024
+ struct DeRecBuffer events_json;
1025
+ /**
1026
+ * Whether `channel_id` is set. False on success and when the bytes
1027
+ * were not a decodable envelope.
1028
+ */
1029
+ bool has_channel_id;
1030
+ /**
1031
+ * The channel the failing message came from.
1032
+ */
1033
+ uint64_t channel_id;
1034
+ } DeRecProtocolProcessResult;
1035
+
1004
1036
  /**
1005
1037
  * Result of [`derec_protocol_restore`].
1006
1038
  *
@@ -1685,7 +1717,6 @@ struct ProcessPrePairResponseMessageResult process_pre_pair_response_message(con
1685
1717
  * "secret_id": "12345678901234567890",
1686
1718
  * "own_transports": [{ "uri": "https://example.com/derec", "protocol": "https" }],
1687
1719
  * "threshold": 3,
1688
- * "keep_versions_count": 3,
1689
1720
  * "auto_respond_on_failure": false,
1690
1721
  * "unpair_ack": 0,
1691
1722
  * "auto_reply_to": false,
@@ -1730,10 +1761,8 @@ struct ProcessPrePairResponseMessageResult process_pre_pair_response_message(con
1730
1761
  * two entries of the same protocol are rejected. Required: an absent or
1731
1762
  * empty list is refused, since a node with no endpoint cannot be reached
1732
1763
  * by any peer.
1733
- * - `threshold` / `keep_versions_count`: omitted means
1734
- * [`crate::protocol::DEFAULT_THRESHOLD`] /
1735
- * [`crate::protocol::DEFAULT_KEEP_VERSIONS_COUNT`]. A `threshold` below
1736
- * `2` is rejected with `DEREC_CODE_INVALID_INPUT`.
1764
+ * - `threshold`: omitted means [`crate::protocol::DEFAULT_THRESHOLD`]. A
1765
+ * `threshold` below `2` is rejected with `DEREC_CODE_INVALID_INPUT`.
1737
1766
  * - `unpair_ack`: `0` / `"required"` = Required, `1` / `"not_required"` =
1738
1767
  * NotRequired; omitted means Required.
1739
1768
  * Any other value is rejected with `DEREC_CODE_FFI_INVALID_ENUM`.
@@ -1880,7 +1909,8 @@ struct DeRecProtocolEventsResult derec_protocol_start(struct DeRecProtocolHandle
1880
1909
 
1881
1910
  /**
1882
1911
  * Process an inbound `DeRecMessage` envelope. See
1883
- * [`crate::protocol::DeRecProtocol::process`].
1912
+ * [`crate::protocol::DeRecProtocol::process`]. On failure the result still
1913
+ * carries the events produced before it and the failing channel.
1884
1914
  *
1885
1915
  * # Safety
1886
1916
  *
@@ -1888,9 +1918,9 @@ struct DeRecProtocolEventsResult derec_protocol_start(struct DeRecProtocolHandle
1888
1918
  * [`super::derec_protocol_new`]. `message_ptr`/`message_len` must
1889
1919
  * describe a readable byte range.
1890
1920
  */
1891
- struct DeRecProtocolEventsResult derec_protocol_process(struct DeRecProtocolHandle *handle,
1892
- const uint8_t *message_ptr,
1893
- size_t message_len);
1921
+ struct DeRecProtocolProcessResult derec_protocol_process(struct DeRecProtocolHandle *handle,
1922
+ const uint8_t *message_ptr,
1923
+ size_t message_len);
1894
1924
 
1895
1925
  /**
1896
1926
  * Advance time-driven state without an inbound message. See
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/ios/DeRecFFI.xcframework/ios-arm64_x86_64-simulator/libderec_library.a
3
- size: 38440744 bytes
4
- sha256: d9c6ce3666c5b27a2fad3b79a4e82af22204b72fb203ddb08bc5a6d6cc498bab
3
+ size: 38454248 bytes
4
+ sha256: 8e4fa480cdb006907de3fbd3c13011ee89816dc78b4c1c22b6a730297f29521a
package/lib/errors.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { DeRecEvent } from './types';
1
2
  /**
2
3
  * Every category name `derec_error_category_name` can return, and no others.
3
4
  *
@@ -27,4 +28,12 @@ export interface DeRecError {
27
28
  * Clear exactly these and retry `restore`.
28
29
  */
29
30
  channel_ids?: string[];
31
+ /** The channel the failing inbound message arrived on, when `process()` could tell. */
32
+ channel_id?: string;
33
+ /**
34
+ * On a failed `process()`: the events it produced before failing, such as
35
+ * sharing-round and unpair timeouts. They are not reported again, so
36
+ * handle them as you would a successful call's events.
37
+ */
38
+ events?: DeRecEvent[];
30
39
  }
package/lib/protocol.d.ts CHANGED
@@ -38,9 +38,9 @@ export declare function buildRestoreParams(recoveredSecret: Extract<DeRecEvent,
38
38
  *
39
39
  * Every value the caller supplies is forwarded verbatim — defaults, clamping
40
40
  * and the meaning of a disabled cleanup policy are library decisions.
41
- * `threshold`, `keep_versions_count`, `auto_respond_on_failure`,
42
- * `unpair_ack`, `auto_reply_to` and `auto_accept` are omitted entirely from
43
- * the emitted config unless their setter was called: `ProtocolConfig` has a
41
+ * `threshold`, `auto_respond_on_failure`, `unpair_ack`, `auto_reply_to`
42
+ * and `auto_accept` are omitted entirely from the emitted config unless
43
+ * their setter was called: `ProtocolConfig` has a
44
44
  * `#[serde(default = "...")]` on each of them, reading the same constants
45
45
  * `DeRecProtocolBuilder::new` uses, so an absent key resolves to the
46
46
  * library's own default rather than one frozen into this shim.
@@ -74,8 +74,6 @@ export declare class DeRecProtocolBuilder {
74
74
  withOwnTransports(transports: Endpoint[]): this;
75
75
  /** Default: 3. */
76
76
  withThreshold(threshold: number): this;
77
- /** Default: 3. */
78
- withKeepVersionsCount(count: number): this;
79
77
  /**
80
78
  * Every field is optional; not calling this at all leaves all four at the
81
79
  * library's defaults — `timeouts` is genuinely absent from the JSON in
@@ -191,11 +189,24 @@ export declare class DeRecProtocol {
191
189
  accept(actionBytes: Uint8Array): Promise<DeRecEvent[]>;
192
190
  reject(actionBytes: Uint8Array, status: StatusEnum, memo: string): Promise<void>;
193
191
  getFingerprint(channelId: bigint | number): Promise<string>;
192
+ /**
193
+ * Verify `fingerprint` against the channel's locally-derived one. On
194
+ * match, the channel transitions from `Pending` to `Paired`. Returns
195
+ * `true` on confirmation, `false` on mismatch.
196
+ *
197
+ * On a replica destination, confirming is also the decision to adopt the
198
+ * group's vault: the source's publish is then installed as it arrives,
199
+ * with no further prompt. Ask the user before calling this; to decline,
200
+ * never confirm.
201
+ */
194
202
  verifyFingerprint(channelId: bigint | number, fingerprint: string): Promise<boolean>;
195
203
  removeExpiredChannels(olderThanSecs: number): Promise<string[]>;
196
204
  /**
197
205
  * Rebuild this protocol's `secret_id` namespace from a recovered `Secret`.
198
206
  * Pass the `secret` carried by the `SecretRecovered` event verbatim.
207
+ * Recovery and restore are separate steps on purpose: `SecretRecovered`
208
+ * writes nothing. Show the user what was recovered, or ask them, before
209
+ * calling `restore`, which commits it to this device.
199
210
  * A helper or member whose `transports` is empty gets no channel: it is
200
211
  * reported as a `PeerNotRestored` event in the returned array and the rest
201
212
  * of the roster is restored.
package/lib/protocol.js CHANGED
@@ -234,9 +234,9 @@ function toBigInt(id) {
234
234
  *
235
235
  * Every value the caller supplies is forwarded verbatim — defaults, clamping
236
236
  * and the meaning of a disabled cleanup policy are library decisions.
237
- * `threshold`, `keep_versions_count`, `auto_respond_on_failure`,
238
- * `unpair_ack`, `auto_reply_to` and `auto_accept` are omitted entirely from
239
- * the emitted config unless their setter was called: `ProtocolConfig` has a
237
+ * `threshold`, `auto_respond_on_failure`, `unpair_ack`, `auto_reply_to`
238
+ * and `auto_accept` are omitted entirely from the emitted config unless
239
+ * their setter was called: `ProtocolConfig` has a
240
240
  * `#[serde(default = "...")]` on each of them, reading the same constants
241
241
  * `DeRecProtocolBuilder::new` uses, so an absent key resolves to the
242
242
  * library's own default rather than one frozen into this shim.
@@ -295,11 +295,6 @@ class DeRecProtocolBuilder {
295
295
  this.config.threshold = threshold;
296
296
  return this;
297
297
  }
298
- /** Default: 3. */
299
- withKeepVersionsCount(count) {
300
- this.config.keep_versions_count = count;
301
- return this;
302
- }
303
298
  /**
304
299
  * Every field is optional; not calling this at all leaves all four at the
305
300
  * library's defaults — `timeouts` is genuinely absent from the JSON in
@@ -478,7 +473,17 @@ class DeRecProtocol {
478
473
  return decodeEvents(buffer);
479
474
  }
480
475
  async process(message) {
481
- const buffer = (await this.host.process(message));
476
+ let buffer;
477
+ try {
478
+ buffer = (await this.host.process(message));
479
+ }
480
+ catch (error) {
481
+ const failure = error;
482
+ if (Array.isArray(failure.events)) {
483
+ failure.events = reviveEventBytes(failure.events);
484
+ }
485
+ throw error;
486
+ }
482
487
  return decodeEvents(buffer);
483
488
  }
484
489
  async tick() {
@@ -495,6 +500,16 @@ class DeRecProtocol {
495
500
  async getFingerprint(channelId) {
496
501
  return (await this.host.getFingerprint(toBigInt(channelId)));
497
502
  }
503
+ /**
504
+ * Verify `fingerprint` against the channel's locally-derived one. On
505
+ * match, the channel transitions from `Pending` to `Paired`. Returns
506
+ * `true` on confirmation, `false` on mismatch.
507
+ *
508
+ * On a replica destination, confirming is also the decision to adopt the
509
+ * group's vault: the source's publish is then installed as it arrives,
510
+ * with no further prompt. Ask the user before calling this; to decline,
511
+ * never confirm.
512
+ */
498
513
  async verifyFingerprint(channelId, fingerprint) {
499
514
  return (await this.host.verifyFingerprint(toBigInt(channelId), fingerprint));
500
515
  }
@@ -505,6 +520,9 @@ class DeRecProtocol {
505
520
  /**
506
521
  * Rebuild this protocol's `secret_id` namespace from a recovered `Secret`.
507
522
  * Pass the `secret` carried by the `SecretRecovered` event verbatim.
523
+ * Recovery and restore are separate steps on purpose: `SecretRecovered`
524
+ * writes nothing. Show the user what was recovered, or ask them, before
525
+ * calling `restore`, which commits it to this device.
508
526
  * A helper or member whose `transports` is empty gets no channel: it is
509
527
  * reported as a `PeerNotRestored` event in the returned array and the rest
510
528
  * of the roster is restored.
package/lib/types.d.ts CHANGED
@@ -191,6 +191,35 @@ export interface ShareStore {
191
191
  save(secretId: string, channelId: string, share: Share): Promise<void>;
192
192
  latestVersion(secretId: string): Promise<number | null>;
193
193
  removeChannel(secretId: string, channelId: string): Promise<void>;
194
+ /**
195
+ * Drop the shares stored under `(secretId, channelId)` at each of
196
+ * `versions`. Idempotent: a version that is not stored is skipped, and
197
+ * an empty array is a no-op.
198
+ *
199
+ * A helper calls this to apply `StoreShareRequestMessage.keepList`, the
200
+ * complete set of versions the owner wants retained: every stored
201
+ * version outside it is removed once the incoming share is persisted.
202
+ * Shares under other channels or partitions must be left untouched.
203
+ */
204
+ removeVersions(secretId: string, channelId: string, versions: number[]): Promise<void>;
205
+ /**
206
+ * Owner only: the versions every helper keeps after the owner distributes
207
+ * `version`. Asked once per sharing round, before anything is sent,
208
+ * including the rounds the library starts itself; the answer becomes
209
+ * `keepList` for every helper.
210
+ *
211
+ * Return `null` or `undefined` to send no `keepList` (it goes out empty):
212
+ * helpers then keep every version they hold. An app that wants to cap how
213
+ * many versions helpers retain returns that cap here. A returned list is
214
+ * used as is, plus `version`, which the library always adds. Helpers
215
+ * delete every version that is not listed, so list every version that
216
+ * could still become the latest: those that committed (for example, whose
217
+ * `SharingComplete` reported `threshold_met`) and those whose round is
218
+ * still open. Leave out only versions whose round failed or that the user
219
+ * rolled back; a list that leaves out too much can make the secret
220
+ * unrecoverable.
221
+ */
222
+ keepList(secretId: string, version: number): Promise<number[] | null | undefined>;
194
223
  }
195
224
  export interface UserSecretEntry {
196
225
  id: Uint8Array;
@@ -519,6 +548,15 @@ export interface Timeouts {
519
548
  /** `ReplicaDiscovery` takes no parameters: the group and this device's own version
520
549
  * are both read from the stores. The argument may be omitted entirely. */
521
550
  export type ReplicaDiscoveryParams = Record<string, never>;
551
+ /** Any member may remove any member, the source included: a lost or stolen
552
+ * source must be removable by the devices that remain, and the library
553
+ * checks no role. Ask the user before starting this flow, above all when it
554
+ * names the source. Removing the source promotes the first remaining member
555
+ * in the order the channel store's `listReplicas` returns. The removed
556
+ * member is not asked and gets no event when told to leave: when a roster
557
+ * excluding it arrives it drops its whole `secret_id` partition and emits
558
+ * `SelfRemovedFromGroup`. The secret survives on the remaining members and
559
+ * the helpers. */
522
560
  export interface UnpairReplicaParams {
523
561
  /** The member to remove, as a **decimal** `u64` string — the same form
524
562
  * `ReplicaPaired.peer_replica_id` hands back. A value naming no current
@@ -601,8 +639,11 @@ export type DeRecEvent = {
601
639
  }
602
640
  /** A group member refused a secret sync. Keyed by `replica_id`, not
603
641
  * `channel_id`: every member answers on the one group channel. A
604
- * `VERSION_CONFLICT` status means the round must be resolved and
605
- * republished at a new version. */
642
+ * `VERSION_CONFLICT` status means another member holds a different copy
643
+ * of this version: do not publish from this device again until the
644
+ * conflict is resolved. Run `start(FlowKind.ReplicaDiscovery)` to receive
645
+ * the group's copy as `ReplicaVersionConflict`, merge, and publish the
646
+ * result once with `start(FlowKind.ProtectSecret)`. */
606
647
  | {
607
648
  type: "ReplicaSyncRejected";
608
649
  replica_id: string;
@@ -636,7 +677,8 @@ export type DeRecEvent = {
636
677
  /** This device left the group and dropped its whole `secret_id` partition —
637
678
  * group channel, helper channels, shares, secrets and the snapshot. Fires
638
679
  * only once it was told to leave *and* has since seen a roster excluding
639
- * it; absence alone never destroys a copy of the secret. */
680
+ * it; absence alone never destroys a copy of the secret. The teardown is
681
+ * automatic: this device is not asked first and gets no earlier event. */
640
682
  | {
641
683
  type: "SelfRemovedFromGroup";
642
684
  version: number;
@@ -662,6 +704,16 @@ export type DeRecEvent = {
662
704
  type: "ShareVerified";
663
705
  channel_id: string;
664
706
  version: number;
707
+ }
708
+ /** A helper refused a verification challenge: its response carried a
709
+ * non-OK `status` instead of a proof. The challenge is spent; a new
710
+ * `VerifyShares` round challenges the helper again. */
711
+ | {
712
+ type: "ShareVerifyRejected";
713
+ channel_id: string;
714
+ version: number;
715
+ status: StatusEnum;
716
+ memo: string;
665
717
  } | {
666
718
  type: "SecretsDiscovered";
667
719
  channel_id: string;
@@ -682,6 +734,33 @@ export type DeRecEvent = {
682
734
  shares_received: number;
683
735
  error: string;
684
736
  }
737
+ /** A helper refused a recovery share request: its response carried a
738
+ * non-OK `status` (e.g. `UNKNOWN_SHARE_VERSION`) instead of a share. The
739
+ * refusal is not collected — it does not count towards `shares_received`
740
+ * and the recovery stays open for the other helpers' shares — but it does
741
+ * answer that helper's `RecoverSecretStarted`. */
742
+ | {
743
+ type: "RecoveryShareRefused";
744
+ channel_id: string;
745
+ version: number;
746
+ status: StatusEnum;
747
+ memo: string;
748
+ }
749
+ /** A helper answered with a share that cannot be part of the secret;
750
+ * `reason` says how it failed. `Malformed` and `InvalidProof` are judged
751
+ * on arrival; `Inconsistent` (valid on its own but disagreeing with the
752
+ * shares the secret was rebuilt from) is reported alongside
753
+ * `SecretRecovered`, once per helper. The share is set aside — it does
754
+ * not count towards `shares_received` and never blocks the recovery. An
755
+ * honest helper never sends one, so the app may treat it as a sign of a
756
+ * damaged or compromised helper, e.g. offer to unpair it. It also
757
+ * answers that helper's `RecoverSecretStarted`. */
758
+ | {
759
+ type: "RecoveryShareCorrupted";
760
+ channel_id: string;
761
+ version: number;
762
+ reason: CorruptionReason;
763
+ }
685
764
  /** Recovery completed — the typed `Secret` snapshot the owner
686
765
  * originally protected. Mirrors `ReplicaSecretReceived.secret`:
687
766
  * `secrets` is the user-facing `Vec<UserSecret>` the application
@@ -866,7 +945,10 @@ export type DeRecEvent = {
866
945
  * `ReplicaSyncRejected`. Both copies are complete states — the held one
867
946
  * is in the local stores, the incoming one is `secret`. Resolve by
868
947
  * publishing the chosen state with `start(FlowKind.ProtectSecret)`; the
869
- * next version supersedes both on every member and helper.
948
+ * next version supersedes both on every member and helper. Until then,
949
+ * do not publish from this device: any further `ProtectSecret` is a
950
+ * higher version that every other member applies over its own copy,
951
+ * losing the change it never merged.
870
952
  *
871
953
  * `held_author_replica_id` / `incoming_author_replica_id` are the
872
954
  * decimal `replica_id` of each copy's publisher, or `null` when that
@@ -1177,6 +1259,13 @@ export type IgnoreReason = "PendingVerification" | "Expired";
1177
1259
  /** Why a `PeerNotRestored` event left a roster entry without a channel.
1178
1260
  * Matches the Rust `NotRestoredReason` discriminants one-for-one. */
1179
1261
  export type NotRestoredReason = "NoTransports";
1262
+ /** Why a `RecoveryShareCorrupted` event set a helper's share aside.
1263
+ * Matches the Rust `CorruptionReason` discriminants one-for-one:
1264
+ * `Malformed` — no decodable share for the requested secret and version;
1265
+ * `InvalidProof` — the share fails its own Merkle proof;
1266
+ * `Inconsistent` — valid on its own, but its commitment root or ciphertext
1267
+ * disagrees with the shares the secret was rebuilt from. */
1268
+ export type CorruptionReason = "Malformed" | "InvalidProof" | "Inconsistent";
1180
1269
  /**
1181
1270
  * The label vocabulary for `ActionRequired.action_kind` and
1182
1271
  * `AutoAccepted.action_kind` — one value per pending-action kind the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@derec-alliance/react-native",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
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
@@ -1,6 +1,8 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
3
 
4
+ import type { DeRecEvent } from './types';
5
+
4
6
  /**
5
7
  * Every category name `derec_error_category_name` can return, and no others.
6
8
  *
@@ -49,4 +51,12 @@ export interface DeRecError {
49
51
  * Clear exactly these and retry `restore`.
50
52
  */
51
53
  channel_ids?: string[];
54
+ /** The channel the failing inbound message arrived on, when `process()` could tell. */
55
+ channel_id?: string;
56
+ /**
57
+ * On a failed `process()`: the events it produced before failing, such as
58
+ * sharing-round and unpair timeouts. They are not reported again, so
59
+ * handle them as you would a successful call's events.
60
+ */
61
+ events?: DeRecEvent[];
52
62
  }
package/src/protocol.ts CHANGED
@@ -280,9 +280,9 @@ function toBigInt(id: bigint | number): bigint {
280
280
  *
281
281
  * Every value the caller supplies is forwarded verbatim — defaults, clamping
282
282
  * and the meaning of a disabled cleanup policy are library decisions.
283
- * `threshold`, `keep_versions_count`, `auto_respond_on_failure`,
284
- * `unpair_ack`, `auto_reply_to` and `auto_accept` are omitted entirely from
285
- * the emitted config unless their setter was called: `ProtocolConfig` has a
283
+ * `threshold`, `auto_respond_on_failure`, `unpair_ack`, `auto_reply_to`
284
+ * and `auto_accept` are omitted entirely from the emitted config unless
285
+ * their setter was called: `ProtocolConfig` has a
286
286
  * `#[serde(default = "...")]` on each of them, reading the same constants
287
287
  * `DeRecProtocolBuilder::new` uses, so an absent key resolves to the
288
288
  * library's own default rather than one frozen into this shim.
@@ -352,12 +352,6 @@ export class DeRecProtocolBuilder {
352
352
  return this;
353
353
  }
354
354
 
355
- /** Default: 3. */
356
- withKeepVersionsCount(count: number): this {
357
- this.config.keep_versions_count = count;
358
- return this;
359
- }
360
-
361
355
  /**
362
356
  * Every field is optional; not calling this at all leaves all four at the
363
357
  * library's defaults — `timeouts` is genuinely absent from the JSON in
@@ -579,7 +573,16 @@ export class DeRecProtocol {
579
573
  }
580
574
 
581
575
  async process(message: Uint8Array): Promise<DeRecEvent[]> {
582
- const buffer = (await this.host.process(message)) as ArrayBuffer;
576
+ let buffer: ArrayBuffer;
577
+ try {
578
+ buffer = (await this.host.process(message)) as ArrayBuffer;
579
+ } catch (error) {
580
+ const failure = error as { events?: unknown };
581
+ if (Array.isArray(failure.events)) {
582
+ failure.events = reviveEventBytes(failure.events);
583
+ }
584
+ throw error;
585
+ }
583
586
  return decodeEvents(buffer);
584
587
  }
585
588
 
@@ -601,6 +604,16 @@ export class DeRecProtocol {
601
604
  return (await this.host.getFingerprint(toBigInt(channelId))) as string;
602
605
  }
603
606
 
607
+ /**
608
+ * Verify `fingerprint` against the channel's locally-derived one. On
609
+ * match, the channel transitions from `Pending` to `Paired`. Returns
610
+ * `true` on confirmation, `false` on mismatch.
611
+ *
612
+ * On a replica destination, confirming is also the decision to adopt the
613
+ * group's vault: the source's publish is then installed as it arrives,
614
+ * with no further prompt. Ask the user before calling this; to decline,
615
+ * never confirm.
616
+ */
604
617
  async verifyFingerprint(channelId: bigint | number, fingerprint: string): Promise<boolean> {
605
618
  return (await this.host.verifyFingerprint(toBigInt(channelId), fingerprint)) as boolean;
606
619
  }
@@ -613,6 +626,9 @@ export class DeRecProtocol {
613
626
  /**
614
627
  * Rebuild this protocol's `secret_id` namespace from a recovered `Secret`.
615
628
  * Pass the `secret` carried by the `SecretRecovered` event verbatim.
629
+ * Recovery and restore are separate steps on purpose: `SecretRecovered`
630
+ * writes nothing. Show the user what was recovered, or ask them, before
631
+ * calling `restore`, which commits it to this device.
616
632
  * A helper or member whose `transports` is empty gets no channel: it is
617
633
  * reported as a `PeerNotRestored` event in the returned array and the rest
618
634
  * of the roster is restored.
package/src/types.ts CHANGED
@@ -261,6 +261,42 @@ export interface ShareStore {
261
261
  save(secretId: string, channelId: string, share: Share): Promise<void>;
262
262
  latestVersion(secretId: string): Promise<number | null>;
263
263
  removeChannel(secretId: string, channelId: string): Promise<void>;
264
+ /**
265
+ * Drop the shares stored under `(secretId, channelId)` at each of
266
+ * `versions`. Idempotent: a version that is not stored is skipped, and
267
+ * an empty array is a no-op.
268
+ *
269
+ * A helper calls this to apply `StoreShareRequestMessage.keepList`, the
270
+ * complete set of versions the owner wants retained: every stored
271
+ * version outside it is removed once the incoming share is persisted.
272
+ * Shares under other channels or partitions must be left untouched.
273
+ */
274
+ removeVersions(
275
+ secretId: string,
276
+ channelId: string,
277
+ versions: number[],
278
+ ): Promise<void>;
279
+ /**
280
+ * Owner only: the versions every helper keeps after the owner distributes
281
+ * `version`. Asked once per sharing round, before anything is sent,
282
+ * including the rounds the library starts itself; the answer becomes
283
+ * `keepList` for every helper.
284
+ *
285
+ * Return `null` or `undefined` to send no `keepList` (it goes out empty):
286
+ * helpers then keep every version they hold. An app that wants to cap how
287
+ * many versions helpers retain returns that cap here. A returned list is
288
+ * used as is, plus `version`, which the library always adds. Helpers
289
+ * delete every version that is not listed, so list every version that
290
+ * could still become the latest: those that committed (for example, whose
291
+ * `SharingComplete` reported `threshold_met`) and those whose round is
292
+ * still open. Leave out only versions whose round failed or that the user
293
+ * rolled back; a list that leaves out too much can make the secret
294
+ * unrecoverable.
295
+ */
296
+ keepList(
297
+ secretId: string,
298
+ version: number,
299
+ ): Promise<number[] | null | undefined>;
264
300
  }
265
301
 
266
302
  export interface UserSecretEntry {
@@ -653,6 +689,15 @@ export interface Timeouts {
653
689
  * are both read from the stores. The argument may be omitted entirely. */
654
690
  export type ReplicaDiscoveryParams = Record<string, never>;
655
691
 
692
+ /** Any member may remove any member, the source included: a lost or stolen
693
+ * source must be removable by the devices that remain, and the library
694
+ * checks no role. Ask the user before starting this flow, above all when it
695
+ * names the source. Removing the source promotes the first remaining member
696
+ * in the order the channel store's `listReplicas` returns. The removed
697
+ * member is not asked and gets no event when told to leave: when a roster
698
+ * excluding it arrives it drops its whole `secret_id` partition and emits
699
+ * `SelfRemovedFromGroup`. The secret survives on the remaining members and
700
+ * the helpers. */
656
701
  export interface UnpairReplicaParams {
657
702
  /** The member to remove, as a **decimal** `u64` string — the same form
658
703
  * `ReplicaPaired.peer_replica_id` hands back. A value naming no current
@@ -724,8 +769,11 @@ export type DeRecEvent =
724
769
  | { type: "SharingComplete"; version: number; confirmed_count: number; failed_count: number; threshold_met: boolean }
725
770
  /** A group member refused a secret sync. Keyed by `replica_id`, not
726
771
  * `channel_id`: every member answers on the one group channel. A
727
- * `VERSION_CONFLICT` status means the round must be resolved and
728
- * republished at a new version. */
772
+ * `VERSION_CONFLICT` status means another member holds a different copy
773
+ * of this version: do not publish from this device again until the
774
+ * conflict is resolved. Run `start(FlowKind.ReplicaDiscovery)` to receive
775
+ * the group's copy as `ReplicaVersionConflict`, merge, and publish the
776
+ * result once with `start(FlowKind.ProtectSecret)`. */
729
777
  | {
730
778
  type: "ReplicaSyncRejected";
731
779
  replica_id: string;
@@ -748,7 +796,8 @@ export type DeRecEvent =
748
796
  /** This device left the group and dropped its whole `secret_id` partition —
749
797
  * group channel, helper channels, shares, secrets and the snapshot. Fires
750
798
  * only once it was told to leave *and* has since seen a roster excluding
751
- * it; absence alone never destroys a copy of the secret. */
799
+ * it; absence alone never destroys a copy of the secret. The teardown is
800
+ * automatic: this device is not asked first and gets no earlier event. */
752
801
  | { type: "SelfRemovedFromGroup"; version: number }
753
802
  /** A replica catch-up finished. `fetched_from` is absent when this device
754
803
  * was already current, in which case no hydration event follows. */
@@ -764,6 +813,10 @@ export type DeRecEvent =
764
813
  * library keeps no durable per-member sync state. */
765
814
  | { type: "ReplicaSyncComplete"; version: number; synced: string[]; behind: string[] }
766
815
  | { type: "ShareVerified"; channel_id: string; version: number }
816
+ /** A helper refused a verification challenge: its response carried a
817
+ * non-OK `status` instead of a proof. The challenge is spent; a new
818
+ * `VerifyShares` round challenges the helper again. */
819
+ | { type: "ShareVerifyRejected"; channel_id: string; version: number; status: StatusEnum; memo: string }
767
820
  | {
768
821
  type: "SecretsDiscovered";
769
822
  channel_id: string;
@@ -772,6 +825,22 @@ export type DeRecEvent =
772
825
  }
773
826
  | { type: "RecoveryShareReceived"; channel_id: string; shares_received: number }
774
827
  | { type: "RecoveryShareError"; channel_id: string; shares_received: number; error: string }
828
+ /** A helper refused a recovery share request: its response carried a
829
+ * non-OK `status` (e.g. `UNKNOWN_SHARE_VERSION`) instead of a share. The
830
+ * refusal is not collected — it does not count towards `shares_received`
831
+ * and the recovery stays open for the other helpers' shares — but it does
832
+ * answer that helper's `RecoverSecretStarted`. */
833
+ | { type: "RecoveryShareRefused"; channel_id: string; version: number; status: StatusEnum; memo: string }
834
+ /** A helper answered with a share that cannot be part of the secret;
835
+ * `reason` says how it failed. `Malformed` and `InvalidProof` are judged
836
+ * on arrival; `Inconsistent` (valid on its own but disagreeing with the
837
+ * shares the secret was rebuilt from) is reported alongside
838
+ * `SecretRecovered`, once per helper. The share is set aside — it does
839
+ * not count towards `shares_received` and never blocks the recovery. An
840
+ * honest helper never sends one, so the app may treat it as a sign of a
841
+ * damaged or compromised helper, e.g. offer to unpair it. It also
842
+ * answers that helper's `RecoverSecretStarted`. */
843
+ | { type: "RecoveryShareCorrupted"; channel_id: string; version: number; reason: CorruptionReason }
775
844
  /** Recovery completed — the typed `Secret` snapshot the owner
776
845
  * originally protected. Mirrors `ReplicaSecretReceived.secret`:
777
846
  * `secrets` is the user-facing `Vec<UserSecret>` the application
@@ -949,7 +1018,10 @@ export type DeRecEvent =
949
1018
  * `ReplicaSyncRejected`. Both copies are complete states — the held one
950
1019
  * is in the local stores, the incoming one is `secret`. Resolve by
951
1020
  * publishing the chosen state with `start(FlowKind.ProtectSecret)`; the
952
- * next version supersedes both on every member and helper.
1021
+ * next version supersedes both on every member and helper. Until then,
1022
+ * do not publish from this device: any further `ProtectSecret` is a
1023
+ * higher version that every other member applies over its own copy,
1024
+ * losing the change it never merged.
953
1025
  *
954
1026
  * `held_author_replica_id` / `incoming_author_replica_id` are the
955
1027
  * decimal `replica_id` of each copy's publisher, or `null` when that
@@ -1232,6 +1304,14 @@ export type IgnoreReason = "PendingVerification" | "Expired";
1232
1304
  * Matches the Rust `NotRestoredReason` discriminants one-for-one. */
1233
1305
  export type NotRestoredReason = "NoTransports";
1234
1306
 
1307
+ /** Why a `RecoveryShareCorrupted` event set a helper's share aside.
1308
+ * Matches the Rust `CorruptionReason` discriminants one-for-one:
1309
+ * `Malformed` — no decodable share for the requested secret and version;
1310
+ * `InvalidProof` — the share fails its own Merkle proof;
1311
+ * `Inconsistent` — valid on its own, but its commitment root or ciphertext
1312
+ * disagrees with the shares the secret was rebuilt from. */
1313
+ export type CorruptionReason = "Malformed" | "InvalidProof" | "Inconsistent";
1314
+
1235
1315
  /**
1236
1316
  * The label vocabulary for `ActionRequired.action_kind` and
1237
1317
  * `AutoAccepted.action_kind` — one value per pending-action kind the