@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.
@@ -3,7 +3,9 @@
3
3
 
4
4
  #include "Primitives.h"
5
5
 
6
+ #include <cmath>
6
7
  #include <cstring>
8
+ #include <limits>
7
9
  #include <string>
8
10
  #include <vector>
9
11
 
@@ -118,17 +120,79 @@ jsi::Value toUint8ArrayVal(jsi::Runtime& rt, const std::vector<uint8_t>& bytes)
118
120
  }
119
121
 
120
122
  uint64_t asU64(jsi::Runtime& rt, const jsi::Value& value) {
123
+ auto reject = [&]() -> uint64_t {
124
+ throw jsi::JSError(rt, "expected an integer in 0..=18446744073709551615");
125
+ };
121
126
  if (value.isBigInt()) {
122
- return value.asBigInt(rt).asUint64(rt);
127
+ jsi::BigInt big = value.getBigInt(rt);
128
+ return big.isUint64(rt) ? big.getUint64(rt) : reject();
123
129
  }
124
- return static_cast<uint64_t>(value.asNumber());
130
+ if (!value.isNumber()) {
131
+ return reject();
132
+ }
133
+ double number = value.getNumber();
134
+ // 18446744073709551616.0 is 2^64, the first double above `uint64_t`'s range.
135
+ if (!(number >= 0.0) || number >= 18446744073709551616.0 || std::trunc(number) != number) {
136
+ return reject();
137
+ }
138
+ return static_cast<uint64_t>(number);
125
139
  }
126
140
 
127
141
  namespace {
128
142
 
143
+ /// Read an integer losslessly: a `bigint` or `number` holding an integer in
144
+ /// `0..=max`. Anything else throws `<name> expects <expected>`, so a value is
145
+ /// never truncated, rounded, or wrapped on its way to the core.
146
+ uint64_t asBoundedInteger(jsi::Runtime& rt,
147
+ const char* name,
148
+ const jsi::Value& element,
149
+ uint64_t max,
150
+ const std::string& expected) {
151
+ auto reject = [&]() -> uint64_t {
152
+ throw jsi::JSError(rt, std::string(name) + " expects " + expected);
153
+ };
154
+ if (element.isBigInt()) {
155
+ jsi::BigInt big = element.getBigInt(rt);
156
+ if (!big.isUint64(rt)) {
157
+ return reject();
158
+ }
159
+ uint64_t number = big.getUint64(rt);
160
+ return number <= max ? number : reject();
161
+ }
162
+ if (!element.isNumber()) {
163
+ return reject();
164
+ }
165
+ double number = element.getNumber();
166
+ // 18446744073709551616.0 is 2^64, the first double above `uint64_t`'s range.
167
+ if (!(number >= 0.0) || number >= 18446744073709551616.0 || std::trunc(number) != number) {
168
+ return reject();
169
+ }
170
+ auto integer = static_cast<uint64_t>(number);
171
+ return integer <= max ? integer : reject();
172
+ }
173
+
174
+ /// Read one element of an integer array with `asBoundedInteger`.
175
+ uint64_t asArrayInteger(jsi::Runtime& rt,
176
+ const char* name,
177
+ const jsi::Value& element,
178
+ uint64_t max,
179
+ const char* range) {
180
+ return asBoundedInteger(rt, name, element, max, std::string("an array of integers in ") + range);
181
+ }
182
+
183
+ /// Read the scalar argument `argument` with `asBoundedInteger`.
184
+ uint64_t asScalarInteger(jsi::Runtime& rt,
185
+ const char* name,
186
+ const char* argument,
187
+ const jsi::Value& value,
188
+ uint64_t max) {
189
+ return asBoundedInteger(rt, name, value, max,
190
+ std::string(argument) + " to be an integer in 0..=" + std::to_string(max));
191
+ }
192
+
129
193
  /// Read a JS array of `bigint`/`number` elements as a `uint64_t` vector.
130
194
  /// Used for `protect_secret`'s `channels` argument.
131
- std::vector<uint64_t> asU64Array(jsi::Runtime& rt, const jsi::Value& value) {
195
+ std::vector<uint64_t> asU64Array(jsi::Runtime& rt, const char* name, const jsi::Value& value) {
132
196
  std::vector<uint64_t> out;
133
197
  if (value.isNull() || value.isUndefined()) {
134
198
  return out;
@@ -137,14 +201,17 @@ std::vector<uint64_t> asU64Array(jsi::Runtime& rt, const jsi::Value& value) {
137
201
  size_t length = array.size(rt);
138
202
  out.reserve(length);
139
203
  for (size_t i = 0; i < length; ++i) {
140
- out.push_back(asU64(rt, array.getValueAtIndex(rt, i)));
204
+ out.push_back(asArrayInteger(rt, name, array.getValueAtIndex(rt, i),
205
+ std::numeric_limits<uint64_t>::max(),
206
+ "0..=18446744073709551615"));
141
207
  }
142
208
  return out;
143
209
  }
144
210
 
145
211
  /// Read a JS array of `number` elements as a `uint32_t` vector. Used for
146
- /// `produce_store_share_request_message`'s `keep_list` argument.
147
- std::vector<uint32_t> asU32Array(jsi::Runtime& rt, const jsi::Value& value) {
212
+ /// `protect_secret`'s `weights` and
213
+ /// `produce_store_share_request_message`'s `keep_list` arguments.
214
+ std::vector<uint32_t> asU32Array(jsi::Runtime& rt, const char* name, const jsi::Value& value) {
148
215
  std::vector<uint32_t> out;
149
216
  if (value.isNull() || value.isUndefined()) {
150
217
  return out;
@@ -153,7 +220,9 @@ std::vector<uint32_t> asU32Array(jsi::Runtime& rt, const jsi::Value& value) {
153
220
  size_t length = array.size(rt);
154
221
  out.reserve(length);
155
222
  for (size_t i = 0; i < length; ++i) {
156
- out.push_back(static_cast<uint32_t>(array.getValueAtIndex(rt, i).asNumber()));
223
+ out.push_back(static_cast<uint32_t>(
224
+ asArrayInteger(rt, name, array.getValueAtIndex(rt, i),
225
+ std::numeric_limits<uint32_t>::max(), "0..=4294967295")));
157
226
  }
158
227
  return out;
159
228
  }
@@ -336,9 +405,11 @@ jsi::Value createContactMessage(jsi::Runtime& rt,
336
405
  size_t count) {
337
406
  requireArgs(rt, "create_contact_message", count, 5);
338
407
  uint64_t channelId = asU64(rt, args[0]);
339
- auto contactMode = static_cast<int32_t>(args[1].asNumber());
408
+ auto contactMode = static_cast<int32_t>(asScalarInteger(
409
+ rt, "create_contact_message", "contact_mode", args[1], std::numeric_limits<int32_t>::max()));
340
410
  ByteView transportProtocols = asBytes(rt, args[2]);
341
- auto hasNonce = static_cast<uint32_t>(args[3].asNumber());
411
+ auto hasNonce = static_cast<uint32_t>(asScalarInteger(
412
+ rt, "create_contact_message", "has_nonce", args[3], std::numeric_limits<uint32_t>::max()));
342
413
  uint64_t nonce = asU64(rt, args[4]);
343
414
  CreateContactMessageResult result = create_contact_message(
344
415
  channelId, contactMode, transportProtocols.ptr, transportProtocols.len,
@@ -412,7 +483,8 @@ jsi::Value decodeMessageJson(jsi::Runtime& rt,
412
483
  const jsi::Value* args,
413
484
  size_t count) {
414
485
  requireArgs(rt, "decode_message_json", count, 2);
415
- auto kind = static_cast<int32_t>(args[0].asNumber());
486
+ auto kind = static_cast<int32_t>(asScalarInteger(
487
+ rt, "decode_message_json", "kind", args[0], std::numeric_limits<int32_t>::max()));
416
488
  ByteView proto = asBytes(rt, args[1]);
417
489
  DeRecMessageJsonResult result =
418
490
  derec_decode_message_json(kind, proto.ptr, proto.len);
@@ -432,7 +504,8 @@ jsi::Value encodeMessageJson(jsi::Runtime& rt,
432
504
  const jsi::Value* args,
433
505
  size_t count) {
434
506
  requireArgs(rt, "encode_message_json", count, 2);
435
- auto kind = static_cast<int32_t>(args[0].asNumber());
507
+ auto kind = static_cast<int32_t>(asScalarInteger(
508
+ rt, "encode_message_json", "kind", args[0], std::numeric_limits<int32_t>::max()));
436
509
  ByteView json = asBytes(rt, args[1]);
437
510
  DeRecMessageJsonResult result =
438
511
  derec_encode_message_json(kind, json.ptr, json.len);
@@ -453,7 +526,8 @@ jsi::Value producePairRequestMessage(jsi::Runtime& rt,
453
526
  const jsi::Value* args,
454
527
  size_t count) {
455
528
  requireArgs(rt, "produce_pair_request_message", count, 5);
456
- auto senderKind = static_cast<int32_t>(args[0].asNumber());
529
+ auto senderKind = static_cast<int32_t>(asScalarInteger(
530
+ rt, "produce_pair_request_message", "sender_kind", args[0], std::numeric_limits<int32_t>::max()));
457
531
  ByteView transportProtocols = asBytes(rt, args[1]);
458
532
  ByteView contactMessage = asBytes(rt, args[2]);
459
533
  ByteView communicationInfo = asBytes(rt, args[3]);
@@ -796,7 +870,8 @@ jsi::Value produceGetShareRequestMessage(jsi::Runtime& rt,
796
870
  requireArgs(rt, "produce_get_share_request_message", count, 5);
797
871
  uint64_t channelId = asU64(rt, args[0]);
798
872
  uint64_t secretId = asU64(rt, args[1]);
799
- auto version = static_cast<uint32_t>(args[2].asNumber());
873
+ auto version = static_cast<uint32_t>(asScalarInteger(
874
+ rt, "produce_get_share_request_message", "version", args[2], std::numeric_limits<uint32_t>::max()));
800
875
  ByteView sharedKey = asBytes(rt, args[3]);
801
876
  ByteView replyTo = asBytes(rt, args[4]);
802
877
  ProduceGetShareRequestMessageResult result = produce_get_share_request_message(
@@ -886,7 +961,8 @@ jsi::Value recoverFromShareResponses(jsi::Runtime& rt,
886
961
  requireArgs(rt, "recover_from_share_responses", count, 3);
887
962
  ByteView responses = asBytes(rt, args[0]);
888
963
  uint64_t secretId = asU64(rt, args[1]);
889
- auto version = static_cast<uint32_t>(args[2].asNumber());
964
+ auto version = static_cast<uint32_t>(asScalarInteger(
965
+ rt, "recover_from_share_responses", "version", args[2], std::numeric_limits<uint32_t>::max()));
890
966
  RecoverFromShareResponsesResult result = recover_from_share_responses(
891
967
  responses.ptr, responses.len, secretId, version);
892
968
  if (result.error.code != 0) {
@@ -897,20 +973,29 @@ jsi::Value recoverFromShareResponses(jsi::Runtime& rt,
897
973
  }
898
974
 
899
975
  /// `protect_secret(secretId: bigint, secretData: ArrayBuffer, channels:
900
- /// (bigint|number)[], threshold: number, version: number) -> ArrayBuffer`
976
+ /// (bigint|number)[], weights: number[], threshold: number, version:
977
+ /// number) -> ArrayBuffer`
901
978
  jsi::Value protectSecret(jsi::Runtime& rt,
902
979
  const jsi::Value&,
903
980
  const jsi::Value* args,
904
981
  size_t count) {
905
- requireArgs(rt, "protect_secret", count, 5);
982
+ requireArgs(rt, "protect_secret", count, 6);
906
983
  uint64_t secretId = asU64(rt, args[0]);
907
984
  ByteView secretData = asBytes(rt, args[1]);
908
- std::vector<uint64_t> channels = asU64Array(rt, args[2]);
909
- auto threshold = static_cast<size_t>(args[3].asNumber());
910
- auto version = static_cast<uint32_t>(args[4].asNumber());
985
+ std::vector<uint64_t> channels = asU64Array(rt, "protect_secret", args[2]);
986
+ std::vector<uint32_t> weights = asU32Array(rt, "protect_secret", args[3]);
987
+ if (channels.size() != weights.size()) {
988
+ throw jsi::JSError(rt,
989
+ "protect_secret: channels and weights must have "
990
+ "the same length");
991
+ }
992
+ auto threshold = static_cast<size_t>(asScalarInteger(
993
+ rt, "protect_secret", "threshold", args[4], std::numeric_limits<size_t>::max()));
994
+ auto version = static_cast<uint32_t>(asScalarInteger(
995
+ rt, "protect_secret", "version", args[5], std::numeric_limits<uint32_t>::max()));
911
996
  ProtectSecretResult result = protect_secret(
912
997
  secretId, secretData.ptr, secretData.len, channels.data(),
913
- channels.size(), threshold, version);
998
+ weights.data(), channels.size(), threshold, version);
914
999
  if (result.error.code != 0) {
915
1000
  throwDeRecError(rt, result.error);
916
1001
  }
@@ -928,10 +1013,11 @@ jsi::Value produceStoreShareRequestMessage(jsi::Runtime& rt,
928
1013
  size_t count) {
929
1014
  requireArgs(rt, "produce_store_share_request_message", count, 8);
930
1015
  uint64_t channelId = asU64(rt, args[0]);
931
- auto version = static_cast<uint32_t>(args[1].asNumber());
1016
+ auto version = static_cast<uint32_t>(asScalarInteger(
1017
+ rt, "produce_store_share_request_message", "version", args[1], std::numeric_limits<uint32_t>::max()));
932
1018
  uint64_t secretId = asU64(rt, args[2]);
933
1019
  ByteView committedShare = asBytes(rt, args[3]);
934
- std::vector<uint32_t> keepList = asU32Array(rt, args[4]);
1020
+ std::vector<uint32_t> keepList = asU32Array(rt, "produce_store_share_request_message", args[4]);
935
1021
  ByteView description = asBytes(rt, args[5]);
936
1022
  ByteView sharedKey = asBytes(rt, args[6]);
937
1023
  ByteView replyTo = asBytes(rt, args[7]);
@@ -1035,7 +1121,8 @@ jsi::Value processStoreShareResponseMessage(jsi::Runtime& rt,
1035
1121
  const jsi::Value* args,
1036
1122
  size_t count) {
1037
1123
  requireArgs(rt, "process_store_share_response_message", count, 2);
1038
- auto version = static_cast<uint32_t>(args[0].asNumber());
1124
+ auto version = static_cast<uint32_t>(asScalarInteger(
1125
+ rt, "process_store_share_response_message", "version", args[0], std::numeric_limits<uint32_t>::max()));
1039
1126
  ByteView responseProto = asBytes(rt, args[1]);
1040
1127
  ProcessStoreShareResponseMessageResult result =
1041
1128
  process_store_share_response_message(version, responseProto.ptr,
@@ -1159,7 +1246,8 @@ jsi::Value produceVerifyShareRequestMessage(jsi::Runtime& rt,
1159
1246
  requireArgs(rt, "produce_verify_share_request_message", count, 5);
1160
1247
  uint64_t channelId = asU64(rt, args[0]);
1161
1248
  uint64_t secretId = asU64(rt, args[1]);
1162
- auto version = static_cast<uint32_t>(args[2].asNumber());
1249
+ auto version = static_cast<uint32_t>(asScalarInteger(
1250
+ rt, "produce_verify_share_request_message", "version", args[2], std::numeric_limits<uint32_t>::max()));
1163
1251
  ByteView sharedKey = asBytes(rt, args[3]);
1164
1252
  ByteView replyTo = asBytes(rt, args[4]);
1165
1253
  ProduceVerifyShareRequestMessageResult result =
@@ -1321,7 +1409,7 @@ void installPrimitives(jsi::Runtime& rt, jsi::Object& host) {
1321
1409
  produceGetShareResponseMessage);
1322
1410
  bind(rt, host, "extract_get_share_response", 2, extractGetShareResponse);
1323
1411
  bind(rt, host, "recover_from_share_responses", 3, recoverFromShareResponses);
1324
- bind(rt, host, "protect_secret", 5, protectSecret);
1412
+ bind(rt, host, "protect_secret", 6, protectSecret);
1325
1413
  bind(rt, host, "produce_store_share_request_message", 8,
1326
1414
  produceStoreShareRequestMessage);
1327
1415
  bind(rt, host, "extract_store_share_request", 2, extractStoreShareRequest);
@@ -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) == 10 * sizeof(void*),
30
+ static_assert(sizeof(ShareStoreCallbacks) == 11 * 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");
@@ -245,6 +245,21 @@ CallResult toShareListResult(jsi::Runtime& rt, const jsi::Value& value) {
245
245
  text += "{\"secret_id\":\"" + secretIdStr + "\",\"version\":" + std::to_string(version) +
246
246
  ",\"bytes\":";
247
247
  appendByteArrayJson(text, bytes.ptr, bytes.len);
248
+ // `subshares` and `threshold`: `null`/`undefined` is none and is omitted,
249
+ // as Rust omits it; anything else must be a `u32` to cross as one.
250
+ for (const char* field : {"subshares", "threshold"}) {
251
+ jsi::Value value = entry.getProperty(rt, field);
252
+ if (value.isNull() || value.isUndefined()) continue;
253
+ if (!value.isNumber()) {
254
+ return CallResult{kBackendFailure, {}};
255
+ }
256
+ double number = value.asNumber();
257
+ if (!(number >= 0 && number <= 4294967295.0) ||
258
+ number != static_cast<double>(static_cast<uint32_t>(number))) {
259
+ return CallResult{kBackendFailure, {}};
260
+ }
261
+ text += std::string(",\"") + field + "\":" + std::to_string(static_cast<uint32_t>(number));
262
+ }
248
263
  text += '}';
249
264
  }
250
265
  text += ']';
@@ -354,6 +369,38 @@ CallResult toKeepListResult(jsi::Runtime& rt, const jsi::Value& value) {
354
369
  return CallResult{0, std::vector<uint8_t>(text.begin(), text.end())};
355
370
  }
356
371
 
372
+ /// Reads the `refusal` property a `ShareStore.save`/`recordVerification`
373
+ /// throws, or rejects its promise with, to refuse the call — see
374
+ /// `shareStoreRefusal` in `src/types.ts`. Maps the wire string to the FFI
375
+ /// return code `library/src/interop/ffi/error.rs` names
376
+ /// `DEREC_SHARE_STORE_REFUSED_*`; a missing or unknown property is an
377
+ /// ordinary backend failure, never any other interpretation.
378
+ int32_t shareRefusalCode(jsi::Runtime& rt, const jsi::Value& value) {
379
+ if (!value.isObject()) {
380
+ return kBackendFailure;
381
+ }
382
+ jsi::Object obj = value.asObject(rt);
383
+ if (!obj.hasProperty(rt, "refusal")) {
384
+ return kBackendFailure;
385
+ }
386
+ jsi::Value refusal = obj.getProperty(rt, "refusal");
387
+ if (!refusal.isString()) {
388
+ return kBackendFailure;
389
+ }
390
+ std::string reason = refusal.asString(rt).utf8(rt);
391
+ if (reason == "too_frequent") return DEREC_SHARE_STORE_REFUSED_TOO_FREQUENT;
392
+ if (reason == "size_limit_exceeded") return DEREC_SHARE_STORE_REFUSED_SIZE_LIMIT_EXCEEDED;
393
+ if (reason == "rejected") return DEREC_SHARE_STORE_REFUSED_REJECTED;
394
+ return kBackendFailure;
395
+ }
396
+
397
+ /// `ResultConverter` form of `shareRefusalCode`, passed as `ShareStore.save`/
398
+ /// `recordVerification`'s `settleFromPromise` error converter and used
399
+ /// directly on a synchronous throw.
400
+ CallResult toShareRefusalResult(jsi::Runtime& rt, const jsi::Value& value) {
401
+ return CallResult{shareRefusalCode(rt, value), {}};
402
+ }
403
+
357
404
  /// `UserSecretStore.loadLatest(): Promise<UserSecrets | null | undefined>`.
358
405
  CallResult toUserSecretsResult(jsi::Runtime& rt, const jsi::Value& value) {
359
406
  if (value.isNull() || value.isUndefined()) {
@@ -811,13 +858,59 @@ extern "C" int32_t shareStoreSave(void* userData, uint64_t secretId, uint64_t ch
811
858
  jsi::Value(rt, jsi::String::createFromUtf8(rt, decoded.secretId)));
812
859
  shareObj.setProperty(rt, "version", jsi::Value(static_cast<int>(decoded.version)));
813
860
  shareObj.setProperty(rt, "bytes", toUint8ArrayVal(rt, decoded.bytes));
814
- jsi::Value promise = method.callWithThis(rt, *store, idVal(rt, secretId), idVal(rt, channelId),
815
- jsi::Value(rt, shareObj));
816
- keepAlive->settleFromPromise(rt, std::move(promise), std::move(settle), toVoidResult);
861
+ if (decoded.subshares) {
862
+ shareObj.setProperty(rt, "subshares", jsi::Value(static_cast<double>(*decoded.subshares)));
863
+ }
864
+ if (decoded.threshold) {
865
+ shareObj.setProperty(rt, "threshold", jsi::Value(static_cast<double>(*decoded.threshold)));
866
+ }
867
+ // A refusal thrown synchronously (rather than via a rejected promise)
868
+ // never reaches `settleFromPromise`'s `onErr`, so it is read here,
869
+ // before it would otherwise fall into `JsCallbackBridge::callSync`'s
870
+ // generic backend-failure catch.
871
+ try {
872
+ jsi::Value promise = method.callWithThis(rt, *store, idVal(rt, secretId), idVal(rt, channelId),
873
+ jsi::Value(rt, shareObj));
874
+ keepAlive->settleFromPromise(rt, std::move(promise), settle, toVoidResult, toShareRefusalResult);
875
+ } catch (jsi::JSError& e) {
876
+ settle(toShareRefusalResult(rt, e.value()));
877
+ }
817
878
  });
818
879
  return result.code;
819
880
  }
820
881
 
882
+ /// `ShareStore.recordVerification(secretId, channelId, version) -> void`
883
+ ///
884
+ /// Called once per accepted verification challenge, after the challenged
885
+ /// share is found and before the helper answers — see
886
+ /// `DeRecShareStore::record_verification` on the Rust side. A refusal
887
+ /// (thrown, or a rejected promise, tagged with `shareStoreRefusal`'s
888
+ /// `refusal` property) maps to the matching FFI return code exactly as
889
+ /// `ShareStore.save` does; any other failure is an ordinary backend failure.
890
+ extern "C" int32_t shareStoreRecordVerification(void* userData, uint64_t secretId, uint64_t channelId,
891
+ uint32_t version) {
892
+ auto* self = static_cast<StoreBindings*>(userData);
893
+ // Kept alive past this call so the lambda handed to `callSync` — which the
894
+ // JavaScript CallInvoker's queue may still be holding after this object's
895
+ // owner has released its own reference — has somewhere safe to run.
896
+ auto keepAlive = self->shared_from_this();
897
+ CallResult result = keepAlive->bridge().callSync(
898
+ [keepAlive, secretId, channelId, version](std::function<void(CallResult)> settle) {
899
+ jsi::Runtime& rt = keepAlive->runtime();
900
+ auto store = keepAlive->shareStore();
901
+ auto method = store->getPropertyAsFunction(rt, "recordVerification");
902
+ try {
903
+ jsi::Value promise = method.callWithThis(rt, *store, idVal(rt, secretId), idVal(rt, channelId),
904
+ jsi::Value(static_cast<double>(version)));
905
+ keepAlive->settleFromPromise(rt, std::move(promise), settle, toVoidResult,
906
+ toShareRefusalResult);
907
+ } catch (jsi::JSError& e) {
908
+ settle(toShareRefusalResult(rt, e.value()));
909
+ }
910
+ });
911
+ return result.code;
912
+ }
913
+
821
914
  /// `ShareStore.removeChannel(secretId, channelId) -> void`
822
915
  extern "C" int32_t shareStoreRemoveChannel(void* userData, uint64_t secretId, uint64_t channelId) {
823
916
  auto* self = static_cast<StoreBindings*>(userData);
@@ -1110,7 +1203,7 @@ StoreBindings::StoreBindings(jsi::Runtime& rt, JsCallbackBridge& bridge,
1110
1203
 
1111
1204
  void StoreBindings::settleFromPromise(jsi::Runtime& rt, jsi::Value value,
1112
1205
  std::function<void(CallResult)> settle,
1113
- ResultConverter convert) {
1206
+ ResultConverter convert, ResultConverter convertError) {
1114
1207
  // A store may legitimately return a plain value; only route through
1115
1208
  // `then` when the result actually is a thenable.
1116
1209
  if (!value.isObject()) {
@@ -1150,13 +1243,16 @@ void StoreBindings::settleFromPromise(jsi::Runtime& rt, jsi::Value value,
1150
1243
 
1151
1244
  auto onErr = jsi::Function::createFromHostFunction(
1152
1245
  rt, jsi::PropNameID::forAscii(rt, "onErr"), 1,
1153
- [shared](jsi::Runtime&, const jsi::Value&, const jsi::Value*, size_t) -> jsi::Value {
1246
+ [shared, convertError](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args,
1247
+ size_t count) -> jsi::Value {
1154
1248
  try {
1155
- (*shared)(CallResult{kBackendFailure, {}});
1249
+ CallResult result = convertError != nullptr
1250
+ ? (count > 0 ? convertError(rt, args[0])
1251
+ : convertError(rt, jsi::Value::undefined()))
1252
+ : CallResult{kBackendFailure, {}};
1253
+ (*shared)(std::move(result));
1156
1254
  } catch (...) {
1157
- // `settle` on an already-resolved slot is a documented no-op, but
1158
- // guard uniformly with `onOk` in case a future `settle`
1159
- // implementation grows a fallible step.
1255
+ (*shared)(CallResult{kBackendFailure, {}});
1160
1256
  }
1161
1257
  return jsi::Value::undefined();
1162
1258
  });
@@ -1198,16 +1294,17 @@ std::shared_ptr<StoreBindings> StoreBindings::create(jsi::Runtime& rt, jsi::Obje
1198
1294
  storeFreeBuffer, // free_buffer
1199
1295
  };
1200
1296
  bindings->shareCallbacks_ = ShareStoreCallbacks{
1201
- self, // user_data
1202
- shareStoreLoad, // load
1203
- shareStoreLoadMany, // load_many
1204
- shareStoreLoadAll, // load_all
1205
- shareStoreLatestVersion, // latest_version
1206
- shareStoreSave, // save
1207
- shareStoreRemoveChannel, // remove_channel
1208
- shareStoreRemoveVersions, // remove_versions
1209
- shareStoreKeepList, // keep_list
1210
- storeFreeBuffer, // free_buffer
1297
+ self, // user_data
1298
+ shareStoreLoad, // load
1299
+ shareStoreLoadMany, // load_many
1300
+ shareStoreLoadAll, // load_all
1301
+ shareStoreLatestVersion, // latest_version
1302
+ shareStoreSave, // save
1303
+ shareStoreRemoveChannel, // remove_channel
1304
+ shareStoreRemoveVersions, // remove_versions
1305
+ shareStoreKeepList, // keep_list
1306
+ shareStoreRecordVerification, // record_verification
1307
+ storeFreeBuffer, // free_buffer
1211
1308
  };
1212
1309
  bindings->userSecretCallbacks_ = UserSecretStoreCallbacks{
1213
1310
  self, // user_data
@@ -1255,6 +1352,8 @@ std::shared_ptr<StoreBindings> StoreBindings::create(jsi::Runtime& rt, jsi::Obje
1255
1352
  requireField(rt, bindings->shareCallbacks_.remove_channel, "ShareStoreCallbacks.remove_channel");
1256
1353
  requireField(rt, bindings->shareCallbacks_.remove_versions, "ShareStoreCallbacks.remove_versions");
1257
1354
  requireField(rt, bindings->shareCallbacks_.keep_list, "ShareStoreCallbacks.keep_list");
1355
+ requireField(rt, bindings->shareCallbacks_.record_verification,
1356
+ "ShareStoreCallbacks.record_verification");
1258
1357
  requireField(rt, bindings->shareCallbacks_.free_buffer, "ShareStoreCallbacks.free_buffer");
1259
1358
 
1260
1359
  requireField(rt, bindings->userSecretCallbacks_.user_data, "UserSecretStoreCallbacks.user_data");
@@ -82,8 +82,15 @@ class StoreBindings : public std::enable_shared_from_this<StoreBindings> {
82
82
  /// through `convert`, dispatching via `then` only when the value is
83
83
  /// actually thenable. `convert` runs on the JavaScript thread, with `rt`
84
84
  /// live, before `settle` is invoked — never after.
85
+ ///
86
+ /// `convertError`, when non-null, replaces the default "any rejection is a
87
+ /// backend failure" handling: it runs on the rejected value instead, so a
88
+ /// caller that needs to read something off that value (e.g.
89
+ /// `ShareStore.save`'s `refusal` tag) can. Left null, a rejection settles
90
+ /// with `kBackendFailure` exactly as before.
85
91
  void settleFromPromise(facebook::jsi::Runtime& rt, facebook::jsi::Value value,
86
- std::function<void(CallResult)> settle, ResultConverter convert);
92
+ std::function<void(CallResult)> settle, ResultConverter convert,
93
+ ResultConverter convertError = nullptr);
87
94
 
88
95
  private:
89
96
  StoreBindings(facebook::jsi::Runtime& rt, JsCallbackBridge& bridge,