@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.
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
 
@@ -125,6 +125,27 @@ longer needed (safe to call more than once).
125
125
 
126
126
  ---
127
127
 
128
+ ## When `process()` fails
129
+
130
+ `process()` settles expired deadlines (sharing-round and unpair timeouts)
131
+ before it handles the message, and those are never reported again. So when
132
+ the message then fails, the thrown `DeRecError` carries them: `events` holds
133
+ every event produced before the failure, and `channel_id` names the channel
134
+ the message came from (absent when the bytes were not a decodable envelope).
135
+ Handle the events as you would a successful call's, then the error:
136
+
137
+ ```ts
138
+ try {
139
+ handle(await protocol.process(bytes));
140
+ } catch (error) {
141
+ const failure = error as DeRecError;
142
+ handle(failure.events ?? []);
143
+ report(failure.channel_id, failure);
144
+ }
145
+ ```
146
+
147
+ ---
148
+
128
149
  ## Store interfaces
129
150
 
130
151
  The application layer owns all persistence; the library never writes to disk
@@ -134,6 +155,9 @@ declared in `src/types.ts`:
134
155
  - **`ChannelStore`** — pairing state: helper channels and replica-group
135
156
  membership, keyed by `(channelId, replicaId)`.
136
157
  - **`ShareStore`** — versioned VSS shares this device holds as a helper.
158
+ On an owner, `keepList` chooses which versions helpers keep; returning
159
+ `null` sends no keepList, so helpers keep every version; see
160
+ [the owner's keep list](https://github.com/derecalliance/lib-derec/tree/main/library#share-store-the-owners-keep-list).
137
161
  - **`SecretStore`** — small per-channel key material (shared keys, etc.),
138
162
  distinct from share bytes.
139
163
  - **`UserSecretStore`** — the most recent user-facing secret snapshot per
@@ -141,9 +165,14 @@ declared in `src/types.ts`:
141
165
  - **`StateStore`** — in-flight orchestrator bookkeeping (pending
142
166
  verifications, recoveries, unpairs, sharing rounds, replica discoveries) so a
143
167
  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.
168
+ - **`Transport`** — outbound delivery.
169
+ `send(endpoints: ReadonlyArray<{ protocol: string; uri: string }>, message: Uint8Array): Promise<void>`
170
+ posts a wire-format envelope to a peer. `endpoints` is every address that
171
+ peer advertised, in its own order (`protocol` is `"https"` or `"grpc"`);
172
+ deliver once, to the first one that accepts it, and reject only when none
173
+ did. `sequentialFailover` and `singleEndpointTransport` implement that
174
+ choice for you. It is a mailbox, not a request/response call, since not
175
+ every peer (e.g. a phone behind NAT) can be dialed back.
147
176
 
148
177
  Every method is `async` and keyed by decimal-string ids — see the next
149
178
  section for why that differs from `DeRecProtocol`'s own methods. Full field-
@@ -215,17 +244,6 @@ after whatever is in flight.
215
244
 
216
245
  ## Known differences from `@derec-alliance/nodejs`
217
246
 
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
247
  **The `DeRecErrorCategory` union differs from nodejs's.** This SDK's union
230
248
  omits `"wasm"`, which a C-ABI binding can never emit, and adds `"ok"`,
231
249
  `"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,19 @@ 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) {
23
+ throwDeRecError(rt, error, conflictingChannelIdsJson, {});
24
+ }
25
+
26
+ void throwDeRecError(jsi::Runtime& rt,
27
+ const DeRecError& error,
28
+ const std::vector<uint8_t>& conflictingChannelIdsJson,
29
+ const ProcessFailureDetails& process) {
17
30
  auto names = errorName(error.category, error.code);
18
31
  // `message` and `peer_memo` are Rust-owned strings; copy before releasing.
19
32
  std::string message =
@@ -42,6 +55,20 @@ void throwDeRecError(jsi::Runtime& rt, const DeRecError& error) {
42
55
  payload.setProperty(rt, "expected", jsi::Value(static_cast<int>(expected)));
43
56
  payload.setProperty(rt, "got", jsi::Value(static_cast<int>(got)));
44
57
  }
58
+ if (!conflictingChannelIdsJson.empty()) {
59
+ payload.setProperty(rt, "channel_ids",
60
+ jsi::Value::createFromJsonUtf8(rt, conflictingChannelIdsJson.data(),
61
+ conflictingChannelIdsJson.size()));
62
+ }
63
+ if (process.hasChannelId) {
64
+ payload.setProperty(rt, "channel_id",
65
+ jsi::String::createFromUtf8(rt, std::to_string(process.channelId)));
66
+ }
67
+ if (!process.eventsJson.empty()) {
68
+ payload.setProperty(rt, "events",
69
+ jsi::Value::createFromJsonUtf8(rt, process.eventsJson.data(),
70
+ process.eventsJson.size()));
71
+ }
45
72
  throw jsi::JSError(rt, jsi::Value(rt, payload));
46
73
  }
47
74
 
@@ -74,6 +101,22 @@ jsi::Value toArrayBuffer(jsi::Runtime& rt, const uint8_t* bytes, size_t len) {
74
101
  return jsi::Value(rt, result);
75
102
  }
76
103
 
104
+ /// Build a genuine `Uint8Array` (not a bare `ArrayBuffer`) — every store
105
+ /// interface types its byte payloads as `Uint8Array`, and application code
106
+ /// is entitled to index or slice it directly.
107
+ jsi::Value toUint8ArrayVal(jsi::Runtime& rt, const uint8_t* bytes, size_t len) {
108
+ jsi::Uint8Array arr(rt, len);
109
+ if (len > 0 && bytes != nullptr) {
110
+ jsi::ArrayBuffer buffer = arr.buffer(rt);
111
+ std::memcpy(buffer.data(rt), bytes, len);
112
+ }
113
+ return jsi::Value(rt, std::move(arr));
114
+ }
115
+
116
+ jsi::Value toUint8ArrayVal(jsi::Runtime& rt, const std::vector<uint8_t>& bytes) {
117
+ return toUint8ArrayVal(rt, bytes.data(), bytes.size());
118
+ }
119
+
77
120
  uint64_t asU64(jsi::Runtime& rt, const jsi::Value& value) {
78
121
  if (value.isBigInt()) {
79
122
  return value.asBigInt(rt).asUint64(rt);
@@ -262,6 +305,28 @@ jsi::Value readTraceId(jsi::Runtime& rt,
262
305
  return jsi::Value(rt, jsi::BigInt::fromUint64(rt, result.trace_id));
263
306
  }
264
307
 
308
+ /// `version() -> { major: number, minor: number }` — the protocol version the
309
+ /// core writes into every envelope it produces.
310
+ jsi::Value protocolVersion(jsi::Runtime& rt,
311
+ const jsi::Value&,
312
+ const jsi::Value*,
313
+ size_t) {
314
+ DeRecProtocolVersion version = derec_protocol_version();
315
+ auto payload = jsi::Object(rt);
316
+ payload.setProperty(rt, "major", jsi::Value(static_cast<double>(version.major)));
317
+ payload.setProperty(rt, "minor", jsi::Value(static_cast<double>(version.minor)));
318
+ return jsi::Value(rt, payload);
319
+ }
320
+
321
+ /// `generate_replica_id() -> bigint` — a fresh replica identity from the
322
+ /// core, never `0`. The application persists it once per device.
323
+ jsi::Value generateReplicaId(jsi::Runtime& rt,
324
+ const jsi::Value&,
325
+ const jsi::Value*,
326
+ size_t) {
327
+ return jsi::Value(rt, jsi::BigInt::fromUint64(rt, derec_generate_replica_id()));
328
+ }
329
+
265
330
  /// `create_contact_message(channelId: bigint, contactMode: number,
266
331
  /// transportProtocols: ArrayBuffer, hasNonce: number, nonce: bigint)
267
332
  /// -> { contact_wire_bytes, secret_key_material }`
@@ -291,21 +356,6 @@ jsi::Value createContactMessage(jsi::Runtime& rt,
291
356
  return jsi::Value(rt, payload);
292
357
  }
293
358
 
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
359
  /// `encode_contact_message(contactJson: ArrayBuffer) -> ArrayBuffer`
310
360
  jsi::Value encodeContactMessage(jsi::Runtime& rt,
311
361
  const jsi::Value&,
@@ -338,6 +388,20 @@ jsi::Value decodeContactMessage(jsi::Runtime& rt,
338
388
  return toArrayBuffer(rt, bytes.data(), bytes.size());
339
389
  }
340
390
 
391
+ /// `transport_protocol_discriminant(name: string) -> number`
392
+ ///
393
+ /// The `derec_proto::Protocol` discriminant the crate assigns to `name`, or
394
+ /// `-1` when it names none; forwarded unchanged for Rust to validate.
395
+ jsi::Value transportProtocolDiscriminant(jsi::Runtime& rt,
396
+ const jsi::Value&,
397
+ const jsi::Value* args,
398
+ size_t count) {
399
+ requireArgs(rt, "transport_protocol_discriminant", count, 1);
400
+ std::string name = args[0].asString(rt).utf8(rt);
401
+ return jsi::Value(static_cast<int>(derec_transport_protocol_discriminant(
402
+ reinterpret_cast<const uint8_t*>(name.data()), name.size())));
403
+ }
404
+
341
405
  /// `decode_message_json(kind: number, protoBytes: ArrayBuffer) -> ArrayBuffer`
342
406
  ///
343
407
  /// Turns the opaque `*_proto_bytes` an `extract_*` call returns into the JSON
@@ -415,17 +479,19 @@ jsi::Value producePairRequestMessage(jsi::Runtime& rt,
415
479
  return jsi::Value(rt, payload);
416
480
  }
417
481
 
418
- /// `extract_pair_request(request: ArrayBuffer, secretKeyMaterial: ArrayBuffer)
419
- /// -> { channel_id, request_proto_bytes }`
482
+ /// `extract_pair_request(request: ArrayBuffer, secretKeyMaterial: ArrayBuffer,
483
+ /// parameterRange: ArrayBuffer|null) -> { channel_id, request_proto_bytes }`
420
484
  jsi::Value extractPairRequest(jsi::Runtime& rt,
421
485
  const jsi::Value&,
422
486
  const jsi::Value* args,
423
487
  size_t count) {
424
- requireArgs(rt, "extract_pair_request", count, 2);
488
+ requireArgs(rt, "extract_pair_request", count, 3);
425
489
  ByteView request = asBytes(rt, args[0]);
426
490
  ByteView secretKeyMaterial = asBytes(rt, args[1]);
491
+ ByteView parameterRange = asBytes(rt, args[2]);
427
492
  ExtractPairRequestResult result = extract_pair_request(
428
- request.ptr, request.len, secretKeyMaterial.ptr, secretKeyMaterial.len);
493
+ request.ptr, request.len, secretKeyMaterial.ptr, secretKeyMaterial.len,
494
+ parameterRange.ptr, parameterRange.len);
429
495
  if (result.error.code != 0) {
430
496
  throwDeRecError(rt, result.error);
431
497
  }
@@ -506,19 +572,21 @@ jsi::Value extractPairResponse(jsi::Runtime& rt,
506
572
  }
507
573
 
508
574
  /// `process_pair_response_message(contactMessage: ArrayBuffer, responseProto:
509
- /// ArrayBuffer, secretKeyMaterial: ArrayBuffer) -> { shared_key,
510
- /// channel_id }`
575
+ /// ArrayBuffer, secretKeyMaterial: ArrayBuffer, parameterRange:
576
+ /// ArrayBuffer|null) -> { shared_key, channel_id }`
511
577
  jsi::Value processPairResponseMessage(jsi::Runtime& rt,
512
578
  const jsi::Value&,
513
579
  const jsi::Value* args,
514
580
  size_t count) {
515
- requireArgs(rt, "process_pair_response_message", count, 3);
581
+ requireArgs(rt, "process_pair_response_message", count, 4);
516
582
  ByteView contactMessage = asBytes(rt, args[0]);
517
583
  ByteView responseProto = asBytes(rt, args[1]);
518
584
  ByteView secretKeyMaterial = asBytes(rt, args[2]);
585
+ ByteView parameterRange = asBytes(rt, args[3]);
519
586
  ProcessPairResponseMessageResult result = process_pair_response_message(
520
587
  contactMessage.ptr, contactMessage.len, responseProto.ptr,
521
- responseProto.len, secretKeyMaterial.ptr, secretKeyMaterial.len);
588
+ responseProto.len, secretKeyMaterial.ptr, secretKeyMaterial.len,
589
+ parameterRange.ptr, parameterRange.len);
522
590
  if (result.error.code != 0) {
523
591
  throwDeRecError(rt, result.error);
524
592
  }
@@ -645,6 +713,79 @@ jsi::Value processPrePairResponseMessage(jsi::Runtime& rt,
645
713
  return jsi::Value(rt, payload);
646
714
  }
647
715
 
716
+ /// `produce_pre_pair_no_keys_response_message(channelId: bigint,
717
+ /// requestProto: ArrayBuffer) -> { envelope_wire_bytes,
718
+ /// secret_key_material }`
719
+ jsi::Value producePrePairNoKeysResponseMessage(jsi::Runtime& rt,
720
+ const jsi::Value&,
721
+ const jsi::Value* args,
722
+ size_t count) {
723
+ requireArgs(rt, "produce_pre_pair_no_keys_response_message", count, 2);
724
+ uint64_t channelId = asU64(rt, args[0]);
725
+ ByteView requestProto = asBytes(rt, args[1]);
726
+ ProducePrePairNoKeysResponseMessageResult result =
727
+ produce_pre_pair_no_keys_response_message(channelId, requestProto.ptr,
728
+ requestProto.len);
729
+ if (result.error.code != 0) {
730
+ throwDeRecError(rt, result.error);
731
+ }
732
+ std::vector<uint8_t> envelope = takeBuffer(result.envelope_wire_bytes);
733
+ std::vector<uint8_t> secretKeyMaterial = takeBuffer(result.secret_key_material);
734
+ auto payload = jsi::Object(rt);
735
+ payload.setProperty(rt, "envelope_wire_bytes",
736
+ toArrayBuffer(rt, envelope.data(), envelope.size()));
737
+ payload.setProperty(
738
+ rt, "secret_key_material",
739
+ toArrayBuffer(rt, secretKeyMaterial.data(), secretKeyMaterial.size()));
740
+ return jsi::Value(rt, payload);
741
+ }
742
+
743
+ /// `process_pre_pair_no_keys_response_message(contactMessage: ArrayBuffer,
744
+ /// responseProto: ArrayBuffer) -> { mlkem_encapsulation_key,
745
+ /// ecies_public_key, nonce }`
746
+ jsi::Value processPrePairNoKeysResponseMessage(jsi::Runtime& rt,
747
+ const jsi::Value&,
748
+ const jsi::Value* args,
749
+ size_t count) {
750
+ requireArgs(rt, "process_pre_pair_no_keys_response_message", count, 2);
751
+ ByteView contactMessage = asBytes(rt, args[0]);
752
+ ByteView responseProto = asBytes(rt, args[1]);
753
+ ProcessPrePairResponseMessageResult result =
754
+ process_pre_pair_no_keys_response_message(contactMessage.ptr,
755
+ contactMessage.len,
756
+ responseProto.ptr,
757
+ responseProto.len);
758
+ if (result.error.code != 0) {
759
+ throwDeRecError(rt, result.error);
760
+ }
761
+ std::vector<uint8_t> mlkemKey = takeBuffer(result.mlkem_encapsulation_key);
762
+ std::vector<uint8_t> eciesKey = takeBuffer(result.ecies_public_key);
763
+ auto payload = jsi::Object(rt);
764
+ payload.setProperty(rt, "mlkem_encapsulation_key",
765
+ toArrayBuffer(rt, mlkemKey.data(), mlkemKey.size()));
766
+ payload.setProperty(rt, "ecies_public_key",
767
+ toArrayBuffer(rt, eciesKey.data(), eciesKey.size()));
768
+ payload.setProperty(rt, "nonce",
769
+ jsi::Value(rt, jsi::BigInt::fromUint64(rt, result.nonce)));
770
+ return jsi::Value(rt, payload);
771
+ }
772
+
773
+ /// `pairing_fingerprint(sharedKey: ArrayBuffer) -> string`
774
+ jsi::Value pairingFingerprint(jsi::Runtime& rt,
775
+ const jsi::Value&,
776
+ const jsi::Value* args,
777
+ size_t count) {
778
+ requireArgs(rt, "pairing_fingerprint", count, 1);
779
+ ByteView sharedKey = asBytes(rt, args[0]);
780
+ PairingFingerprintResult result =
781
+ pairing_fingerprint(sharedKey.ptr, sharedKey.len);
782
+ if (result.error.code != 0) {
783
+ throwDeRecError(rt, result.error);
784
+ }
785
+ std::string fingerprint = takeString(result.fingerprint);
786
+ return jsi::String::createFromUtf8(rt, fingerprint);
787
+ }
788
+
648
789
  /// `produce_get_share_request_message(channelId: bigint, secretId: bigint,
649
790
  /// version: number, sharedKey: ArrayBuffer, replyTo: ArrayBuffer|null)
650
791
  /// -> ArrayBuffer`
@@ -1134,6 +1275,8 @@ void bind(jsi::Runtime& rt,
1134
1275
  } // namespace
1135
1276
 
1136
1277
  void installPrimitives(jsi::Runtime& rt, jsi::Object& host) {
1278
+ bind(rt, host, "version", 0, protocolVersion);
1279
+ bind(rt, host, "generate_replica_id", 0, generateReplicaId);
1137
1280
  bind(rt, host, "produce_get_secret_ids_versions_request_message", 3,
1138
1281
  produceGetSecretIdsVersionsRequestMessage);
1139
1282
  bind(rt, host, "extract_get_secret_ids_versions_request", 2,
@@ -1147,16 +1290,17 @@ void installPrimitives(jsi::Runtime& rt, jsi::Object& host) {
1147
1290
  bind(rt, host, "apply_trace_id_to_envelope", 2, applyTraceId);
1148
1291
  bind(rt, host, "read_trace_id_from_envelope", 1, readTraceId);
1149
1292
  bind(rt, host, "create_contact_message", 5, createContactMessage);
1150
- bind(rt, host, "validate_contact_message", 1, validateContactMessage);
1151
1293
  bind(rt, host, "encode_contact_message", 1, encodeContactMessage);
1152
1294
  bind(rt, host, "decode_contact_message", 1, decodeContactMessage);
1295
+ bind(rt, host, "transport_protocol_discriminant", 1,
1296
+ transportProtocolDiscriminant);
1153
1297
  bind(rt, host, "decode_message_json", 2, decodeMessageJson);
1154
1298
  bind(rt, host, "encode_message_json", 2, encodeMessageJson);
1155
1299
  bind(rt, host, "produce_pair_request_message", 5, producePairRequestMessage);
1156
- bind(rt, host, "extract_pair_request", 2, extractPairRequest);
1300
+ bind(rt, host, "extract_pair_request", 3, extractPairRequest);
1157
1301
  bind(rt, host, "produce_pair_response_message", 7, producePairResponseMessage);
1158
1302
  bind(rt, host, "extract_pair_response", 2, extractPairResponse);
1159
- bind(rt, host, "process_pair_response_message", 3, processPairResponseMessage);
1303
+ bind(rt, host, "process_pair_response_message", 4, processPairResponseMessage);
1160
1304
  bind(rt, host, "produce_pre_pair_request_message", 2,
1161
1305
  producePrePairRequestMessage);
1162
1306
  bind(rt, host, "extract_pre_pair_request", 1, extractPrePairRequest);
@@ -1165,6 +1309,11 @@ void installPrimitives(jsi::Runtime& rt, jsi::Object& host) {
1165
1309
  bind(rt, host, "extract_pre_pair_response", 1, extractPrePairResponse);
1166
1310
  bind(rt, host, "process_pre_pair_response_message", 2,
1167
1311
  processPrePairResponseMessage);
1312
+ bind(rt, host, "produce_pre_pair_no_keys_response_message", 2,
1313
+ producePrePairNoKeysResponseMessage);
1314
+ bind(rt, host, "process_pre_pair_no_keys_response_message", 2,
1315
+ processPrePairNoKeysResponseMessage);
1316
+ bind(rt, host, "pairing_fingerprint", 1, pairingFingerprint);
1168
1317
  bind(rt, host, "produce_get_share_request_message", 5,
1169
1318
  produceGetShareRequestMessage);
1170
1319
  bind(rt, host, "extract_get_share_request", 2, extractGetShareRequest);
package/cpp/Primitives.h CHANGED
@@ -5,6 +5,9 @@
5
5
 
6
6
  #include <jsi/jsi.h>
7
7
 
8
+ #include <cstdint>
9
+ #include <vector>
10
+
8
11
  extern "C" {
9
12
  #include "derec_ffi.h"
10
13
  }
@@ -16,6 +19,29 @@ namespace derec {
16
19
  /// application error handling ports between the JavaScript SDKs unchanged.
17
20
  [[noreturn]] void throwDeRecError(facebook::jsi::Runtime& rt, const DeRecError& error);
18
21
 
22
+ /// As above, additionally attaching `channel_ids` — the JSON array of
23
+ /// decimal-string ids in `conflictingChannelIdsJson`, verbatim — when that
24
+ /// buffer is non-empty. Only `restore` produces one.
25
+ [[noreturn]] void throwDeRecError(facebook::jsi::Runtime& rt,
26
+ const DeRecError& error,
27
+ const std::vector<uint8_t>& conflictingChannelIdsJson);
28
+
29
+ /// What a failed `derec_protocol_process` adds to its error: the events it
30
+ /// produced before failing, and the channel the failing message came from.
31
+ struct ProcessFailureDetails {
32
+ std::vector<uint8_t> eventsJson;
33
+ bool hasChannelId = false;
34
+ uint64_t channelId = 0;
35
+ };
36
+
37
+ /// As above, additionally attaching `channel_id` (a decimal string) when
38
+ /// `process.hasChannelId` is set, and `events` (the JSON array verbatim)
39
+ /// when `process.eventsJson` is non-empty. Only `process` produces these.
40
+ [[noreturn]] void throwDeRecError(facebook::jsi::Runtime& rt,
41
+ const DeRecError& error,
42
+ const std::vector<uint8_t>& conflictingChannelIdsJson,
43
+ const ProcessFailureDetails& process);
44
+
19
45
  /// Read an `ArrayBuffer` or typed-array argument as a byte range.
20
46
  /// Returns `{nullptr, 0}` for `null`/`undefined`.
21
47
  struct ByteView {
@@ -30,6 +56,13 @@ facebook::jsi::Value toArrayBuffer(facebook::jsi::Runtime& rt,
30
56
  size_t len);
31
57
 
32
58
  /// Read a `bigint` or `number` argument as a `uint64_t`.
59
+ /// A genuine `Uint8Array` holding a copy of `bytes`.
60
+ facebook::jsi::Value toUint8ArrayVal(facebook::jsi::Runtime& rt,
61
+ const uint8_t* bytes,
62
+ size_t len);
63
+ facebook::jsi::Value toUint8ArrayVal(facebook::jsi::Runtime& rt,
64
+ const std::vector<uint8_t>& bytes);
65
+
33
66
  uint64_t asU64(facebook::jsi::Runtime& rt, const facebook::jsi::Value& value);
34
67
 
35
68
  /// Install every stateless FFI primitive as a host function on `host`, named