@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 +33 -15
- package/android/CMakeLists.txt +1 -1
- package/android/src/main/jniLibs/arm64-v8a/libderec_library.a +0 -0
- package/android/src/main/jniLibs/armeabi-v7a/libderec_library.a +0 -0
- package/android/src/main/jniLibs/x86_64/libderec_library.a +0 -0
- package/cpp/Convert.cpp +125 -0
- package/cpp/Convert.h +35 -0
- package/cpp/DeRecInstaller.cpp +0 -12
- package/cpp/Primitives.cpp +175 -26
- package/cpp/Primitives.h +33 -0
- package/cpp/ProtocolHost.cpp +38 -50
- package/cpp/StoreCallbacks.cpp +179 -301
- package/cpp/UserSecretsJson.cpp +109 -0
- package/cpp/UserSecretsJson.h +28 -0
- package/cpp/derec_ffi.h +298 -132
- package/ios/DeRecFFI.xcframework/Info.plist +5 -5
- package/ios/DeRecFFI.xcframework/ios-arm64/Headers/derec_ffi.h +298 -132
- package/ios/DeRecFFI.xcframework/ios-arm64/libderec_library.a +0 -0
- package/ios/DeRecFFI.xcframework/ios-arm64_x86_64-simulator/Headers/derec_ffi.h +298 -132
- package/ios/DeRecFFI.xcframework/ios-arm64_x86_64-simulator/libderec_library.a +2 -2
- package/lib/errors.d.ts +16 -0
- package/lib/native.d.ts +5 -1
- package/lib/primitives.d.ts +18 -21
- package/lib/primitives.js +48 -25
- package/lib/protocol.d.ts +65 -102
- package/lib/protocol.js +98 -171
- package/lib/types.d.ts +299 -110
- package/lib/types.js +43 -13
- package/package.json +1 -1
- package/src/errors.ts +17 -0
- package/src/native.ts +2 -1
- package/src/primitives.ts +82 -25
- package/src/protocol.ts +105 -189
- package/src/types.ts +304 -99
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
|
-
.
|
|
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.
|
|
145
|
-
|
|
146
|
-
|
|
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
|
package/android/CMakeLists.txt
CHANGED
|
@@ -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}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
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
|
package/cpp/DeRecInstaller.cpp
CHANGED
|
@@ -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(
|
package/cpp/Primitives.cpp
CHANGED
|
@@ -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,
|
|
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
|
|
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,
|
|
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",
|
|
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",
|
|
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
|