@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.
- package/DeRec.podspec +25 -0
- package/LICENSE +201 -0
- package/README.md +222 -0
- package/android/CMakeLists.txt +38 -0
- package/android/build.gradle +79 -0
- package/android/src/main/AndroidManifest.xml +12 -0
- package/android/src/main/java/org/derec/DeRecModule.kt +56 -0
- package/android/src/main/java/org/derec/DeRecPackage.kt +24 -0
- package/android/src/main/jni/OnLoad.cpp +39 -0
- 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 +50 -0
- package/cpp/Convert.h +38 -0
- package/cpp/DeRecInstaller.cpp +186 -0
- package/cpp/DeRecInstaller.h +28 -0
- package/cpp/Invoker.h +20 -0
- package/cpp/JsCallbackBridge.cpp +144 -0
- package/cpp/JsCallbackBridge.h +148 -0
- package/cpp/Primitives.cpp +1193 -0
- package/cpp/Primitives.h +39 -0
- package/cpp/ProtocolHost.cpp +613 -0
- package/cpp/ProtocolHost.h +88 -0
- package/cpp/RuntimeInvoker.h +29 -0
- package/cpp/StoreCallbacks.cpp +1293 -0
- package/cpp/StoreCallbacks.h +111 -0
- package/cpp/WorkerThread.cpp +74 -0
- package/cpp/WorkerThread.h +54 -0
- package/cpp/derec_ffi.h +2145 -0
- package/ios/DeRec.h +23 -0
- package/ios/DeRec.mm +39 -0
- package/ios/DeRecFFI.xcframework/Info.plist +48 -0
- package/ios/DeRecFFI.xcframework/ios-arm64/Headers/derec_ffi.h +2145 -0
- package/ios/DeRecFFI.xcframework/ios-arm64/libderec_library.a +0 -0
- package/ios/DeRecFFI.xcframework/ios-arm64_x86_64-simulator/Headers/derec_ffi.h +2145 -0
- package/ios/DeRecFFI.xcframework/ios-arm64_x86_64-simulator/libderec_library.a +0 -0
- package/lib/binary.d.ts +19 -0
- package/lib/binary.js +164 -0
- package/lib/codec.d.ts +16 -0
- package/lib/codec.js +80 -0
- package/lib/errors.d.ts +23 -0
- package/lib/errors.js +35 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +27 -0
- package/lib/messages.d.ts +41 -0
- package/lib/messages.js +192 -0
- package/lib/native.d.ts +11 -0
- package/lib/native.js +40 -0
- package/lib/primitives.d.ts +137 -0
- package/lib/primitives.js +329 -0
- package/lib/protocol.d.ts +188 -0
- package/lib/protocol.js +514 -0
- package/lib/types.d.ts +1014 -0
- package/lib/types.js +63 -0
- package/package.json +55 -0
- package/react-native.config.js +19 -0
- package/src/binary.ts +182 -0
- package/src/codec.ts +79 -0
- package/src/errors.ts +45 -0
- package/src/index.ts +7 -0
- package/src/messages.ts +192 -0
- package/src/native.ts +52 -0
- package/src/primitives.ts +770 -0
- package/src/protocol.ts +621 -0
- 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';
|
package/src/messages.ts
ADDED
|
@@ -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
|
+
}
|