@derec-alliance/react-native 0.0.5 → 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.
@@ -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,9 +25,9 @@ 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
- static_assert(sizeof(ShareStoreCallbacks) == 8 * sizeof(void*),
30
+ static_assert(sizeof(ShareStoreCallbacks) == 10 * sizeof(void*),
30
31
  "ShareStoreCallbacks layout changed; update the bindings");
31
32
  static_assert(sizeof(UserSecretStoreCallbacks) == 5 * sizeof(void*),
32
33
  "UserSecretStoreCallbacks 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,43 +320,38 @@ 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])));
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'}};
428
334
  }
429
- return arr;
430
- }
431
-
432
- std::vector<uint8_t> numberArrayToBytes(jsi::Runtime& rt, jsi::Array arr) {
335
+ if (!value.isObject() || !value.asObject(rt).isArray(rt)) {
336
+ return CallResult{kBackendFailure, {}};
337
+ }
338
+ jsi::Array arr = value.asObject(rt).asArray(rt);
433
339
  size_t n = arr.size(rt);
434
- std::vector<uint8_t> out;
435
- out.reserve(n);
340
+ std::string text = "[";
436
341
  for (size_t i = 0; i < n; ++i) {
437
- out.push_back(static_cast<uint8_t>(arr.getValueAtIndex(rt, i).asNumber()));
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));
438
352
  }
439
- return out;
353
+ text += ']';
354
+ return CallResult{0, std::vector<uint8_t>(text.begin(), text.end())};
440
355
  }
441
356
 
442
357
  /// `UserSecretStore.loadLatest(): Promise<UserSecrets | null | undefined>`.
@@ -444,61 +359,7 @@ CallResult toUserSecretsResult(jsi::Runtime& rt, const jsi::Value& value) {
444
359
  if (value.isNull() || value.isUndefined()) {
445
360
  return CallResult{1, {}};
446
361
  }
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);
362
+ return CallResult{0, userSecretsToWire(rt, value.asObject(rt))};
502
363
  }
503
364
 
504
365
  /// Releases a buffer this binding handed to Rust through an out-parameter.
@@ -654,7 +515,7 @@ extern "C" int32_t channelStoreListReplicas(void* userData, uint64_t secretId, c
654
515
  return result.code;
655
516
  }
656
517
 
657
- /// `ChannelStore.linkChannel(secretId, channelId, linkedChannelId) -> void`
518
+ /// `ChannelStore.linkChannel(secretId, a, b) -> void`
658
519
  extern "C" int32_t channelStoreLinkChannel(void* userData, uint64_t secretId, uint64_t a, uint64_t b) {
659
520
  auto* self = static_cast<StoreBindings*>(userData);
660
521
  // Kept alive past this call so the lambda handed to `callSync` — which the
@@ -728,6 +589,39 @@ extern "C" int32_t secretStoreLoad(void* userData, uint64_t secretId, uint64_t c
728
589
  return result.code;
729
590
  }
730
591
 
592
+ /// `SecretStore.loadMany(secretId, channelIds, kind) -> Array<Uint8Array | null>`
593
+ extern "C" int32_t secretStoreLoadMany(void* userData, uint64_t secretId, const uint8_t* channelIdsJsonPtr,
594
+ size_t channelIdsJsonLen, uint32_t kind, uint8_t** outPtr,
595
+ size_t* outLen) {
596
+ auto* self = static_cast<StoreBindings*>(userData);
597
+ // Kept alive past this call so the lambda handed to `callSync` — which the
598
+ // JavaScript CallInvoker's queue may still be holding after this object's
599
+ // owner has released its own reference — has somewhere safe to run.
600
+ auto keepAlive = self->shared_from_this();
601
+ std::vector<uint64_t> channelIds = parseUnsignedJsonArray(channelIdsJsonPtr, channelIdsJsonLen);
602
+ CallResult result = keepAlive->bridge().callSync(
603
+ [keepAlive, secretId, channelIds, kind](std::function<void(CallResult)> settle) {
604
+ jsi::Runtime& rt = keepAlive->runtime();
605
+ auto store = keepAlive->secretStore();
606
+ auto method = store->getPropertyAsFunction(rt, "loadMany");
607
+ jsi::Value promise =
608
+ method.callWithThis(rt, *store, idVal(rt, secretId),
609
+ jsi::Value(rt, u64VectorToJsStringArray(rt, channelIds)),
610
+ jsi::Value(static_cast<int>(kind)));
611
+ keepAlive->settleFromPromise(rt, std::move(promise), std::move(settle),
612
+ makeSecretLoadManyResult(kind));
613
+ });
614
+ if (result.code == 0) {
615
+ if (!writeOut(result.bytes, outPtr, outLen)) {
616
+ return kBackendFailure;
617
+ }
618
+ } else {
619
+ *outPtr = nullptr;
620
+ *outLen = 0;
621
+ }
622
+ return result.code;
623
+ }
624
+
731
625
  /// `SecretStore.save(secretId, channelId, kind, value) -> void`
732
626
  extern "C" int32_t secretStoreSave(void* userData, uint64_t secretId, uint64_t channelId, uint32_t kind,
733
627
  const uint8_t* bytes, size_t len) {
@@ -899,14 +793,22 @@ extern "C" int32_t shareStoreSave(void* userData, uint64_t secretId, uint64_t ch
899
793
  // JavaScript CallInvoker's queue may still be holding after this object's
900
794
  // owner has released its own reference — has somewhere safe to run.
901
795
  auto keepAlive = self->shared_from_this();
902
- DecodedShare decoded = decodeShareRecord(shareJsonPtr, shareJsonLen);
796
+ std::optional<DecodedShare> parsed = decodeShareRecord(shareJsonPtr, shareJsonLen);
797
+ if (!parsed) {
798
+ return kBackendFailure;
799
+ }
800
+ // `share.secretId` is the record's own `secret_id`, not the partition
801
+ // `secretId`: on a helper the share carries the owner's secret id, which is
802
+ // what tells apart several secrets held in one partition.
803
+ DecodedShare decoded = std::move(*parsed);
903
804
  CallResult result = keepAlive->bridge().callSync([keepAlive, secretId, channelId,
904
805
  decoded](std::function<void(CallResult)> settle) {
905
806
  jsi::Runtime& rt = keepAlive->runtime();
906
807
  auto store = keepAlive->shareStore();
907
808
  auto method = store->getPropertyAsFunction(rt, "save");
908
809
  jsi::Object shareObj(rt);
909
- shareObj.setProperty(rt, "secretId", idVal(rt, secretId));
810
+ shareObj.setProperty(rt, "secretId",
811
+ jsi::Value(rt, jsi::String::createFromUtf8(rt, decoded.secretId)));
910
812
  shareObj.setProperty(rt, "version", jsi::Value(static_cast<int>(decoded.version)));
911
813
  shareObj.setProperty(rt, "bytes", toUint8ArrayVal(rt, decoded.bytes));
912
814
  jsi::Value promise = method.callWithThis(rt, *store, idVal(rt, secretId), idVal(rt, channelId),
@@ -934,6 +836,53 @@ extern "C" int32_t shareStoreRemoveChannel(void* userData, uint64_t secretId, ui
934
836
  return result.code;
935
837
  }
936
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
+
937
886
  /// `UserSecretStore.loadLatest(secretId) -> UserSecrets | null`
938
887
  extern "C" int32_t userSecretStoreLoadLatest(void* userData, uint64_t secretId, uint8_t** outPtr,
939
888
  size_t* outLen) {
@@ -973,7 +922,7 @@ extern "C" int32_t userSecretStoreSaveLatest(void* userData, uint64_t secretId,
973
922
  jsi::Runtime& rt = keepAlive->runtime();
974
923
  auto store = keepAlive->userSecretStore();
975
924
  auto method = store->getPropertyAsFunction(rt, "saveLatest");
976
- jsi::Value userSecrets = buildUserSecretsFromWire(rt, payload.data(), payload.size());
925
+ jsi::Value userSecrets = userSecretsFromWire(rt, payload.data(), payload.size());
977
926
  jsi::Value promise = method.callWithThis(rt, *store, idVal(rt, secretId), std::move(userSecrets));
978
927
  keepAlive->settleFromPromise(rt, std::move(promise), std::move(settle), toVoidResult);
979
928
  });
@@ -1092,64 +1041,6 @@ extern "C" int32_t stateStoreLoadAll(void* userData, uint64_t secretId, uint32_t
1092
1041
  return result.code;
1093
1042
  }
1094
1043
 
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
1044
  /// `Transport.send(endpoints, message) -> void`
1154
1045
  ///
1155
1046
  /// `endpoints` is every address the peer advertised, in the peer's order,
@@ -1164,44 +1055,25 @@ extern "C" int32_t transportSend(void* userData, const uint8_t* endpointsPtr, si
1164
1055
  // owner has released its own reference — has somewhere safe to run.
1165
1056
  auto keepAlive = self->shared_from_this();
1166
1057
 
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;
1058
+ // The crate decodes its own endpoint framing, names each protocol, and
1059
+ // fails the send on a discriminant it cannot name; this layer only parses
1060
+ // the JSON array `Transport.send` receives.
1061
+ DeRecMessageJsonResult decoded = derec_transport_endpoints_json(endpointsPtr, endpointsLen);
1062
+ if (decoded.error.code != 0) {
1063
+ derec_free_error(&decoded.error);
1064
+ return -1;
1185
1065
  }
1066
+ std::vector<uint8_t> endpointsJson = takeBuffer(decoded.bytes);
1186
1067
 
1187
1068
  std::vector<uint8_t> message(bytes, bytes + len);
1188
- CallResult result = keepAlive->bridge().callSync([keepAlive, endpoints,
1069
+ CallResult result = keepAlive->bridge().callSync([keepAlive, endpointsJson,
1189
1070
  message](std::function<void(CallResult)> settle) {
1190
1071
  jsi::Runtime& rt = keepAlive->runtime();
1191
1072
  auto store = keepAlive->transportStore();
1192
1073
  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
- }
1074
+ jsi::Value endpointsJs = jsonParseUtf8(rt, endpointsJson.data(), endpointsJson.size());
1203
1075
  jsi::Value promise =
1204
- method.callWithThis(rt, *store, jsi::Value(rt, endpointsJs), toUint8ArrayVal(rt, message));
1076
+ method.callWithThis(rt, *store, endpointsJs, toUint8ArrayVal(rt, message));
1205
1077
  keepAlive->settleFromPromise(rt, std::move(promise), std::move(settle), toVoidResult);
1206
1078
  });
1207
1079
  return result.code;
@@ -1318,11 +1190,12 @@ std::shared_ptr<StoreBindings> StoreBindings::create(jsi::Runtime& rt, jsi::Obje
1318
1190
  storeFreeBuffer, // free_buffer
1319
1191
  };
1320
1192
  bindings->secretCallbacks_ = SecretStoreCallbacks{
1321
- self, // user_data
1322
- secretStoreLoad, // load
1323
- secretStoreSave, // save
1324
- secretStoreRemove, // remove
1325
- storeFreeBuffer, // free_buffer
1193
+ self, // user_data
1194
+ secretStoreLoad, // load
1195
+ secretStoreLoadMany, // load_many
1196
+ secretStoreSave, // save
1197
+ secretStoreRemove, // remove
1198
+ storeFreeBuffer, // free_buffer
1326
1199
  };
1327
1200
  bindings->shareCallbacks_ = ShareStoreCallbacks{
1328
1201
  self, // user_data
@@ -1331,8 +1204,10 @@ std::shared_ptr<StoreBindings> StoreBindings::create(jsi::Runtime& rt, jsi::Obje
1331
1204
  shareStoreLoadAll, // load_all
1332
1205
  shareStoreLatestVersion, // latest_version
1333
1206
  shareStoreSave, // save
1334
- shareStoreRemoveChannel, // remove_channel
1335
- storeFreeBuffer, // free_buffer
1207
+ shareStoreRemoveChannel, // remove_channel
1208
+ shareStoreRemoveVersions, // remove_versions
1209
+ shareStoreKeepList, // keep_list
1210
+ storeFreeBuffer, // free_buffer
1336
1211
  };
1337
1212
  bindings->userSecretCallbacks_ = UserSecretStoreCallbacks{
1338
1213
  self, // user_data
@@ -1366,6 +1241,7 @@ std::shared_ptr<StoreBindings> StoreBindings::create(jsi::Runtime& rt, jsi::Obje
1366
1241
 
1367
1242
  requireField(rt, bindings->secretCallbacks_.user_data, "SecretStoreCallbacks.user_data");
1368
1243
  requireField(rt, bindings->secretCallbacks_.load, "SecretStoreCallbacks.load");
1244
+ requireField(rt, bindings->secretCallbacks_.load_many, "SecretStoreCallbacks.load_many");
1369
1245
  requireField(rt, bindings->secretCallbacks_.save, "SecretStoreCallbacks.save");
1370
1246
  requireField(rt, bindings->secretCallbacks_.remove, "SecretStoreCallbacks.remove");
1371
1247
  requireField(rt, bindings->secretCallbacks_.free_buffer, "SecretStoreCallbacks.free_buffer");
@@ -1377,6 +1253,8 @@ std::shared_ptr<StoreBindings> StoreBindings::create(jsi::Runtime& rt, jsi::Obje
1377
1253
  requireField(rt, bindings->shareCallbacks_.latest_version, "ShareStoreCallbacks.latest_version");
1378
1254
  requireField(rt, bindings->shareCallbacks_.save, "ShareStoreCallbacks.save");
1379
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");
1380
1258
  requireField(rt, bindings->shareCallbacks_.free_buffer, "ShareStoreCallbacks.free_buffer");
1381
1259
 
1382
1260
  requireField(rt, bindings->userSecretCallbacks_.user_data, "UserSecretStoreCallbacks.user_data");