@derec-alliance/react-native 0.0.4 → 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.
package/README.md CHANGED
@@ -98,7 +98,7 @@ const protocol = new DeRecProtocolBuilder(secretId) // bigint | number
98
98
  .withUserSecretStore(myUserSecretStore)
99
99
  .withStateStore(myStateStore)
100
100
  .withTransport(myTransport)
101
- .withOwnTransport({ uri: 'https://owner.example.com', protocol: 'https' })
101
+ .withOwnTransports([{ uri: 'https://owner.example.com', protocol: 'https' }])
102
102
  .withThreshold(2)
103
103
  .build();
104
104
 
@@ -141,9 +141,14 @@ declared in `src/types.ts`:
141
141
  - **`StateStore`** — in-flight orchestrator bookkeeping (pending
142
142
  verifications, recoveries, unpairs, sharing rounds, replica discoveries) so a
143
143
  multi-round flow survives an app restart.
144
- - **`Transport`** — outbound delivery. `send(endpoint, message)` posts a
145
- wire-format envelope to a peer; it is a mailbox, not a request/response
146
- call, since not every peer (e.g. a phone behind NAT) can be dialed back.
144
+ - **`Transport`** — outbound delivery.
145
+ `send(endpoints: ReadonlyArray<{ protocol: string; uri: string }>, message: Uint8Array): Promise<void>`
146
+ posts a wire-format envelope to a peer. `endpoints` is every address that
147
+ peer advertised, in its own order (`protocol` is `"https"` or `"grpc"`);
148
+ deliver once, to the first one that accepts it, and reject only when none
149
+ did. `sequentialFailover` and `singleEndpointTransport` implement that
150
+ choice for you. It is a mailbox, not a request/response call, since not
151
+ every peer (e.g. a phone behind NAT) can be dialed back.
147
152
 
148
153
  Every method is `async` and keyed by decimal-string ids — see the next
149
154
  section for why that differs from `DeRecProtocol`'s own methods. Full field-
@@ -215,17 +220,6 @@ after whatever is in flight.
215
220
 
216
221
  ## Known differences from `@derec-alliance/nodejs`
217
222
 
218
- **`setCommunicationInfo()`, `setOwnTransport()` and `setOwnTransports()` return a `Promise`.**
219
- `@derec-alliance/nodejs` declares both `void`. Here they must not run on the
220
- JavaScript thread: the underlying FFI setter takes the same handle lock a
221
- running flow holds across its store callbacks, and those callbacks block the
222
- worker until JavaScript settles them — so a synchronous setter would block
223
- the JavaScript thread on a lock only the JavaScript thread can release. The
224
- binding queues them onto the same serial worker every flow runs on instead.
225
- The nodejs SDK is WASM and single-threaded, so it cannot hit this. Code that
226
- ignores the return value — the way nodejs code calls these — is unaffected;
227
- awaiting them additionally lets a failure surface instead of vanishing.
228
-
229
223
  **The `DeRecErrorCategory` union differs from nodejs's.** This SDK's union
230
224
  omits `"wasm"`, which a C-ABI binding can never emit, and adds `"ok"`,
231
225
  `"ffi"` and `"state_store"`, which it can. A test pins the union to the crate's shared enum fixture so it stays in step
@@ -14,7 +14,7 @@ find_package(fbjni REQUIRED CONFIG)
14
14
 
15
15
  # Non-recursive on purpose: `cpp/tests/` holds the host-only test harness,
16
16
  # which must not be compiled into the device library.
17
- file(GLOB DEREC_CPP_SOURCES "${CMAKE_SOURCE_DIR}/../cpp/*.cpp")
17
+ file(GLOB DEREC_CPP_SOURCES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/../cpp/*.cpp")
18
18
 
19
19
  add_library(derec_jsi SHARED
20
20
  ${DEREC_CPP_SOURCES}
package/cpp/Convert.cpp CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  #include <cstdlib>
7
7
  #include <cstring>
8
+ #include <utility>
8
9
 
9
10
  namespace derec {
10
11
 
@@ -33,6 +34,23 @@ std::vector<uint8_t> takeBuffer(DeRecBuffer& buffer) {
33
34
  return out;
34
35
  }
35
36
 
37
+ RestoreOutcome takeRestoreResult(DeRecProtocolRestoreResult& result) {
38
+ RestoreOutcome out{result.error, {}, {}};
39
+ try {
40
+ out.events = takeBuffer(result.events_json);
41
+ out.conflictingChannelIdsJson = takeBuffer(result.conflicting_channel_ids_json);
42
+ } catch (...) {
43
+ // `takeBuffer` clears a buffer only after releasing it, so whichever is
44
+ // still set here was never released.
45
+ derec_free_buffer(result.events_json.ptr, result.events_json.len);
46
+ derec_free_buffer(result.conflicting_channel_ids_json.ptr,
47
+ result.conflicting_channel_ids_json.len);
48
+ derec_free_error(&result.error);
49
+ throw;
50
+ }
51
+ return out;
52
+ }
53
+
36
54
  std::string takeString(char* owned) {
37
55
  if (owned == nullptr) {
38
56
  return {};
@@ -47,4 +65,111 @@ std::pair<std::string, std::string> errorName(int32_t category, int32_t code) {
47
65
  std::string(derec_error_code_name(code))};
48
66
  }
49
67
 
68
+
69
+ // Minimal decoders for the wire's "bare JSON array/object of unsigned
70
+ // integers" convention (`versions_json`, `channel_ids_json`, the `bytes`
71
+ // field of a `ShareRecord` / `SecretValueRecord`). These are produced by
72
+ // `serde_json`'s default compact, whitespace-free output over a fixed,
73
+ // known shape, so a substring scan is sufficient and — critically — never
74
+ // routes a `u64` through a JS `number`, which cannot represent every value
75
+ // in that range exactly.
76
+
77
+ namespace {
78
+
79
+ /// Offset just past `"key":` in a flat, compact JSON object, or
80
+ /// `std::string::npos` if `key` is absent.
81
+ size_t fieldValueOffset(const std::string& json, const char* key) {
82
+ std::string needle = std::string("\"") + key + "\":";
83
+ size_t pos = json.find(needle);
84
+ return pos == std::string::npos ? std::string::npos : pos + needle.size();
85
+ }
86
+
87
+ uint64_t parseUnsignedAt(const std::string& json, size_t pos) {
88
+ uint64_t value = 0;
89
+ while (pos < json.size() && json[pos] >= '0' && json[pos] <= '9') {
90
+ value = value * 10 + static_cast<uint64_t>(json[pos] - '0');
91
+ ++pos;
92
+ }
93
+ return value;
94
+ }
95
+
96
+ std::vector<uint8_t> parseByteArrayAt(const std::string& json, size_t pos) {
97
+ std::vector<uint8_t> out;
98
+ while (pos < json.size() && json[pos] != '[') ++pos;
99
+ if (pos >= json.size()) return out;
100
+ ++pos;
101
+ while (pos < json.size() && json[pos] != ']') {
102
+ if (json[pos] < '0' || json[pos] > '9') {
103
+ ++pos;
104
+ continue;
105
+ }
106
+ uint64_t value = 0;
107
+ while (pos < json.size() && json[pos] >= '0' && json[pos] <= '9') {
108
+ value = value * 10 + static_cast<uint64_t>(json[pos] - '0');
109
+ ++pos;
110
+ }
111
+ out.push_back(static_cast<uint8_t>(value));
112
+ }
113
+ return out;
114
+ }
115
+
116
+ /// The string inside `"key":"..."` in a flat, compact JSON object, or empty
117
+ /// if `key` is absent.
118
+ std::optional<std::string> quotedFieldValue(const std::string& json, const char* key) {
119
+ size_t pos = fieldValueOffset(json, key);
120
+ if (pos == std::string::npos || pos >= json.size() || json[pos] != '"') return std::nullopt;
121
+ size_t end = json.find('"', pos + 1);
122
+ if (end == std::string::npos) return std::nullopt;
123
+ return json.substr(pos + 1, end - pos - 1);
124
+ }
125
+
126
+ } // namespace
127
+
128
+ /// Parse a bare `[n, n, ...]` array of unsigned decimal integers.
129
+ std::vector<uint64_t> parseUnsignedJsonArray(const uint8_t* ptr, size_t len) {
130
+ std::vector<uint64_t> out;
131
+ size_t i = 0;
132
+ while (i < len && ptr[i] != '[') ++i;
133
+ if (i >= len) return out;
134
+ ++i;
135
+ while (i < len && ptr[i] != ']') {
136
+ if (ptr[i] < '0' || ptr[i] > '9') {
137
+ ++i;
138
+ continue;
139
+ }
140
+ uint64_t value = 0;
141
+ while (i < len && ptr[i] >= '0' && ptr[i] <= '9') {
142
+ value = value * 10 + static_cast<uint64_t>(ptr[i] - '0');
143
+ ++i;
144
+ }
145
+ out.push_back(value);
146
+ }
147
+ return out;
148
+ }
149
+
150
+ /// `{"kind":<n>,"bytes":[<n>,...]}` — the wire shape `SecretStore.save`
151
+ /// receives; `kind` is already carried as a separate FFI argument so only
152
+ /// `bytes` needs to be read back out.
153
+ std::vector<uint8_t> decodeSecretValueBytes(const uint8_t* ptr, size_t len) {
154
+ std::string json(reinterpret_cast<const char*>(ptr), len);
155
+ size_t pos = fieldValueOffset(json, "bytes");
156
+ if (pos == std::string::npos) return {};
157
+ return parseByteArrayAt(json, pos);
158
+ }
159
+
160
+ std::optional<DecodedShare> decodeShareRecord(const uint8_t* ptr, size_t len) {
161
+ std::string json(reinterpret_cast<const char*>(ptr), len);
162
+ std::optional<std::string> secretId = quotedFieldValue(json, "secret_id");
163
+ size_t versionPos = fieldValueOffset(json, "version");
164
+ size_t bytesPos = fieldValueOffset(json, "bytes");
165
+ if (!secretId || versionPos == std::string::npos || bytesPos == std::string::npos) {
166
+ return std::nullopt;
167
+ }
168
+ DecodedShare out;
169
+ out.secretId = std::move(*secretId);
170
+ out.version = static_cast<uint32_t>(parseUnsignedAt(json, versionPos));
171
+ out.bytes = parseByteArrayAt(json, bytesPos);
172
+ return out;
173
+ }
174
+
50
175
  } // namespace derec
package/cpp/Convert.h CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  #include <cstddef>
7
7
  #include <cstdint>
8
+ #include <optional>
8
9
  #include <string>
9
10
  #include <utility>
10
11
  #include <vector>
@@ -32,7 +33,41 @@ std::vector<uint8_t> takeBuffer(DeRecBuffer& buffer);
32
33
  /// Copy a Rust-owned C string and release it via `derec_free_string`.
33
34
  std::string takeString(char* owned);
34
35
 
36
+ /// Every owned part of a `DeRecProtocolRestoreResult`, copied out with both
37
+ /// buffers already released. `error`'s strings stay owned: they are released
38
+ /// by `throwDeRecError`, as for every other result.
39
+ struct RestoreOutcome {
40
+ DeRecError error;
41
+ std::vector<uint8_t> events;
42
+ std::vector<uint8_t> conflictingChannelIdsJson;
43
+ };
44
+
45
+ /// Releases both buffers of `result` on every path, including a failed copy.
46
+ RestoreOutcome takeRestoreResult(DeRecProtocolRestoreResult& result);
47
+
35
48
  /// Resolve the `(category, code)` name pair from the crate's accessors.
36
49
  std::pair<std::string, std::string> errorName(int32_t category, int32_t code);
37
50
 
51
+ // Decoders for the payloads Rust hands the store callbacks.
52
+ // They hold no `jsi` state, so the standalone test harness exercises them
53
+ // against the real crate.
54
+
55
+ /// Parse a bare `[n, n, ...]` array of unsigned decimal integers.
56
+ std::vector<uint64_t> parseUnsignedJsonArray(const uint8_t* ptr, size_t len);
57
+
58
+ /// The `bytes` field of a `{"kind":<n>,"bytes":[<n>,...]}` `SecretValueRecord`.
59
+ std::vector<uint8_t> decodeSecretValueBytes(const uint8_t* ptr, size_t len);
60
+
61
+ /// A `ShareRecord` as `ShareStore.save` receives it. `secretId` is the
62
+ /// record's own `secret_id`, kept as the decimal string Rust wrote.
63
+ struct DecodedShare {
64
+ std::string secretId;
65
+ uint32_t version;
66
+ std::vector<uint8_t> bytes;
67
+ };
68
+
69
+ /// Decode `{"secret_id":"...","version":<n>,"bytes":[<n>,...]}`. Empty when
70
+ /// any of the three fields is absent.
71
+ std::optional<DecodedShare> decodeShareRecord(const uint8_t* ptr, size_t len);
72
+
38
73
  } // namespace derec
@@ -157,18 +157,6 @@ void install(jsi::Runtime& rt, std::shared_ptr<Invoker> invoker) {
157
157
 
158
158
  auto host = jsi::Object(rt);
159
159
 
160
- host.setProperty(
161
- rt, "version",
162
- jsi::Function::createFromHostFunction(
163
- rt, jsi::PropNameID::forAscii(rt, "version"), 0,
164
- [](jsi::Runtime& rt, const jsi::Value&, const jsi::Value*, size_t)
165
- -> jsi::Value {
166
- DeRecProtocolVersion version = derec_protocol_version();
167
- std::string text = std::to_string(version.major) + "." +
168
- std::to_string(version.minor);
169
- return jsi::String::createFromUtf8(rt, text);
170
- }));
171
-
172
160
  installPrimitives(rt, host);
173
161
 
174
162
  host.setProperty(
@@ -14,6 +14,12 @@ using namespace facebook;
14
14
  namespace derec {
15
15
 
16
16
  void throwDeRecError(jsi::Runtime& rt, const DeRecError& error) {
17
+ throwDeRecError(rt, error, {});
18
+ }
19
+
20
+ void throwDeRecError(jsi::Runtime& rt,
21
+ const DeRecError& error,
22
+ const std::vector<uint8_t>& conflictingChannelIdsJson) {
17
23
  auto names = errorName(error.category, error.code);
18
24
  // `message` and `peer_memo` are Rust-owned strings; copy before releasing.
19
25
  std::string message =
@@ -42,6 +48,11 @@ void throwDeRecError(jsi::Runtime& rt, const DeRecError& error) {
42
48
  payload.setProperty(rt, "expected", jsi::Value(static_cast<int>(expected)));
43
49
  payload.setProperty(rt, "got", jsi::Value(static_cast<int>(got)));
44
50
  }
51
+ if (!conflictingChannelIdsJson.empty()) {
52
+ payload.setProperty(rt, "channel_ids",
53
+ jsi::Value::createFromJsonUtf8(rt, conflictingChannelIdsJson.data(),
54
+ conflictingChannelIdsJson.size()));
55
+ }
45
56
  throw jsi::JSError(rt, jsi::Value(rt, payload));
46
57
  }
47
58
 
@@ -74,6 +85,22 @@ jsi::Value toArrayBuffer(jsi::Runtime& rt, const uint8_t* bytes, size_t len) {
74
85
  return jsi::Value(rt, result);
75
86
  }
76
87
 
88
+ /// Build a genuine `Uint8Array` (not a bare `ArrayBuffer`) — every store
89
+ /// interface types its byte payloads as `Uint8Array`, and application code
90
+ /// is entitled to index or slice it directly.
91
+ jsi::Value toUint8ArrayVal(jsi::Runtime& rt, const uint8_t* bytes, size_t len) {
92
+ jsi::Uint8Array arr(rt, len);
93
+ if (len > 0 && bytes != nullptr) {
94
+ jsi::ArrayBuffer buffer = arr.buffer(rt);
95
+ std::memcpy(buffer.data(rt), bytes, len);
96
+ }
97
+ return jsi::Value(rt, std::move(arr));
98
+ }
99
+
100
+ jsi::Value toUint8ArrayVal(jsi::Runtime& rt, const std::vector<uint8_t>& bytes) {
101
+ return toUint8ArrayVal(rt, bytes.data(), bytes.size());
102
+ }
103
+
77
104
  uint64_t asU64(jsi::Runtime& rt, const jsi::Value& value) {
78
105
  if (value.isBigInt()) {
79
106
  return value.asBigInt(rt).asUint64(rt);
@@ -262,6 +289,28 @@ jsi::Value readTraceId(jsi::Runtime& rt,
262
289
  return jsi::Value(rt, jsi::BigInt::fromUint64(rt, result.trace_id));
263
290
  }
264
291
 
292
+ /// `version() -> { major: number, minor: number }` — the protocol version the
293
+ /// core writes into every envelope it produces.
294
+ jsi::Value protocolVersion(jsi::Runtime& rt,
295
+ const jsi::Value&,
296
+ const jsi::Value*,
297
+ size_t) {
298
+ DeRecProtocolVersion version = derec_protocol_version();
299
+ auto payload = jsi::Object(rt);
300
+ payload.setProperty(rt, "major", jsi::Value(static_cast<double>(version.major)));
301
+ payload.setProperty(rt, "minor", jsi::Value(static_cast<double>(version.minor)));
302
+ return jsi::Value(rt, payload);
303
+ }
304
+
305
+ /// `generate_replica_id() -> bigint` — a fresh replica identity from the
306
+ /// core, never `0`. The application persists it once per device.
307
+ jsi::Value generateReplicaId(jsi::Runtime& rt,
308
+ const jsi::Value&,
309
+ const jsi::Value*,
310
+ size_t) {
311
+ return jsi::Value(rt, jsi::BigInt::fromUint64(rt, derec_generate_replica_id()));
312
+ }
313
+
265
314
  /// `create_contact_message(channelId: bigint, contactMode: number,
266
315
  /// transportProtocols: ArrayBuffer, hasNonce: number, nonce: bigint)
267
316
  /// -> { contact_wire_bytes, secret_key_material }`
@@ -291,21 +340,6 @@ jsi::Value createContactMessage(jsi::Runtime& rt,
291
340
  return jsi::Value(rt, payload);
292
341
  }
293
342
 
294
- /// `validate_contact_message(contactMessage: ArrayBuffer) -> undefined`
295
- jsi::Value validateContactMessage(jsi::Runtime& rt,
296
- const jsi::Value&,
297
- const jsi::Value* args,
298
- size_t count) {
299
- requireArgs(rt, "validate_contact_message", count, 1);
300
- ByteView contactMessage = asBytes(rt, args[0]);
301
- DeRecError error =
302
- validate_contact_message(contactMessage.ptr, contactMessage.len);
303
- if (error.code != 0) {
304
- throwDeRecError(rt, error);
305
- }
306
- return jsi::Value::undefined();
307
- }
308
-
309
343
  /// `encode_contact_message(contactJson: ArrayBuffer) -> ArrayBuffer`
310
344
  jsi::Value encodeContactMessage(jsi::Runtime& rt,
311
345
  const jsi::Value&,
@@ -338,6 +372,20 @@ jsi::Value decodeContactMessage(jsi::Runtime& rt,
338
372
  return toArrayBuffer(rt, bytes.data(), bytes.size());
339
373
  }
340
374
 
375
+ /// `transport_protocol_discriminant(name: string) -> number`
376
+ ///
377
+ /// The `derec_proto::Protocol` discriminant the crate assigns to `name`, or
378
+ /// `-1` when it names none; forwarded unchanged for Rust to validate.
379
+ jsi::Value transportProtocolDiscriminant(jsi::Runtime& rt,
380
+ const jsi::Value&,
381
+ const jsi::Value* args,
382
+ size_t count) {
383
+ requireArgs(rt, "transport_protocol_discriminant", count, 1);
384
+ std::string name = args[0].asString(rt).utf8(rt);
385
+ return jsi::Value(static_cast<int>(derec_transport_protocol_discriminant(
386
+ reinterpret_cast<const uint8_t*>(name.data()), name.size())));
387
+ }
388
+
341
389
  /// `decode_message_json(kind: number, protoBytes: ArrayBuffer) -> ArrayBuffer`
342
390
  ///
343
391
  /// Turns the opaque `*_proto_bytes` an `extract_*` call returns into the JSON
@@ -415,17 +463,19 @@ jsi::Value producePairRequestMessage(jsi::Runtime& rt,
415
463
  return jsi::Value(rt, payload);
416
464
  }
417
465
 
418
- /// `extract_pair_request(request: ArrayBuffer, secretKeyMaterial: ArrayBuffer)
419
- /// -> { channel_id, request_proto_bytes }`
466
+ /// `extract_pair_request(request: ArrayBuffer, secretKeyMaterial: ArrayBuffer,
467
+ /// parameterRange: ArrayBuffer|null) -> { channel_id, request_proto_bytes }`
420
468
  jsi::Value extractPairRequest(jsi::Runtime& rt,
421
469
  const jsi::Value&,
422
470
  const jsi::Value* args,
423
471
  size_t count) {
424
- requireArgs(rt, "extract_pair_request", count, 2);
472
+ requireArgs(rt, "extract_pair_request", count, 3);
425
473
  ByteView request = asBytes(rt, args[0]);
426
474
  ByteView secretKeyMaterial = asBytes(rt, args[1]);
475
+ ByteView parameterRange = asBytes(rt, args[2]);
427
476
  ExtractPairRequestResult result = extract_pair_request(
428
- request.ptr, request.len, secretKeyMaterial.ptr, secretKeyMaterial.len);
477
+ request.ptr, request.len, secretKeyMaterial.ptr, secretKeyMaterial.len,
478
+ parameterRange.ptr, parameterRange.len);
429
479
  if (result.error.code != 0) {
430
480
  throwDeRecError(rt, result.error);
431
481
  }
@@ -506,19 +556,21 @@ jsi::Value extractPairResponse(jsi::Runtime& rt,
506
556
  }
507
557
 
508
558
  /// `process_pair_response_message(contactMessage: ArrayBuffer, responseProto:
509
- /// ArrayBuffer, secretKeyMaterial: ArrayBuffer) -> { shared_key,
510
- /// channel_id }`
559
+ /// ArrayBuffer, secretKeyMaterial: ArrayBuffer, parameterRange:
560
+ /// ArrayBuffer|null) -> { shared_key, channel_id }`
511
561
  jsi::Value processPairResponseMessage(jsi::Runtime& rt,
512
562
  const jsi::Value&,
513
563
  const jsi::Value* args,
514
564
  size_t count) {
515
- requireArgs(rt, "process_pair_response_message", count, 3);
565
+ requireArgs(rt, "process_pair_response_message", count, 4);
516
566
  ByteView contactMessage = asBytes(rt, args[0]);
517
567
  ByteView responseProto = asBytes(rt, args[1]);
518
568
  ByteView secretKeyMaterial = asBytes(rt, args[2]);
569
+ ByteView parameterRange = asBytes(rt, args[3]);
519
570
  ProcessPairResponseMessageResult result = process_pair_response_message(
520
571
  contactMessage.ptr, contactMessage.len, responseProto.ptr,
521
- responseProto.len, secretKeyMaterial.ptr, secretKeyMaterial.len);
572
+ responseProto.len, secretKeyMaterial.ptr, secretKeyMaterial.len,
573
+ parameterRange.ptr, parameterRange.len);
522
574
  if (result.error.code != 0) {
523
575
  throwDeRecError(rt, result.error);
524
576
  }
@@ -645,6 +697,79 @@ jsi::Value processPrePairResponseMessage(jsi::Runtime& rt,
645
697
  return jsi::Value(rt, payload);
646
698
  }
647
699
 
700
+ /// `produce_pre_pair_no_keys_response_message(channelId: bigint,
701
+ /// requestProto: ArrayBuffer) -> { envelope_wire_bytes,
702
+ /// secret_key_material }`
703
+ jsi::Value producePrePairNoKeysResponseMessage(jsi::Runtime& rt,
704
+ const jsi::Value&,
705
+ const jsi::Value* args,
706
+ size_t count) {
707
+ requireArgs(rt, "produce_pre_pair_no_keys_response_message", count, 2);
708
+ uint64_t channelId = asU64(rt, args[0]);
709
+ ByteView requestProto = asBytes(rt, args[1]);
710
+ ProducePrePairNoKeysResponseMessageResult result =
711
+ produce_pre_pair_no_keys_response_message(channelId, requestProto.ptr,
712
+ requestProto.len);
713
+ if (result.error.code != 0) {
714
+ throwDeRecError(rt, result.error);
715
+ }
716
+ std::vector<uint8_t> envelope = takeBuffer(result.envelope_wire_bytes);
717
+ std::vector<uint8_t> secretKeyMaterial = takeBuffer(result.secret_key_material);
718
+ auto payload = jsi::Object(rt);
719
+ payload.setProperty(rt, "envelope_wire_bytes",
720
+ toArrayBuffer(rt, envelope.data(), envelope.size()));
721
+ payload.setProperty(
722
+ rt, "secret_key_material",
723
+ toArrayBuffer(rt, secretKeyMaterial.data(), secretKeyMaterial.size()));
724
+ return jsi::Value(rt, payload);
725
+ }
726
+
727
+ /// `process_pre_pair_no_keys_response_message(contactMessage: ArrayBuffer,
728
+ /// responseProto: ArrayBuffer) -> { mlkem_encapsulation_key,
729
+ /// ecies_public_key, nonce }`
730
+ jsi::Value processPrePairNoKeysResponseMessage(jsi::Runtime& rt,
731
+ const jsi::Value&,
732
+ const jsi::Value* args,
733
+ size_t count) {
734
+ requireArgs(rt, "process_pre_pair_no_keys_response_message", count, 2);
735
+ ByteView contactMessage = asBytes(rt, args[0]);
736
+ ByteView responseProto = asBytes(rt, args[1]);
737
+ ProcessPrePairResponseMessageResult result =
738
+ process_pre_pair_no_keys_response_message(contactMessage.ptr,
739
+ contactMessage.len,
740
+ responseProto.ptr,
741
+ responseProto.len);
742
+ if (result.error.code != 0) {
743
+ throwDeRecError(rt, result.error);
744
+ }
745
+ std::vector<uint8_t> mlkemKey = takeBuffer(result.mlkem_encapsulation_key);
746
+ std::vector<uint8_t> eciesKey = takeBuffer(result.ecies_public_key);
747
+ auto payload = jsi::Object(rt);
748
+ payload.setProperty(rt, "mlkem_encapsulation_key",
749
+ toArrayBuffer(rt, mlkemKey.data(), mlkemKey.size()));
750
+ payload.setProperty(rt, "ecies_public_key",
751
+ toArrayBuffer(rt, eciesKey.data(), eciesKey.size()));
752
+ payload.setProperty(rt, "nonce",
753
+ jsi::Value(rt, jsi::BigInt::fromUint64(rt, result.nonce)));
754
+ return jsi::Value(rt, payload);
755
+ }
756
+
757
+ /// `pairing_fingerprint(sharedKey: ArrayBuffer) -> string`
758
+ jsi::Value pairingFingerprint(jsi::Runtime& rt,
759
+ const jsi::Value&,
760
+ const jsi::Value* args,
761
+ size_t count) {
762
+ requireArgs(rt, "pairing_fingerprint", count, 1);
763
+ ByteView sharedKey = asBytes(rt, args[0]);
764
+ PairingFingerprintResult result =
765
+ pairing_fingerprint(sharedKey.ptr, sharedKey.len);
766
+ if (result.error.code != 0) {
767
+ throwDeRecError(rt, result.error);
768
+ }
769
+ std::string fingerprint = takeString(result.fingerprint);
770
+ return jsi::String::createFromUtf8(rt, fingerprint);
771
+ }
772
+
648
773
  /// `produce_get_share_request_message(channelId: bigint, secretId: bigint,
649
774
  /// version: number, sharedKey: ArrayBuffer, replyTo: ArrayBuffer|null)
650
775
  /// -> ArrayBuffer`
@@ -1134,6 +1259,8 @@ void bind(jsi::Runtime& rt,
1134
1259
  } // namespace
1135
1260
 
1136
1261
  void installPrimitives(jsi::Runtime& rt, jsi::Object& host) {
1262
+ bind(rt, host, "version", 0, protocolVersion);
1263
+ bind(rt, host, "generate_replica_id", 0, generateReplicaId);
1137
1264
  bind(rt, host, "produce_get_secret_ids_versions_request_message", 3,
1138
1265
  produceGetSecretIdsVersionsRequestMessage);
1139
1266
  bind(rt, host, "extract_get_secret_ids_versions_request", 2,
@@ -1147,16 +1274,17 @@ void installPrimitives(jsi::Runtime& rt, jsi::Object& host) {
1147
1274
  bind(rt, host, "apply_trace_id_to_envelope", 2, applyTraceId);
1148
1275
  bind(rt, host, "read_trace_id_from_envelope", 1, readTraceId);
1149
1276
  bind(rt, host, "create_contact_message", 5, createContactMessage);
1150
- bind(rt, host, "validate_contact_message", 1, validateContactMessage);
1151
1277
  bind(rt, host, "encode_contact_message", 1, encodeContactMessage);
1152
1278
  bind(rt, host, "decode_contact_message", 1, decodeContactMessage);
1279
+ bind(rt, host, "transport_protocol_discriminant", 1,
1280
+ transportProtocolDiscriminant);
1153
1281
  bind(rt, host, "decode_message_json", 2, decodeMessageJson);
1154
1282
  bind(rt, host, "encode_message_json", 2, encodeMessageJson);
1155
1283
  bind(rt, host, "produce_pair_request_message", 5, producePairRequestMessage);
1156
- bind(rt, host, "extract_pair_request", 2, extractPairRequest);
1284
+ bind(rt, host, "extract_pair_request", 3, extractPairRequest);
1157
1285
  bind(rt, host, "produce_pair_response_message", 7, producePairResponseMessage);
1158
1286
  bind(rt, host, "extract_pair_response", 2, extractPairResponse);
1159
- bind(rt, host, "process_pair_response_message", 3, processPairResponseMessage);
1287
+ bind(rt, host, "process_pair_response_message", 4, processPairResponseMessage);
1160
1288
  bind(rt, host, "produce_pre_pair_request_message", 2,
1161
1289
  producePrePairRequestMessage);
1162
1290
  bind(rt, host, "extract_pre_pair_request", 1, extractPrePairRequest);
@@ -1165,6 +1293,11 @@ void installPrimitives(jsi::Runtime& rt, jsi::Object& host) {
1165
1293
  bind(rt, host, "extract_pre_pair_response", 1, extractPrePairResponse);
1166
1294
  bind(rt, host, "process_pre_pair_response_message", 2,
1167
1295
  processPrePairResponseMessage);
1296
+ bind(rt, host, "produce_pre_pair_no_keys_response_message", 2,
1297
+ producePrePairNoKeysResponseMessage);
1298
+ bind(rt, host, "process_pre_pair_no_keys_response_message", 2,
1299
+ processPrePairNoKeysResponseMessage);
1300
+ bind(rt, host, "pairing_fingerprint", 1, pairingFingerprint);
1168
1301
  bind(rt, host, "produce_get_share_request_message", 5,
1169
1302
  produceGetShareRequestMessage);
1170
1303
  bind(rt, host, "extract_get_share_request", 2, extractGetShareRequest);
package/cpp/Primitives.h CHANGED
@@ -5,6 +5,8 @@
5
5
 
6
6
  #include <jsi/jsi.h>
7
7
 
8
+ #include <vector>
9
+
8
10
  extern "C" {
9
11
  #include "derec_ffi.h"
10
12
  }
@@ -16,6 +18,13 @@ namespace derec {
16
18
  /// application error handling ports between the JavaScript SDKs unchanged.
17
19
  [[noreturn]] void throwDeRecError(facebook::jsi::Runtime& rt, const DeRecError& error);
18
20
 
21
+ /// As above, additionally attaching `channel_ids` — the JSON array of
22
+ /// decimal-string ids in `conflictingChannelIdsJson`, verbatim — when that
23
+ /// buffer is non-empty. Only `restore` produces one.
24
+ [[noreturn]] void throwDeRecError(facebook::jsi::Runtime& rt,
25
+ const DeRecError& error,
26
+ const std::vector<uint8_t>& conflictingChannelIdsJson);
27
+
19
28
  /// Read an `ArrayBuffer` or typed-array argument as a byte range.
20
29
  /// Returns `{nullptr, 0}` for `null`/`undefined`.
21
30
  struct ByteView {
@@ -30,6 +39,13 @@ facebook::jsi::Value toArrayBuffer(facebook::jsi::Runtime& rt,
30
39
  size_t len);
31
40
 
32
41
  /// Read a `bigint` or `number` argument as a `uint64_t`.
42
+ /// A genuine `Uint8Array` holding a copy of `bytes`.
43
+ facebook::jsi::Value toUint8ArrayVal(facebook::jsi::Runtime& rt,
44
+ const uint8_t* bytes,
45
+ size_t len);
46
+ facebook::jsi::Value toUint8ArrayVal(facebook::jsi::Runtime& rt,
47
+ const std::vector<uint8_t>& bytes);
48
+
33
49
  uint64_t asU64(facebook::jsi::Runtime& rt, const facebook::jsi::Value& value);
34
50
 
35
51
  /// Install every stateless FFI primitive as a host function on `host`, named