@derec-alliance/react-native 0.0.1

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.
Files changed (65) hide show
  1. package/DeRec.podspec +25 -0
  2. package/LICENSE +201 -0
  3. package/README.md +222 -0
  4. package/android/CMakeLists.txt +38 -0
  5. package/android/build.gradle +79 -0
  6. package/android/src/main/AndroidManifest.xml +12 -0
  7. package/android/src/main/java/org/derec/DeRecModule.kt +56 -0
  8. package/android/src/main/java/org/derec/DeRecPackage.kt +24 -0
  9. package/android/src/main/jni/OnLoad.cpp +39 -0
  10. package/android/src/main/jniLibs/arm64-v8a/libderec_library.a +0 -0
  11. package/android/src/main/jniLibs/armeabi-v7a/libderec_library.a +0 -0
  12. package/android/src/main/jniLibs/x86_64/libderec_library.a +0 -0
  13. package/cpp/Convert.cpp +50 -0
  14. package/cpp/Convert.h +38 -0
  15. package/cpp/DeRecInstaller.cpp +186 -0
  16. package/cpp/DeRecInstaller.h +28 -0
  17. package/cpp/Invoker.h +20 -0
  18. package/cpp/JsCallbackBridge.cpp +144 -0
  19. package/cpp/JsCallbackBridge.h +148 -0
  20. package/cpp/Primitives.cpp +1193 -0
  21. package/cpp/Primitives.h +39 -0
  22. package/cpp/ProtocolHost.cpp +613 -0
  23. package/cpp/ProtocolHost.h +88 -0
  24. package/cpp/RuntimeInvoker.h +29 -0
  25. package/cpp/StoreCallbacks.cpp +1293 -0
  26. package/cpp/StoreCallbacks.h +111 -0
  27. package/cpp/WorkerThread.cpp +74 -0
  28. package/cpp/WorkerThread.h +54 -0
  29. package/cpp/derec_ffi.h +2145 -0
  30. package/ios/DeRec.h +23 -0
  31. package/ios/DeRec.mm +39 -0
  32. package/ios/DeRecFFI.xcframework/Info.plist +48 -0
  33. package/ios/DeRecFFI.xcframework/ios-arm64/Headers/derec_ffi.h +2145 -0
  34. package/ios/DeRecFFI.xcframework/ios-arm64/libderec_library.a +0 -0
  35. package/ios/DeRecFFI.xcframework/ios-arm64_x86_64-simulator/Headers/derec_ffi.h +2145 -0
  36. package/ios/DeRecFFI.xcframework/ios-arm64_x86_64-simulator/libderec_library.a +0 -0
  37. package/lib/binary.d.ts +19 -0
  38. package/lib/binary.js +164 -0
  39. package/lib/codec.d.ts +16 -0
  40. package/lib/codec.js +80 -0
  41. package/lib/errors.d.ts +23 -0
  42. package/lib/errors.js +35 -0
  43. package/lib/index.d.ts +4 -0
  44. package/lib/index.js +27 -0
  45. package/lib/messages.d.ts +41 -0
  46. package/lib/messages.js +192 -0
  47. package/lib/native.d.ts +11 -0
  48. package/lib/native.js +40 -0
  49. package/lib/primitives.d.ts +137 -0
  50. package/lib/primitives.js +329 -0
  51. package/lib/protocol.d.ts +188 -0
  52. package/lib/protocol.js +514 -0
  53. package/lib/types.d.ts +1014 -0
  54. package/lib/types.js +63 -0
  55. package/package.json +55 -0
  56. package/react-native.config.js +19 -0
  57. package/src/binary.ts +182 -0
  58. package/src/codec.ts +79 -0
  59. package/src/errors.ts +45 -0
  60. package/src/index.ts +7 -0
  61. package/src/messages.ts +192 -0
  62. package/src/native.ts +52 -0
  63. package/src/primitives.ts +770 -0
  64. package/src/protocol.ts +621 -0
  65. package/src/types.ts +1040 -0
@@ -0,0 +1,621 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
+
4
+ import { jsonFromBytes, jsonToBytes } from './codec';
5
+ import { getNative } from './native';
6
+ import { primitives } from './primitives';
7
+ import { FlowKind } from './types';
8
+ import type {
9
+ AutoAcceptPolicy,
10
+ ChannelStore,
11
+ ContactMessage,
12
+ ContactMode,
13
+ DeRecEvent,
14
+ DiscoveryParams,
15
+ PairingParams,
16
+ ProtectSecretParams,
17
+ RecoverSecretParams,
18
+ RemoveReplicaParams,
19
+ SecretStore,
20
+ ShareStore,
21
+ StateStore,
22
+ SyncCheckParams,
23
+ Target,
24
+ Timeouts,
25
+ Transport,
26
+ UnpairAck,
27
+ UnpairParams,
28
+ UpdateChannelInfoParams,
29
+ UserSecretStore,
30
+ VerifySharesParams,
31
+ } from './types';
32
+
33
+ /** Shape of the `__DeRec` host object as seen from this file: every member is
34
+ * a plain callable, since the JSI layer exposes host functions this way and
35
+ * TypeScript has no static knowledge of them (see `primitives.ts`'s `call`
36
+ * helper, which uses the same escape hatch). */
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ type NativeHost = Record<string, (...args: any[]) => any>;
39
+
40
+ /**
41
+ * Event fields carrying `Vec<u8>`.
42
+ *
43
+ * `serde_json` writes them as arrays of decimal byte values, while
44
+ * {@link DeRecEvent} declares them `Uint8Array` — the shape
45
+ * `@derec-alliance/nodejs` produces, where `serde-wasm-bindgen` makes the
46
+ * conversion natively. Without the conversion below the declared type is a
47
+ * lie, and the first thing an application does with `ActionRequired.action` is
48
+ * hand it back to `accept()`, where the native layer reads `.buffer` off it
49
+ * and finds nothing.
50
+ *
51
+ * Unlike the message codec in `./messages`, ids are deliberately left alone:
52
+ * every id on an event is declared as a decimal `string`, not a `bigint`.
53
+ */
54
+ const EVENT_BYTE_FIELDS = new Set([
55
+ 'action',
56
+ 'shared_key',
57
+ 'committed_share',
58
+ 'id',
59
+ 'data',
60
+ ]);
61
+
62
+ function reviveEventBytes(value: unknown): unknown {
63
+ if (Array.isArray(value)) {
64
+ return value.map(reviveEventBytes);
65
+ }
66
+ if (value === null || typeof value !== 'object') {
67
+ return value;
68
+ }
69
+ const out: Record<string, unknown> = {};
70
+ for (const [key, field] of Object.entries(value as Record<string, unknown>)) {
71
+ out[key] =
72
+ EVENT_BYTE_FIELDS.has(key) && Array.isArray(field)
73
+ ? Uint8Array.from(field as number[])
74
+ : reviveEventBytes(field);
75
+ }
76
+ return out;
77
+ }
78
+
79
+ function decodeEvents(buffer: ArrayBuffer): DeRecEvent[] {
80
+ return reviveEventBytes(jsonFromBytes(buffer)) as DeRecEvent[];
81
+ }
82
+
83
+ /**
84
+ * Maps a transport protocol name to the `derec_proto::Protocol` discriminant.
85
+ * `bindings/test_fixture/enums.json` — the shared enum fixture every other
86
+ * numeric mapping in this SDK is read from — does not carry this enum (it
87
+ * only covers the Rust-internal serde enums listed under its `"enums"` key);
88
+ * the transport `Protocol` enum lives in `protobufs/transportprotocol.proto`
89
+ * instead, where only `HTTPS = 0` is currently defined.
90
+ */
91
+ export function protocolDiscriminant(protocol: string): number {
92
+ switch (protocol.toLowerCase()) {
93
+ case 'https':
94
+ return 0;
95
+ default:
96
+ throw new Error(`DeRec: unknown transport protocol "${protocol}"`);
97
+ }
98
+ }
99
+
100
+ function encodeTarget(target: Target): unknown {
101
+ if (target === null) {
102
+ return null;
103
+ }
104
+ if (Array.isArray(target)) {
105
+ return target.map((id) => id.toString());
106
+ }
107
+ return target.toString();
108
+ }
109
+
110
+ /**
111
+ * `ProtectSecretParamsJson` (`library/src/interop/ffi/protocol/flow.rs`): every
112
+ * `UserSecret.id`/`.data` byte array travels as a bare JSON array of decimal
113
+ * byte values, matching `UserSecretJson`'s plain `Vec<u8>` fields — no
114
+ * base64, no wrapping.
115
+ *
116
+ * Exported so it can be golden-tested against
117
+ * `bindings/test_fixture/wire_golden.json` without a native module.
118
+ */
119
+ export function buildProtectSecretParams(params: ProtectSecretParams): Record<string, unknown> {
120
+ const out: Record<string, unknown> = {
121
+ secrets: params.secrets.map((secret) => ({
122
+ id: Array.from(secret.id),
123
+ name: secret.name,
124
+ data: Array.from(secret.data),
125
+ })),
126
+ };
127
+ if (params.description !== undefined) {
128
+ out.description = params.description;
129
+ }
130
+ return out;
131
+ }
132
+
133
+ function communicationInfoOrOmit(
134
+ info: Record<string, string> | undefined,
135
+ ): Record<string, string> | undefined {
136
+ return info !== undefined && Object.keys(info).length > 0 ? info : undefined;
137
+ }
138
+
139
+ /**
140
+ * `RestoreParamsJson` (`library/src/interop/ffi/protocol/handle/flow.rs`). Mirrors
141
+ * `SecretWire`'s own shape: `channel_id`/`replica_id` are the decimal `u64`
142
+ * strings the `SecretRecovered` event already carries, forwarded verbatim;
143
+ * `communication_info` is omitted per-entry (not sent as `{}`) whenever a
144
+ * helper or replica member has none, matching the Go SDK's `omitempty` and
145
+ * the shared golden fixture byte-for-byte; `replicas` is omitted entirely
146
+ * when the recovered secret has no replica group.
147
+ *
148
+ * Exported so it can be golden-tested against
149
+ * `bindings/test_fixture/wire_golden.json` without a native module.
150
+ */
151
+ export function buildRestoreParams(
152
+ recoveredSecret: Extract<DeRecEvent, { type: 'SecretRecovered' }>['secret'],
153
+ version: number,
154
+ ): Record<string, unknown> {
155
+ const recovered: Record<string, unknown> = {
156
+ helpers: recoveredSecret.helpers.map((helper) => {
157
+ const out: Record<string, unknown> = {
158
+ channel_id: helper.channel_id,
159
+ transport_uri: helper.transport_uri,
160
+ shared_key: Array.from(helper.shared_key),
161
+ };
162
+ const info = communicationInfoOrOmit(helper.communication_info);
163
+ if (info !== undefined) {
164
+ out.communication_info = info;
165
+ }
166
+ return out;
167
+ }),
168
+ secrets: recoveredSecret.secrets.map((secret) => ({
169
+ id: Array.from(secret.id),
170
+ name: secret.name,
171
+ data: Array.from(secret.data),
172
+ })),
173
+ };
174
+ if (recoveredSecret.replicas !== undefined) {
175
+ recovered.replicas = {
176
+ channel_id: recoveredSecret.replicas.channel_id,
177
+ members: recoveredSecret.replicas.members.map((member) => {
178
+ const out: Record<string, unknown> = {
179
+ replica_id: member.replica_id,
180
+ transport_uri: member.transport_uri,
181
+ role: member.role,
182
+ };
183
+ const info = communicationInfoOrOmit(member.communication_info);
184
+ if (info !== undefined) {
185
+ out.communication_info = info;
186
+ }
187
+ return out;
188
+ }),
189
+ shared_key: Array.from(recoveredSecret.replicas.shared_key),
190
+ };
191
+ }
192
+ return { version, recovered_secret: recovered };
193
+ }
194
+
195
+ /**
196
+ * `PairingParamsJson.contact` (`library/src/interop/ffi/protocol/flow.rs`) is a
197
+ * `Vec<u8>` of proto-encoded `ContactMessage` bytes, so the typed contact the
198
+ * caller holds is re-encoded through the library's own codec on the way in.
199
+ * Encoding in Rust also means the contact is structurally validated before
200
+ * the pairing flow ever sees it.
201
+ */
202
+ function contactToByteArray(contact: ContactMessage): number[] {
203
+ return Array.from(primitives.pairing.request.encode_contact(contact));
204
+ }
205
+
206
+ function buildStartParams(flowKind: FlowKind, params: unknown): Uint8Array {
207
+ switch (flowKind) {
208
+ case FlowKind.Pairing: {
209
+ const p = params as PairingParams;
210
+ const out: Record<string, unknown> = {
211
+ kind: p.kind,
212
+ contact: contactToByteArray(p.contact),
213
+ };
214
+ if (p.peerCommunicationInfo !== undefined) {
215
+ out.peer_communication_info = p.peerCommunicationInfo;
216
+ }
217
+ return jsonToBytes(out);
218
+ }
219
+ case FlowKind.Discovery: {
220
+ const p = (params ?? {}) as DiscoveryParams;
221
+ const out: Record<string, unknown> = {};
222
+ if (p.target !== undefined) {
223
+ out.target = encodeTarget(p.target);
224
+ }
225
+ return jsonToBytes(out);
226
+ }
227
+ case FlowKind.ProtectSecret:
228
+ return jsonToBytes(buildProtectSecretParams(params as ProtectSecretParams));
229
+ case FlowKind.VerifyShares: {
230
+ const p = params as VerifySharesParams;
231
+ const out: Record<string, unknown> = {
232
+ secret_id: p.secretId.toString(),
233
+ version: p.version,
234
+ };
235
+ if (p.target !== undefined) {
236
+ out.target = encodeTarget(p.target);
237
+ }
238
+ return jsonToBytes(out);
239
+ }
240
+ case FlowKind.RecoverSecret: {
241
+ const p = params as RecoverSecretParams;
242
+ return jsonToBytes({ secret_id: p.secretId.toString(), version: p.version });
243
+ }
244
+ case FlowKind.Unpair: {
245
+ const p = params as UnpairParams;
246
+ const out: Record<string, unknown> = { channel_id: p.channel_id };
247
+ if (p.memo !== undefined) {
248
+ out.memo = p.memo;
249
+ }
250
+ return jsonToBytes(out);
251
+ }
252
+ case FlowKind.UpdateChannelInfo: {
253
+ const p = (params ?? {}) as UpdateChannelInfoParams;
254
+ const out: Record<string, unknown> = {};
255
+ if (p.target !== undefined) {
256
+ out.target = encodeTarget(p.target);
257
+ }
258
+ if (p.communication_info !== undefined) {
259
+ out.communication_info = p.communication_info;
260
+ }
261
+ if (p.transport_protocol !== undefined) {
262
+ out.transport_protocol = p.transport_protocol;
263
+ }
264
+ return jsonToBytes(out);
265
+ }
266
+ case FlowKind.SyncCheck:
267
+ // No parameters: the group and this device's own version both come
268
+ // from the stores.
269
+ return jsonToBytes({});
270
+ case FlowKind.RemoveReplica: {
271
+ const p = params as RemoveReplicaParams;
272
+ const out: Record<string, unknown> = { replica_id: p.replica_id };
273
+ if (p.memo !== undefined) {
274
+ out.memo = p.memo;
275
+ }
276
+ return jsonToBytes(out);
277
+ }
278
+ default: {
279
+ const exhaustive: never = flowKind;
280
+ throw new Error(`DeRec: unknown FlowKind ${exhaustive as number}`);
281
+ }
282
+ }
283
+ }
284
+
285
+ function toBigInt(id: bigint | number): bigint {
286
+ return typeof id === 'bigint' ? id : BigInt(id);
287
+ }
288
+
289
+ /**
290
+ * Builder for {@link DeRecProtocol}, mirroring `@derec-alliance/nodejs`'s
291
+ * `DeRecProtocolBuilder` method-for-method. The config JSON shape is dictated
292
+ * by the Rust `ProtocolConfig` struct (`library/src/interop/ffi/protocol/handle/mod.rs`);
293
+ * every field here writes exactly the `snake_case` key that struct expects.
294
+ *
295
+ * Every value the caller supplies is forwarded verbatim — defaults, clamping
296
+ * and the meaning of a disabled cleanup policy are library decisions.
297
+ * `threshold`, `keep_versions_count`, `auto_respond_on_failure`,
298
+ * `unpair_ack`, `auto_reply_to` and `auto_accept` are omitted entirely from
299
+ * the emitted config unless their setter was called: `ProtocolConfig` has a
300
+ * `#[serde(default = "...")]` on each of them, reading the same constants
301
+ * `DeRecProtocolBuilder::new` uses, so an absent key resolves to the
302
+ * library's own default rather than one frozen into this shim.
303
+ */
304
+ export class DeRecProtocolBuilder {
305
+ private readonly secretIdValue: bigint;
306
+ private config: Record<string, unknown> = {};
307
+ private stores: Record<string, unknown> = {};
308
+ private communicationInfo: Record<string, string> | undefined;
309
+
310
+ constructor(secretId: bigint | number) {
311
+ this.secretIdValue = BigInt(secretId);
312
+ }
313
+
314
+ withChannelStore(store: ChannelStore): this {
315
+ this.stores.channelStore = store;
316
+ return this;
317
+ }
318
+
319
+ withShareStore(store: ShareStore): this {
320
+ this.stores.shareStore = store;
321
+ return this;
322
+ }
323
+
324
+ withSecretStore(store: SecretStore): this {
325
+ this.stores.secretStore = store;
326
+ return this;
327
+ }
328
+
329
+ withUserSecretStore(store: UserSecretStore): this {
330
+ this.stores.userSecretStore = store;
331
+ return this;
332
+ }
333
+
334
+ withStateStore(store: StateStore): this {
335
+ this.stores.stateStore = store;
336
+ return this;
337
+ }
338
+
339
+ withTransport(transport: Transport): this {
340
+ this.stores.transport = transport;
341
+ return this;
342
+ }
343
+
344
+ withOwnTransport(endpoint: { uri: string; protocol: string }): this {
345
+ this.config.own_transport_uri = endpoint.uri;
346
+ this.config.own_transport_protocol = protocolDiscriminant(endpoint.protocol);
347
+ return this;
348
+ }
349
+
350
+ /** Default: 3. */
351
+ withThreshold(threshold: number): this {
352
+ this.config.threshold = threshold;
353
+ return this;
354
+ }
355
+
356
+ /** Default: 3. */
357
+ withKeepVersionsCount(count: number): this {
358
+ this.config.keep_versions_count = count;
359
+ return this;
360
+ }
361
+
362
+ /**
363
+ * Every field is optional; not calling this at all leaves all four at the
364
+ * library's defaults — `timeouts` is genuinely absent from the JSON in
365
+ * that case, not filled in with a synthesized object. Values are forwarded
366
+ * verbatim; `Timeouts`'s field names already match `TimeoutsConfig`'s
367
+ * `snake_case` keys one for one.
368
+ */
369
+ withTimeouts(timeouts: Timeouts): this {
370
+ this.config.timeouts = { ...timeouts };
371
+ return this;
372
+ }
373
+
374
+ /** Default: false. */
375
+ withUnsafeHttp(allow: boolean): this {
376
+ this.config.unsafe_http = allow;
377
+ return this;
378
+ }
379
+
380
+ /**
381
+ * Default: empty. `derec_protocol_new`'s own `communication_info` argument
382
+ * is a pre-encoded `CommunicationInfo` protobuf message, and this SDK has
383
+ * no encoder for that message — the C ABI exposes one only for
384
+ * `ContactMessage`. Hand-rolling one here would be SDK-side protocol logic
385
+ * with no test coverage — exactly what this codebase's "Rust decides, the
386
+ * SDK only marshals" rule forbids. Instead,
387
+ * `build()` constructs the protocol with an empty `communication_info`
388
+ * (a legitimate, always-accepted value — see `decode_communication_info`
389
+ * in `library/src/interop/ffi/protocol/handle/mod.rs`, which treats a zero-length
390
+ * buffer as "no entries") and then calls the already-bound
391
+ * {@link DeRecProtocol.setCommunicationInfo}, which takes the same map as
392
+ * plain JSON (`derec_protocol_set_communication_info` in
393
+ * `library/src/interop/ffi/protocol/handle/config.rs`). That setter is queued on
394
+ * the protocol's serial worker before `build()` returns, so it is ordered
395
+ * ahead of every call the caller can subsequently make.
396
+ */
397
+ withCommunicationInfo(info: Record<string, string>): this {
398
+ this.communicationInfo = info;
399
+ return this;
400
+ }
401
+
402
+ /** Default: false. */
403
+ withAutoRespondOnFailure(enabled: boolean): this {
404
+ this.config.auto_respond_on_failure = enabled;
405
+ return this;
406
+ }
407
+
408
+ /** Default: "required". */
409
+ withUnpairAck(ack: UnpairAck): this {
410
+ if (ack === 'required') {
411
+ this.config.unpair_ack = 0;
412
+ } else if (ack === 'not_required') {
413
+ this.config.unpair_ack = 1;
414
+ } else {
415
+ throw new Error(`DeRec: unknown unpair ack "${ack as string}"`);
416
+ }
417
+ return this;
418
+ }
419
+
420
+ /** Default: false. */
421
+ withAutoReplyTo(enabled: boolean): this {
422
+ this.config.auto_reply_to = enabled;
423
+ return this;
424
+ }
425
+
426
+ /** Default: empty policy (every flow off). */
427
+ withAutoAccept(policy: AutoAcceptPolicy): this {
428
+ this.config.auto_accept = {
429
+ pairing: policy.pairing ?? false,
430
+ pre_pair: policy.prePair ?? false,
431
+ store_share: policy.storeShare ?? false,
432
+ verify_share: policy.verifyShare ?? false,
433
+ discovery: policy.discovery ?? false,
434
+ get_share: policy.getShare ?? false,
435
+ unpair: policy.unpair ?? false,
436
+ update_channel_info: policy.updateChannelInfo ?? false,
437
+ };
438
+ return this;
439
+ }
440
+
441
+ /** Default: unset. */
442
+ withReplicaId(replicaId: bigint | number): this {
443
+ this.config.replica_id = BigInt(replicaId).toString();
444
+ return this;
445
+ }
446
+
447
+ build(): DeRecProtocol {
448
+ const config = {
449
+ secret_id: this.secretIdValue.toString(),
450
+ replica_id: null,
451
+ own_transport_uri: '',
452
+ own_transport_protocol: 0,
453
+ ...this.config,
454
+ };
455
+ const host = (getNative() as unknown as NativeHost).protocol_new(
456
+ JSON.stringify(config),
457
+ new Uint8Array(0),
458
+ this.stores,
459
+ ) as NativeHost;
460
+ const protocol = DeRecProtocol.fromHost(host);
461
+ if (this.communicationInfo !== undefined) {
462
+ // `setCommunicationInfo` is asynchronous, but `build()` stays
463
+ // synchronous: the guarantee callers need is not that the map has been
464
+ // applied by the time `build()` returns, it is that no flow can run
465
+ // before it. Posting it here puts it on the protocol's serial worker
466
+ // queue ahead of anything the caller has had the chance to start, so
467
+ // every subsequent call observes it. Left unhandled deliberately — a
468
+ // failure reaches the runtime's unhandled-rejection reporting rather
469
+ // than being silently discarded, and `build()` has no synchronous
470
+ // channel to raise it on.
471
+ void protocol.setCommunicationInfo(this.communicationInfo);
472
+ }
473
+ return protocol;
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Thin wrapper around the `ProtocolHost` JSI host object, mirroring
479
+ * `@derec-alliance/nodejs`'s `DeRecProtocol` — same methods, and the same
480
+ * sync/async split except for `setCommunicationInfo` and `setOwnTransport`,
481
+ * which return a `Promise` here (see their own doc comments for why);
482
+ * `secretId` is synchronous and everything else returns a `Promise`. Every method encodes its
483
+ * arguments into the wire shape the host function expects and decodes its
484
+ * result back into the typed value declared here; no protocol decision is
485
+ * made in this class.
486
+ */
487
+ export class DeRecProtocol {
488
+ private readonly host: NativeHost;
489
+
490
+ /** Use {@link DeRecProtocolBuilder} to construct instances. */
491
+ private constructor(host: NativeHost) {
492
+ this.host = host;
493
+ }
494
+
495
+ /**
496
+ * @internal Constructs an instance from a native `ProtocolHost`. Not part
497
+ * of the public surface — {@link DeRecProtocolBuilder.build} is the only
498
+ * caller. Exists because the constructor is `private` (matching
499
+ * `@derec-alliance/nodejs`'s `index.d.ts`), and a private constructor is
500
+ * only callable from within this class's own body, not from
501
+ * `DeRecProtocolBuilder`.
502
+ */
503
+ static fromHost(host: NativeHost): DeRecProtocol {
504
+ return new DeRecProtocol(host);
505
+ }
506
+
507
+ secretId(): bigint {
508
+ return this.host.secretId() as bigint;
509
+ }
510
+
511
+ /**
512
+ * Returns a `Promise`, where `@derec-alliance/nodejs` declares this
513
+ * `void`. The native setter takes the same handle mutex a running flow
514
+ * holds across its store callbacks, so calling it on the JavaScript thread
515
+ * would block that thread until the flow released the mutex — which it
516
+ * cannot do while it is waiting on the JavaScript thread. The binding
517
+ * therefore queues the setter onto the same serial worker every flow runs
518
+ * on, which orders it after any call already in flight. The nodejs SDK is
519
+ * WASM and single-threaded, so it has no such hazard.
520
+ *
521
+ * Callers that ignore the result behave exactly as before, so this stays
522
+ * source-compatible with nodejs-shaped code; awaiting it additionally
523
+ * surfaces a failure that would otherwise be swallowed.
524
+ */
525
+ setCommunicationInfo(info: Record<string, string>): Promise<void> {
526
+ return this.host.setCommunicationInfo(jsonToBytes(info)) as Promise<void>;
527
+ }
528
+
529
+ /** Returns a `Promise` for the same reason {@link setCommunicationInfo} does. */
530
+ setOwnTransport(uri: string, protocol: string): Promise<void> {
531
+ return this.host.setOwnTransport(
532
+ uri,
533
+ protocolDiscriminant(protocol),
534
+ ) as Promise<void>;
535
+ }
536
+
537
+ /**
538
+ * Decodes the `contact_wire_bytes` `derec_protocol_create_contact`
539
+ * produces into a typed `ContactMessage`, so an application can read the
540
+ * contact's `nonce`, `transport_protocol` and mode without a protobuf
541
+ * codec of its own. Feed the returned object straight back into
542
+ * `start(FlowKind.Pairing, { contact })`, which re-encodes it.
543
+ */
544
+ async createContact(
545
+ channelId: bigint | number | null | undefined,
546
+ contactMode: ContactMode,
547
+ nonce?: bigint | number | null,
548
+ ): Promise<ContactMessage> {
549
+ const hasChannelId = channelId !== null && channelId !== undefined ? 1 : 0;
550
+ const hasNonce = nonce !== null && nonce !== undefined ? 1 : 0;
551
+ const buffer = (await this.host.createContact(
552
+ hasChannelId,
553
+ toBigInt(channelId ?? 0),
554
+ contactMode,
555
+ hasNonce,
556
+ toBigInt(nonce ?? 0),
557
+ )) as ArrayBuffer;
558
+ return primitives.pairing.request.decode_contact(new Uint8Array(buffer));
559
+ }
560
+
561
+ start(flowKind: FlowKind.Pairing, params: PairingParams): Promise<DeRecEvent[]>;
562
+ start(flowKind: FlowKind.Discovery, params?: DiscoveryParams): Promise<DeRecEvent[]>;
563
+ start(flowKind: FlowKind.ProtectSecret, params: ProtectSecretParams): Promise<DeRecEvent[]>;
564
+ start(flowKind: FlowKind.VerifyShares, params: VerifySharesParams): Promise<DeRecEvent[]>;
565
+ start(flowKind: FlowKind.RecoverSecret, params: RecoverSecretParams): Promise<DeRecEvent[]>;
566
+ start(flowKind: FlowKind.Unpair, params: UnpairParams): Promise<DeRecEvent[]>;
567
+ start(flowKind: FlowKind.UpdateChannelInfo, params: UpdateChannelInfoParams): Promise<DeRecEvent[]>;
568
+ start(flowKind: FlowKind.SyncCheck, params?: SyncCheckParams): Promise<DeRecEvent[]>;
569
+ start(flowKind: FlowKind.RemoveReplica, params: RemoveReplicaParams): Promise<DeRecEvent[]>;
570
+ async start(flowKind: FlowKind, params?: unknown): Promise<DeRecEvent[]> {
571
+ const paramsBytes = buildStartParams(flowKind, params);
572
+ const buffer = (await this.host.start(flowKind, paramsBytes)) as ArrayBuffer;
573
+ return decodeEvents(buffer);
574
+ }
575
+
576
+ async process(message: Uint8Array): Promise<DeRecEvent[]> {
577
+ const buffer = (await this.host.process(message)) as ArrayBuffer;
578
+ return decodeEvents(buffer);
579
+ }
580
+
581
+ async tick(): Promise<DeRecEvent[]> {
582
+ const buffer = (await this.host.tick()) as ArrayBuffer;
583
+ return decodeEvents(buffer);
584
+ }
585
+
586
+ async accept(actionBytes: Uint8Array): Promise<DeRecEvent[]> {
587
+ const buffer = (await this.host.accept(actionBytes)) as ArrayBuffer;
588
+ return decodeEvents(buffer);
589
+ }
590
+
591
+ async reject(actionBytes: Uint8Array, status: number, memo: string): Promise<void> {
592
+ await this.host.reject(actionBytes, status, memo);
593
+ }
594
+
595
+ async getFingerprint(channelId: bigint | number): Promise<string> {
596
+ return (await this.host.getFingerprint(toBigInt(channelId))) as string;
597
+ }
598
+
599
+ async verifyFingerprint(channelId: bigint | number, fingerprint: string): Promise<boolean> {
600
+ return (await this.host.verifyFingerprint(toBigInt(channelId), fingerprint)) as boolean;
601
+ }
602
+
603
+ async removeExpiredChannels(olderThanSecs: number): Promise<string[]> {
604
+ const buffer = (await this.host.removeExpiredChannels(olderThanSecs)) as ArrayBuffer;
605
+ return jsonFromBytes(buffer) as string[];
606
+ }
607
+
608
+ async restore(
609
+ recoveredSecret: Extract<DeRecEvent, { type: 'SecretRecovered' }>['secret'],
610
+ version: number,
611
+ ): Promise<DeRecEvent[]> {
612
+ const params = buildRestoreParams(recoveredSecret, version);
613
+ const buffer = (await this.host.restore(jsonToBytes(params))) as ArrayBuffer;
614
+ return decodeEvents(buffer);
615
+ }
616
+
617
+ /** Release the native handle. Safe to call more than once. */
618
+ free(): void {
619
+ this.host.free();
620
+ }
621
+ }