@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,496 @@
1
+ #include "KernelRuntime.hpp"
2
+
3
+ #include "KernelBuffer.hpp"
4
+ #include "KernelGlobals.hpp"
5
+
6
+ #include <jsi/hermes-interfaces.h>
7
+
8
+ #if defined(__ANDROID__)
9
+ #include "platform/KernelPbkdf2.hpp"
10
+ #include <fbjni/fbjni.h>
11
+ #include <jni.h>
12
+ #endif
13
+
14
+ #include <atomic>
15
+ #include <condition_variable>
16
+ #include <deque>
17
+ #include <map>
18
+ #include <mutex>
19
+ #include <optional>
20
+ #include <stdexcept>
21
+ #include <thread>
22
+ #include <unordered_map>
23
+ #include <utility>
24
+
25
+ #if defined(__APPLE__) || defined(__ANDROID__)
26
+ #include <pthread.h>
27
+ #endif
28
+
29
+ namespace qntx {
30
+
31
+ namespace jsi = facebook::jsi;
32
+
33
+ namespace {
34
+
35
+ // Turns the current exception into a description. Called only inside catch
36
+ // blocks so the exception_ptr — and any jsi values a JSError holds — die
37
+ // while the runtime that owns them is still alive.
38
+ std::string describe(const std::exception_ptr& error) {
39
+ try {
40
+ std::rethrow_exception(error);
41
+ } catch (const jsi::JSError& e) {
42
+ std::string message = e.getMessage();
43
+ const std::string& stack = e.getStack();
44
+ if (!stack.empty()) {
45
+ message += "\n" + stack;
46
+ }
47
+ return "jsi::JSError: " + message;
48
+ } catch (const std::exception& e) {
49
+ return std::string(e.what());
50
+ } catch (...) {
51
+ return "unknown error";
52
+ }
53
+ }
54
+
55
+ hermes::vm::RuntimeConfig runtimeConfigFor(KernelProfile profile) {
56
+ auto builder = profile == KernelProfile::hardened
57
+ ? facebook::hermes::hardenedHermesRuntimeConfig().rebuild()
58
+ : hermes::vm::RuntimeConfig::Builder();
59
+ return builder.withMicrotaskQueue(true)
60
+ .withEnableEval(false)
61
+ .withAsyncBreakCheckInEval(true)
62
+ .build();
63
+ }
64
+
65
+ void nameKernelThread() {
66
+ #if defined(__APPLE__)
67
+ pthread_setname_np("qntx-kernel");
68
+ #elif defined(__ANDROID__)
69
+ pthread_setname_np(pthread_self(), "qntx-kernel");
70
+ #endif
71
+ }
72
+
73
+ } // namespace
74
+
75
+ struct KernelRuntime::Impl {
76
+ KernelScript script;
77
+ Hooks hooks;
78
+ KernelProfile profile = KernelProfile::standard;
79
+ std::shared_ptr<KernelGlobalsState> globals;
80
+
81
+ // Guards `immediate`, and the `stopping` store in destroy() so the loop's
82
+ // cv predicate can never miss a wakeup.
83
+ std::mutex mutex;
84
+ std::condition_variable cv;
85
+ std::deque<std::function<void()>> immediate;
86
+
87
+ // Inbound frame deliveries queued but not yet run; capped at
88
+ // kMaxInboundFrames — the kernel must keep up or the host exits integrity.
89
+ static constexpr size_t kMaxInboundFrames = 64;
90
+ size_t pendingPosts = 0;
91
+
92
+ std::atomic_bool stopping{false};
93
+
94
+ // Publishes the live runtime's IHermes so destroy() can interrupt running
95
+ // JS. The kernel thread sets it after makeHermesRuntime and withdraws it
96
+ // under the same mutex BEFORE rt.reset(), so asyncTriggerTimeout can never
97
+ // touch a destroyed runtime.
98
+ std::mutex interruptMutex;
99
+ facebook::hermes::IHermes* interruptable = nullptr;
100
+
101
+ std::mutex stoppedMutex;
102
+ std::condition_variable stoppedCv;
103
+ bool stopped = false;
104
+
105
+ // Kernel thread only.
106
+ std::unique_ptr<jsi::Runtime> rt;
107
+ std::optional<std::string> fatal;
108
+ struct Timer {
109
+ uint64_t id;
110
+ jsi::Function fn;
111
+ };
112
+ using TimedMap = std::multimap<std::chrono::steady_clock::time_point, Timer>;
113
+ TimedMap timed;
114
+ std::unordered_map<uint64_t, TimedMap::iterator> timerIndex;
115
+ uint64_t nextTimerId = 0;
116
+
117
+ // Read and written only under `mutex`: enqueue() may run on the KDF worker
118
+ // thread while teardown() runs on the kernel thread.
119
+ bool tornDown = false;
120
+
121
+ std::atomic_bool running{false};
122
+
123
+ void run();
124
+ void initRuntime();
125
+ void evaluate();
126
+ void loop();
127
+ void fail(std::string message);
128
+ void teardown();
129
+ void deliver(std::vector<uint8_t> bytes);
130
+ // Queues a task that runs fn(rt) on the kernel thread. Accepts work even
131
+ // after `stopping`: queued-but-unrun tasks are destroyed by teardown, which
132
+ // runs before the runtime itself is destroyed — so jsi values captured by
133
+ // tasks are always released on the kernel thread while the runtime lives.
134
+ void enqueue(std::function<void(jsi::Runtime&)> task);
135
+ KernelGlobalsState::TimerCancel scheduleTimer(
136
+ std::chrono::steady_clock::time_point deadline,
137
+ jsi::Function fn);
138
+ };
139
+
140
+ KernelRuntime::KernelRuntime() : impl_(std::make_shared<Impl>()) {}
141
+
142
+ KernelRuntime::~KernelRuntime() {
143
+ destroy();
144
+ }
145
+
146
+ std::shared_ptr<KernelRuntime> KernelRuntime::start(
147
+ KernelScript script,
148
+ Hooks hooks,
149
+ std::string vaultDirectory,
150
+ KernelProfile profile) {
151
+ if (script.bytes == nullptr || script.bytes->empty()) {
152
+ throw std::invalid_argument("KernelRuntime::start: empty script");
153
+ }
154
+ auto self = std::shared_ptr<KernelRuntime>(new KernelRuntime());
155
+ auto& impl = *self->impl_;
156
+ impl.profile = profile;
157
+ impl.script = std::move(script);
158
+ impl.hooks = std::move(hooks);
159
+ impl.globals = std::make_shared<KernelGlobalsState>();
160
+ impl.globals->vaultDirectory = std::move(vaultDirectory);
161
+ impl.globals->active = std::make_shared<std::atomic_bool>(true);
162
+ impl.globals->postToHost = impl.hooks.onPost;
163
+ impl.globals->reportFatal = [&impl](std::string message) {
164
+ impl.fatal = std::move(message);
165
+ };
166
+ impl.globals->scheduleTimer =
167
+ [&impl](std::chrono::steady_clock::time_point deadline, jsi::Function fn) {
168
+ return impl.scheduleTimer(deadline, std::move(fn));
169
+ };
170
+ // Async completions (e.g. pbkdf2) may arrive after the KernelRuntime object
171
+ // is destroyed, so reach the impl through a weak_ptr instead of a reference.
172
+ std::weak_ptr<Impl> weakImpl = self->impl_;
173
+ impl.globals->enqueueTask =
174
+ [weakImpl](std::function<void(jsi::Runtime&)> task) mutable {
175
+ if (auto impl = weakImpl.lock()) {
176
+ impl->enqueue(std::move(task));
177
+ }
178
+ };
179
+ impl.running.store(true, std::memory_order_release);
180
+ // The detached thread holds the Impl, not the KernelRuntime: dropping the
181
+ // last public handle just requests shutdown through ~KernelRuntime →
182
+ // destroy(), and the kernel thread finishes teardown on its own.
183
+ std::thread([impl = self->impl_]() { impl->run(); }).detach();
184
+ return self;
185
+ }
186
+
187
+ bool KernelRuntime::running() const noexcept {
188
+ return impl_->running.load(std::memory_order_acquire);
189
+ }
190
+
191
+ bool KernelRuntime::waitUntilStopped(std::chrono::milliseconds timeout) const {
192
+ auto& impl = *impl_;
193
+ std::unique_lock<std::mutex> lock(impl.stoppedMutex);
194
+ return impl.stoppedCv.wait_for(lock, timeout, [&] { return impl.stopped; });
195
+ }
196
+
197
+ void KernelRuntime::post(std::vector<uint8_t> bytes) {
198
+ auto& impl = *impl_;
199
+ std::function<void()> integrity;
200
+ {
201
+ std::lock_guard<std::mutex> lock(impl.mutex);
202
+ if (impl.stopping.load(std::memory_order_acquire) ||
203
+ !impl.running.load(std::memory_order_acquire)) {
204
+ return;
205
+ }
206
+ if (impl.pendingPosts >= Impl::kMaxInboundFrames) {
207
+ integrity = impl.hooks.onIntegrity;
208
+ } else {
209
+ impl.pendingPosts += 1;
210
+ impl.immediate.push_back([&impl, bytes = std::move(bytes)]() mutable {
211
+ {
212
+ std::lock_guard<std::mutex> inner(impl.mutex);
213
+ impl.pendingPosts -= 1;
214
+ }
215
+ impl.deliver(std::move(bytes));
216
+ });
217
+ }
218
+ }
219
+ impl.cv.notify_one();
220
+ if (integrity) {
221
+ integrity();
222
+ }
223
+ }
224
+
225
+ void KernelRuntime::destroy() noexcept {
226
+ auto& impl = *impl_;
227
+ {
228
+ std::lock_guard<std::mutex> lock(impl.mutex);
229
+ if (impl.stopping.load(std::memory_order_acquire)) {
230
+ return;
231
+ }
232
+ impl.stopping.store(true, std::memory_order_release);
233
+ }
234
+ impl.cv.notify_all();
235
+ {
236
+ std::lock_guard<std::mutex> lock(impl.interruptMutex);
237
+ if (impl.interruptable != nullptr) {
238
+ impl.interruptable->asyncTriggerTimeout();
239
+ }
240
+ }
241
+ }
242
+
243
+ void KernelRuntime::Impl::run() {
244
+ nameKernelThread();
245
+ auto work = [this] {
246
+ std::optional<std::string> error;
247
+ try {
248
+ initRuntime();
249
+ loop();
250
+ } catch (...) {
251
+ // describe() runs inside the catch so a JSError (and the jsi values it
252
+ // holds) is destroyed while the runtime still exists.
253
+ try {
254
+ error = describe(std::current_exception());
255
+ } catch (...) {
256
+ error = "undescribable kernel error";
257
+ }
258
+ }
259
+ // An exception raised after a destroy() request — the async interrupt or
260
+ // anything racing it — is a normal stop, not a kernel failure.
261
+ if (error.has_value() && !stopping.load(std::memory_order_acquire)) {
262
+ fail(std::move(*error));
263
+ }
264
+ teardown();
265
+ };
266
+ #if defined(__ANDROID__)
267
+ // Run the kernel body inside a Java call frame via fbjni. A plain
268
+ // AttachCurrentThread gives the thread a JNIEnv but FindClass still resolves
269
+ // through the boot class loader, which cannot see app-dex classes — Hermes
270
+ // delegates String.prototype.normalize/localeCompare to
271
+ // com.facebook.hermes.unicode.AndroidUnicodeUtils, and Nitro's registry is a
272
+ // Java class; the former throws fbjni::JniException across a noexcept
273
+ // boundary (std::terminate) on a raw attached thread. WithClassLoader makes
274
+ // app classes resolvable and attaches the thread for the whole duration.
275
+ facebook::jni::ThreadScope::WithClassLoader(std::move(work));
276
+ #else
277
+ work();
278
+ #endif
279
+ }
280
+
281
+ void KernelRuntime::Impl::initRuntime() {
282
+ rt = facebook::hermes::makeHermesRuntime(runtimeConfigFor(profile));
283
+ {
284
+ std::lock_guard<std::mutex> lock(interruptMutex);
285
+ interruptable = jsi::castInterface<facebook::hermes::IHermes>(rt.get());
286
+ }
287
+ if (interruptable == nullptr) {
288
+ throw std::runtime_error("hermes runtime does not expose IHermes");
289
+ }
290
+ if (stopping.load(std::memory_order_acquire)) {
291
+ // destroy() won the race before the script ran: skip globals and
292
+ // evaluation; run() then returns from loop() at once into teardown.
293
+ return;
294
+ }
295
+
296
+ installKernelGlobals(*rt, globals);
297
+
298
+ if (hooks.beforeEvaluate) {
299
+ hooks.beforeEvaluate(*rt);
300
+ }
301
+
302
+ evaluate();
303
+ rt->drainMicrotasks();
304
+ if (fatal.has_value()) {
305
+ throw std::runtime_error(*fatal);
306
+ }
307
+ }
308
+
309
+ void KernelRuntime::Impl::evaluate() {
310
+ const auto& bytes = *script.bytes;
311
+ if (script.kind == KernelScript::Kind::bytecode) {
312
+ auto* api = jsi::castInterface<facebook::hermes::IHermesRootAPI>(
313
+ facebook::hermes::makeHermesRootAPI());
314
+ if (api == nullptr) {
315
+ throw std::runtime_error("Hermes root API unavailable");
316
+ }
317
+ if (!api->isHermesBytecode(bytes.data(), bytes.size())) {
318
+ throw std::runtime_error("embedded code is not Hermes bytecode");
319
+ }
320
+ std::string sanityError;
321
+ if (!api->hermesBytecodeSanityCheck(bytes.data(), bytes.size(), &sanityError)) {
322
+ throw std::runtime_error(
323
+ "embedded Hermes bytecode failed sanity check (version " +
324
+ std::to_string(api->getBytecodeVersion()) + "): " + sanityError);
325
+ }
326
+ rt->evaluateJavaScript(
327
+ std::make_shared<VectorBuffer>(script.bytes), "kernel.hbc");
328
+ } else {
329
+ rt->evaluateJavaScript(
330
+ std::make_shared<jsi::StringBuffer>(
331
+ std::string(reinterpret_cast<const char*>(bytes.data()), bytes.size())),
332
+ "kernel.ts");
333
+ }
334
+ }
335
+
336
+ void KernelRuntime::Impl::loop() {
337
+ for (;;) {
338
+ std::function<void()> task;
339
+ std::optional<Timer> due;
340
+ {
341
+ std::unique_lock<std::mutex> lock(mutex);
342
+ for (;;) {
343
+ if (stopping.load(std::memory_order_acquire)) {
344
+ return;
345
+ }
346
+ const auto now = std::chrono::steady_clock::now();
347
+ // Timers live on the kernel thread only; the mutex here just happens
348
+ // to be held while picking the next piece of work.
349
+ if (!timed.empty() && timed.begin()->first <= now) {
350
+ auto node = timed.extract(timed.begin());
351
+ timerIndex.erase(node.mapped().id);
352
+ due = std::move(node.mapped());
353
+ break;
354
+ }
355
+ if (!immediate.empty()) {
356
+ task = std::move(immediate.front());
357
+ immediate.pop_front();
358
+ break;
359
+ }
360
+ if (timed.empty()) {
361
+ cv.wait(lock);
362
+ } else {
363
+ cv.wait_until(lock, timed.begin()->first);
364
+ }
365
+ }
366
+ }
367
+
368
+ std::optional<std::string> error;
369
+ try {
370
+ if (task) {
371
+ task();
372
+ } else if (due.has_value()) {
373
+ due->fn.call(*rt);
374
+ }
375
+ rt->drainMicrotasks();
376
+ } catch (...) {
377
+ error = describe(std::current_exception());
378
+ }
379
+ // Drop jsi values while the runtime is alive, whichever way we exit.
380
+ due.reset();
381
+ task = nullptr;
382
+ if (error.has_value()) {
383
+ if (stopping.load(std::memory_order_acquire)) {
384
+ // The destroy() interrupt surfaced as an exception — a normal stop.
385
+ return;
386
+ }
387
+ fail("kernel task failed: " + std::move(*error));
388
+ return;
389
+ }
390
+ if (fatal.has_value()) {
391
+ fail(std::move(*fatal));
392
+ return;
393
+ }
394
+ }
395
+ }
396
+
397
+ void KernelRuntime::Impl::deliver(std::vector<uint8_t> bytes) {
398
+ if (!globals->messageHandler.has_value()) {
399
+ return;
400
+ }
401
+ auto buffer = rt->createArrayBuffer(std::make_shared<OwningBuffer>(std::move(bytes)));
402
+ globals->messageHandler->call(*rt, jsi::Value(std::move(buffer)));
403
+ }
404
+
405
+ void KernelRuntime::Impl::enqueue(std::function<void(jsi::Runtime&)> task) {
406
+ {
407
+ std::lock_guard<std::mutex> lock(mutex);
408
+ if (tornDown) {
409
+ // The loop is gone and teardown already ran: drop the task here so its
410
+ // captures (wiping buffers, dead pending calls) release immediately.
411
+ return;
412
+ }
413
+ immediate.push_back(
414
+ [this, task = std::move(task)]() mutable { task(*rt); });
415
+ }
416
+ cv.notify_one();
417
+ }
418
+
419
+ KernelGlobalsState::TimerCancel KernelRuntime::Impl::scheduleTimer(
420
+ std::chrono::steady_clock::time_point deadline,
421
+ jsi::Function fn) {
422
+ // Kernel thread only — host functions and the loop run there, so no lock.
423
+ const uint64_t id = nextTimerId++;
424
+ auto it = timed.emplace(deadline, Timer{id, std::move(fn)});
425
+ timerIndex.emplace(id, it);
426
+ return [this, id] {
427
+ if (tornDown) {
428
+ return;
429
+ }
430
+ auto found = timerIndex.find(id);
431
+ if (found != timerIndex.end()) {
432
+ // Erasing releases the stored jsi::Function immediately instead of
433
+ // parking it until the deadline.
434
+ timed.erase(found->second);
435
+ timerIndex.erase(found);
436
+ }
437
+ };
438
+ }
439
+
440
+ void KernelRuntime::Impl::fail(std::string message) {
441
+ // Kernel thread only.
442
+ stopping.store(true, std::memory_order_release);
443
+ if (hooks.onError) {
444
+ try {
445
+ hooks.onError(std::move(message));
446
+ } catch (...) {
447
+ }
448
+ }
449
+ teardown();
450
+ }
451
+
452
+ void KernelRuntime::Impl::teardown() {
453
+ // Kernel thread only; tornDown is shared with enqueue() on other threads,
454
+ // so it is set under the same mutex that guards the queue.
455
+ {
456
+ std::lock_guard<std::mutex> lock(mutex);
457
+ if (tornDown) {
458
+ return;
459
+ }
460
+ tornDown = true;
461
+ // Queued-but-unrun tasks die here, on the kernel thread, while the
462
+ // runtime is alive; enqueue() drops anything that arrives after this.
463
+ immediate.clear();
464
+ }
465
+ // Drop every jsi value before the runtime itself.
466
+ globals->intrinsics.reset();
467
+ globals->messageHandler.reset();
468
+ globals->integrityHandler.reset();
469
+ for (const auto& call : globals->pending) {
470
+ call->done.reset();
471
+ }
472
+ globals->pending.clear();
473
+ // A KDF worker that sees inactive drops its job — safe only because `done`
474
+ // was already cleared above, on this thread, while the runtime was alive.
475
+ globals->active->store(false, std::memory_order_release);
476
+ // A system prompt still on screen belongs to nobody now: dismiss it.
477
+ for (const auto& cancel : globals->cancelCeremonies) {
478
+ cancel();
479
+ }
480
+ // Timed/timerIndex are kernel-thread-only state.
481
+ timed.clear();
482
+ timerIndex.clear();
483
+ {
484
+ std::lock_guard<std::mutex> lock(interruptMutex);
485
+ interruptable = nullptr;
486
+ }
487
+ rt.reset();
488
+ running.store(false, std::memory_order_release);
489
+ {
490
+ std::lock_guard<std::mutex> lock(stoppedMutex);
491
+ stopped = true;
492
+ }
493
+ stoppedCv.notify_all();
494
+ }
495
+
496
+ } // namespace qntx
@@ -0,0 +1,94 @@
1
+ #pragma once
2
+
3
+ #include <hermes/hermes.h>
4
+ #include <jsi/jsi.h>
5
+
6
+ #include <chrono>
7
+ #include <cstdint>
8
+ #include <functional>
9
+ #include <memory>
10
+ #include <string>
11
+ #include <vector>
12
+
13
+ namespace qntx {
14
+
15
+ // Hermes hardening level for the kernel runtime. `standard` runs a plain
16
+ // HermesVM config; `hardened` starts from hermes' hardenedHermesRuntimeConfig()
17
+ // (untrusted-code profile). Both disable eval, enable the runtime-owned
18
+ // microtask queue, and emit async break checks in eval-compiled code.
19
+ enum class KernelProfile { standard, hardened };
20
+
21
+ // Immutable script payload: bundled kernel source or compiled Hermes bytecode.
22
+ // Bytecode must be built with hermesc -emit-async-break-check so destroy() can
23
+ // interrupt running JS.
24
+ struct KernelScript {
25
+ enum class Kind { source, bytecode };
26
+ Kind kind;
27
+ std::shared_ptr<const std::vector<uint8_t>> bytes;
28
+ };
29
+
30
+ // A Hermes runtime running the wallet kernel on its own thread, driven by a
31
+ // task queue instead of the JS event loop.
32
+ //
33
+ // Lifecycle:
34
+ // - start() returns a shared_ptr; the kernel thread is detached and owns the
35
+ // shared state, so dropping the last KernelRuntime does not leak or hang.
36
+ // - destroy() requests shutdown: it is noexcept, idempotent, callable from
37
+ // any thread (including the kernel thread itself), and never blocks — it
38
+ // interrupts running JS via Hermes' async timeout. Destruction calls it.
39
+ // - running() is true from start() until teardown finishes on the kernel
40
+ // thread.
41
+ // - waitUntilStopped() blocks until teardown finished; it exists for tests
42
+ // and probes only — never call it on a UI thread.
43
+ //
44
+ // Host integration:
45
+ // - Hooks.beforeEvaluate runs on the kernel thread after globals install and
46
+ // before the script evaluates.
47
+ // - Hooks.onPost receives bytes the script posts via __qntx.post.
48
+ // - Hooks.onError receives a single fatal error (script exception, task
49
+ // exception, unhandled rejection); the runtime is torn down afterwards.
50
+ // Errors that only exist because shutdown was requested never reach it.
51
+ class KernelRuntime final : public std::enable_shared_from_this<KernelRuntime> {
52
+ public:
53
+ struct Hooks {
54
+ std::function<void(facebook::jsi::Runtime&)> beforeEvaluate;
55
+ std::function<void(std::vector<uint8_t>)> onPost;
56
+ std::function<void(std::string)> onError;
57
+ // Fired when post() exceeds the inbound frame queue cap (64) — a transport
58
+ // violation the host maps to an `integrity` exit, not a kernel crash.
59
+ std::function<void()> onIntegrity;
60
+ };
61
+
62
+ // `vaultDirectory` is the directory the kernel's vault file lives in (the
63
+ // host resolves it via platformVaultDirectory); the __qntx.file capabilities
64
+ // capture it for kernelFileRead/kernelFileWrite.
65
+ static std::shared_ptr<KernelRuntime> start(
66
+ KernelScript script,
67
+ Hooks hooks,
68
+ std::string vaultDirectory,
69
+ KernelProfile profile = KernelProfile::standard);
70
+
71
+ ~KernelRuntime();
72
+
73
+ KernelRuntime(const KernelRuntime&) = delete;
74
+ KernelRuntime& operator=(const KernelRuntime&) = delete;
75
+ KernelRuntime(KernelRuntime&&) = delete;
76
+ KernelRuntime& operator=(KernelRuntime&&) = delete;
77
+
78
+ // Delivers bytes to the script's message handler. Safe after destroy():
79
+ // returns immediately if the runtime is stopping or gone.
80
+ void post(std::vector<uint8_t> bytes);
81
+
82
+ void destroy() noexcept;
83
+ bool running() const noexcept;
84
+ bool waitUntilStopped(std::chrono::milliseconds timeout) const;
85
+
86
+ private:
87
+ struct Impl;
88
+
89
+ KernelRuntime();
90
+
91
+ std::shared_ptr<Impl> impl_;
92
+ };
93
+
94
+ } // namespace qntx