@derec-alliance/react-native 0.0.5 → 0.0.6

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.
@@ -26,7 +26,12 @@ namespace {
26
26
  /// failure.
27
27
  struct FfiFailure {
28
28
  explicit FfiFailure(DeRecError e) : error(e) {}
29
+ FfiFailure(DeRecError e, std::vector<uint8_t> channelIdsJson)
30
+ : error(e), conflictingChannelIdsJson(std::move(channelIdsJson)) {}
29
31
  DeRecError error;
32
+ /// `restore`'s JSON array of colliding channel ids, empty for every other
33
+ /// failure. Carried as raw bytes: parsing needs the runtime.
34
+ std::vector<uint8_t> conflictingChannelIdsJson;
30
35
  };
31
36
 
32
37
  constexpr const char* kReentrancyMessage =
@@ -170,10 +175,12 @@ jsi::Value ProtocolHost::runAsync(
170
175
  bool failed = false;
171
176
  bool unknownFailure = false;
172
177
  DeRecError capturedError{};
178
+ std::vector<uint8_t> capturedChannelIdsJson;
173
179
  try {
174
180
  bytes = body();
175
- } catch (const FfiFailure& failure) {
181
+ } catch (FfiFailure& failure) {
176
182
  capturedError = failure.error;
183
+ capturedChannelIdsJson = std::move(failure.conflictingChannelIdsJson);
177
184
  failed = true;
178
185
  } catch (...) {
179
186
  // Anything other than `FfiFailure` — `std::bad_alloc` from
@@ -198,7 +205,9 @@ jsi::Value ProtocolHost::runAsync(
198
205
 
199
206
  invoker->invokeAsync([state, runtime, resolve, reject, convert,
200
207
  bytes = std::move(bytes), failed,
201
- unknownFailure, capturedError]() mutable {
208
+ unknownFailure, capturedError,
209
+ capturedChannelIdsJson =
210
+ std::move(capturedChannelIdsJson)]() mutable {
202
211
  if (state->runtimeInvalidated.load()) {
203
212
  // Queued before the runtime was torn down, reached only
204
213
  // afterwards. Every JSI call below — including the ones the
@@ -212,7 +221,7 @@ jsi::Value ProtocolHost::runAsync(
212
221
  // Reuses the exact error shape every synchronous FFI
213
222
  // wrapper throws, rather than inventing a second one for
214
223
  // the asynchronous path.
215
- throwDeRecError(jsRt, capturedError);
224
+ throwDeRecError(jsRt, capturedError, capturedChannelIdsJson);
216
225
  }
217
226
  if (unknownFailure) {
218
227
  throw jsi::JSError(jsRt, "DeRec: an unexpected native error occurred");
@@ -269,9 +278,8 @@ jsi::Value ProtocolHost::get(jsi::Runtime& rt, const jsi::PropNameID& name) {
269
278
  size_t) -> jsi::Value { return hostSecretId(rt2, secretId_); });
270
279
  }
271
280
 
272
- // `setCommunicationInfo` and `setOwnTransport` return a Promise, unlike
273
- // `@derec-alliance/nodejs` where both are synchronous. They must not run on
274
- // the JavaScript thread: `derec_protocol_set_*` takes the same handle mutex
281
+ // `setCommunicationInfo` and `setOwnTransports` return a Promise, as in
282
+ // `@derec-alliance/nodejs`. They must not run on the JavaScript thread: `derec_protocol_set_*` takes the same handle mutex
275
283
  // that `derec_protocol_process`/`start`/`restore` hold for the whole
276
284
  // duration of a flow, including across store callbacks that park the worker
277
285
  // waiting on the JavaScript thread. Calling the setter synchronously would
@@ -281,9 +289,7 @@ jsi::Value ProtocolHost::get(jsi::Runtime& rt, const jsi::PropNameID& name) {
281
289
  // callback into a backend failure. The nodejs SDK is WASM and
282
290
  // single-threaded, so the hazard cannot arise there. Queuing onto the
283
291
  // serial worker instead orders the setter behind any in-flight flow, and
284
- // returning a Promise keeps a caller that ignores the result — which is how
285
- // nodejs code calls these — source-compatible, while letting a failure
286
- // surface as a rejection rather than being swallowed.
292
+ // a failure surfaces as a rejection rather than being swallowed.
287
293
  if (prop == "setCommunicationInfo") {
288
294
  return jsi::Function::createFromHostFunction(
289
295
  rt, name, 1,
@@ -309,38 +315,6 @@ jsi::Value ProtocolHost::get(jsi::Runtime& rt, const jsi::PropNameID& name) {
309
315
  });
310
316
  }
311
317
 
312
- if (prop == "setOwnTransport") {
313
- return jsi::Function::createFromHostFunction(
314
- rt, name, 2,
315
- [this](jsi::Runtime& rt2, const jsi::Value&, const jsi::Value* args,
316
- size_t count) -> jsi::Value {
317
- requireArgs(rt2, "setOwnTransport", count, 2);
318
- std::string uri = args[0].asString(rt2).utf8(rt2);
319
- auto protocol = static_cast<int32_t>(args[1].asNumber());
320
- DeRecProtocolHandle* handle = handle_;
321
-
322
- auto body = [handle, uri, protocol]() -> std::vector<uint8_t> {
323
- // This binding *is* the deprecated single-endpoint path, so it calls the
324
- // deprecated export on purpose — the same reason the Rust side carries
325
- // `#[allow(deprecated)]` on its compatibility paths. Both go at 0.0.5.
326
- #pragma GCC diagnostic push
327
- #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
328
- DeRecError error = derec_protocol_set_own_transport(
329
- handle, reinterpret_cast<const uint8_t*>(uri.data()), uri.size(),
330
- protocol);
331
- #pragma GCC diagnostic pop
332
- if (error.code != DEREC_CODE_OK) {
333
- throw FfiFailure(error);
334
- }
335
- return {};
336
- };
337
- auto convert = [](jsi::Runtime&, std::vector<uint8_t>&) {
338
- return jsi::Value::undefined();
339
- };
340
- return runAsync(rt2, std::move(body), std::move(convert));
341
- });
342
- }
343
-
344
318
  // Takes the endpoint list already JSON-encoded, the same way
345
319
  // `setCommunicationInfo` takes its map: the FFI parses JSON on the other
346
320
  // side, so encoding in JavaScript keeps this bridge free of a shape it
@@ -599,12 +573,14 @@ jsi::Value ProtocolHost::get(jsi::Runtime& rt, const jsi::PropNameID& name) {
599
573
 
600
574
  auto body = [handle,
601
575
  params = std::move(params)]() -> std::vector<uint8_t> {
602
- DeRecProtocolEventsResult result =
576
+ DeRecProtocolRestoreResult result =
603
577
  derec_protocol_restore(handle, params.data(), params.size());
604
- if (result.error.code != DEREC_CODE_OK) {
605
- throw FfiFailure(result.error);
578
+ RestoreOutcome outcome = takeRestoreResult(result);
579
+ if (outcome.error.code != DEREC_CODE_OK) {
580
+ throw FfiFailure(outcome.error,
581
+ std::move(outcome.conflictingChannelIdsJson));
606
582
  }
607
- return takeBuffer(result.events_json);
583
+ return std::move(outcome.events);
608
584
  };
609
585
  return runAsync(rt2, std::move(body), bytesToArrayBuffer);
610
586
  });
@@ -632,8 +608,7 @@ jsi::Value ProtocolHost::get(jsi::Runtime& rt, const jsi::PropNameID& name) {
632
608
 
633
609
  std::vector<jsi::PropNameID> ProtocolHost::getPropertyNames(jsi::Runtime& rt) {
634
610
  const char* names[] = {
635
- "secretId", "setCommunicationInfo", "setOwnTransport",
636
- "setOwnTransports",
611
+ "secretId", "setCommunicationInfo", "setOwnTransports",
637
612
  "createContact", "start", "process",
638
613
  "tick", "accept", "reject",
639
614
  "getFingerprint", "verifyFingerprint", "removeExpiredChannels",
@@ -10,6 +10,7 @@
10
10
 
11
11
  #include "Convert.h"
12
12
  #include "Primitives.h"
13
+ #include "UserSecretsJson.h"
13
14
 
14
15
  using namespace facebook;
15
16
 
@@ -24,7 +25,7 @@ namespace {
24
25
 
25
26
  static_assert(sizeof(ChannelStoreCallbacks) == 9 * sizeof(void*),
26
27
  "ChannelStoreCallbacks layout changed; update the bindings");
27
- static_assert(sizeof(SecretStoreCallbacks) == 5 * sizeof(void*),
28
+ static_assert(sizeof(SecretStoreCallbacks) == 6 * sizeof(void*),
28
29
  "SecretStoreCallbacks layout changed; update the bindings");
29
30
  static_assert(sizeof(ShareStoreCallbacks) == 8 * sizeof(void*),
30
31
  "ShareStoreCallbacks layout changed; update the bindings");
@@ -74,118 +75,6 @@ jsi::Value idVal(jsi::Runtime& rt, uint64_t id) {
74
75
  return jsi::Value(rt, jsi::String::createFromUtf8(rt, std::to_string(id)));
75
76
  }
76
77
 
77
- /// Build a genuine `Uint8Array` (not a bare `ArrayBuffer`) — every store
78
- /// interface types its byte payloads as `Uint8Array`, and application code
79
- /// is entitled to index or slice it directly.
80
- jsi::Value toUint8ArrayVal(jsi::Runtime& rt, const uint8_t* bytes, size_t len) {
81
- jsi::Uint8Array arr(rt, len);
82
- if (len > 0 && bytes != nullptr) {
83
- jsi::ArrayBuffer buffer = arr.buffer(rt);
84
- std::memcpy(buffer.data(rt), bytes, len);
85
- }
86
- return jsi::Value(rt, std::move(arr));
87
- }
88
-
89
- jsi::Value toUint8ArrayVal(jsi::Runtime& rt, const std::vector<uint8_t>& bytes) {
90
- return toUint8ArrayVal(rt, bytes.data(), bytes.size());
91
- }
92
-
93
- // Minimal decoders for the wire's "bare JSON array/object of unsigned
94
- // integers" convention (`versions_json`, `channel_ids_json`, the `bytes`
95
- // field of a `ShareRecord` / `SecretValueRecord`). These are produced by
96
- // `serde_json`'s default compact, whitespace-free output over a fixed,
97
- // known shape, so a substring scan is sufficient and — critically — never
98
- // routes a `u64` through a JS `number`, which cannot represent every value
99
- // in that range exactly.
100
-
101
- /// Parse a bare `[n, n, ...]` array of unsigned decimal integers.
102
- std::vector<uint64_t> parseUnsignedJsonArray(const uint8_t* ptr, size_t len) {
103
- std::vector<uint64_t> out;
104
- size_t i = 0;
105
- while (i < len && ptr[i] != '[') ++i;
106
- if (i >= len) return out;
107
- ++i;
108
- while (i < len && ptr[i] != ']') {
109
- if (ptr[i] < '0' || ptr[i] > '9') {
110
- ++i;
111
- continue;
112
- }
113
- uint64_t value = 0;
114
- while (i < len && ptr[i] >= '0' && ptr[i] <= '9') {
115
- value = value * 10 + static_cast<uint64_t>(ptr[i] - '0');
116
- ++i;
117
- }
118
- out.push_back(value);
119
- }
120
- return out;
121
- }
122
-
123
- /// Offset just past `"key":` in a flat, compact JSON object, or
124
- /// `std::string::npos` if `key` is absent.
125
- size_t fieldValueOffset(const std::string& json, const char* key) {
126
- std::string needle = std::string("\"") + key + "\":";
127
- size_t pos = json.find(needle);
128
- return pos == std::string::npos ? std::string::npos : pos + needle.size();
129
- }
130
-
131
- uint64_t parseUnsignedAt(const std::string& json, size_t pos) {
132
- uint64_t value = 0;
133
- while (pos < json.size() && json[pos] >= '0' && json[pos] <= '9') {
134
- value = value * 10 + static_cast<uint64_t>(json[pos] - '0');
135
- ++pos;
136
- }
137
- return value;
138
- }
139
-
140
- std::vector<uint8_t> parseByteArrayAt(const std::string& json, size_t pos) {
141
- std::vector<uint8_t> out;
142
- while (pos < json.size() && json[pos] != '[') ++pos;
143
- if (pos >= json.size()) return out;
144
- ++pos;
145
- while (pos < json.size() && json[pos] != ']') {
146
- if (json[pos] < '0' || json[pos] > '9') {
147
- ++pos;
148
- continue;
149
- }
150
- uint64_t value = 0;
151
- while (pos < json.size() && json[pos] >= '0' && json[pos] <= '9') {
152
- value = value * 10 + static_cast<uint64_t>(json[pos] - '0');
153
- ++pos;
154
- }
155
- out.push_back(static_cast<uint8_t>(value));
156
- }
157
- return out;
158
- }
159
-
160
- /// `{"kind":<n>,"bytes":[<n>,...]}` — the wire shape `SecretStore.save`
161
- /// receives; `kind` is already carried as a separate FFI argument so only
162
- /// `bytes` needs to be read back out.
163
- std::vector<uint8_t> decodeSecretValueBytes(const uint8_t* ptr, size_t len) {
164
- std::string json(reinterpret_cast<const char*>(ptr), len);
165
- size_t pos = fieldValueOffset(json, "bytes");
166
- if (pos == std::string::npos) return {};
167
- return parseByteArrayAt(json, pos);
168
- }
169
-
170
- /// `{"secret_id":"...","version":<n>,"bytes":[<n>,...]}` — the wire shape
171
- /// `ShareStore.save` receives. `secret_id` duplicates the FFI's own
172
- /// `secret_id` argument, so only `version` and `bytes` are decoded.
173
- struct DecodedShare {
174
- uint32_t version;
175
- std::vector<uint8_t> bytes;
176
- };
177
-
178
- DecodedShare decodeShareRecord(const uint8_t* ptr, size_t len) {
179
- std::string json(reinterpret_cast<const char*>(ptr), len);
180
- size_t versionPos = fieldValueOffset(json, "version");
181
- size_t bytesPos = fieldValueOffset(json, "bytes");
182
- DecodedShare out{};
183
- out.version =
184
- versionPos == std::string::npos ? 0 : static_cast<uint32_t>(parseUnsignedAt(json, versionPos));
185
- out.bytes = bytesPos == std::string::npos ? std::vector<uint8_t>{} : parseByteArrayAt(json, bytesPos);
186
- return out;
187
- }
188
-
189
78
  /// True only for a non-empty run of ASCII decimal digits.
190
79
  ///
191
80
  /// Every `u64` id crosses this boundary as a decimal string, and the wire
@@ -376,6 +265,37 @@ ResultConverter makeSecretLoadResult(uint32_t kind) {
376
265
  };
377
266
  }
378
267
 
268
+ /// `SecretStore.loadMany(): Promise<Array<Uint8Array | null | undefined>>`.
269
+ /// Encodes the resolved array element by element, in order, as the wire's
270
+ /// `Vec<Option<SecretValueRecord>>` JSON: the record `load` builds, or `null`
271
+ /// for a `null`/`undefined` entry. The library pairs entries with the
272
+ /// requested ids and decides what a missing one means.
273
+ ///
274
+ /// A non-array return is a backend failure, never an empty list.
275
+ ResultConverter makeSecretLoadManyResult(uint32_t kind) {
276
+ return [kind](jsi::Runtime& rt, const jsi::Value& value) -> CallResult {
277
+ if (!value.isObject()) {
278
+ return CallResult{kBackendFailure, {}};
279
+ }
280
+ jsi::Array arr = value.asObject(rt).asArray(rt);
281
+ size_t n = arr.size(rt);
282
+ std::string text = "[";
283
+ for (size_t i = 0; i < n; ++i) {
284
+ if (i != 0) text += ',';
285
+ jsi::Value entry = arr.getValueAtIndex(rt, i);
286
+ if (entry.isNull() || entry.isUndefined()) {
287
+ text += "null";
288
+ continue;
289
+ }
290
+ ByteView bytes = asBytes(rt, entry);
291
+ std::vector<uint8_t> record = secretValueRecordJson(kind, bytes.ptr, bytes.len);
292
+ text.append(record.begin(), record.end());
293
+ }
294
+ text += ']';
295
+ return CallResult{0, std::vector<uint8_t>(text.begin(), text.end())};
296
+ };
297
+ }
298
+
379
299
  /// `StateStore.loadAll(): Promise<Uint8Array[]>`. Each element is already
380
300
  /// the exact item-JSON blob the wire expects; splice the raw bytes into an
381
301
  /// array rather than re-parsing and re-serialising them.
@@ -400,105 +320,12 @@ CallResult toStateLoadAllResult(jsi::Runtime& rt, const jsi::Value& value) {
400
320
  return CallResult{0, std::vector<uint8_t>(text.begin(), text.end())};
401
321
  }
402
322
 
403
- // `UserSecrets` JSON <-> JS object conversion. Unlike the flat integer-only
404
- // records above, `UserSecrets` carries application-controlled `name` /
405
- // `description` strings, so building or reading its wire JSON goes through
406
- // the runtime's own `JSON.parse` / `JSON.stringify` rather than a hand-rolled
407
- // scanner — the only place in this file that needs real string escaping.
408
-
409
- jsi::Value jsonParseUtf8(jsi::Runtime& rt, const uint8_t* bytes, size_t len) {
410
- jsi::Object json = rt.global().getPropertyAsObject(rt, "JSON");
411
- jsi::Function parse = json.getPropertyAsFunction(rt, "parse");
412
- std::string text(reinterpret_cast<const char*>(bytes), len);
413
- return parse.call(rt, jsi::Value(rt, jsi::String::createFromUtf8(rt, text)));
414
- }
415
-
416
- std::vector<uint8_t> jsonStringifyToUtf8(jsi::Runtime& rt, const jsi::Value& value) {
417
- jsi::Object json = rt.global().getPropertyAsObject(rt, "JSON");
418
- jsi::Function stringify = json.getPropertyAsFunction(rt, "stringify");
419
- jsi::Value result = stringify.call(rt, value);
420
- std::string text = result.asString(rt).utf8(rt);
421
- return std::vector<uint8_t>(text.begin(), text.end());
422
- }
423
-
424
- jsi::Array bytesToNumberArray(jsi::Runtime& rt, ByteView view) {
425
- jsi::Array arr(rt, view.len);
426
- for (size_t i = 0; i < view.len; ++i) {
427
- arr.setValueAtIndex(rt, i, jsi::Value(static_cast<int>(view.ptr[i])));
428
- }
429
- return arr;
430
- }
431
-
432
- std::vector<uint8_t> numberArrayToBytes(jsi::Runtime& rt, jsi::Array arr) {
433
- size_t n = arr.size(rt);
434
- std::vector<uint8_t> out;
435
- out.reserve(n);
436
- for (size_t i = 0; i < n; ++i) {
437
- out.push_back(static_cast<uint8_t>(arr.getValueAtIndex(rt, i).asNumber()));
438
- }
439
- return out;
440
- }
441
-
442
323
  /// `UserSecretStore.loadLatest(): Promise<UserSecrets | null | undefined>`.
443
324
  CallResult toUserSecretsResult(jsi::Runtime& rt, const jsi::Value& value) {
444
325
  if (value.isNull() || value.isUndefined()) {
445
326
  return CallResult{1, {}};
446
327
  }
447
- jsi::Object src = value.asObject(rt);
448
- jsi::Object wire(rt);
449
- wire.setProperty(rt, "version", jsi::Value(rt, src.getProperty(rt, "version")));
450
- jsi::Array srcSecrets = src.getProperty(rt, "secrets").asObject(rt).asArray(rt);
451
- size_t n = srcSecrets.size(rt);
452
- jsi::Array wireSecrets(rt, n);
453
- for (size_t i = 0; i < n; ++i) {
454
- jsi::Object entry = srcSecrets.getValueAtIndex(rt, i).asObject(rt);
455
- jsi::Object wireEntry(rt);
456
- wireEntry.setProperty(rt, "id", jsi::Value(rt, bytesToNumberArray(rt, asBytes(rt, entry.getProperty(rt, "id")))));
457
- wireEntry.setProperty(rt, "name", jsi::Value(rt, entry.getProperty(rt, "name")));
458
- wireEntry.setProperty(
459
- rt, "data", jsi::Value(rt, bytesToNumberArray(rt, asBytes(rt, entry.getProperty(rt, "data")))));
460
- wireSecrets.setValueAtIndex(rt, i, jsi::Value(rt, wireEntry));
461
- }
462
- wire.setProperty(rt, "secrets", jsi::Value(rt, wireSecrets));
463
- if (src.hasProperty(rt, "description")) {
464
- jsi::Value description = src.getProperty(rt, "description");
465
- if (!description.isUndefined()) {
466
- wire.setProperty(rt, "description", description);
467
- }
468
- }
469
- return CallResult{0, jsonStringifyToUtf8(rt, jsi::Value(rt, wire))};
470
- }
471
-
472
- /// Decode `UserSecretStore.saveLatest`'s incoming wire JSON into the JS
473
- /// `UserSecrets` shape `src/types.ts` declares.
474
- jsi::Value buildUserSecretsFromWire(jsi::Runtime& rt, const uint8_t* ptr, size_t len) {
475
- jsi::Value parsed = jsonParseUtf8(rt, ptr, len);
476
- jsi::Object src = parsed.asObject(rt);
477
- jsi::Object out(rt);
478
- out.setProperty(rt, "version", jsi::Value(rt, src.getProperty(rt, "version")));
479
- jsi::Array srcSecrets = src.getProperty(rt, "secrets").asObject(rt).asArray(rt);
480
- size_t n = srcSecrets.size(rt);
481
- jsi::Array outSecrets(rt, n);
482
- for (size_t i = 0; i < n; ++i) {
483
- jsi::Object entry = srcSecrets.getValueAtIndex(rt, i).asObject(rt);
484
- jsi::Object outEntry(rt);
485
- std::vector<uint8_t> idBytes =
486
- numberArrayToBytes(rt, entry.getProperty(rt, "id").asObject(rt).asArray(rt));
487
- std::vector<uint8_t> dataBytes =
488
- numberArrayToBytes(rt, entry.getProperty(rt, "data").asObject(rt).asArray(rt));
489
- outEntry.setProperty(rt, "id", toUint8ArrayVal(rt, idBytes));
490
- outEntry.setProperty(rt, "name", jsi::Value(rt, entry.getProperty(rt, "name")));
491
- outEntry.setProperty(rt, "data", toUint8ArrayVal(rt, dataBytes));
492
- outSecrets.setValueAtIndex(rt, i, jsi::Value(rt, outEntry));
493
- }
494
- out.setProperty(rt, "secrets", jsi::Value(rt, outSecrets));
495
- if (src.hasProperty(rt, "description")) {
496
- jsi::Value description = src.getProperty(rt, "description");
497
- if (!description.isUndefined()) {
498
- out.setProperty(rt, "description", description);
499
- }
500
- }
501
- return jsi::Value(rt, out);
328
+ return CallResult{0, userSecretsToWire(rt, value.asObject(rt))};
502
329
  }
503
330
 
504
331
  /// Releases a buffer this binding handed to Rust through an out-parameter.
@@ -654,7 +481,7 @@ extern "C" int32_t channelStoreListReplicas(void* userData, uint64_t secretId, c
654
481
  return result.code;
655
482
  }
656
483
 
657
- /// `ChannelStore.linkChannel(secretId, channelId, linkedChannelId) -> void`
484
+ /// `ChannelStore.linkChannel(secretId, a, b) -> void`
658
485
  extern "C" int32_t channelStoreLinkChannel(void* userData, uint64_t secretId, uint64_t a, uint64_t b) {
659
486
  auto* self = static_cast<StoreBindings*>(userData);
660
487
  // Kept alive past this call so the lambda handed to `callSync` — which the
@@ -728,6 +555,39 @@ extern "C" int32_t secretStoreLoad(void* userData, uint64_t secretId, uint64_t c
728
555
  return result.code;
729
556
  }
730
557
 
558
+ /// `SecretStore.loadMany(secretId, channelIds, kind) -> Array<Uint8Array | null>`
559
+ extern "C" int32_t secretStoreLoadMany(void* userData, uint64_t secretId, const uint8_t* channelIdsJsonPtr,
560
+ size_t channelIdsJsonLen, uint32_t kind, uint8_t** outPtr,
561
+ size_t* outLen) {
562
+ auto* self = static_cast<StoreBindings*>(userData);
563
+ // Kept alive past this call so the lambda handed to `callSync` — which the
564
+ // JavaScript CallInvoker's queue may still be holding after this object's
565
+ // owner has released its own reference — has somewhere safe to run.
566
+ auto keepAlive = self->shared_from_this();
567
+ std::vector<uint64_t> channelIds = parseUnsignedJsonArray(channelIdsJsonPtr, channelIdsJsonLen);
568
+ CallResult result = keepAlive->bridge().callSync(
569
+ [keepAlive, secretId, channelIds, kind](std::function<void(CallResult)> settle) {
570
+ jsi::Runtime& rt = keepAlive->runtime();
571
+ auto store = keepAlive->secretStore();
572
+ auto method = store->getPropertyAsFunction(rt, "loadMany");
573
+ jsi::Value promise =
574
+ method.callWithThis(rt, *store, idVal(rt, secretId),
575
+ jsi::Value(rt, u64VectorToJsStringArray(rt, channelIds)),
576
+ jsi::Value(static_cast<int>(kind)));
577
+ keepAlive->settleFromPromise(rt, std::move(promise), std::move(settle),
578
+ makeSecretLoadManyResult(kind));
579
+ });
580
+ if (result.code == 0) {
581
+ if (!writeOut(result.bytes, outPtr, outLen)) {
582
+ return kBackendFailure;
583
+ }
584
+ } else {
585
+ *outPtr = nullptr;
586
+ *outLen = 0;
587
+ }
588
+ return result.code;
589
+ }
590
+
731
591
  /// `SecretStore.save(secretId, channelId, kind, value) -> void`
732
592
  extern "C" int32_t secretStoreSave(void* userData, uint64_t secretId, uint64_t channelId, uint32_t kind,
733
593
  const uint8_t* bytes, size_t len) {
@@ -899,14 +759,22 @@ extern "C" int32_t shareStoreSave(void* userData, uint64_t secretId, uint64_t ch
899
759
  // JavaScript CallInvoker's queue may still be holding after this object's
900
760
  // owner has released its own reference — has somewhere safe to run.
901
761
  auto keepAlive = self->shared_from_this();
902
- DecodedShare decoded = decodeShareRecord(shareJsonPtr, shareJsonLen);
762
+ std::optional<DecodedShare> parsed = decodeShareRecord(shareJsonPtr, shareJsonLen);
763
+ if (!parsed) {
764
+ return kBackendFailure;
765
+ }
766
+ // `share.secretId` is the record's own `secret_id`, not the partition
767
+ // `secretId`: on a helper the share carries the owner's secret id, which is
768
+ // what tells apart several secrets held in one partition.
769
+ DecodedShare decoded = std::move(*parsed);
903
770
  CallResult result = keepAlive->bridge().callSync([keepAlive, secretId, channelId,
904
771
  decoded](std::function<void(CallResult)> settle) {
905
772
  jsi::Runtime& rt = keepAlive->runtime();
906
773
  auto store = keepAlive->shareStore();
907
774
  auto method = store->getPropertyAsFunction(rt, "save");
908
775
  jsi::Object shareObj(rt);
909
- shareObj.setProperty(rt, "secretId", idVal(rt, secretId));
776
+ shareObj.setProperty(rt, "secretId",
777
+ jsi::Value(rt, jsi::String::createFromUtf8(rt, decoded.secretId)));
910
778
  shareObj.setProperty(rt, "version", jsi::Value(static_cast<int>(decoded.version)));
911
779
  shareObj.setProperty(rt, "bytes", toUint8ArrayVal(rt, decoded.bytes));
912
780
  jsi::Value promise = method.callWithThis(rt, *store, idVal(rt, secretId), idVal(rt, channelId),
@@ -973,7 +841,7 @@ extern "C" int32_t userSecretStoreSaveLatest(void* userData, uint64_t secretId,
973
841
  jsi::Runtime& rt = keepAlive->runtime();
974
842
  auto store = keepAlive->userSecretStore();
975
843
  auto method = store->getPropertyAsFunction(rt, "saveLatest");
976
- jsi::Value userSecrets = buildUserSecretsFromWire(rt, payload.data(), payload.size());
844
+ jsi::Value userSecrets = userSecretsFromWire(rt, payload.data(), payload.size());
977
845
  jsi::Value promise = method.callWithThis(rt, *store, idVal(rt, secretId), std::move(userSecrets));
978
846
  keepAlive->settleFromPromise(rt, std::move(promise), std::move(settle), toVoidResult);
979
847
  });
@@ -1092,64 +960,6 @@ extern "C" int32_t stateStoreLoadAll(void* userData, uint64_t secretId, uint32_t
1092
960
  return result.code;
1093
961
  }
1094
962
 
1095
- /// Maps the wire's `derec_proto::Protocol` discriminant to the string
1096
- /// `Transport.send`'s endpoint carries. Only `HTTPS = 0` is defined today;
1097
- /// the protocol is explicitly extensible, so an unrecognised discriminant
1098
- /// is reported rather than guessed at.
1099
- std::string protocolToString(int32_t protocol) { return protocol == 0 ? "https" : "unknown"; }
1100
-
1101
- /// Minimal reader for an encoded `TransportProtocol`: field 1 is the URI
1102
- /// (length-delimited string), field 2 the protocol discriminant (varint).
1103
- /// Hand-rolled rather than linking a protobuf runtime into the JSI layer for
1104
- /// two fields; unknown fields are skipped so a newer library stays readable.
1105
- static std::optional<std::pair<std::string, int32_t>> decodeTransportProtocol(
1106
- const uint8_t* data, size_t len) {
1107
- std::string uri;
1108
- int32_t protocol = 0;
1109
- size_t i = 0;
1110
-
1111
- auto readVarint = [&](uint64_t& out) -> bool {
1112
- out = 0;
1113
- int shift = 0;
1114
- while (i < len) {
1115
- uint8_t b = data[i++];
1116
- out |= static_cast<uint64_t>(b & 0x7F) << shift;
1117
- if ((b & 0x80) == 0) return true;
1118
- shift += 7;
1119
- if (shift > 63) return false;
1120
- }
1121
- return false;
1122
- };
1123
-
1124
- while (i < len) {
1125
- uint64_t tag = 0;
1126
- if (!readVarint(tag)) return std::nullopt;
1127
- uint32_t field = static_cast<uint32_t>(tag >> 3);
1128
- uint32_t wire = static_cast<uint32_t>(tag & 0x7);
1129
-
1130
- if (field == 1 && wire == 2) {
1131
- uint64_t size = 0;
1132
- if (!readVarint(size) || i + size > len) return std::nullopt;
1133
- uri.assign(reinterpret_cast<const char*>(data + i), size);
1134
- i += size;
1135
- } else if (field == 2 && wire == 0) {
1136
- uint64_t value = 0;
1137
- if (!readVarint(value)) return std::nullopt;
1138
- protocol = static_cast<int32_t>(value);
1139
- } else if (wire == 0) {
1140
- uint64_t skip = 0;
1141
- if (!readVarint(skip)) return std::nullopt;
1142
- } else if (wire == 2) {
1143
- uint64_t size = 0;
1144
- if (!readVarint(size) || i + size > len) return std::nullopt;
1145
- i += size;
1146
- } else {
1147
- return std::nullopt;
1148
- }
1149
- }
1150
- return std::make_pair(uri, protocol);
1151
- }
1152
-
1153
963
  /// `Transport.send(endpoints, message) -> void`
1154
964
  ///
1155
965
  /// `endpoints` is every address the peer advertised, in the peer's order,
@@ -1164,44 +974,25 @@ extern "C" int32_t transportSend(void* userData, const uint8_t* endpointsPtr, si
1164
974
  // owner has released its own reference — has somewhere safe to run.
1165
975
  auto keepAlive = self->shared_from_this();
1166
976
 
1167
- // Length-delimited TransportProtocol sequence: each entry preceded by its
1168
- // protobuf varint byte length.
1169
- std::vector<std::pair<std::string, int32_t>> endpoints;
1170
- size_t offset = 0;
1171
- while (offset < endpointsLen) {
1172
- uint64_t size = 0;
1173
- int shift = 0;
1174
- while (offset < endpointsLen) {
1175
- uint8_t b = endpointsPtr[offset++];
1176
- size |= static_cast<uint64_t>(b & 0x7F) << shift;
1177
- if ((b & 0x80) == 0) break;
1178
- shift += 7;
1179
- }
1180
- if (offset + size > endpointsLen) return -1;
1181
- auto parsed = decodeTransportProtocol(endpointsPtr + offset, size);
1182
- if (!parsed) return -1;
1183
- endpoints.push_back(*parsed);
1184
- offset += size;
977
+ // The crate decodes its own endpoint framing, names each protocol, and
978
+ // fails the send on a discriminant it cannot name; this layer only parses
979
+ // the JSON array `Transport.send` receives.
980
+ DeRecMessageJsonResult decoded = derec_transport_endpoints_json(endpointsPtr, endpointsLen);
981
+ if (decoded.error.code != 0) {
982
+ derec_free_error(&decoded.error);
983
+ return -1;
1185
984
  }
985
+ std::vector<uint8_t> endpointsJson = takeBuffer(decoded.bytes);
1186
986
 
1187
987
  std::vector<uint8_t> message(bytes, bytes + len);
1188
- CallResult result = keepAlive->bridge().callSync([keepAlive, endpoints,
988
+ CallResult result = keepAlive->bridge().callSync([keepAlive, endpointsJson,
1189
989
  message](std::function<void(CallResult)> settle) {
1190
990
  jsi::Runtime& rt = keepAlive->runtime();
1191
991
  auto store = keepAlive->transportStore();
1192
992
  auto method = store->getPropertyAsFunction(rt, "send");
1193
- jsi::Array endpointsJs(rt, endpoints.size());
1194
- for (size_t i = 0; i < endpoints.size(); ++i) {
1195
- jsi::Object endpoint(rt);
1196
- endpoint.setProperty(rt, "protocol",
1197
- jsi::Value(rt, jsi::String::createFromUtf8(
1198
- rt, protocolToString(endpoints[i].second))));
1199
- endpoint.setProperty(rt, "uri",
1200
- jsi::Value(rt, jsi::String::createFromUtf8(rt, endpoints[i].first)));
1201
- endpointsJs.setValueAtIndex(rt, i, endpoint);
1202
- }
993
+ jsi::Value endpointsJs = jsonParseUtf8(rt, endpointsJson.data(), endpointsJson.size());
1203
994
  jsi::Value promise =
1204
- method.callWithThis(rt, *store, jsi::Value(rt, endpointsJs), toUint8ArrayVal(rt, message));
995
+ method.callWithThis(rt, *store, endpointsJs, toUint8ArrayVal(rt, message));
1205
996
  keepAlive->settleFromPromise(rt, std::move(promise), std::move(settle), toVoidResult);
1206
997
  });
1207
998
  return result.code;
@@ -1318,11 +1109,12 @@ std::shared_ptr<StoreBindings> StoreBindings::create(jsi::Runtime& rt, jsi::Obje
1318
1109
  storeFreeBuffer, // free_buffer
1319
1110
  };
1320
1111
  bindings->secretCallbacks_ = SecretStoreCallbacks{
1321
- self, // user_data
1322
- secretStoreLoad, // load
1323
- secretStoreSave, // save
1324
- secretStoreRemove, // remove
1325
- storeFreeBuffer, // free_buffer
1112
+ self, // user_data
1113
+ secretStoreLoad, // load
1114
+ secretStoreLoadMany, // load_many
1115
+ secretStoreSave, // save
1116
+ secretStoreRemove, // remove
1117
+ storeFreeBuffer, // free_buffer
1326
1118
  };
1327
1119
  bindings->shareCallbacks_ = ShareStoreCallbacks{
1328
1120
  self, // user_data
@@ -1366,6 +1158,7 @@ std::shared_ptr<StoreBindings> StoreBindings::create(jsi::Runtime& rt, jsi::Obje
1366
1158
 
1367
1159
  requireField(rt, bindings->secretCallbacks_.user_data, "SecretStoreCallbacks.user_data");
1368
1160
  requireField(rt, bindings->secretCallbacks_.load, "SecretStoreCallbacks.load");
1161
+ requireField(rt, bindings->secretCallbacks_.load_many, "SecretStoreCallbacks.load_many");
1369
1162
  requireField(rt, bindings->secretCallbacks_.save, "SecretStoreCallbacks.save");
1370
1163
  requireField(rt, bindings->secretCallbacks_.remove, "SecretStoreCallbacks.remove");
1371
1164
  requireField(rt, bindings->secretCallbacks_.free_buffer, "SecretStoreCallbacks.free_buffer");