@qntx/wallet-native 0.0.0-stage → 0.5.2

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 (71) hide show
  1. package/LICENSE-APACHE +201 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +123 -2
  4. package/WalletNative.podspec +74 -0
  5. package/android/CMakeLists.txt +99 -0
  6. package/android/build.gradle +177 -0
  7. package/android/consumer-rules.pro +4 -0
  8. package/android/src/main/AndroidManifest.xml +2 -0
  9. package/android/src/main/cpp/cpp-adapter.cpp +29 -0
  10. package/android/src/main/java/com/margelo/nitro/qntxwallet/AppLifecycle.kt +35 -0
  11. package/android/src/main/java/com/margelo/nitro/qntxwallet/KernelPbkdf2.java +59 -0
  12. package/android/src/main/java/com/margelo/nitro/qntxwallet/Keystore.kt +280 -0
  13. package/android/src/main/java/com/margelo/nitro/qntxwallet/Passkey.kt +147 -0
  14. package/android/src/main/java/com/margelo/nitro/qntxwallet/VaultDirectory.kt +28 -0
  15. package/android/src/main/java/com/margelo/nitro/qntxwallet/WalletNativePackage.java +30 -0
  16. package/cpp/host/HybridKernelHost.cpp +162 -0
  17. package/cpp/host/HybridKernelHost.hpp +49 -0
  18. package/cpp/host/KernelFile.cpp +120 -0
  19. package/cpp/host/KernelFile.hpp +24 -0
  20. package/cpp/host/Keystore.hpp +70 -0
  21. package/cpp/host/Lifecycle.cpp +77 -0
  22. package/cpp/host/Lifecycle.hpp +71 -0
  23. package/cpp/host/Passkey.hpp +39 -0
  24. package/cpp/host/VaultDirectory.hpp +16 -0
  25. package/cpp/platform/KeystoreAndroid.cpp +175 -0
  26. package/cpp/platform/KeystoreIos.mm +300 -0
  27. package/cpp/platform/LifecycleAndroid.cpp +55 -0
  28. package/cpp/platform/LifecycleIos.mm +36 -0
  29. package/cpp/platform/PasskeyAndroid.cpp +118 -0
  30. package/cpp/platform/PasskeyIos.mm +20 -0
  31. package/cpp/platform/VaultDirectory.mm +58 -0
  32. package/cpp/platform/VaultDirectoryAndroid.cpp +35 -0
  33. package/cpp/runtime/KernelBackends.cpp +297 -0
  34. package/cpp/runtime/KernelBackends.hpp +20 -0
  35. package/cpp/runtime/KernelBuffer.cpp +12 -0
  36. package/cpp/runtime/KernelBuffer.hpp +96 -0
  37. package/cpp/runtime/KernelGlobals.cpp +535 -0
  38. package/cpp/runtime/KernelGlobals.hpp +107 -0
  39. package/cpp/runtime/KernelJs.cpp +155 -0
  40. package/cpp/runtime/KernelJs.hpp +81 -0
  41. package/cpp/runtime/KernelKeystore.cpp +224 -0
  42. package/cpp/runtime/KernelKeystore.hpp +28 -0
  43. package/cpp/runtime/KernelPasskey.cpp +137 -0
  44. package/cpp/runtime/KernelPasskey.hpp +23 -0
  45. package/cpp/runtime/KernelRuntime.cpp +496 -0
  46. package/cpp/runtime/KernelRuntime.hpp +94 -0
  47. package/cpp/runtime/platform/KernelPbkdf2.cpp +198 -0
  48. package/cpp/runtime/platform/KernelPbkdf2.hpp +38 -0
  49. package/dist/index.d.mts +44 -0
  50. package/dist/index.mjs +170 -0
  51. package/dist/index.mjs.map +1 -0
  52. package/kernel/generated/kernel-entry.js +42967 -0
  53. package/kernel/generated/kernel.manifest.json +80 -0
  54. package/nitrogen/generated/.gitattributes +1 -0
  55. package/nitrogen/generated/android/WalletNative+autolinking.cmake +81 -0
  56. package/nitrogen/generated/android/WalletNative+autolinking.gradle +27 -0
  57. package/nitrogen/generated/android/WalletNativeOnLoad.cpp +49 -0
  58. package/nitrogen/generated/android/WalletNativeOnLoad.hpp +34 -0
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/qntxwallet/WalletNativeOnLoad.kt +35 -0
  60. package/nitrogen/generated/ios/WalletNative+autolinking.rb +62 -0
  61. package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.cpp +17 -0
  62. package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.hpp +27 -0
  63. package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Umbrella.hpp +38 -0
  64. package/nitrogen/generated/ios/WalletNativeAutolinking.mm +35 -0
  65. package/nitrogen/generated/ios/WalletNativeAutolinking.swift +16 -0
  66. package/nitrogen/generated/shared/c++/HybridKernelHostSpec.cpp +25 -0
  67. package/nitrogen/generated/shared/c++/HybridKernelHostSpec.hpp +69 -0
  68. package/nitrogen/generated/shared/c++/KernelExitReason.hpp +84 -0
  69. package/package.json +65 -4
  70. package/react-native.config.js +14 -0
  71. package/scripts/compile-kernel.mjs +118 -0
@@ -0,0 +1,96 @@
1
+ #pragma once
2
+
3
+ #include <jsi/jsi.h>
4
+
5
+ #include <cstdint>
6
+ #include <cstring>
7
+ #include <memory>
8
+ #include <vector>
9
+
10
+ namespace qntx {
11
+
12
+ namespace jsi = facebook::jsi;
13
+
14
+ // Zeroes secret bytes without letting the optimizer elide the store.
15
+ void kernelWipe(uint8_t* data, size_t size);
16
+ void kernelWipe(std::vector<uint8_t>& bytes);
17
+
18
+ // Read-only buffer over script bytes retained by shared_ptr; used for embedded
19
+ // bytecode, which Hermes may read lazily for the runtime's lifetime.
20
+ class VectorBuffer final : public jsi::Buffer {
21
+ public:
22
+ explicit VectorBuffer(std::shared_ptr<const std::vector<uint8_t>> bytes)
23
+ : bytes_(std::move(bytes)) {}
24
+ size_t size() const override {
25
+ return bytes_->size();
26
+ }
27
+ const uint8_t* data() const override {
28
+ return bytes_->data();
29
+ }
30
+
31
+ private:
32
+ std::shared_ptr<const std::vector<uint8_t>> bytes_;
33
+ };
34
+
35
+ // Byte storage that always wipes itself on destruction. Move-only: a moved-from
36
+ // instance carries no bytes, so each live copy has exactly one wiper. Used for
37
+ // KDF inputs and outputs, where a dropped result must not linger in memory.
38
+ class SecretBytes {
39
+ public:
40
+ SecretBytes() = default;
41
+ explicit SecretBytes(std::vector<uint8_t> bytes) : bytes_(std::move(bytes)) {}
42
+ explicit SecretBytes(size_t size) : bytes_(size) {}
43
+ SecretBytes(const uint8_t* data, size_t size) : bytes_(data, data + size) {}
44
+
45
+ SecretBytes(const SecretBytes&) = delete;
46
+ SecretBytes& operator=(const SecretBytes&) = delete;
47
+
48
+ SecretBytes(SecretBytes&& other) noexcept = default;
49
+ SecretBytes& operator=(SecretBytes&& other) noexcept {
50
+ if (this != &other) {
51
+ kernelWipe(bytes_);
52
+ bytes_ = std::move(other.bytes_);
53
+ }
54
+ return *this;
55
+ }
56
+
57
+ ~SecretBytes() {
58
+ kernelWipe(bytes_);
59
+ }
60
+
61
+ const uint8_t* data() const {
62
+ return bytes_.data();
63
+ }
64
+ uint8_t* data() {
65
+ return bytes_.data();
66
+ }
67
+ size_t size() const {
68
+ return bytes_.size();
69
+ }
70
+
71
+ private:
72
+ std::vector<uint8_t> bytes_;
73
+ };
74
+
75
+ // Owning MutableBuffer; the runtime shares ownership of the byte storage and
76
+ // the bytes are wiped when the last reference goes away, because these buffers
77
+ // back ArrayBuffers that can hold secrets.
78
+ class OwningBuffer final : public jsi::MutableBuffer {
79
+ public:
80
+ explicit OwningBuffer(std::vector<uint8_t> storage) : storage_(std::move(storage)) {}
81
+ explicit OwningBuffer(size_t size) : storage_(size) {}
82
+ ~OwningBuffer() override {
83
+ kernelWipe(storage_);
84
+ }
85
+ size_t size() const override {
86
+ return storage_.size();
87
+ }
88
+ uint8_t* data() override {
89
+ return storage_.data();
90
+ }
91
+
92
+ private:
93
+ std::vector<uint8_t> storage_;
94
+ };
95
+
96
+ } // namespace qntx
@@ -0,0 +1,535 @@
1
+ #include "KernelGlobals.hpp"
2
+
3
+ #include "../host/KernelFile.hpp"
4
+
5
+ #include "KernelBackends.hpp"
6
+ #include "KernelKeystore.hpp"
7
+ #include "KernelPasskey.hpp"
8
+ #include "KernelBuffer.hpp"
9
+ #include "KernelJs.hpp"
10
+ #include "platform/KernelPbkdf2.hpp"
11
+
12
+ #include <atomic>
13
+ #include <chrono>
14
+ #include <cmath>
15
+ #include <condition_variable>
16
+ #include <cstdlib>
17
+ #include <cstring>
18
+ #include <deque>
19
+ #include <mutex>
20
+ #include <stdexcept>
21
+ #include <string>
22
+ #include <thread>
23
+
24
+ #if defined(__APPLE__) || defined(__ANDROID__)
25
+ #include <pthread.h>
26
+ #include <unistd.h>
27
+ #elif defined(__linux__)
28
+ #include <cerrno>
29
+ #include <sys/random.h>
30
+ #include <unistd.h>
31
+ #endif
32
+
33
+ #if defined(__ANDROID__)
34
+ #include <fbjni/fbjni.h>
35
+ #endif
36
+
37
+ namespace qntx {
38
+
39
+ namespace {
40
+
41
+ constexpr size_t kRandomLimit = 65536;
42
+ constexpr double kTimerDelayMax = 2147483647.0;
43
+ constexpr double kPbkdf2IterationsMax = 10000000.0;
44
+ constexpr size_t kPbkdf2LengthMax = 1024;
45
+ constexpr size_t kKdfQueueLimit = 8;
46
+
47
+ void fillRandom(uint8_t* out, size_t size) {
48
+ #if defined(__APPLE__) || defined(__ANDROID__)
49
+ arc4random_buf(out, size);
50
+ #elif defined(__linux__)
51
+ size_t filled = 0;
52
+ while (filled < size) {
53
+ const ssize_t got = getrandom(out + filled, size - filled, 0);
54
+ if (got < 0) {
55
+ if (errno == EINTR) {
56
+ continue;
57
+ }
58
+ std::abort();
59
+ }
60
+ filled += static_cast<size_t>(got);
61
+ }
62
+ #else
63
+ #error "kernel runtime: no platform random source"
64
+ #endif
65
+ }
66
+
67
+ // One process-wide worker runs every PBKDF2 job, so concurrent kernels cannot
68
+ // spawn unbounded derivation threads. The queue is bounded; the worker thread
69
+ // is intentionally leaked rather than joined (it parks on the condition
70
+ // variable and holds no runtime state).
71
+ struct KdfJob {
72
+ KernelPbkdf2Hash hash;
73
+ SecretBytes password;
74
+ SecretBytes salt;
75
+ uint32_t iterations;
76
+ size_t length;
77
+ std::shared_ptr<KernelPendingCall> call;
78
+ std::shared_ptr<KernelGlobalsState> state;
79
+ };
80
+
81
+ struct KdfQueue {
82
+ std::mutex mutex;
83
+ std::condition_variable cv;
84
+ std::deque<KdfJob> jobs;
85
+ bool workerStarted = false;
86
+ };
87
+
88
+ KdfQueue& kdfQueue() {
89
+ static auto* queue = new KdfQueue();
90
+ return *queue;
91
+ }
92
+
93
+ void kdfWorkerRun() {
94
+ auto& queue = kdfQueue();
95
+ for (;;) {
96
+ KdfJob job;
97
+ {
98
+ std::unique_lock<std::mutex> lock(queue.mutex);
99
+ queue.cv.wait(lock, [&] { return !queue.jobs.empty(); });
100
+ job = std::move(queue.jobs.front());
101
+ queue.jobs.pop_front();
102
+ }
103
+ if (!job.state->active->load(std::memory_order_acquire)) {
104
+ continue;
105
+ }
106
+ SecretBytes derived;
107
+ std::string error;
108
+ try {
109
+ derived = kernelPbkdf2(
110
+ job.hash, job.password, job.salt, job.iterations, job.length);
111
+ } catch (const std::exception& e) {
112
+ error = e.what();
113
+ } catch (...) {
114
+ error = "unknown pbkdf2 error";
115
+ }
116
+ if (!job.state->active->load(std::memory_order_acquire)) {
117
+ // Runtime torn down mid-derivation: drop the job; the parked call's
118
+ // `done` was already cleared by teardown, and SecretBytes wipe here.
119
+ continue;
120
+ }
121
+ auto state = job.state;
122
+ auto call = job.call;
123
+ // std::function needs a copyable closure: share the result; whichever
124
+ // side drops it last wipes it.
125
+ auto result = std::make_shared<SecretBytes>(std::move(derived));
126
+ state->enqueueTask(
127
+ [state, call, result, error = std::move(error)](
128
+ jsi::Runtime& rt) mutable {
129
+ auto done = std::move(call->done);
130
+ state->pending.erase(call);
131
+ if (!done.has_value()) {
132
+ return;
133
+ }
134
+ if (!error.empty()) {
135
+ done->call(
136
+ rt,
137
+ jsi::Value::undefined(),
138
+ jsi::Value(rt, jsi::String::createFromUtf8(rt, error)));
139
+ return;
140
+ }
141
+ auto buffer =
142
+ arrayBufferFromBytes(rt, result->data(), result->size());
143
+ auto array = makeUint8Array(rt, *state->intrinsics, std::move(buffer));
144
+ done->call(rt, jsi::Value(std::move(array)), jsi::Value::undefined());
145
+ });
146
+ }
147
+ }
148
+
149
+ void kdfWorkerMain() {
150
+ #if defined(__APPLE__)
151
+ pthread_setname_np("qntx-kdf");
152
+ #elif defined(__ANDROID__)
153
+ pthread_setname_np(pthread_self(), "qntx-kdf");
154
+ #endif
155
+ auto body = [] { kdfWorkerRun(); };
156
+ #if defined(__ANDROID__)
157
+ // The worker calls into javax.crypto over JNI; attach it to the JVM once for
158
+ // its lifetime (and let the app class loader resolve KernelPbkdf2).
159
+ facebook::jni::ThreadScope::WithClassLoader(std::move(body));
160
+ #else
161
+ body();
162
+ #endif
163
+ }
164
+
165
+ bool kdfSubmit(KdfJob job) {
166
+ auto& queue = kdfQueue();
167
+ std::lock_guard<std::mutex> lock(queue.mutex);
168
+ if (queue.jobs.size() >= kKdfQueueLimit) {
169
+ return false;
170
+ }
171
+ queue.jobs.push_back(std::move(job));
172
+ if (!queue.workerStarted) {
173
+ queue.workerStarted = true;
174
+ std::thread(kdfWorkerMain).detach();
175
+ }
176
+ queue.cv.notify_one();
177
+ return true;
178
+ }
179
+
180
+ void installPbkdf2(
181
+ jsi::Runtime& rt,
182
+ jsi::Object& qntx,
183
+ const std::shared_ptr<KernelGlobalsState>& state) {
184
+ qntx.setProperty(
185
+ rt,
186
+ "pbkdf2",
187
+ hostFn(rt, "pbkdf2", 6, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
188
+ if (count < 6) {
189
+ throw jsi::JSError(
190
+ rt, "__qntx.pbkdf2 expects (hash, password, salt, iterations, length, done)");
191
+ }
192
+ const auto& intrinsics = *state->intrinsics;
193
+ if (!args[0].isString()) {
194
+ throw jsi::JSError(
195
+ rt, "__qntx.pbkdf2 expects (hash, password, salt, iterations, length, done)");
196
+ }
197
+ const auto hashName = args[0].asString(rt).utf8(rt);
198
+ KernelPbkdf2Hash hash;
199
+ if (hashName == "sha256") {
200
+ hash = KernelPbkdf2Hash::sha256;
201
+ } else if (hashName == "sha512") {
202
+ hash = KernelPbkdf2Hash::sha512;
203
+ } else {
204
+ throw jsi::JSError(rt, "__qntx.pbkdf2 hash must be sha256 or sha512");
205
+ }
206
+ auto passwordView =
207
+ byteViewFromValue(rt, args[1], "__qntx.pbkdf2 password", intrinsics);
208
+ SecretBytes password(passwordView.data, passwordView.size);
209
+ auto saltView =
210
+ byteViewFromValue(rt, args[2], "__qntx.pbkdf2 salt", intrinsics);
211
+ SecretBytes salt(saltView.data, saltView.size);
212
+ if (!args[5].isObject() || !args[5].asObject(rt).isFunction(rt)) {
213
+ throw jsi::JSError(
214
+ rt, "__qntx.pbkdf2 expects (hash, password, salt, iterations, length, done)");
215
+ }
216
+ const auto iterations = static_cast<uint32_t>(
217
+ integerInRange(rt, args[3], "__qntx.pbkdf2 iterations", 1, kPbkdf2IterationsMax));
218
+ const auto length = static_cast<size_t>(
219
+ integerInRange(rt, args[4], "__qntx.pbkdf2 length", 1, kPbkdf2LengthMax));
220
+ auto call = std::make_shared<KernelPendingCall>();
221
+ call->done = args[5].asObject(rt).asFunction(rt);
222
+ state->pending.insert(call);
223
+
224
+ KdfJob job{
225
+ hash,
226
+ std::move(password),
227
+ std::move(salt),
228
+ iterations,
229
+ length,
230
+ call,
231
+ state};
232
+ if (!kdfSubmit(std::move(job))) {
233
+ state->enqueueTask([state, call](jsi::Runtime& rt) mutable {
234
+ auto done = std::move(call->done);
235
+ state->pending.erase(call);
236
+ if (done.has_value()) {
237
+ done->call(
238
+ rt,
239
+ jsi::Value::undefined(),
240
+ jsi::Value(rt, jsi::String::createFromAscii(rt, "kdf queue full")));
241
+ }
242
+ });
243
+ }
244
+ return jsi::Value::undefined();
245
+ }));
246
+ }
247
+
248
+ // Runs once before the kernel script: locks down the native TextDecoder,
249
+ // installs the setTimeout/clearTimeout globals Hermes' bundled rejection
250
+ // tracker defers through, and wires that tracker to the private reportUnhandled
251
+ // native. The expression evaluates to the function object; C++ invokes it with
252
+ // `natives`.
253
+ const char kKernelPrelude[] = R"JS((function (natives) {
254
+ var defineHidden = function (name, value) {
255
+ Object.defineProperty(globalThis, name, {
256
+ value: value,
257
+ writable: false,
258
+ configurable: false,
259
+ enumerable: false,
260
+ });
261
+ };
262
+
263
+ // Hermes 260318099+ (RN 0.88) ships a native TextDecoder — verified against
264
+ // the host vectors: WHATWG fatal/replacement semantics, BOM strip/ignoreBOM,
265
+ // streaming, RangeError on unknown labels. Adopt it instead of a shell:
266
+ // freeze and rebind it hidden so a script cannot swap or tamper with it. The
267
+ // prelude runs before any script, so Object.freeze is still pristine here.
268
+ if (typeof TextDecoder !== "function") {
269
+ throw new Error("kernel prelude: TextDecoder missing");
270
+ }
271
+ Object.freeze(TextDecoder.prototype);
272
+ Object.freeze(TextDecoder);
273
+ defineHidden("TextDecoder", TextDecoder);
274
+
275
+ // Hermes' bundled rejection tracker defers onUnhandled through the literal
276
+ // globals setTimeout/clearTimeout resolved at call time — Promise.then and
277
+ // async/await use the engine queue instead and need nothing. These are the
278
+ // only two timer globals the runtime installs (measured: without them
279
+ // onUnhandled never fires).
280
+ var timerSeq = 0;
281
+ var timerCancels = {};
282
+ defineHidden("setTimeout", function (fn, ms) {
283
+ var id = ++timerSeq;
284
+ var cancel = natives.setTimer(ms === undefined ? 0 : ms, function () {
285
+ delete timerCancels[id];
286
+ fn();
287
+ });
288
+ timerCancels[id] = cancel;
289
+ return id;
290
+ });
291
+ defineHidden("clearTimeout", function (id) {
292
+ var cancel = timerCancels[id];
293
+ if (cancel !== undefined) {
294
+ delete timerCancels[id];
295
+ cancel();
296
+ }
297
+ });
298
+
299
+ if (
300
+ typeof HermesInternal === "object" &&
301
+ HermesInternal !== null &&
302
+ typeof HermesInternal.enablePromiseRejectionTracker === "function"
303
+ ) {
304
+ HermesInternal.enablePromiseRejectionTracker({
305
+ allRejections: true,
306
+ onHandled: function () {},
307
+ onUnhandled: function (id, reason) {
308
+ natives.reportUnhandled(
309
+ id,
310
+ reason == null
311
+ ? "unknown"
312
+ : String(reason && reason.stack ? reason.stack : reason));
313
+ },
314
+ });
315
+ }
316
+ }))JS";
317
+
318
+ } // namespace
319
+
320
+ void installKernelGlobals(
321
+ jsi::Runtime& rt,
322
+ const std::shared_ptr<KernelGlobalsState>& state) {
323
+ auto intrinsics = captureIntrinsics(rt);
324
+ state->intrinsics = intrinsics;
325
+
326
+ auto global = rt.global();
327
+ auto qntx = jsi::Object(rt);
328
+
329
+ qntx.setProperty(
330
+ rt,
331
+ "random",
332
+ hostFn(rt, "random", 1, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
333
+ if (count < 1) {
334
+ throw jsi::JSError(rt, "__qntx.random expects a byte length");
335
+ }
336
+ const auto n = static_cast<size_t>(
337
+ integerInRange(rt, args[0], "__qntx.random length", 0, kRandomLimit));
338
+ auto buffer = arrayBufferFromBytes(rt, nullptr, n);
339
+ fillRandom(buffer.data(rt), buffer.size(rt));
340
+ return jsi::Value(rt, makeUint8Array(rt, *state->intrinsics, std::move(buffer)));
341
+ }));
342
+
343
+ qntx.setProperty(
344
+ rt,
345
+ "now",
346
+ hostFn(rt, "now", 0, [](jsi::Runtime&, const jsi::Value&, const jsi::Value*, size_t) -> jsi::Value {
347
+ // Whole milliseconds like Date.now(): vault headers require integer
348
+ // createdAt timestamps.
349
+ return jsi::Value(static_cast<double>(
350
+ std::chrono::duration_cast<std::chrono::milliseconds>(
351
+ std::chrono::system_clock::now().time_since_epoch())
352
+ .count()));
353
+ }));
354
+
355
+ qntx.setProperty(
356
+ rt,
357
+ "elapsed",
358
+ hostFn(rt, "elapsed", 0, [](jsi::Runtime&, const jsi::Value&, const jsi::Value*, size_t) -> jsi::Value {
359
+ return jsi::Value(std::chrono::duration<double, std::milli>(
360
+ std::chrono::steady_clock::now().time_since_epoch())
361
+ .count());
362
+ }));
363
+
364
+ const auto makeSetTimer = [&state](jsi::Runtime& rt) -> jsi::Function {
365
+ return hostFn(rt, "setTimer", 2, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
366
+ if (count < 2 || !args[1].isObject() || !args[1].asObject(rt).isFunction(rt)) {
367
+ throw jsi::JSError(rt, "__qntx.setTimer expects (delayMs, fn)");
368
+ }
369
+ const double delayMs = finiteNumber(rt, args[0], "__qntx.setTimer delay");
370
+ if (delayMs < 0 || delayMs > kTimerDelayMax) {
371
+ throw jsi::JSError(rt, "__qntx.setTimer delay out of range");
372
+ }
373
+ const auto deadline = std::chrono::steady_clock::now() +
374
+ std::chrono::milliseconds(static_cast<int64_t>(std::ceil(delayMs)));
375
+ auto cancel = state->scheduleTimer(deadline, args[1].asObject(rt).asFunction(rt));
376
+ return jsi::Value(rt, hostFn(rt, "cancel", 0, [cancel = std::move(cancel)](jsi::Runtime&, const jsi::Value&, const jsi::Value*, size_t) mutable -> jsi::Value {
377
+ cancel();
378
+ return jsi::Value::undefined();
379
+ }));
380
+ });
381
+ };
382
+ qntx.setProperty(rt, "setTimer", makeSetTimer(rt));
383
+
384
+ installPbkdf2(rt, qntx, state);
385
+
386
+ // __qntx.frames — the W2b injection-table shape over the same plumbing:
387
+ // post, onMessage, and onIntegrityViolation (the host fires that handler
388
+ // when it refuses a frame on integrity grounds).
389
+ auto frames = jsi::Object(rt);
390
+ frames.setProperty(
391
+ rt,
392
+ "post",
393
+ hostFn(rt, "frames.post", 1, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
394
+ if (count < 1 || !args[0].isObject() || !args[0].asObject(rt).isArrayBuffer(rt)) {
395
+ throw jsi::JSError(rt, "__qntx.frames.post expects an ArrayBuffer");
396
+ }
397
+ auto buffer = args[0].asObject(rt).getArrayBuffer(rt);
398
+ const auto* data = buffer.data(rt);
399
+ state->postToHost(std::vector<uint8_t>(data, data + buffer.size(rt)));
400
+ return jsi::Value::undefined();
401
+ }));
402
+ frames.setProperty(
403
+ rt,
404
+ "onMessage",
405
+ hostFn(rt, "frames.onMessage", 1, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
406
+ if (count < 1 || !args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) {
407
+ throw jsi::JSError(rt, "__qntx.frames.onMessage expects a function");
408
+ }
409
+ state->messageHandler = args[0].asObject(rt).asFunction(rt);
410
+ return jsi::Value::undefined();
411
+ }));
412
+ frames.setProperty(
413
+ rt,
414
+ "onIntegrityViolation",
415
+ hostFn(rt, "frames.onIntegrityViolation", 1, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
416
+ if (count < 1 || !args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) {
417
+ throw jsi::JSError(rt, "__qntx.frames.onIntegrityViolation expects a function");
418
+ }
419
+ state->integrityHandler = args[0].asObject(rt).asFunction(rt);
420
+ return jsi::Value::undefined();
421
+ }));
422
+ intrinsics->freeze.call(rt, jsi::Value(rt, frames));
423
+ qntx.setProperty(rt, "frames", jsi::Value(std::move(frames)));
424
+
425
+ // __qntx.file — one opaque file image: read() -> Uint8Array | undefined,
426
+ // write(bytes) atomically replaces. The C++ primitive owns durability
427
+ // (temp + fsync + rename + dir fsync); failures throw so the file store
428
+ // rejects instead of silently degrading.
429
+ auto file = jsi::Object(rt);
430
+ file.setProperty(
431
+ rt,
432
+ "read",
433
+ hostFn(rt, "file.read", 0, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value*, size_t) -> jsi::Value {
434
+ const auto bytes = kernelFileRead(state->vaultDirectory);
435
+ if (!bytes.has_value()) {
436
+ return jsi::Value::undefined();
437
+ }
438
+ auto buffer = arrayBufferFromBytes(rt, bytes->data(), bytes->size());
439
+ return jsi::Value(rt, makeUint8Array(rt, *state->intrinsics, std::move(buffer)));
440
+ }));
441
+ file.setProperty(
442
+ rt,
443
+ "write",
444
+ hostFn(rt, "file.write", 1, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
445
+ if (count < 1) {
446
+ throw jsi::JSError(rt, "__qntx.file.write expects an ArrayBuffer or TypedArray");
447
+ }
448
+ const auto view =
449
+ byteViewFromValue(rt, args[0], "__qntx.file.write", *state->intrinsics);
450
+ kernelFileWrite(
451
+ state->vaultDirectory, std::vector<uint8_t>(view.data, view.data + view.size));
452
+ return jsi::Value::undefined();
453
+ }));
454
+ intrinsics->freeze.call(rt, jsi::Value(rt, file));
455
+ qntx.setProperty(rt, "file", jsi::Value(std::move(file)));
456
+
457
+ installKernelBackends(rt, qntx, *intrinsics);
458
+ installKernelKeystore(rt, qntx, state);
459
+ installKernelPasskey(rt, qntx, state);
460
+
461
+ intrinsics->freeze.call(rt, jsi::Value(rt, qntx));
462
+ defineHiddenGlobal(rt, *intrinsics, "__qntx", jsi::Value(std::move(qntx)));
463
+
464
+ auto cryptoObject = jsi::Object(rt);
465
+ cryptoObject.setProperty(
466
+ rt,
467
+ "getRandomValues",
468
+ hostFn(rt, "getRandomValues", 1, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
469
+ if (count < 1 || !args[0].isObject()) {
470
+ throw jsi::JSError(rt, "crypto.getRandomValues expects a TypedArray");
471
+ }
472
+ const auto& intrinsics = *state->intrinsics;
473
+ auto view = args[0].asObject(rt);
474
+ const jsi::Value tag = intrinsics.taTagGet.callWithThis(rt, view);
475
+ static const char* const kIntegerArrays[] = {
476
+ "Int8Array",
477
+ "Uint8Array",
478
+ "Uint8ClampedArray",
479
+ "Int16Array",
480
+ "Uint16Array",
481
+ "Int32Array",
482
+ "Uint32Array",
483
+ "BigInt64Array",
484
+ "BigUint64Array",
485
+ };
486
+ const std::string tagName =
487
+ tag.isString() ? tag.asString(rt).utf8(rt) : std::string();
488
+ bool integerTyped = false;
489
+ for (const char* allowed : kIntegerArrays) {
490
+ if (tagName == allowed) {
491
+ integerTyped = true;
492
+ break;
493
+ }
494
+ }
495
+ if (!integerTyped) {
496
+ throw jsi::JSError(
497
+ rt, "crypto.getRandomValues accepts only integer TypedArrays");
498
+ }
499
+ auto bytes =
500
+ byteViewFromValue(rt, args[0], "crypto.getRandomValues", intrinsics);
501
+ if (bytes.size > kRandomLimit) {
502
+ throw jsi::JSError(
503
+ rt, "QuotaExceededError: crypto.getRandomValues view exceeds 65536 bytes");
504
+ }
505
+ fillRandom(bytes.data, bytes.size);
506
+ return jsi::Value(rt, args[0]);
507
+ }));
508
+ intrinsics->freeze.call(rt, jsi::Value(rt, cryptoObject));
509
+ defineHiddenGlobal(
510
+ rt, *intrinsics, "crypto", jsi::Value(std::move(cryptoObject)));
511
+
512
+ auto natives = jsi::Object(rt);
513
+ natives.setProperty(rt, "setTimer", makeSetTimer(rt));
514
+ natives.setProperty(
515
+ rt,
516
+ "reportUnhandled",
517
+ hostFn(rt, "reportUnhandled", 2, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
518
+ std::string reason = "unhandled promise rejection";
519
+ if (count > 0 && args[0].isNumber()) {
520
+ reason += " id=" +
521
+ std::to_string(static_cast<long long>(args[0].getNumber()));
522
+ }
523
+ if (count > 1 && args[1].isString()) {
524
+ reason += ": " + args[1].asString(rt).utf8(rt);
525
+ }
526
+ state->reportFatal(std::move(reason));
527
+ return jsi::Value::undefined();
528
+ }));
529
+
530
+ auto prelude = rt.evaluateJavaScript(
531
+ std::make_shared<jsi::StringBuffer>(kKernelPrelude), "<kernel-prelude>");
532
+ prelude.asObject(rt).asFunction(rt).call(rt, jsi::Value(std::move(natives)));
533
+ }
534
+
535
+ } // namespace qntx