@supermachine/core 0.4.25

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/index.js ADDED
@@ -0,0 +1,290 @@
1
+ // @supermachine/core — entry point.
2
+ //
3
+ // 1. Locates the platform-specific .node addon (shipped in
4
+ // @supermachine/core-<platform>-<arch>, installed via npm's
5
+ // optionalDependencies + os/cpu filter).
6
+ // 2. Sets SUPERMACHINE_WORKER_BIN in process.env so the underlying
7
+ // Rust crate finds the pre-signed worker binary that ships
8
+ // alongside the .node file. Done BEFORE require()'ing the
9
+ // addon so the Rust module_init picks it up.
10
+ // 3. Re-exports the napi classes (Image, Pool, Vm).
11
+
12
+ const { existsSync } = require("node:fs");
13
+ const { join, dirname } = require("node:path");
14
+
15
+ // Map (process.platform, process.arch) → npm package name + binary
16
+ // filename. napi-rs's CLI auto-generates these names per platform;
17
+ // keep this table in sync with the matrix in the release workflow.
18
+ const TARGETS = {
19
+ "darwin-arm64": {
20
+ pkg: "@supermachine/core-darwin-arm64",
21
+ nodeFile: "supermachine-core.darwin-arm64.node",
22
+ workerFile: "supermachine-worker",
23
+ },
24
+ // Future targets — add when KVM/WHP backends land:
25
+ // "linux-arm64-gnu": { pkg: "@supermachine/core-linux-arm64-gnu",
26
+ // nodeFile: "supermachine-core.linux-arm64-gnu.node",
27
+ // workerFile: "supermachine-worker" },
28
+ // "linux-x64-gnu": { ... },
29
+ // "win32-x64-msvc": { ... },
30
+ };
31
+
32
+ function platformKey() {
33
+ // libc detection (musl vs glibc) goes here when we add linux
34
+ // targets; for now darwin-arm64 is the only supported combo.
35
+ return `${process.platform}-${process.arch}`;
36
+ }
37
+
38
+ function resolvePlatformPackage() {
39
+ const key = platformKey();
40
+ const target = TARGETS[key];
41
+ if (!target) {
42
+ const supported = Object.keys(TARGETS).join(", ");
43
+ throw new Error(
44
+ `@supermachine/core: no prebuilt binary for ${key}. ` +
45
+ `Supported: ${supported}. ` +
46
+ `Linux KVM and Windows WHP support are on the roadmap; ` +
47
+ `for now use macOS on Apple Silicon, or call the Rust crate ` +
48
+ `directly via FFI.`,
49
+ );
50
+ }
51
+
52
+ // Two resolution strategies, in order:
53
+ // 1. Sibling node_modules — production install: the platform
54
+ // package was installed as an optionalDependency.
55
+ // 2. Local dev path — when working in this monorepo, the
56
+ // .node file sits in npm/<platform>/ next to this index.js.
57
+ let pkgDir;
58
+ try {
59
+ // require.resolve points at the package.json; dirname is the
60
+ // package root.
61
+ pkgDir = dirname(require.resolve(`${target.pkg}/package.json`));
62
+ } catch (_) {
63
+ pkgDir = join(__dirname, "npm", key);
64
+ }
65
+
66
+ const nodePath = join(pkgDir, target.nodeFile);
67
+ if (!existsSync(nodePath)) {
68
+ throw new Error(
69
+ `@supermachine/core: native addon not found at ${nodePath}. ` +
70
+ `If you installed via npm, this is a bug — please report it. ` +
71
+ `If you're running from source, build it first: ` +
72
+ `\`cd npm/supermachine-core && npm run build\`.`,
73
+ );
74
+ }
75
+
76
+ const workerPath = join(pkgDir, target.workerFile);
77
+ return { nodePath, workerPath };
78
+ }
79
+
80
+ const { nodePath, workerPath } = resolvePlatformPackage();
81
+
82
+ // Point the Rust crate's worker-locator at the bundled binary.
83
+ // Must happen BEFORE require()'ing the addon — the addon's
84
+ // module_init reads SUPERMACHINE_WORKER_BIN on first call.
85
+ //
86
+ // Don't clobber an explicit user-set value (devs running against
87
+ // a custom-built worker need that to win).
88
+ if (!process.env.SUPERMACHINE_WORKER_BIN && existsSync(workerPath)) {
89
+ process.env.SUPERMACHINE_WORKER_BIN = workerPath;
90
+ }
91
+
92
+ const addon = require(nodePath);
93
+
94
+ // Wrap addon.Image.build to massage the JS-friendly API into the
95
+ // Rust signature:
96
+ // - JS user writes: `Image.build({ ref: "...", warmup: async (vm) => ... })`
97
+ // - Rust expects: `Image.build(options, warmup?)` (two args, warmup
98
+ // can't be inside the options object because
99
+ // JsFunction isn't Send)
100
+ // - Inside the warmup callback, JS user calls `vm.exec(...)`. The
101
+ // Rust side passes us a string (the exec-socket path); we wrap
102
+ // it into a Vm-like object that delegates to addon._execAtPath.
103
+ //
104
+ // This is invisible to JS users — they see the documented API:
105
+ //
106
+ // Image.build({
107
+ // ref: "rust:1-slim",
108
+ // warmup: async (vm) => { await vm.exec({ argv: [...] }) },
109
+ // })
110
+ class Image {
111
+ static async build(options = {}) {
112
+ const { warmup, ...rest } = options;
113
+ if (warmup === undefined) {
114
+ return wrapImageInstance(await addon.Image.build(rest, null));
115
+ }
116
+ // Wrap user's warmup with a Vm-like adapter. Rust passes the
117
+ // exec-socket path as the callback arg; we build a Vm object
118
+ // whose .exec delegates to _execAtPath. The Rust side awaits
119
+ // the Promise we return.
120
+ const wrappedWarmup = async (execPath) => {
121
+ const warmupVm = makeWarmupVm(execPath);
122
+ try {
123
+ await warmup(warmupVm);
124
+ } finally {
125
+ warmupVm._invalidate();
126
+ }
127
+ };
128
+ return wrapImageInstance(await addon.Image.build(rest, wrappedWarmup));
129
+ }
130
+ static async fromSnapshot(path) {
131
+ return wrapImageInstance(await addon.Image.fromSnapshot(path));
132
+ }
133
+ }
134
+
135
+ // Wrap a native Image instance with our prototype-extending shim so
136
+ // `image.pool(...)` returns Pool (instead of the raw addon Pool).
137
+ // Image getters (snapshotPath / memoryMib / vcpus) are read-once at
138
+ // wrap time — they're stable for the lifetime of the snapshot, and
139
+ // re-reading on every access would burn an unnecessary napi
140
+ // roundtrip per property read.
141
+ function wrapImageInstance(native) {
142
+ return {
143
+ async pool(options) {
144
+ return wrapPoolInstance(await native.pool(options ?? null));
145
+ },
146
+ get snapshotPath() {
147
+ return native.snapshotPath;
148
+ },
149
+ get memoryMib() {
150
+ return native.memoryMib;
151
+ },
152
+ get vcpus() {
153
+ return native.vcpus;
154
+ },
155
+ };
156
+ }
157
+
158
+ function wrapPoolInstance(native) {
159
+ return {
160
+ async acquire() {
161
+ return wrapVmInstance(await native.acquire());
162
+ },
163
+ async shutdown() {
164
+ return native.shutdown();
165
+ },
166
+ // Sync — fast (~1 µs, just a mutex lock under the hood).
167
+ stats() {
168
+ return native.stats();
169
+ },
170
+ };
171
+ }
172
+
173
+ function wrapVmInstance(native) {
174
+ const handle = {
175
+ async exec(options) {
176
+ return native.exec(options);
177
+ },
178
+ async spawn(options) {
179
+ return wrapExecChild(await native.spawn(options));
180
+ },
181
+ async writeFile(path, bytes) {
182
+ return native.writeFile(path, bytes);
183
+ },
184
+ async readFile(path) {
185
+ return native.readFile(path);
186
+ },
187
+ async snapshot(destDir) {
188
+ return wrapImageInstance(await native.snapshot(destDir));
189
+ },
190
+ async workloadSignal(signum) {
191
+ return native.workloadSignal(signum);
192
+ },
193
+ async exposeTcp(hostPort, guestPort) {
194
+ return wrapTcpForwarder(await native.exposeTcp(hostPort, guestPort));
195
+ },
196
+ // Path accessors are sync — they're just String getters on the
197
+ // native side. Throw if the Vm has been released (the native
198
+ // getter rejects with a descriptive Error).
199
+ get vsockPath() {
200
+ return native.vsockPath;
201
+ },
202
+ get execPath() {
203
+ return native.execPath;
204
+ },
205
+ async release() {
206
+ return native.release();
207
+ },
208
+ async dispose() {
209
+ return native.dispose();
210
+ },
211
+ };
212
+ // Symbol.asyncDispose support (Node 20+ disposer pattern).
213
+ if (typeof Symbol.asyncDispose === "symbol") {
214
+ handle[Symbol.asyncDispose] = () => handle.release();
215
+ }
216
+ return handle;
217
+ }
218
+
219
+ function wrapExecChild(native) {
220
+ return {
221
+ async writeStdin(bytes) {
222
+ return native.writeStdin(bytes);
223
+ },
224
+ async closeStdin() {
225
+ return native.closeStdin();
226
+ },
227
+ async readStdout(maxBytes) {
228
+ return native.readStdout(maxBytes ?? null);
229
+ },
230
+ async readStderr(maxBytes) {
231
+ return native.readStderr(maxBytes ?? null);
232
+ },
233
+ async signal(signum) {
234
+ return native.signal(signum);
235
+ },
236
+ async resize(cols, rows) {
237
+ return native.resize(cols, rows);
238
+ },
239
+ async wait() {
240
+ return native.wait();
241
+ },
242
+ };
243
+ }
244
+
245
+ // Thin wrapper around the napi TcpForwarder class. We could return
246
+ // the raw instance, but the shim approach keeps the JS API
247
+ // consistent (everything else is a plain object) and lets us add
248
+ // JS-only behavior (e.g. a `Symbol.dispose` future) without
249
+ // reaching into the napi class.
250
+ function wrapTcpForwarder(native) {
251
+ return {
252
+ get localAddr() {
253
+ return native.localAddr;
254
+ },
255
+ async stop() {
256
+ return native.stop();
257
+ },
258
+ };
259
+ }
260
+
261
+ // Build a Vm-like object backed by an exec-socket path. Used inside
262
+ // the warmup callback — the Rust side passes us the path string,
263
+ // JS code uses this object as if it were a normal Vm.
264
+ function makeWarmupVm(execPath) {
265
+ let valid = true;
266
+ const vm = {
267
+ async exec(options) {
268
+ if (!valid) {
269
+ throw new Error(
270
+ "warmup Vm is no longer valid — the warmup callback returned. " +
271
+ "Don't hold references to the Vm past the callback; do all " +
272
+ "your exec calls inside it.",
273
+ );
274
+ }
275
+ return addon.execAtPath(execPath, options);
276
+ },
277
+ _invalidate() {
278
+ valid = false;
279
+ },
280
+ };
281
+ // Disallow `using vm` on warmup vms — they're not regular pool
282
+ // entries and don't have a release-to-pool semantics.
283
+ return vm;
284
+ }
285
+
286
+ module.exports = {
287
+ Image,
288
+ Pool: addon.Pool,
289
+ Vm: addon.Vm,
290
+ };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@supermachine/core",
3
+ "version": "0.4.25",
4
+ "description": "Run any OCI/Docker image as a hardware-isolated microVM. Node/Bun/Deno binding for the supermachine Rust crate.",
5
+ "license": "Apache-2.0",
6
+ "main": "index.js",
7
+ "types": "index.d.ts",
8
+ "files": [
9
+ "index.js",
10
+ "index.d.ts",
11
+ "README.md",
12
+ "LICENSE",
13
+ "examples/"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18.17.0"
17
+ },
18
+ "os": [
19
+ "darwin"
20
+ ],
21
+ "cpu": [
22
+ "arm64"
23
+ ],
24
+ "optionalDependencies": {
25
+ "@supermachine/core-darwin-arm64": "0.4.25"
26
+ },
27
+ "scripts": {
28
+ "build:rust": "TYPE_DEF_TMP_PATH=$(pwd)/scripts/.napi_type_defs.tmp cargo build --release -p supermachine-napi && cargo build --release --bin supermachine-worker -p supermachine",
29
+ "build:dts": "node ./scripts/gen-dts.js ./scripts/.napi_type_defs.tmp ./index.d.ts",
30
+ "build": "rm -f ./scripts/.napi_type_defs.tmp && npm run build:rust && npm run build:dts && node ./scripts/postbuild.js",
31
+ "test": "vitest run",
32
+ "test:watch": "vitest"
33
+ },
34
+ "devDependencies": {
35
+ "@napi-rs/cli": "^3.6.2",
36
+ "typescript": "^6.0.3",
37
+ "vitest": "^2.1.0"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/supercorp/supermachine"
42
+ },
43
+ "homepage": "https://github.com/supercorp/supermachine/tree/main/npm/supermachine-core#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/supercorp/supermachine/issues"
46
+ },
47
+ "keywords": [
48
+ "microvm",
49
+ "hypervisor",
50
+ "oci",
51
+ "docker",
52
+ "snapshot",
53
+ "sandbox",
54
+ "vm",
55
+ "macos",
56
+ "hvf",
57
+ "apple-silicon"
58
+ ]
59
+ }