@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
package/lib/types.js ADDED
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // Copyright (c) 2026 DeRec Alliance. All rights reserved.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.FlowKind = exports.ContactMode = exports.SenderKind = void 0;
6
+ var SenderKind;
7
+ (function (SenderKind) {
8
+ SenderKind[SenderKind["Owner"] = 0] = "Owner";
9
+ SenderKind[SenderKind["Helper"] = 1] = "Helper";
10
+ SenderKind[SenderKind["ReplicaSource"] = 3] = "ReplicaSource";
11
+ SenderKind[SenderKind["ReplicaDestination"] = 4] = "ReplicaDestination";
12
+ })(SenderKind || (exports.SenderKind = SenderKind = {}));
13
+ /**
14
+ * Selects how the initiator's public encryption material is delivered in a
15
+ * `ContactMessage`.
16
+ *
17
+ * - `InlineKeys` (default): keys are embedded in the contact itself.
18
+ * - `HashedKeys`: only a SHA-384 commitment to the keys is in the contact;
19
+ * the scanner must fetch the actual keys over the wire via the `PrePair`
20
+ * round-trip and verify them against the commitment before pairing.
21
+ * - `NoKeys`: no key material and no commitment. The contact carries only
22
+ * `channel_id`, `nonce`, and `transport_protocol` — small enough to be
23
+ * hand-typed or dictated. Keys are generated on the fly by the contact
24
+ * creator when the `PrePairRequest` arrives; the scanner accepts them
25
+ * without cryptographic verification. Trust rests entirely on the OOB
26
+ * delivery channel being fully trusted (e.g. a verified email from an
27
+ * already-KYC-authenticated institution). Applications MUST rate-limit
28
+ * inbound `PrePairRequest`s per channel and expire outstanding NoKeys
29
+ * contacts on a short timer.
30
+ *
31
+ * Because nothing binds the published keys to the contact, the channel is
32
+ * held `Pending` until `verifyFingerprint` succeeds on both sides: it is
33
+ * not a publish target, not a recovery source, and inbound messages on it
34
+ * are ignored. A man-in-the-middle on the plaintext `PrePair` leg leaves
35
+ * the two sides with different shared keys and so different fingerprints,
36
+ * which is what the comparison catches — the role `contact_binding_hash`
37
+ * plays for `HashedKeys`.
38
+ */
39
+ var ContactMode;
40
+ (function (ContactMode) {
41
+ ContactMode[ContactMode["InlineKeys"] = 0] = "InlineKeys";
42
+ ContactMode[ContactMode["HashedKeys"] = 1] = "HashedKeys";
43
+ ContactMode[ContactMode["NoKeys"] = 2] = "NoKeys";
44
+ })(ContactMode || (exports.ContactMode = ContactMode = {}));
45
+ var FlowKind;
46
+ (function (FlowKind) {
47
+ FlowKind[FlowKind["Pairing"] = 0] = "Pairing";
48
+ FlowKind[FlowKind["Discovery"] = 1] = "Discovery";
49
+ FlowKind[FlowKind["ProtectSecret"] = 2] = "ProtectSecret";
50
+ FlowKind[FlowKind["VerifyShares"] = 3] = "VerifyShares";
51
+ FlowKind[FlowKind["RecoverSecret"] = 4] = "RecoverSecret";
52
+ FlowKind[FlowKind["Unpair"] = 5] = "Unpair";
53
+ FlowKind[FlowKind["UpdateChannelInfo"] = 6] = "UpdateChannelInfo";
54
+ /** Ask the replica group whether this device is behind, and catch up if it
55
+ * is. Replica-only, and takes no parameters — the group and this device's
56
+ * own version both come from the stores. */
57
+ FlowKind[FlowKind["SyncCheck"] = 7] = "SyncCheck";
58
+ /** Remove a member from the replica group. Replica-only. Naming this device
59
+ * is a voluntary departure; naming another is an eviction. Params:
60
+ * `{ replica_id: string; memo?: string }` — `replica_id` is a decimal
61
+ * string so ids above 2^53 survive JS number handling. */
62
+ FlowKind[FlowKind["RemoveReplica"] = 8] = "RemoveReplica";
63
+ })(FlowKind || (exports.FlowKind = FlowKind = {}));
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@derec-alliance/react-native",
3
+ "version": "0.0.1",
4
+ "description": "React Native bindings for derec-library, the Rust SDK for the DeRec protocol.",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/derecalliance/lib-derec"
9
+ },
10
+ "main": "lib/index.js",
11
+ "types": "lib/index.d.ts",
12
+ "files": [
13
+ "lib",
14
+ "src",
15
+ "cpp",
16
+ "ios",
17
+ "android",
18
+ "!android/build",
19
+ "!android/.cxx",
20
+ "!cpp/tests",
21
+ "DeRec.podspec",
22
+ "react-native.config.js",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc --project tsconfig.build.json",
28
+ "test": "jest",
29
+ "typecheck": "tsc --noEmit"
30
+ },
31
+ "peerDependencies": {
32
+ "react": ">=18.2.0",
33
+ "react-native": ">=0.76.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/jest": "^30.0.0",
37
+ "@types/node": "^26.3.0",
38
+ "jest": "^30.4.2",
39
+ "react-native": "^0.87.0",
40
+ "ts-jest": "^29.4.12",
41
+ "typescript": "^5.9.3"
42
+ },
43
+ "keywords": [
44
+ "derec",
45
+ "decentralized",
46
+ "recovery",
47
+ "secret-sharing",
48
+ "sdk",
49
+ "react-native",
50
+ "ios",
51
+ "android",
52
+ "typescript",
53
+ "jsi"
54
+ ]
55
+ }
@@ -0,0 +1,19 @@
1
+ // Autolinking configuration.
2
+ //
3
+ // `sourceDir` must be RELATIVE to the package root: the CLI does
4
+ // `path.join(root, sourceDir)` (`cli-config-android/build/config/index.js`),
5
+ // so an absolute path is appended to the package root and resolves to a
6
+ // directory that does not exist. The Android config then returns null and the
7
+ // module is dropped from the generated `PackageList` — silently, with no build
8
+ // error and no autolinking warning.
9
+ //
10
+ // iOS needs no entry at all: CocoaPods discovers `DeRec.podspec` at the package
11
+ // root by convention, and naming it explicitly under `dependency.platforms.ios`
12
+ // fails the CLI's config schema ("podspecPath is not allowed").
13
+ module.exports = {
14
+ dependency: {
15
+ platforms: {
16
+ android: { sourceDir: 'android' },
17
+ },
18
+ },
19
+ };
package/src/binary.ts ADDED
@@ -0,0 +1,182 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
+
4
+ /**
5
+ * The three FFI container formats.
6
+ *
7
+ * Most values cross this boundary as protobuf or JSON. Three do not: the C ABI
8
+ * ferries a set of items as a single hand-packed little-endian buffer, because
9
+ * a C signature cannot carry a variable-length array of variable-length items
10
+ * without one. Each format is specified in the module header of the Rust file
11
+ * that owns it, and the encoders below follow those specs exactly:
12
+ *
13
+ * - the discovery secret list (`library/src/interop/ffi/discovery.rs`)
14
+ * - the recovery response set (`library/src/interop/ffi/recovery.rs`)
15
+ * - the committed-share map (`library/src/interop/ffi/sharing.rs`)
16
+ *
17
+ * Everything here is representation, not protocol: no field is defaulted,
18
+ * reordered or interpreted.
19
+ */
20
+
21
+ import { utf8Decode, utf8Encode } from './codec';
22
+ import type { SecretVersionEntry, VersionEntry } from './types';
23
+
24
+ class Writer {
25
+ private readonly chunks: Uint8Array[] = [];
26
+
27
+ u32(value: number): void {
28
+ const buffer = new Uint8Array(4);
29
+ new DataView(buffer.buffer).setUint32(0, value, true);
30
+ this.chunks.push(buffer);
31
+ }
32
+
33
+ u64(value: bigint): void {
34
+ const buffer = new Uint8Array(8);
35
+ new DataView(buffer.buffer).setBigUint64(0, value, true);
36
+ this.chunks.push(buffer);
37
+ }
38
+
39
+ bytes(value: Uint8Array): void {
40
+ this.chunks.push(value);
41
+ }
42
+
43
+ finish(): Uint8Array {
44
+ let length = 0;
45
+ for (const chunk of this.chunks) {
46
+ length += chunk.length;
47
+ }
48
+ const out = new Uint8Array(length);
49
+ let offset = 0;
50
+ for (const chunk of this.chunks) {
51
+ out.set(chunk, offset);
52
+ offset += chunk.length;
53
+ }
54
+ return out;
55
+ }
56
+ }
57
+
58
+ class Reader {
59
+ private offset = 0;
60
+ private readonly view: DataView;
61
+
62
+ constructor(private readonly data: Uint8Array) {
63
+ this.view = new DataView(data.buffer, data.byteOffset, data.byteLength);
64
+ }
65
+
66
+ private require(count: number): void {
67
+ if (this.offset + count > this.data.length) {
68
+ // A truncated container means the SDK and the library disagree about
69
+ // the format. Failing loudly beats returning a half-read set that a
70
+ // caller would take for the whole thing.
71
+ throw new Error(
72
+ `DeRec: malformed FFI container — wanted ${count} bytes at offset ${this.offset}, have ${
73
+ this.data.length - this.offset
74
+ }`,
75
+ );
76
+ }
77
+ }
78
+
79
+ u32(): number {
80
+ this.require(4);
81
+ const value = this.view.getUint32(this.offset, true);
82
+ this.offset += 4;
83
+ return value;
84
+ }
85
+
86
+ u64(): bigint {
87
+ this.require(8);
88
+ const value = this.view.getBigUint64(this.offset, true);
89
+ this.offset += 8;
90
+ return value;
91
+ }
92
+
93
+ bytes(count: number): Uint8Array {
94
+ this.require(count);
95
+ const value = this.data.subarray(this.offset, this.offset + count);
96
+ this.offset += count;
97
+ return value;
98
+ }
99
+
100
+ atEnd(): boolean {
101
+ return this.offset === this.data.length;
102
+ }
103
+ }
104
+
105
+ function asBytes(buffer: Uint8Array | ArrayBuffer): Uint8Array {
106
+ return buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
107
+ }
108
+
109
+ /** Discovery secret list — see `library/src/interop/ffi/discovery.rs`. */
110
+ export function encodeSecretList(entries: SecretVersionEntry[]): Uint8Array {
111
+ const writer = new Writer();
112
+ writer.u32(entries.length);
113
+ for (const entry of entries) {
114
+ writer.u64(entry.secret_id);
115
+ writer.u32(entry.versions.length);
116
+ for (const version of entry.versions) {
117
+ writer.u32(version.version);
118
+ const description = utf8Encode(version.description);
119
+ writer.u32(description.length);
120
+ writer.bytes(description);
121
+ }
122
+ }
123
+ return writer.finish();
124
+ }
125
+
126
+ export function decodeSecretList(buffer: Uint8Array | ArrayBuffer): SecretVersionEntry[] {
127
+ const reader = new Reader(asBytes(buffer));
128
+ const count = reader.u32();
129
+ const entries: SecretVersionEntry[] = [];
130
+ for (let i = 0; i < count; i++) {
131
+ const secretId = reader.u64();
132
+ const versionCount = reader.u32();
133
+ const versions: VersionEntry[] = [];
134
+ for (let v = 0; v < versionCount; v++) {
135
+ const version = reader.u32();
136
+ const descriptionLength = reader.u32();
137
+ versions.push({
138
+ version,
139
+ description: utf8Decode(reader.bytes(descriptionLength)),
140
+ });
141
+ }
142
+ entries.push({ secret_id: secretId, versions });
143
+ }
144
+ return entries;
145
+ }
146
+
147
+ /**
148
+ * Recovery response set — see `library/src/interop/ffi/recovery.rs`. Each
149
+ * entry is a serialized `GetShareResponseMessage`, which the caller obtained
150
+ * from `extract_get_share_response`.
151
+ */
152
+ export function encodeShareResponses(responses: Uint8Array[]): Uint8Array {
153
+ const writer = new Writer();
154
+ writer.u32(responses.length);
155
+ for (const response of responses) {
156
+ writer.u32(response.length);
157
+ writer.bytes(response);
158
+ }
159
+ return writer.finish();
160
+ }
161
+
162
+ /**
163
+ * Committed-share map — see `library/src/interop/ffi/sharing.rs`. Entries
164
+ * arrive sorted by channel id; the `Map` preserves that insertion order, so
165
+ * iterating the result is deterministic.
166
+ */
167
+ export function decodeCommittedShareEntries(
168
+ buffer: Uint8Array | ArrayBuffer,
169
+ ): Array<{ channelId: bigint; shareProto: Uint8Array }> {
170
+ const reader = new Reader(asBytes(buffer));
171
+ const count = reader.u32();
172
+ const entries: Array<{ channelId: bigint; shareProto: Uint8Array }> = [];
173
+ for (let i = 0; i < count; i++) {
174
+ const channelId = reader.u64();
175
+ const shareLength = reader.u32();
176
+ entries.push({ channelId, shareProto: reader.bytes(shareLength) });
177
+ }
178
+ if (!reader.atEnd()) {
179
+ throw new Error('DeRec: trailing bytes after the committed-share map');
180
+ }
181
+ return entries;
182
+ }
package/src/codec.ts ADDED
@@ -0,0 +1,79 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
+
4
+ /**
5
+ * UTF-8 byte <-> string codec, shared by every JSON payload that crosses the
6
+ * JSI boundary (config, flow params, the events array, the `ContactMessage`
7
+ * JSON the pairing codec entry points exchange). Those payloads travel as
8
+ * bytes, not a JS string — the host functions read `ArrayBuffer`/typed-array
9
+ * arguments and hand back `ArrayBuffer` results (see `ProtocolHost.cpp`'s
10
+ * `bytesToArrayBuffer`). `TextDecoder`/`TextEncoder` are not declared as
11
+ * ambient globals under this package's `lib: ["ES2020"]` (no DOM lib) and the
12
+ * installed `@types/node` exposes them only from `node:util`, which does not
13
+ * exist in the Hermes runtime this code actually ships on. A small
14
+ * dependency-free codec avoids both problems.
15
+ */
16
+
17
+ export function utf8Encode(text: string): Uint8Array {
18
+ const bytes: number[] = [];
19
+ for (let i = 0; i < text.length; i++) {
20
+ let code = text.charCodeAt(i);
21
+ if (code >= 0xd800 && code <= 0xdbff && i + 1 < text.length) {
22
+ const low = text.charCodeAt(i + 1);
23
+ if (low >= 0xdc00 && low <= 0xdfff) {
24
+ code = (code - 0xd800) * 0x400 + (low - 0xdc00) + 0x10000;
25
+ i++;
26
+ }
27
+ }
28
+ if (code < 0x80) {
29
+ bytes.push(code);
30
+ } else if (code < 0x800) {
31
+ bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
32
+ } else if (code < 0x10000) {
33
+ bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
34
+ } else {
35
+ bytes.push(
36
+ 0xf0 | (code >> 18),
37
+ 0x80 | ((code >> 12) & 0x3f),
38
+ 0x80 | ((code >> 6) & 0x3f),
39
+ 0x80 | (code & 0x3f),
40
+ );
41
+ }
42
+ }
43
+ return new Uint8Array(bytes);
44
+ }
45
+
46
+ export function utf8Decode(bytes: Uint8Array): string {
47
+ let result = '';
48
+ let i = 0;
49
+ while (i < bytes.length) {
50
+ const byte1 = bytes[i++];
51
+ if (byte1 < 0x80) {
52
+ result += String.fromCharCode(byte1);
53
+ } else if (byte1 < 0xe0) {
54
+ const byte2 = bytes[i++];
55
+ result += String.fromCharCode(((byte1 & 0x1f) << 6) | (byte2 & 0x3f));
56
+ } else if (byte1 < 0xf0) {
57
+ const byte2 = bytes[i++];
58
+ const byte3 = bytes[i++];
59
+ result += String.fromCharCode(((byte1 & 0x0f) << 12) | ((byte2 & 0x3f) << 6) | (byte3 & 0x3f));
60
+ } else {
61
+ const byte2 = bytes[i++];
62
+ const byte3 = bytes[i++];
63
+ const byte4 = bytes[i++];
64
+ const code =
65
+ (((byte1 & 0x07) << 18) | ((byte2 & 0x3f) << 12) | ((byte3 & 0x3f) << 6) | (byte4 & 0x3f)) -
66
+ 0x10000;
67
+ result += String.fromCharCode(0xd800 + (code >> 10), 0xdc00 + (code & 0x3ff));
68
+ }
69
+ }
70
+ return result;
71
+ }
72
+
73
+ export function jsonToBytes(value: unknown): Uint8Array {
74
+ return utf8Encode(JSON.stringify(value));
75
+ }
76
+
77
+ export function jsonFromBytes(buffer: ArrayBuffer): unknown {
78
+ return JSON.parse(utf8Decode(new Uint8Array(buffer)));
79
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,45 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
+
4
+ /**
5
+ * Every category name `derec_error_category_name` can return, and no others.
6
+ *
7
+ * This is the authoritative list for this SDK, mirrored by a test against
8
+ * `bindings/test_fixture/enums.json` so it cannot drift from the Rust enum.
9
+ * It deliberately differs from `@derec-alliance/nodejs`'s union, which was
10
+ * the original source of this file: that union lists `"wasm"`, a category no
11
+ * C-ABI binding can ever emit, and omits `"ok"`, `"ffi"` and `"state_store"`,
12
+ * which are live arms of `library/src/interop/ffi/error_names.rs` and reachable here.
13
+ * The nodejs union is arguably wrong on its own terms too, but correcting it
14
+ * is a change to that package, not this one.
15
+ */
16
+ export const DEREC_ERROR_CATEGORIES = [
17
+ "ok",
18
+ "ffi",
19
+ "pairing",
20
+ "sharing",
21
+ "recovery",
22
+ "verification",
23
+ "discovery",
24
+ "unpairing",
25
+ "derec_message",
26
+ "secret_store",
27
+ "channel_store",
28
+ "share_store",
29
+ "input",
30
+ "protobuf",
31
+ "invariant",
32
+ "state_store",
33
+ ] as const;
34
+
35
+ export type DeRecErrorCategory = (typeof DEREC_ERROR_CATEGORIES)[number];
36
+
37
+ export interface DeRecError {
38
+ category: DeRecErrorCategory;
39
+ code: string;
40
+ message: string;
41
+ status?: number;
42
+ memo?: string;
43
+ expected?: number;
44
+ got?: number;
45
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
+
4
+ export * from './types';
5
+ export * from './errors';
6
+ export { primitives, envelope } from './primitives';
7
+ export { DeRecProtocol, DeRecProtocolBuilder } from './protocol';
@@ -0,0 +1,192 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
+
4
+ /**
5
+ * Message marshalling.
6
+ *
7
+ * Every `extract_*` entry point in the C ABI hands back protobuf wire bytes,
8
+ * and every `produce_*` / `process_*` that takes a message takes the same.
9
+ * Go and .NET decode those with generated protobuf bindings; this SDK has
10
+ * none, so it goes through `derec_decode_message_json` /
11
+ * `derec_encode_message_json` instead (`library/src/interop/ffi/message_json.rs`),
12
+ * which re-emits each message as the JSON mirror the WASM SDKs already
13
+ * surface. The wire format therefore stays a single Rust-side decision — this
14
+ * file only changes representation.
15
+ *
16
+ * Two representation changes are needed to reach the exact JavaScript shape
17
+ * `@derec-alliance/nodejs` exposes, where `serde-wasm-bindgen` produces it
18
+ * natively:
19
+ *
20
+ * - Wide numerics cross the JSON seam as decimal strings and are `bigint`
21
+ * in the typed surface. `serde_json` would otherwise emit them as JSON
22
+ * numbers, and `JSON.parse` silently rounds anything above 2^53 — the
23
+ * corruption only appears once a real id happens to be large.
24
+ * - Byte fields cross as arrays of decimal byte values (`serde_json` draws
25
+ * no distinction for `serde_bytes`) and are `Uint8Array` in the typed
26
+ * surface.
27
+ *
28
+ * Both conversions are keyed by field name, matching the lists in
29
+ * `message_json.rs`. A name-keyed walk is deliberate: `keep_list` is a
30
+ * genuine `number[]` and must stay one, so a structural "array of numbers is
31
+ * bytes" rule would corrupt it.
32
+ */
33
+
34
+ import { jsonFromBytes, jsonToBytes } from './codec';
35
+ import { getNative } from './native';
36
+
37
+ /**
38
+ * Selects which message the JSON codec operates on.
39
+ *
40
+ * Mirrors `MessageKind` in `bindings/test_fixture/enums.json`, which
41
+ * `library/src/interop/ffi/message_json.rs` asserts against — so a message
42
+ * added in Rust cannot reach this SDK as a silently unhandled value.
43
+ */
44
+ export enum MessageKind {
45
+ PairRequest = 0,
46
+ PairResponse = 1,
47
+ PrePairRequest = 2,
48
+ PrePairResponse = 3,
49
+ GetSecretIdsVersionsRequest = 4,
50
+ GetSecretIdsVersionsResponse = 5,
51
+ GetShareRequest = 6,
52
+ GetShareResponse = 7,
53
+ StoreShareRequest = 8,
54
+ StoreShareResponse = 9,
55
+ UnpairRequest = 10,
56
+ UnpairResponse = 11,
57
+ VerifyShareRequest = 12,
58
+ VerifyShareResponse = 13,
59
+ TransportProtocol = 14,
60
+ CommunicationInfo = 15,
61
+ ParameterRange = 16,
62
+ CommittedDeRecShare = 17,
63
+ }
64
+
65
+ /** Fields the codec widens to decimal strings; `bigint` on this side. */
66
+ const BIGINT_STRING_FIELDS = new Set([
67
+ 'channel_id',
68
+ 'nonce',
69
+ 'secret_id',
70
+ 'replica_id',
71
+ 'min_share_size',
72
+ 'max_share_size',
73
+ 'min_time_between_verifications',
74
+ 'max_time_between_verifications',
75
+ 'min_time_between_share_updates',
76
+ 'max_time_between_share_updates',
77
+ 'min_unresponsive_deletion_timeout',
78
+ 'max_unresponsive_deletion_timeout',
79
+ 'min_unresponsive_deactivation_timeout',
80
+ 'max_unresponsive_deactivation_timeout',
81
+ ]);
82
+
83
+ /**
84
+ * `Timestamp.seconds` is an `i64` holding a Unix seconds value. It stays a
85
+ * JSON number — no realistic timestamp approaches 2^53 — but the typed
86
+ * surface declares it `bigint`, matching `@derec-alliance/nodejs`.
87
+ */
88
+ const BIGINT_NUMBER_FIELDS = new Set(['seconds']);
89
+
90
+ /** Fields carrying `Vec<u8>`; `Uint8Array` on this side. */
91
+ const BYTE_FIELDS = new Set([
92
+ 'mlkem_ciphertext',
93
+ 'mlkem_encapsulation_key',
94
+ 'ecies_public_key',
95
+ 'contact_binding_hash',
96
+ 'bytes_value',
97
+ 'share',
98
+ 'committed_de_rec_share',
99
+ 'de_rec_share',
100
+ 'commitment',
101
+ 'hash',
102
+ ]);
103
+
104
+ /** JSON as it arrives from Rust, turned into the typed surface. */
105
+ export function reviveMessage(value: unknown): unknown {
106
+ if (Array.isArray(value)) {
107
+ return value.map(reviveMessage);
108
+ }
109
+ if (value === null || typeof value !== 'object') {
110
+ return value;
111
+ }
112
+ const out: Record<string, unknown> = {};
113
+ for (const [key, field] of Object.entries(value as Record<string, unknown>)) {
114
+ if (field === null || field === undefined) {
115
+ // A `null` from an absent `Option` reads as `undefined` in the typed
116
+ // surface, matching what `serde-wasm-bindgen` produces.
117
+ out[key] = undefined;
118
+ } else if (BIGINT_STRING_FIELDS.has(key) && typeof field === 'string') {
119
+ out[key] = BigInt(field);
120
+ } else if (BIGINT_NUMBER_FIELDS.has(key) && typeof field === 'number') {
121
+ out[key] = BigInt(field);
122
+ } else if (BYTE_FIELDS.has(key) && Array.isArray(field)) {
123
+ out[key] = Uint8Array.from(field as number[]);
124
+ } else {
125
+ out[key] = reviveMessage(field);
126
+ }
127
+ }
128
+ return out;
129
+ }
130
+
131
+ /** Inverse of {@link reviveMessage}. */
132
+ export function plainMessage(value: unknown): unknown {
133
+ if (Array.isArray(value)) {
134
+ return value.map(plainMessage);
135
+ }
136
+ if (value instanceof Uint8Array) {
137
+ return Array.from(value);
138
+ }
139
+ if (typeof value === 'bigint') {
140
+ return value.toString();
141
+ }
142
+ if (value === null || typeof value !== 'object') {
143
+ return value;
144
+ }
145
+ const out: Record<string, unknown> = {};
146
+ for (const [key, field] of Object.entries(value as Record<string, unknown>)) {
147
+ if (field === undefined) {
148
+ // Left out entirely rather than sent as `null`: serde reads a missing
149
+ // `Option` field as `None`, and an explicit `null` for a field the DTO
150
+ // does not declare optional would fail to deserialize.
151
+ continue;
152
+ }
153
+ if (BIGINT_NUMBER_FIELDS.has(key) && typeof field === 'bigint') {
154
+ out[key] = Number(field);
155
+ } else if (field instanceof Uint8Array) {
156
+ out[key] = Array.from(field);
157
+ } else {
158
+ out[key] = plainMessage(field);
159
+ }
160
+ }
161
+ return out;
162
+ }
163
+
164
+ function host(): Record<string, (...args: unknown[]) => unknown> {
165
+ return getNative() as never as Record<string, (...args: unknown[]) => unknown>;
166
+ }
167
+
168
+ /** Protobuf wire bytes -> the typed message. */
169
+ export function decodeMessage<T>(kind: MessageKind, proto: Uint8Array | ArrayBuffer): T {
170
+ const buffer = host().decode_message_json(kind, proto) as ArrayBuffer;
171
+ return reviveMessage(jsonFromBytes(buffer)) as T;
172
+ }
173
+
174
+ /** The typed message -> protobuf wire bytes. */
175
+ export function encodeMessage(kind: MessageKind, value: unknown): Uint8Array {
176
+ const json = jsonToBytes(plainMessage(value));
177
+ return new Uint8Array(host().encode_message_json(kind, json) as ArrayBuffer);
178
+ }
179
+
180
+ /**
181
+ * Encodes an optional message argument. The C ABI reads a zero-length buffer
182
+ * as "absent" for every optional message parameter, so `null`/`undefined`
183
+ * becomes an empty array rather than a decision made here.
184
+ */
185
+ export function encodeOptionalMessage(
186
+ kind: MessageKind,
187
+ value: unknown | null | undefined,
188
+ ): Uint8Array {
189
+ return value === null || value === undefined
190
+ ? new Uint8Array(0)
191
+ : encodeMessage(kind, value);
192
+ }