@qntx/wallet-native 0.0.0-stage → 0.4.0

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 (69) hide show
  1. package/README.md +123 -2
  2. package/WalletNative.podspec +74 -0
  3. package/android/CMakeLists.txt +99 -0
  4. package/android/build.gradle +177 -0
  5. package/android/consumer-rules.pro +4 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +29 -0
  8. package/android/src/main/java/com/margelo/nitro/qntxwallet/AppLifecycle.kt +35 -0
  9. package/android/src/main/java/com/margelo/nitro/qntxwallet/KernelPbkdf2.java +59 -0
  10. package/android/src/main/java/com/margelo/nitro/qntxwallet/Keystore.kt +280 -0
  11. package/android/src/main/java/com/margelo/nitro/qntxwallet/Passkey.kt +147 -0
  12. package/android/src/main/java/com/margelo/nitro/qntxwallet/VaultDirectory.kt +28 -0
  13. package/android/src/main/java/com/margelo/nitro/qntxwallet/WalletNativePackage.java +30 -0
  14. package/cpp/host/HybridKernelHost.cpp +162 -0
  15. package/cpp/host/HybridKernelHost.hpp +49 -0
  16. package/cpp/host/KernelFile.cpp +120 -0
  17. package/cpp/host/KernelFile.hpp +24 -0
  18. package/cpp/host/Keystore.hpp +70 -0
  19. package/cpp/host/Lifecycle.cpp +77 -0
  20. package/cpp/host/Lifecycle.hpp +71 -0
  21. package/cpp/host/Passkey.hpp +39 -0
  22. package/cpp/host/VaultDirectory.hpp +16 -0
  23. package/cpp/platform/KeystoreAndroid.cpp +175 -0
  24. package/cpp/platform/KeystoreIos.mm +300 -0
  25. package/cpp/platform/LifecycleAndroid.cpp +55 -0
  26. package/cpp/platform/LifecycleIos.mm +36 -0
  27. package/cpp/platform/PasskeyAndroid.cpp +118 -0
  28. package/cpp/platform/PasskeyIos.mm +20 -0
  29. package/cpp/platform/VaultDirectory.mm +58 -0
  30. package/cpp/platform/VaultDirectoryAndroid.cpp +35 -0
  31. package/cpp/runtime/KernelBackends.cpp +297 -0
  32. package/cpp/runtime/KernelBackends.hpp +20 -0
  33. package/cpp/runtime/KernelBuffer.cpp +12 -0
  34. package/cpp/runtime/KernelBuffer.hpp +96 -0
  35. package/cpp/runtime/KernelGlobals.cpp +535 -0
  36. package/cpp/runtime/KernelGlobals.hpp +107 -0
  37. package/cpp/runtime/KernelJs.cpp +155 -0
  38. package/cpp/runtime/KernelJs.hpp +81 -0
  39. package/cpp/runtime/KernelKeystore.cpp +224 -0
  40. package/cpp/runtime/KernelKeystore.hpp +28 -0
  41. package/cpp/runtime/KernelPasskey.cpp +137 -0
  42. package/cpp/runtime/KernelPasskey.hpp +23 -0
  43. package/cpp/runtime/KernelRuntime.cpp +496 -0
  44. package/cpp/runtime/KernelRuntime.hpp +94 -0
  45. package/cpp/runtime/platform/KernelPbkdf2.cpp +198 -0
  46. package/cpp/runtime/platform/KernelPbkdf2.hpp +38 -0
  47. package/dist/index.d.mts +44 -0
  48. package/dist/index.mjs +170 -0
  49. package/dist/index.mjs.map +1 -0
  50. package/kernel/generated/kernel-entry.js +42967 -0
  51. package/kernel/generated/kernel.manifest.json +80 -0
  52. package/nitrogen/generated/.gitattributes +1 -0
  53. package/nitrogen/generated/android/WalletNative+autolinking.cmake +81 -0
  54. package/nitrogen/generated/android/WalletNative+autolinking.gradle +27 -0
  55. package/nitrogen/generated/android/WalletNativeOnLoad.cpp +49 -0
  56. package/nitrogen/generated/android/WalletNativeOnLoad.hpp +34 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/qntxwallet/WalletNativeOnLoad.kt +35 -0
  58. package/nitrogen/generated/ios/WalletNative+autolinking.rb +62 -0
  59. package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.cpp +17 -0
  60. package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.hpp +27 -0
  61. package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Umbrella.hpp +38 -0
  62. package/nitrogen/generated/ios/WalletNativeAutolinking.mm +35 -0
  63. package/nitrogen/generated/ios/WalletNativeAutolinking.swift +16 -0
  64. package/nitrogen/generated/shared/c++/HybridKernelHostSpec.cpp +25 -0
  65. package/nitrogen/generated/shared/c++/HybridKernelHostSpec.hpp +69 -0
  66. package/nitrogen/generated/shared/c++/KernelExitReason.hpp +84 -0
  67. package/package.json +61 -4
  68. package/react-native.config.js +14 -0
  69. package/scripts/compile-kernel.mjs +118 -0
@@ -0,0 +1,198 @@
1
+ #include "KernelPbkdf2.hpp"
2
+
3
+ #include <stdexcept>
4
+ #include <string>
5
+
6
+ #if defined(__APPLE__)
7
+ #include <CommonCrypto/CommonCryptoError.h>
8
+ #include <CommonCrypto/CommonKeyDerivation.h>
9
+ #endif
10
+
11
+ #if defined(__APPLE__)
12
+
13
+ qntx::SecretBytes qntx::kernelPbkdf2(
14
+ KernelPbkdf2Hash hash,
15
+ const SecretBytes& password,
16
+ const SecretBytes& salt,
17
+ uint32_t iterations,
18
+ size_t length) {
19
+ const CCPseudoRandomAlgorithm algorithm =
20
+ hash == KernelPbkdf2Hash::sha256 ? kCCPRFHmacAlgSHA256 : kCCPRFHmacAlgSHA512;
21
+ SecretBytes out(length);
22
+ const int status = CCKeyDerivationPBKDF(
23
+ kCCPBKDF2,
24
+ reinterpret_cast<const char*>(password.data()),
25
+ password.size(),
26
+ salt.data(),
27
+ salt.size(),
28
+ algorithm,
29
+ iterations,
30
+ out.data(),
31
+ out.size());
32
+ if (status != kCCSuccess) {
33
+ throw std::runtime_error("CCKeyDerivationPBKDF failed: " + std::to_string(status));
34
+ }
35
+ return out;
36
+ }
37
+
38
+ #elif defined(__ANDROID__)
39
+
40
+ namespace {
41
+
42
+ JavaVM* g_vm = nullptr;
43
+ jclass g_class = nullptr;
44
+ jmethodID g_derive = nullptr;
45
+
46
+ // Attaches the current thread to the JVM for the scope's lifetime.
47
+ struct JniScope {
48
+ JNIEnv* env = nullptr;
49
+ bool mustDetach = false;
50
+
51
+ JniScope() {
52
+ if (g_vm == nullptr) {
53
+ throw std::runtime_error("kernelPbkdf2OnLoad was not called");
54
+ }
55
+ if (g_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) == JNI_EDETACHED) {
56
+ if (g_vm->AttachCurrentThread(&env, nullptr) != JNI_OK) {
57
+ throw std::runtime_error("AttachCurrentThread failed");
58
+ }
59
+ mustDetach = true;
60
+ }
61
+ }
62
+ ~JniScope() {
63
+ if (mustDetach) {
64
+ g_vm->DetachCurrentThread();
65
+ }
66
+ }
67
+ JniScope(const JniScope&) = delete;
68
+ JniScope& operator=(const JniScope&) = delete;
69
+ };
70
+
71
+ class JniByteArray {
72
+ public:
73
+ JniByteArray(JNIEnv* env, const qntx::SecretBytes& bytes) : env_(env) {
74
+ array_ = env->NewByteArray(static_cast<jsize>(bytes.size()));
75
+ if (array_ == nullptr) {
76
+ throw std::runtime_error("NewByteArray failed");
77
+ }
78
+ if (bytes.size() > 0) {
79
+ env->SetByteArrayRegion(
80
+ array_, 0, static_cast<jsize>(bytes.size()),
81
+ reinterpret_cast<const jbyte*>(bytes.data()));
82
+ }
83
+ }
84
+ ~JniByteArray() {
85
+ if (array_ != nullptr) {
86
+ // Wipe the heap copy before releasing the local ref.
87
+ std::vector<jbyte> zeros(env_->GetArrayLength(array_), 0);
88
+ env_->SetByteArrayRegion(array_, 0, static_cast<jsize>(zeros.size()), zeros.data());
89
+ env_->DeleteLocalRef(array_);
90
+ }
91
+ }
92
+ JniByteArray(const JniByteArray&) = delete;
93
+ JniByteArray& operator=(const JniByteArray&) = delete;
94
+ jbyteArray get() const {
95
+ return array_;
96
+ }
97
+
98
+ private:
99
+ JNIEnv* env_;
100
+ jbyteArray array_ = nullptr;
101
+ };
102
+
103
+ std::string describeJavaException(JNIEnv* env) {
104
+ if (!env->ExceptionCheck()) {
105
+ return {};
106
+ }
107
+ jthrowable throwable = env->ExceptionOccurred();
108
+ env->ExceptionClear();
109
+ auto objectClass = env->GetObjectClass(throwable);
110
+ auto toStringId = env->GetMethodID(objectClass, "toString", "()Ljava/lang/String;");
111
+ auto message = static_cast<jstring>(
112
+ env->CallObjectMethod(throwable, toStringId));
113
+ std::string text = "java exception";
114
+ if (message != nullptr) {
115
+ const char* utf = env->GetStringUTFChars(message, nullptr);
116
+ if (utf != nullptr) {
117
+ text = utf;
118
+ env->ReleaseStringUTFChars(message, utf);
119
+ }
120
+ env->DeleteLocalRef(message);
121
+ }
122
+ env->DeleteLocalRef(objectClass);
123
+ env->DeleteLocalRef(throwable);
124
+ return text;
125
+ }
126
+
127
+ } // namespace
128
+
129
+ JavaVM* qntx::kernelJavaVM() {
130
+ return g_vm;
131
+ }
132
+
133
+ void qntx::kernelPbkdf2OnLoad(JNIEnv* env, const char* className) {
134
+ if (env->GetJavaVM(&g_vm) != JNI_OK) {
135
+ throw std::runtime_error("GetJavaVM failed");
136
+ }
137
+ jclass local = env->FindClass(className);
138
+ if (local == nullptr) {
139
+ env->ExceptionClear();
140
+ throw std::runtime_error(std::string("pbkdf2 helper class not found: ") + className);
141
+ }
142
+ g_class = static_cast<jclass>(env->NewGlobalRef(local));
143
+ env->DeleteLocalRef(local);
144
+ g_derive = env->GetStaticMethodID(
145
+ g_class, "derive", "(Ljava/lang/String;[B[BII)[B");
146
+ if (g_derive == nullptr) {
147
+ env->ExceptionClear();
148
+ throw std::runtime_error("KernelPbkdf2.derive not found");
149
+ }
150
+ }
151
+
152
+ qntx::SecretBytes qntx::kernelPbkdf2(
153
+ KernelPbkdf2Hash hash,
154
+ const SecretBytes& password,
155
+ const SecretBytes& salt,
156
+ uint32_t iterations,
157
+ size_t length) {
158
+ JniScope scope;
159
+ JNIEnv* env = scope.env;
160
+
161
+ jstring algorithm = env->NewStringUTF(
162
+ hash == KernelPbkdf2Hash::sha256 ? "HmacSHA256" : "HmacSHA512");
163
+ JniByteArray passwordArray(env, password);
164
+ JniByteArray saltArray(env, salt);
165
+
166
+ auto result = static_cast<jbyteArray>(env->CallStaticObjectMethod(
167
+ g_class,
168
+ g_derive,
169
+ algorithm,
170
+ passwordArray.get(),
171
+ saltArray.get(),
172
+ static_cast<jint>(iterations),
173
+ static_cast<jint>(length)));
174
+ env->DeleteLocalRef(algorithm);
175
+ if (result == nullptr) {
176
+ throw std::runtime_error("pbkdf2 failed: " + describeJavaException(env));
177
+ }
178
+ const jsize size = env->GetArrayLength(result);
179
+ SecretBytes out(static_cast<size_t>(size));
180
+ env->GetByteArrayRegion(result, 0, size, reinterpret_cast<jbyte*>(out.data()));
181
+ std::vector<jbyte> zeros(size, 0);
182
+ env->SetByteArrayRegion(result, 0, size, zeros.data());
183
+ env->DeleteLocalRef(result);
184
+ return out;
185
+ }
186
+
187
+ #else
188
+
189
+ qntx::SecretBytes qntx::kernelPbkdf2(
190
+ KernelPbkdf2Hash,
191
+ const SecretBytes&,
192
+ const SecretBytes&,
193
+ uint32_t,
194
+ size_t) {
195
+ throw std::runtime_error("kernelPbkdf2: unsupported platform");
196
+ }
197
+
198
+ #endif
@@ -0,0 +1,38 @@
1
+ #pragma once
2
+
3
+ #include "../KernelBuffer.hpp"
4
+
5
+ #include <cstdint>
6
+ #include <vector>
7
+
8
+ #ifdef __ANDROID__
9
+ #include <jni.h>
10
+ #endif
11
+
12
+ namespace qntx {
13
+
14
+ enum class KernelPbkdf2Hash { sha256, sha512 };
15
+
16
+ // Synchronous PBKDF2: iOS/macOS via CommonCrypto, Android via JNI into
17
+ // javax.crypto.Mac. Callers run it on the KDF worker thread — never on the
18
+ // kernel thread. Throws std::runtime_error on failure. Inputs and the result
19
+ // are SecretBytes, so any path that drops them still wipes them.
20
+ SecretBytes kernelPbkdf2(
21
+ KernelPbkdf2Hash hash,
22
+ const SecretBytes& password,
23
+ const SecretBytes& salt,
24
+ uint32_t iterations,
25
+ size_t length);
26
+
27
+ #ifdef __ANDROID__
28
+ // Called from the module's JNI_OnLoad while the caller's classloader is
29
+ // available; caches the JavaVM and the Java helper's class/method.
30
+ void kernelPbkdf2OnLoad(JNIEnv* env, const char* className);
31
+
32
+ // The cached JavaVM, or nullptr before kernelPbkdf2OnLoad runs. The kernel
33
+ // thread attaches through this when a probe installs JNI-backed natives
34
+ // (e.g. Nitro's Java-bridged HybridObjectRegistry on Android).
35
+ JavaVM* kernelJavaVM();
36
+ #endif
37
+
38
+ } // namespace qntx
@@ -0,0 +1,44 @@
1
+ import { AutoLockPolicy, PasskeyConfig, Wallet } from "@qntx/wallet";
2
+ import { MainPort } from "@qntx/wallet/host";
3
+ import { ProtocolValue } from "@qntx/wallet/protocol";
4
+ //#region src/host.d.ts
5
+ type KernelExitReason = "locked" | "crashed" | "integrity" | "background";
6
+ /**
7
+ * Structural mirror of the Nitro `KernelHost` spec (W2b registers the real hybrid object; tests
8
+ * drive the same shape over an in-memory frame pipe).
9
+ */
10
+ type KernelHostLike = {
11
+ readonly start: (backgroundLockSeconds: number) => void;
12
+ readonly post: (frame: ArrayBuffer) => void;
13
+ readonly onMessage: (cb: (frame: ArrayBuffer) => void) => void;
14
+ readonly onExit: (cb: (reason: KernelExitReason) => void) => void;
15
+ readonly lock: () => void;
16
+ };
17
+ export declare function spawnFromHost(host: KernelHostLike, backgroundLockSeconds: number): MainPort;
18
+ //#endregion
19
+ //#region src/cbor.d.ts
20
+ export declare const FRAME_MAX_BYTES: number;
21
+ type CborErrorCode = "oversize" | "truncated" | "malformed" | "float" | "unsafe-int" | "bigint" | "undefined" | "tag" | "non-string-key" | "duplicate-key" | "indefinite" | "bad-type";
22
+ export declare class CborError extends Error {
23
+ readonly code: CborErrorCode;
24
+ constructor(code: CborErrorCode, message: string);
25
+ }
26
+ /** Encode a `ProtocolValue` to a CBOR frame, enforcing the 1 MiB cap. */
27
+ export declare function encodeFrame(value: ProtocolValue): ArrayBuffer;
28
+ /** Decode a CBOR frame back to a `ProtocolValue`, enforcing the 1 MiB cap and strictness. */
29
+ export declare function decodeFrame(frame: ArrayBuffer | Uint8Array): ProtocolValue;
30
+ //#endregion
31
+ //#region src/index.d.ts
32
+ export type NativeWalletOptions = {
33
+ readonly passkey: PasskeyConfig;
34
+ readonly autoLock: AutoLockPolicy;
35
+ /**
36
+ * The Nitro `KernelHost` hybrid object (or a like-shaped fake for tests). Defaults to
37
+ * `NitroModules.createHybridObject("KernelHost")` — the only Nitro-registered object.
38
+ */
39
+ readonly host?: KernelHostLike;
40
+ };
41
+ export declare function createNativeWallet(options: NativeWalletOptions): Promise<Wallet>;
42
+ //#endregion
43
+ export type { CborErrorCode, KernelExitReason, KernelHostLike };
44
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,170 @@
1
+ import { WalletError } from "@qntx/wallet";
2
+ import { connectKernel } from "@qntx/wallet/host";
3
+ import { decode, encode } from "cborg";
4
+ //#region src/cbor.ts
5
+ /**
6
+ * CBOR framing for the kernel channel. `ProtocolValue` travels main-side ↔ kernel-side as CBOR; the
7
+ * C++ layer moves bytes only. Decode is deliberately strict: floats, unsafe integers, undefined,
8
+ * tags, non-string map keys, duplicate keys and indefinite-length items are all rejected — the wire
9
+ * format carries exactly one value domain.
10
+ */
11
+ const FRAME_MAX_BYTES = 1048576;
12
+ var CborError = class extends Error {
13
+ code;
14
+ constructor(code, message) {
15
+ super(message);
16
+ this.name = "CborError";
17
+ this.code = code;
18
+ }
19
+ };
20
+ const DECODE_OPTIONS = {
21
+ strict: true,
22
+ useMaps: false,
23
+ rejectDuplicateMapKeys: true,
24
+ allowUndefined: false,
25
+ allowIndefinite: false,
26
+ allowNaN: false,
27
+ allowInfinity: false,
28
+ allowBigInt: false
29
+ };
30
+ /** Reject non-safe-integer numbers and non-string map keys; cborg flags cover the rest. */
31
+ function validate(value) {
32
+ if (value === null || typeof value === "boolean" || typeof value === "string") return;
33
+ if (typeof value === "number") {
34
+ if (!Number.isInteger(value)) throw new CborError("float", "cbor frame: non-integer number");
35
+ if (!Number.isSafeInteger(value)) throw new CborError("unsafe-int", "cbor frame: integer outside the safe range");
36
+ return;
37
+ }
38
+ if (value instanceof Uint8Array) return;
39
+ if (Array.isArray(value)) {
40
+ for (const item of value) validate(item);
41
+ return;
42
+ }
43
+ if (typeof value === "object") {
44
+ for (const [key, item] of Object.entries(value)) {
45
+ if (typeof key !== "string") throw new CborError("non-string-key", "cbor frame: non-string map key");
46
+ validate(item);
47
+ }
48
+ return;
49
+ }
50
+ throw new CborError("bad-type", `cbor frame: unsupported value type ${typeof value}`);
51
+ }
52
+ /** Classify a cborg rejection into the frame error taxonomy. */
53
+ function classify(error) {
54
+ if (error instanceof CborError) return error;
55
+ const text = error instanceof Error ? error.message : String(error);
56
+ for (const [pattern, code] of [
57
+ [/duplicate key|repeat map key/i, "duplicate-key"],
58
+ [/indefinite/i, "indefinite"],
59
+ [/undefined/i, "undefined"],
60
+ [/bigint|safe integer range/i, "unsafe-int"],
61
+ [/tag/i, "tag"],
62
+ [/string/i, "non-string-key"]
63
+ ]) if (pattern.test(text)) return new CborError(code, `cbor frame: ${text}`);
64
+ return new CborError("malformed", `cbor frame: ${text}`);
65
+ }
66
+ /** Encode a `ProtocolValue` to a CBOR frame, enforcing the 1 MiB cap. */
67
+ function encodeFrame(value) {
68
+ validate(value);
69
+ const bytes = encode(value);
70
+ if (bytes.byteLength > 1048576) throw new CborError("oversize", `cbor frame: ${bytes.byteLength} bytes exceeds the 1 MiB cap`);
71
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
72
+ }
73
+ /** Decode a CBOR frame back to a `ProtocolValue`, enforcing the 1 MiB cap and strictness. */
74
+ function decodeFrame(frame) {
75
+ const bytes = frame instanceof Uint8Array ? frame : new Uint8Array(frame);
76
+ if (bytes.byteLength > 1048576) throw new CborError("oversize", `cbor frame: ${bytes.byteLength} bytes exceeds the 1 MiB cap`);
77
+ if (bytes.byteLength === 0) throw new CborError("truncated", "cbor frame: empty frame");
78
+ try {
79
+ const value = decode(bytes, DECODE_OPTIONS);
80
+ validate(value);
81
+ return value;
82
+ } catch (error) {
83
+ throw classify(error);
84
+ }
85
+ }
86
+ //#endregion
87
+ //#region src/host.ts
88
+ function spawnFromHost(host, backgroundLockSeconds) {
89
+ const exitHandlers = /* @__PURE__ */ new Set();
90
+ let exited = false;
91
+ const exit = (reason) => {
92
+ if (exited) return;
93
+ exited = true;
94
+ for (const handler of exitHandlers) handler(reason);
95
+ host.lock();
96
+ };
97
+ host.onMessage((frame) => {
98
+ let message;
99
+ try {
100
+ message = decodeFrame(frame);
101
+ } catch {
102
+ exit("crashed");
103
+ return;
104
+ }
105
+ dispatch(message);
106
+ });
107
+ host.onExit((reason) => {
108
+ exit(reason === "background" ? "background" : "crashed");
109
+ });
110
+ const messageHandlers = /* @__PURE__ */ new Set();
111
+ const dispatch = (message) => {
112
+ for (const handler of messageHandlers) handler(message);
113
+ };
114
+ host.start(backgroundLockSeconds);
115
+ return {
116
+ post: (message, transfer) => {
117
+ const frame = encodeFrame(message);
118
+ host.post(frame);
119
+ for (const buffer of transfer) new Uint8Array(buffer).fill(0);
120
+ },
121
+ listen: (handler) => {
122
+ messageHandlers.add(handler);
123
+ return () => {
124
+ messageHandlers.delete(handler);
125
+ };
126
+ },
127
+ onExit: (handler) => {
128
+ exitHandlers.add(handler);
129
+ return () => {
130
+ exitHandlers.delete(handler);
131
+ };
132
+ },
133
+ terminate: () => {
134
+ exit("crashed");
135
+ }
136
+ };
137
+ }
138
+ //#endregion
139
+ //#region src/index.ts
140
+ /**
141
+ * `@qntx/wallet-native`: the RN host entry. `createNativeWallet` adapts a `KernelHost` (Nitro
142
+ * hybrid from W2b, or any object with the same shape) to the wallet `connectKernel` entry — the
143
+ * returned `Wallet` is the same type as the web/node host.
144
+ */
145
+ const setTimer = (delayMs, fn) => {
146
+ const id = setTimeout(fn, delayMs);
147
+ return () => {
148
+ clearTimeout(id);
149
+ };
150
+ };
151
+ async function defaultHost() {
152
+ const { NitroModules } = await import("react-native-nitro-modules");
153
+ return NitroModules.createHybridObject("KernelHost");
154
+ }
155
+ async function createNativeWallet(options) {
156
+ const { autoLock } = options;
157
+ const host = options.host ?? await defaultHost();
158
+ if (autoLock.platform !== "native") throw new WalletError("invalid-input", "createNativeWallet accepts only the native autoLock policy");
159
+ return connectKernel({
160
+ spawn: () => spawnFromHost(host, autoLock.backgroundSeconds),
161
+ factors: "hardware",
162
+ passkey: options.passkey,
163
+ ceremony: void 0,
164
+ setTimer
165
+ });
166
+ }
167
+ //#endregion
168
+ export { CborError, FRAME_MAX_BYTES, createNativeWallet, decodeFrame, encodeFrame, spawnFromHost };
169
+
170
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/cbor.ts","../src/host.ts","../src/index.ts"],"sourcesContent":["/**\n * CBOR framing for the kernel channel. `ProtocolValue` travels main-side ↔ kernel-side as CBOR; the\n * C++ layer moves bytes only. Decode is deliberately strict: floats, unsafe integers, undefined,\n * tags, non-string map keys, duplicate keys and indefinite-length items are all rejected — the wire\n * format carries exactly one value domain.\n */\nimport { decode, encode } from \"cborg\";\nimport type { DecodeOptions } from \"cborg\";\n\nimport type { ProtocolValue } from \"@qntx/wallet/protocol\";\n\nexport const FRAME_MAX_BYTES: number = 1024 * 1024;\n\nexport type CborErrorCode =\n | \"oversize\"\n | \"truncated\"\n | \"malformed\"\n | \"float\"\n | \"unsafe-int\"\n | \"bigint\"\n | \"undefined\"\n | \"tag\"\n | \"non-string-key\"\n | \"duplicate-key\"\n | \"indefinite\"\n | \"bad-type\";\n\nexport class CborError extends Error {\n readonly code: CborErrorCode;\n constructor(code: CborErrorCode, message: string) {\n super(message);\n this.name = \"CborError\";\n this.code = code;\n }\n}\n\nconst DECODE_OPTIONS: DecodeOptions = {\n strict: true,\n useMaps: false,\n rejectDuplicateMapKeys: true,\n allowUndefined: false,\n allowIndefinite: false,\n allowNaN: false,\n allowInfinity: false,\n allowBigInt: false,\n};\n\n/** Reject non-safe-integer numbers and non-string map keys; cborg flags cover the rest. */\nfunction validate(value: unknown): asserts value is ProtocolValue {\n if (value === null || typeof value === \"boolean\" || typeof value === \"string\") {\n return;\n }\n if (typeof value === \"number\") {\n if (!Number.isInteger(value)) {\n throw new CborError(\"float\", \"cbor frame: non-integer number\");\n }\n if (!Number.isSafeInteger(value)) {\n throw new CborError(\"unsafe-int\", \"cbor frame: integer outside the safe range\");\n }\n return;\n }\n if (value instanceof Uint8Array) {\n return;\n }\n if (Array.isArray(value)) {\n for (const item of value) {\n validate(item);\n }\n return;\n }\n if (typeof value === \"object\") {\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the object came from a CBOR map\n for (const [key, item] of Object.entries(value as Record<string, unknown>)) {\n if (typeof key !== \"string\") {\n throw new CborError(\"non-string-key\", \"cbor frame: non-string map key\");\n }\n validate(item);\n }\n return;\n }\n throw new CborError(\"bad-type\", `cbor frame: unsupported value type ${typeof value}`);\n}\n\n/** Classify a cborg rejection into the frame error taxonomy. */\nfunction classify(error: unknown): CborError {\n if (error instanceof CborError) {\n return error;\n }\n const text = error instanceof Error ? error.message : String(error);\n const table: ReadonlyArray<[RegExp, CborErrorCode]> = [\n [/duplicate key|repeat map key/i, \"duplicate-key\"],\n [/indefinite/i, \"indefinite\"],\n [/undefined/i, \"undefined\"],\n [/bigint|safe integer range/i, \"unsafe-int\"],\n [/tag/i, \"tag\"],\n [/string/i, \"non-string-key\"],\n ];\n for (const [pattern, code] of table) {\n if (pattern.test(text)) {\n return new CborError(code, `cbor frame: ${text}`);\n }\n }\n return new CborError(\"malformed\", `cbor frame: ${text}`);\n}\n\n/** Encode a `ProtocolValue` to a CBOR frame, enforcing the 1 MiB cap. */\nexport function encodeFrame(value: ProtocolValue): ArrayBuffer {\n validate(value);\n const bytes = encode(value);\n if (bytes.byteLength > FRAME_MAX_BYTES) {\n throw new CborError(\"oversize\", `cbor frame: ${bytes.byteLength} bytes exceeds the 1 MiB cap`);\n }\n return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);\n}\n\n/** Decode a CBOR frame back to a `ProtocolValue`, enforcing the 1 MiB cap and strictness. */\nexport function decodeFrame(frame: ArrayBuffer | Uint8Array): ProtocolValue {\n const bytes = frame instanceof Uint8Array ? frame : new Uint8Array(frame);\n if (bytes.byteLength > FRAME_MAX_BYTES) {\n throw new CborError(\"oversize\", `cbor frame: ${bytes.byteLength} bytes exceeds the 1 MiB cap`);\n }\n if (bytes.byteLength === 0) {\n throw new CborError(\"truncated\", \"cbor frame: empty frame\");\n }\n try {\n const value: unknown = decode(bytes, DECODE_OPTIONS);\n validate(value);\n return value;\n } catch (error) {\n throw classify(error);\n }\n}\n","/**\n * `KernelHost` JS wrapper: CBOR frames over a Nitro-host-shaped channel. The host moves bytes only\n * — encode/decode happens here and in the kernel entry — so a malformed inbound frame can never\n * reach the message domain: the port exits `integrity` and the host runtime is destroyed.\n */\nimport type { MainPort, PortExit } from \"@qntx/wallet/host\";\nimport type { ProtocolValue } from \"@qntx/wallet/protocol\";\n\nimport { decodeFrame, encodeFrame } from \"./cbor.ts\";\n\nexport type KernelExitReason = \"locked\" | \"crashed\" | \"integrity\" | \"background\";\n\n/**\n * Structural mirror of the Nitro `KernelHost` spec (W2b registers the real hybrid object; tests\n * drive the same shape over an in-memory frame pipe).\n */\nexport type KernelHostLike = {\n readonly start: (backgroundLockSeconds: number) => void;\n readonly post: (frame: ArrayBuffer) => void;\n readonly onMessage: (cb: (frame: ArrayBuffer) => void) => void;\n readonly onExit: (cb: (reason: KernelExitReason) => void) => void;\n readonly lock: () => void;\n};\n\nexport function spawnFromHost(host: KernelHostLike, backgroundLockSeconds: number): MainPort {\n const exitHandlers = new Set<(reason: PortExit) => void>();\n let exited = false;\n\n const exit = (reason: PortExit): void => {\n if (exited) {\n return;\n }\n exited = true;\n for (const handler of exitHandlers) {\n handler(reason);\n }\n host.lock();\n };\n\n host.onMessage((frame) => {\n let message: ProtocolValue;\n try {\n message = decodeFrame(frame);\n } catch {\n exit(\"crashed\");\n return;\n }\n dispatch(message);\n });\n\n // The platform locked the enclave in the background — report it honestly so\n // the channel takes the lock path, not the crash path.\n host.onExit((reason) => {\n exit(reason === \"background\" ? \"background\" : \"crashed\");\n });\n\n const messageHandlers = new Set<(message: unknown) => void>();\n const dispatch = (message: unknown): void => {\n for (const handler of messageHandlers) {\n handler(message);\n }\n };\n\n host.start(backgroundLockSeconds);\n\n return {\n post: (message, transfer) => {\n const frame = encodeFrame(message);\n host.post(frame);\n // Structured clone would neuter transferred buffers on the web; the main↔kernel\n // contract is the same here — the caller's buffers hold secrets (passwords, PRF\n // output) and are zeroed once their bytes are inside the frame.\n for (const buffer of transfer) {\n new Uint8Array(buffer).fill(0);\n }\n },\n listen: (handler) => {\n messageHandlers.add(handler);\n return () => {\n messageHandlers.delete(handler);\n };\n },\n onExit: (handler) => {\n exitHandlers.add(handler);\n return () => {\n exitHandlers.delete(handler);\n };\n },\n terminate: () => {\n exit(\"crashed\");\n },\n };\n}\n","import { WalletError } from \"@qntx/wallet\";\nimport type { AutoLockPolicy, PasskeyConfig, Wallet } from \"@qntx/wallet\";\nimport type { SetTimer } from \"@qntx/wallet/host\";\n/**\n * `@qntx/wallet-native`: the RN host entry. `createNativeWallet` adapts a `KernelHost` (Nitro\n * hybrid from W2b, or any object with the same shape) to the wallet `connectKernel` entry — the\n * returned `Wallet` is the same type as the web/node host.\n */\nimport { connectKernel } from \"@qntx/wallet/host\";\n\nimport { spawnFromHost } from \"./host.ts\";\nimport type { KernelHostLike } from \"./host.ts\";\nimport type { KernelHost } from \"./specs/kernel-host.nitro.ts\";\n\nexport type NativeWalletOptions = {\n readonly passkey: PasskeyConfig;\n readonly autoLock: AutoLockPolicy;\n /**\n * The Nitro `KernelHost` hybrid object (or a like-shaped fake for tests). Defaults to\n * `NitroModules.createHybridObject(\"KernelHost\")` — the only Nitro-registered object.\n */\n readonly host?: KernelHostLike;\n};\n\nconst setTimer: SetTimer = (delayMs, fn) => {\n const id = setTimeout(fn, delayMs);\n return () => {\n clearTimeout(id);\n };\n};\n\nasync function defaultHost(): Promise<KernelHostLike> {\n const { NitroModules } = await import(\"react-native-nitro-modules\");\n return NitroModules.createHybridObject<KernelHost>(\"KernelHost\");\n}\n\nexport async function createNativeWallet(options: NativeWalletOptions): Promise<Wallet> {\n const { autoLock } = options;\n const host = options.host ?? (await defaultHost());\n if (autoLock.platform !== \"native\") {\n throw new WalletError(\n \"invalid-input\",\n \"createNativeWallet accepts only the native autoLock policy\",\n );\n }\n // The native host owns the auto-lock signal: the platform layer calls host.lock() on\n // background expiry — the kernel never sees a presence source here.\n return connectKernel({\n spawn: () => spawnFromHost(host, autoLock.backgroundSeconds),\n factors: \"hardware\",\n passkey: options.passkey,\n ceremony: undefined,\n setTimer,\n });\n}\n\nexport { spawnFromHost, type KernelHostLike, type KernelExitReason } from \"./host.ts\";\nexport {\n encodeFrame,\n decodeFrame,\n CborError,\n FRAME_MAX_BYTES,\n type CborErrorCode,\n} from \"./cbor.ts\";\n"],"mappings":"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"}