@neosmart/ssclient 2.0.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.
package/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # `ssclient`, the SecureStore CLI
2
+
3
+ This npmjs package contains a universal binary installer for [`ssclient`](https://github.com/neosmart/securestore-rs/tree/master/ssclient) the rust cli companion to the open source, cross-platform, language-agnostic [SecureStore secrets management protocol](https://neosmart.net/SecureStore/).
4
+
5
+ This package will attempt to automatically download and install precompiled, dependency-free binaries for your os/architecture via the npmjs package repos. Please refer to the [`ssclient` documentation](https://github.com/neosmart/securestore-rs/tree/master/ssclient) for more information on the use of `ssclient` to create and manage truly secure secrets containers and help put an end to `.env` madness and insecurity.
6
+
7
+ ## Installation
8
+
9
+ This package is compatible with `npm` and `bun`, and can be installed or run directly with with `npx`/`bunx`.
10
+
11
+ ```sh
12
+ npm install [--global] @neosmart/ssclient
13
+ ```
14
+
15
+ or
16
+
17
+ ```sh
18
+ bun --global add @neosmart/ssclient
19
+ ```
20
+
21
+ Will attempt to locate, download, and install a precompiled, dependency-free `ssclient` binary for your current platform/architecture. If no such precompiled binary currently exists, a WASM-compiled version of the binary [will be deployed instead](https://neosmart.net/blog/ssclient-wasm).
22
+
23
+ ## Usage
24
+
25
+ Please refer to the [`ssclient` documentation](https://github.com/neosmart/securestore-rs/tree/master/ssclient) for full usage information, but the following is a brief guide on creating and access secrets with the `ssclient` cli:
26
+
27
+ Note that you may substitute `npx @neosmart/ssclient` or `bunx @neosmart/ssclient` for `ssclient` in the steps below if you have not globally installed `ssclient` via whatever means you prefer.
28
+
29
+ ### Creating a new secrets container
30
+
31
+ ```bash
32
+ ~> mkdir secrets/
33
+ ~> cd secrets/
34
+ ~> ssclient create --export-key secrets.key
35
+ Password: ************
36
+ Confirm Password: ************
37
+
38
+ # Now you can use `ssclient -p` with your old password
39
+ # or `ssclient -k secrets.key` to encrypt/decrypt with
40
+ # the same keys.
41
+ ```
42
+
43
+ ### Adding secrets
44
+
45
+ Secrets may be added with your password or the equivalent encryption key file, and may be specified in-line as arguments to `ssclient` or more securely at a prompt by omitting the value when calling `ssclient create`:
46
+
47
+ ```bash
48
+ # ssclient defaults to password-based decryption:
49
+ ~> ssclient set aws:s3:accessId AKIAV4EXAMPLE7QWERT
50
+ Password: *********
51
+ ```
52
+
53
+ similarly:
54
+
55
+ ```bash
56
+ # Use `-k secrets.key` to load the encryption key and
57
+ # skip the prompt for the vault password:
58
+ ~> ssclient -k secrets.key set aws:s3:accessKey
59
+ Value: v1Lp9X7mN2B5vR8zQ4tW1eY6uI0oP3aS5dF7gH9j
60
+ ```
61
+
62
+ Note that in the latter example, the secret being stored was not typed into the shell (bash, fish, etc) and, as such, is not contained in/leaked by the shell history.
63
+
64
+ ### Retrieving secrets
65
+
66
+ While you normally use one of the [SecureStore libraries for your language of choice](https://neosmart.net/SecureStore/) to load the SecureStore vault and decrypt secrets with the exported encryption/decryption key (`secrets.key`, in the example above), you can also use `ssclient` to view them at the command line:
67
+
68
+ ```bash
69
+ ~> ssclient get aws:s3:accessKey
70
+ Password: *********
71
+ v1Lp9X7mN2B5vR8zQ4tW1eY6uI0oP3aS5dF7gH9j
72
+ ```
73
+
74
+ or you can export all secrets, either as text or json (again, either with the password or with the equivalent private key).
75
+
76
+ ```bash
77
+ ~> ssclient get --all # defaults to JSON
78
+ Password: *********
79
+ [
80
+ {
81
+ "key": "aws:s3:accessId",
82
+ "value": "AKIAV4EXAMPLE7QWERT"
83
+ },
84
+ {
85
+ "key": "aws:s3:accessKey",
86
+ "value": "v1Lp9X7mN2B5vR8zQ4tW1eY6uI0oP3aS5dF7gH9j"
87
+ }
88
+ ]
89
+ ```
90
+
91
+ or
92
+
93
+ ```bash
94
+ ~> ssclient get --key secrets.key -a --format text
95
+ aws:s3:accessId:AKIAV4EXAMPLE7QWERT
96
+ aws:s3:accessKey:v1Lp9X7mN2B5vR8zQ4tW1eY6uI0oP3aS5dF7gH9j
97
+ ```
98
+
99
+ Please refer to the [`ssclient` documentation](https://github.com/neosmart/securestore-rs/tree/master/ssclient) for full usage information.
@@ -0,0 +1 @@
1
+ This placeholder file will be overridden by our install scripts
Binary file
package/build.js ADDED
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+
3
+ // @ts-check
4
+
5
+ import path from 'node:path';
6
+ import fs from 'node:fs';
7
+ import { spawnSync } from 'node:child_process';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ const cargoRoot = path.resolve(__dirname, '..', '..');
14
+ const srcWasm = path.resolve(cargoRoot, 'target/wasm32-wasip1/release/ssclient.wasm');
15
+ const dstDir = path.resolve(__dirname, 'bin');
16
+ const dstWasm = path.resolve(dstDir, 'ssclient.wasm');
17
+ const ssclient = path.resolve(__dirname, 'ssclient.js');
18
+
19
+ /**
20
+ * Helper to exit with a non-zero status code
21
+ * @param {string} message
22
+ */
23
+ const die = (message) => {
24
+ console.error(`[BUILD ERROR] ${message}`);
25
+ process.exit(1);
26
+ };
27
+
28
+ console.log(`> cd ${cargoRoot}`);
29
+
30
+ /** @type {import('node:child_process').SpawnSyncOptions} */
31
+ const spawnOptions = {
32
+ cwd: cargoRoot,
33
+ stdio: 'inherit',
34
+ env: {
35
+ ...process.env,
36
+ RUSTFLAGS: '', // Explicitly clear RUSTFLAGS
37
+ },
38
+ shell: false
39
+ };
40
+
41
+ const cargoArgs = [
42
+ 'build',
43
+ '--no-default-features',
44
+ '--features', 'rustls',
45
+ '--target', 'wasm32-wasip1',
46
+ '--release'
47
+ ];
48
+
49
+ console.log(`> cargo ${cargoArgs.join(' ')}`);
50
+
51
+ const result = spawnSync('cargo', cargoArgs, spawnOptions);
52
+
53
+ if (result.error) {
54
+ die(`Failed to execute cargo: ${result.error.message}`);
55
+ }
56
+
57
+ if (result.status !== 0) {
58
+ die(`Cargo build failed with exit code ${result.status}`);
59
+ }
60
+
61
+ try {
62
+ if (!fs.existsSync(dstDir)) {
63
+ console.log(`Creating directory: ${dstDir}`);
64
+ fs.mkdirSync(dstDir, { recursive: true });
65
+ }
66
+
67
+ console.log(`Copying build artifact:\n From: ${srcWasm}\n To: ${dstWasm}`);
68
+
69
+ if (!fs.existsSync(srcWasm)) {
70
+ die(`Source file not found at ${srcWasm}`);
71
+ }
72
+
73
+ fs.copyFileSync(srcWasm, dstWasm);
74
+ console.log('Build and copy successful.');
75
+ } catch (err) {
76
+ /** @type {Error} */
77
+ // @ts-ignore
78
+ const error = err;
79
+ die(`File operation failed: ${error.message}`);
80
+ }
81
+
82
+ // Ensure ssclient.js runs ok
83
+ const wasmResult = spawnSync('node', [ssclient, "--version"]);
84
+ if (wasmResult.error) {
85
+ die(`Failed to execute ssclient.wasm (via ssclient.js): ${wasmResult.error.message}`);
86
+ } else if (wasmResult.status !== 0) {
87
+ die(`ssclient.wasm failed with exit code ${wasmResult.status}`);
88
+ }
package/install.js ADDED
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env node
2
+
3
+ // @ts-check
4
+ "use strict";
5
+
6
+ import { mkdir, rm, rename, unlink, symlink, copyFile, chmod, readdir, readFile, writeFile } from "node:fs/promises";
7
+ import { createWriteStream, existsSync } from "node:fs";
8
+ import { finished } from 'node:stream/promises';
9
+ import { join, relative, basename, dirname } from "node:path";
10
+ import { execSync } from "node:child_process";
11
+ import { tmpdir } from "node:os";
12
+ import { randomBytes } from "node:crypto";
13
+ import { fileURLToPath } from 'node:url';
14
+ import pkg from "./package.json" with { type: "json" };
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = dirname(__filename);
18
+
19
+ /**
20
+ * @typedef {Object} BuildEntry
21
+ * @property {string | string[]} url - One or more URLs to try
22
+ * @property {string} [bin_path] - Override default bin_path
23
+ */
24
+
25
+ /**
26
+ * @typedef {Object} Manifest
27
+ * @property {string} bin_path - Default path to entrypoint within archives
28
+ * @property {Record<string, string | string[] | BuildEntry>} precompiled - Mapping of os-arch to builds
29
+ */
30
+
31
+ const MANIFEST_URLS = [
32
+ `${__dirname}/manifest.json`,
33
+ `https://raw.githubusercontent.com/neosmart/securestore-rs/refs/heads/master/ssclient/npm/manifests/v${pkg.version}.json`,
34
+ `https://neosmart.net/SecureStore/ssclient/npm/manifests/v${pkg.version}.json`,
35
+ `https://raw.githubusercontent.com/neosmart/securestore-rs/refs/tags/ssclient/${pkg.version}/ssclient/npm/manifests/v${pkg.version}.json`,
36
+ ];
37
+ const BIN_NAME = "ssclient";
38
+ const PKG_ROOT = __dirname;
39
+ const FALLBACK_JS = join(PKG_ROOT, "ssclient.js");
40
+ const BASE_BIN_DIR = join(PKG_ROOT, "bin");
41
+ const VERSION = pkg.version;
42
+ const VERSIONED_DIR = join(BASE_BIN_DIR, `v${VERSION}`);
43
+ const ENTRY_POINT = join(__dirname, pkg.bin[BIN_NAME]);
44
+
45
+ /** @returns {string} */
46
+ function getPlatformTuple() {
47
+ return `${process.platform}-${process.arch}`;
48
+ }
49
+
50
+ /** @param {string} urlStr @returns "string" */
51
+ function extractUrlFileName(urlStr) {
52
+ if (urlStr.startsWith('http')) {
53
+ const urlPath = new URL(urlStr).pathname;
54
+ return /[^/]+$/.exec(urlPath)?.[0] ?? urlPath;
55
+ }
56
+ const fname = /[^/\\]+$/.exec(urlStr)?.[0];
57
+ if (fname) {
58
+ return fname;
59
+ }
60
+ throw new Error(`Could not extract file name from "${urlStr}"`);
61
+ }
62
+
63
+ /** @param {string} urlStr @returns {"zip" | "tar"} */
64
+ function getArchiveType(urlStr) {
65
+ const fname = extractUrlFileName(urlStr);
66
+ if (fname.endsWith(".zip")) return "zip";
67
+ if (/\.tar(\.[^.]+)?$|\.tgz$/.test(fname)) return "tar";
68
+ throw new Error(`Unsupported archive type for ${fname}`);
69
+ }
70
+
71
+ /**
72
+ * Executes a callback when the variable goes out of scope.
73
+ * @param {() => void} callback
74
+ * @returns {Disposable}
75
+ */
76
+ export const defer = (callback) => ({
77
+ [Symbol.dispose]: callback
78
+ });
79
+
80
+ /**
81
+ * Tries to download from an array of URLs.
82
+ * @param {string[]} urls
83
+ * @param {string} dest
84
+ */
85
+ async function downloadWithRetry(urls, dest) {
86
+ const urlArray = Array.isArray(urls) ? urls : [urls];
87
+ let lastErr;
88
+
89
+ for (const url of urlArray) {
90
+ try {
91
+ console.log(`Downloading: ${url}`);
92
+ if (/^https?:/.test(url)) {
93
+ return await download(url, dest);
94
+ } else if (existsSync(url)) {
95
+ return await copyFile(url, dest);
96
+ }
97
+ throw new Error(`Path not found: ${url}`);
98
+ } catch (err) {
99
+ const msg = err instanceof Error ? err.message : err?.toString();
100
+ console.warn(`Failed to download from ${url}: ${msg}`);
101
+ lastErr = err;
102
+ }
103
+ }
104
+ throw lastErr;
105
+ }
106
+
107
+ /**
108
+ * @param {string | URL} url
109
+ * @param {import("node:fs").PathLike} dest
110
+ */
111
+ async function download(url, dest) {
112
+ const response = await fetch(url);
113
+ if (!response.ok) {
114
+ throw new Error(`HTTP ${response.status}`);
115
+ }
116
+ if (!response.body) {
117
+ throw new Error("Invalid response body");
118
+ }
119
+
120
+ const fileStream = createWriteStream(dest);
121
+ const reader = response.body.getReader();
122
+ while (true) {
123
+ const { done, value } = await reader.read();
124
+ if (done) {
125
+ break;
126
+ }
127
+ fileStream.write(value);
128
+ }
129
+ fileStream.end();
130
+ return finished(fileStream);
131
+ }
132
+
133
+ /**
134
+ * Atomic extraction into the versioned folder.
135
+ * @param {string} archivePath
136
+ * @param {string} binPath - Path to the binary within the archive
137
+ * @returns {Promise<{dir: string, bin: string}>} - Path to the extracted release directory and binary
138
+ */
139
+ async function extractRelease(archivePath, binPath) {
140
+ const archiveType = getArchiveType(archivePath);
141
+ console.log(`Extracting ${archiveType}...`);
142
+
143
+ const stagingDir = join(BASE_BIN_DIR, `.staging-${randomBytes(4).toString("hex")}`);
144
+ await mkdir(stagingDir, { recursive: true });
145
+
146
+ if (archiveType === "tar") {
147
+ try {
148
+ execSync(`tar -xf "${archivePath}" -C "${stagingDir}"`);
149
+ }
150
+ catch (err) {
151
+ if (process.platform === "win32") {
152
+ throw new Error(`Unsupported archive type for platform ${process.platform}`);
153
+ }
154
+ throw err;
155
+ }
156
+ } else {
157
+ if (process.platform === "win32") {
158
+ execSync(`powershell -command "Expand-Archive -Path '${archivePath}' -DestinationPath '${stagingDir}'"`);
159
+ } else {
160
+ execSync(`unzip "${archivePath}" -d "${stagingDir}"`);
161
+ }
162
+ }
163
+
164
+ // Add Windows .exe suffix if not accounted for in manifest
165
+ let sourcePath = join(stagingDir, binPath);
166
+ if (process.platform === "win32" && !sourcePath.endsWith(".exe")) {
167
+ if (!existsSync(sourcePath) && existsSync(sourcePath + ".exe")) {
168
+ sourcePath += ".exe";
169
+ }
170
+ }
171
+
172
+ // Atomically move staging to versioned dir
173
+ if (existsSync(VERSIONED_DIR)) {
174
+ await rm(VERSIONED_DIR, { recursive: true });
175
+ }
176
+ await rename(stagingDir, VERSIONED_DIR);
177
+
178
+ let bin = join(VERSIONED_DIR, binPath);
179
+ if (process.platform === "win32" && !sourcePath.endsWith(".exe")) {
180
+ if (!existsSync(bin) && existsSync(bin + ".exe")) {
181
+ bin += ".exe";
182
+ }
183
+ }
184
+
185
+ return {
186
+ dir: VERSIONED_DIR,
187
+ bin,
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Creates a symlink or copies the file, dependent on platform.
193
+ * @param {string} target
194
+ * @param {string} linkPath
195
+ */
196
+ async function linkBinary(target, linkPath) {
197
+ try {
198
+ // Don't check if it exists first because we need to also remove broken symlinks
199
+ await unlink(linkPath);
200
+ } catch (err) {
201
+ console.error(`Failed to remove existing binary: ${err}`);
202
+ throw err;
203
+ }
204
+
205
+ const rel = relative(dirname(linkPath), target);
206
+ if (process.platform === "win32") {
207
+ const runner = `@ECHO OFF\nSETLOCAL\n"%~dp0${rel}" %*`;
208
+ await writeFile(linkPath, runner, "utf8");
209
+ } else {
210
+ // *nix: Always create relative symlink
211
+ await symlink(rel, linkPath);
212
+ await chmod(target, 0o755);
213
+ }
214
+ }
215
+
216
+ async function removeOldVersions() {
217
+ const items = await readdir(BASE_BIN_DIR);
218
+ const currentName = basename(VERSIONED_DIR);
219
+ for (const item of items) {
220
+ if (item.startsWith(`v`) && item !== currentName) {
221
+ await rm(join(BASE_BIN_DIR, item), { recursive: true, force: true }).catch(() => {});
222
+ }
223
+ }
224
+ }
225
+
226
+ /**
227
+ * @overload
228
+ * @param {string | URL} pathOrUrl
229
+ * @param {"text"} result
230
+ * @returns {Promise<string>}
231
+ */
232
+ /**
233
+ * @overload
234
+ * @param {string | URL} pathOrUrl
235
+ * @param {"json"} result
236
+ * @returns {Promise<any>}
237
+ */
238
+ /**
239
+ * @param {string | URL} pathOrUrl
240
+ * @param {"text" | "json"} result
241
+ * @returns {Promise<string | any>}
242
+ */
243
+ async function resolve(pathOrUrl, result) {
244
+ if (/^https?:/.test(pathOrUrl.toString())) {
245
+ const response = await fetch(pathOrUrl);
246
+ if (!response.ok) {
247
+ throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
248
+ }
249
+ return result === "text" ? await response.text() : await response.json();
250
+ } else if (existsSync(pathOrUrl)) {
251
+ const text = await readFile(pathOrUrl, { encoding: "utf8" });
252
+ return result === "text" ? text : JSON.parse(text);
253
+ } else {
254
+ throw new Error(`Unable to resolve path/url ${pathOrUrl}`);
255
+ }
256
+ }
257
+
258
+ async function main() {
259
+ let downgradeError = false;
260
+ try {
261
+ await mkdir(BASE_BIN_DIR, { recursive: true });
262
+
263
+ console.log("Fetching manifest...");
264
+
265
+ /** @type {Manifest} */
266
+ const manifest = await (async () => {
267
+ for (const url of MANIFEST_URLS) {
268
+ try {
269
+ const manifest = await resolve(url, "json");
270
+ return manifest;
271
+ } catch {
272
+ continue;
273
+ }
274
+ }
275
+ throw new Error("Unable to load application manifest");
276
+ })();
277
+ const tuple = getPlatformTuple();
278
+ const entry = manifest.precompiled[tuple];
279
+
280
+ if (!entry) {
281
+ downgradeError = true;
282
+ throw new Error(`Precompiled binaries for ${tuple} not found`);
283
+ }
284
+
285
+ // Normalize manifest entry:
286
+ // We allow tuples to point to either a "url like" (url or array of urls)
287
+ // or an object containing a bin_path override + a "url like".
288
+ const urls = (typeof entry === "string" ? [entry] : Array.isArray(entry) ? entry : (Array.isArray(entry.url) ? entry.url : [entry.url]))
289
+ .filter(url => !!url);
290
+ const binPathInside = (typeof entry === "object" && !Array.isArray(entry) && entry.bin_path) || manifest.bin_path;
291
+
292
+ if (!urls[0]) {
293
+ throw new Error(`Missing a url for target ${tuple}`);
294
+ }
295
+ const archivePath = join(tmpdir(), extractUrlFileName(urls[0]));
296
+ using _ = defer(async () => {
297
+ if (existsSync(archivePath)) {
298
+ await rm(archivePath);
299
+ }
300
+ });
301
+ await downloadWithRetry(urls, archivePath);
302
+
303
+ const { bin} = await extractRelease(archivePath, binPathInside);
304
+
305
+ await linkBinary(bin, ENTRY_POINT);
306
+ await removeOldVersions();
307
+
308
+ console.log(`Success: Installed precompiled native ${tuple} ${BIN_NAME}`);
309
+ } catch (err) {
310
+ const msg = err instanceof Error ? err.message : err?.toString();
311
+ if (downgradeError) {
312
+ console.info(msg);
313
+ } else {
314
+ console.warn(`Binary installation failed: ${msg}.`);
315
+ }
316
+
317
+ console.info(`Falling back to wasm ${BIN_NAME}...`);
318
+
319
+ if (existsSync(ENTRY_POINT)) {
320
+ await unlink(ENTRY_POINT);
321
+ }
322
+ if (process.platform !== "win32") {
323
+ await linkBinary(FALLBACK_JS, ENTRY_POINT);
324
+ } else {
325
+ const rel = relative(dirname(ENTRY_POINT), FALLBACK_JS);
326
+ console.debug({
327
+ rel,
328
+ ENTRY_POINT,
329
+ FALLBACK_JS,
330
+ });
331
+ const runner = `@ECHO OFF\nSETLOCAL\nnode --experimental-wasi-unstable-preview1 "%~dp0${rel}" %*`;
332
+ await writeFile(ENTRY_POINT, runner, "utf8");
333
+ }
334
+ }
335
+ }
336
+
337
+ main();
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@neosmart/ssclient",
3
+ "description": "SecureStore cli client, cross-platform and sandboxed",
4
+ "version": "2.0.0",
5
+ "keywords": [
6
+ "SecureStore",
7
+ "encryption",
8
+ "secrets"
9
+ ],
10
+ "homepage": "https://neosmart.net/SecureStore/",
11
+ "bugs": "https://github.com/neosmart/securestore-rs",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/neosmart/securestore-rs.git"
15
+ },
16
+ "license": "MIT",
17
+ "author": {
18
+ "name": "Mahmoud Al-Qudsi",
19
+ "email": "mqudsi@neosmart.net",
20
+ "url": "https://github.com/mqudsi"
21
+ },
22
+ "funding": "https://mqudsi.com/donate/",
23
+ "bin": {
24
+ "ssclient": "bin/ssclient.cmd"
25
+ },
26
+ "files": [
27
+ "./README.md",
28
+ "./bin/ssclient.cmd",
29
+ "./bin/ssclient.wasm",
30
+ "./build.js",
31
+ "./install.js",
32
+ "./ssclient.js",
33
+ "./tsconfig.js"
34
+ ],
35
+ "type": "module",
36
+ "scripts": {
37
+ "prepublishOnly": "npm run lint && npm run build",
38
+ "prepare": "echo 'This placeholder file will be overridden by our install scripts' > bin/ssclient.cmd",
39
+ "install": "node ./install.js",
40
+ "ssclient": "node ./ssclient.js",
41
+ "build": "node ./build.js",
42
+ "lint": "tsc"
43
+ },
44
+ "devDependecies": [
45
+ "@types/node"
46
+ ],
47
+ "devDependencies": {
48
+ "@types/node": "^22.0.0",
49
+ "typescript": "^6.0.2"
50
+ }
51
+ }
package/ssclient.js ADDED
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { join, dirname, resolve } from "node:path";
4
+ import { fileURLToPath } from 'node:url';
5
+ import { readFile } from "node:fs/promises";
6
+ import { spawn } from "node:child_process";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ const WASM_FILE = join(__dirname, "bin/ssclient.wasm");
12
+
13
+ // Silence node warnings about WASI being in preview, as we test functionality ourselves
14
+ if (process && process.emitWarning) {
15
+ const emitWarning = process.emitWarning;
16
+ process.emitWarning = (warning, type) => {
17
+ if (type === "ExperimentalWarning") {
18
+ if (warning.toString().includes("WASI")) {
19
+ return false;
20
+ }
21
+ }
22
+ // @ts-ignore
23
+ return emitWarning(arguments);
24
+ };
25
+ }
26
+
27
+ // Import WASI dynamically to ensure our ExperimentalWarning intercept takes place first
28
+ // Also figure out if we need to relaunch with a flag for compatibility with older node versions.
29
+
30
+ /** @type {typeof import("node:wasi").WASI} */
31
+ let WASI;
32
+ try {
33
+ const wasiModule = await import("node:wasi");
34
+ WASI = wasiModule.WASI;
35
+ } catch (e) {
36
+ // If we can't import it, we need the --experimental-wasi-unstable-preview1 flag.
37
+ const scriptPath = fileURLToPath(import.meta.url);
38
+
39
+ if (process.argv.find(arg => arg === "--experimental-wasi-unstable-preview1")) {
40
+ // Already tried launching with this flag and it didn't work
41
+ console.error(`Unable to load WASI module: ${e}`);
42
+ process.exit(1);
43
+ }
44
+
45
+ // Re-spawn the current process with the flag enabled
46
+ const child = spawn(
47
+ process.execPath,
48
+ ["--experimental-wasi-unstable-preview1", scriptPath, ...process.argv.slice(2)],
49
+ { stdio: "inherit" }
50
+ );
51
+
52
+ child.on("exit", (code) => process.exit(code ?? 0));
53
+ // Block indefinitely until child has exited
54
+ await new Promise(() => {});
55
+ }
56
+
57
+ /**
58
+ * @param {string} payload - Path to the WASM binary
59
+ */
60
+ async function runWasm(payload) {
61
+ // Extract node-specific values
62
+ const args = process.argv.slice(2);
63
+ const cwd = process.cwd();
64
+ const env = process.env;
65
+
66
+ const wasi = new WASI({
67
+ version: "preview1",
68
+ args: ["ssclient", ...args],
69
+ env: env,
70
+ // Map paths we need into the sandbox
71
+ preopens: {
72
+ [cwd]: cwd,
73
+ ".": ".",
74
+ },
75
+ });
76
+
77
+ // wasi.getImportObject() isn't yet supported by bun
78
+ // Reported upstream at https://github.com/oven-sh/bun/issues/28534
79
+ /** @type {any} */
80
+ const wasiImportObject = wasi.getImportObject ? wasi.getImportObject()
81
+ : { wasi_snapshot_preview1: wasi.wasiImport };
82
+
83
+ try {
84
+ const wasmPath = resolve(payload);
85
+ const wasmBuffer = await readFile(wasmPath);
86
+ const wasmModule = await WebAssembly.compile(wasmBuffer);
87
+ const instance = await WebAssembly.instantiate(
88
+ wasmModule,
89
+ wasiImportObject,
90
+ );
91
+
92
+ wasi.start(instance);
93
+ } catch (err) {
94
+ const msg = err instanceof Error ? err.message : err?.toString();
95
+ console.error(`Error executing ${payload}: `, msg);
96
+ process.exit(1);
97
+ }
98
+ }
99
+
100
+ await runWasm(WASM_FILE);