@suiflex/safehell 0.0.0 → 0.2.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 CHANGED
@@ -1,12 +1,15 @@
1
1
  # @suiflex/safehell
2
2
 
3
- Placeholder release. It contains no code.
4
-
5
- npm cannot attach a trusted publisher to a package that does not exist, so this
6
- version exists only to create the package name. Install `0.2.0` or later:
3
+ Approval-gated SSH broker for AI coding agents.
7
4
 
8
5
  ```sh
9
6
  npm install -g @suiflex/safehell
7
+ safehell setup
10
8
  ```
11
9
 
12
- Source: https://github.com/suiflex/SafeHell
10
+ Installing downloads the release binary for your platform from
11
+ [suiflex/SafeHell](https://github.com/suiflex/SafeHell) and verifies it against
12
+ the release `SHA256SUMS` before use.
13
+
14
+ Full documentation lives in the
15
+ [repository README](https://github.com/suiflex/SafeHell#readme).
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ // Launcher: hands every argument to the vendored binary and propagates its exit
3
+ // code, so `npx @suiflex/safehell ...` behaves like the real CLI.
4
+
5
+ "use strict";
6
+
7
+ const fs = require("fs");
8
+ const path = require("path");
9
+ const { spawnSync } = require("child_process");
10
+
11
+ const binary = path.join(__dirname, process.platform === "win32" ? "safehell.exe" : "safehell");
12
+
13
+ if (!fs.existsSync(binary)) {
14
+ process.stderr.write(
15
+ "safehell: the binary is missing. The postinstall download may have been " +
16
+ "skipped or blocked.\nReinstall with: npm rebuild @suiflex/safehell\n"
17
+ );
18
+ process.exit(1);
19
+ }
20
+
21
+ const result = spawnSync(binary, process.argv.slice(2), { stdio: "inherit" });
22
+ if (result.error) {
23
+ process.stderr.write(`safehell: ${result.error.message}\n`);
24
+ process.exit(1);
25
+ }
26
+ // A signalled child has a null status; report it the way a shell would.
27
+ process.exit(result.status === null ? 1 : result.status);
package/install.js ADDED
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env node
2
+ // Downloads the SafeHell binary matching this machine from the GitHub release
3
+ // whose tag corresponds to this package's version, and vendors it next to the
4
+ // launcher in bin/.
5
+ //
6
+ // The binaries are far too large to ship six copies of in one npm tarball, and
7
+ // splitting them across per-platform optional dependencies means six more
8
+ // packages to publish and keep in step. Fetching at install time keeps this
9
+ // package to a few kilobytes.
10
+ //
11
+ // Run `node install.js --selftest` to check the platform mapping without
12
+ // touching the network.
13
+
14
+ "use strict";
15
+
16
+ const fs = require("fs");
17
+ const os = require("os");
18
+ const path = require("path");
19
+ const https = require("https");
20
+ const crypto = require("crypto");
21
+ const { execFileSync } = require("child_process");
22
+
23
+ const REPO = "suiflex/SafeHell";
24
+
25
+ // node platform:arch -> [release asset stem, archive extension, binary name]
26
+ const TARGETS = {
27
+ "darwin:arm64": ["safehell-macos-aarch64", "tar.gz", "safehell"],
28
+ "darwin:x64": ["safehell-macos-x86_64", "tar.gz", "safehell"],
29
+ "linux:x64": ["safehell-linux-x86_64", "tar.gz", "safehell"],
30
+ "linux:arm64": ["safehell-linux-aarch64", "tar.gz", "safehell"],
31
+ "win32:x64": ["safehell-windows-x86_64", "zip", "safehell.exe"],
32
+ "win32:arm64": ["safehell-windows-aarch64", "zip", "safehell.exe"],
33
+ };
34
+
35
+ function resolveTarget(platform, arch) {
36
+ const target = TARGETS[`${platform}:${arch}`];
37
+ if (!target) {
38
+ throw new Error(
39
+ `SafeHell has no prebuilt binary for ${platform}/${arch}. ` +
40
+ `Install it from source with: cargo install safehell`
41
+ );
42
+ }
43
+ return { stem: target[0], ext: target[1], binary: target[2] };
44
+ }
45
+
46
+ // Follows redirects by hand: GitHub serves release assets from a signed
47
+ // object-storage URL, and adding a redirect-following HTTP client would be the
48
+ // only dependency this package has.
49
+ function download(url, hops = 5) {
50
+ return new Promise((resolve, reject) => {
51
+ if (hops < 0) {
52
+ reject(new Error("too many redirects"));
53
+ return;
54
+ }
55
+ https
56
+ .get(url, { headers: { "user-agent": "safehell-npm-installer" } }, (response) => {
57
+ const { statusCode, headers } = response;
58
+ if (statusCode >= 300 && statusCode < 400 && headers.location) {
59
+ response.resume();
60
+ resolve(download(new URL(headers.location, url).toString(), hops - 1));
61
+ return;
62
+ }
63
+ if (statusCode !== 200) {
64
+ response.resume();
65
+ reject(new Error(`GET ${url} failed with HTTP ${statusCode}`));
66
+ return;
67
+ }
68
+ const chunks = [];
69
+ response.on("data", (chunk) => chunks.push(chunk));
70
+ response.on("end", () => resolve(Buffer.concat(chunks)));
71
+ response.on("error", reject);
72
+ })
73
+ .on("error", reject);
74
+ });
75
+ }
76
+
77
+ // The download is unpacked and then executed, so it is verified first. The
78
+ // shell installer refuses to install without SHA256SUMS and this must not be
79
+ // the weaker path to the same binary.
80
+ function verify(sums, asset, archive) {
81
+ const line = sums
82
+ .split("\n")
83
+ .find((entry) => entry.trim().endsWith(` ${asset}`) || entry.trim().endsWith(`*${asset}`));
84
+ if (!line) {
85
+ throw new Error(`SHA256SUMS has no entry for ${asset}`);
86
+ }
87
+ const expected = line.trim().split(/\s+/)[0];
88
+ const actual = crypto.createHash("sha256").update(archive).digest("hex");
89
+ if (actual !== expected) {
90
+ throw new Error(`checksum mismatch for ${asset} (expected ${expected}, got ${actual})`);
91
+ }
92
+ }
93
+
94
+ async function main() {
95
+ const { version } = require("./package.json");
96
+ const { stem, ext, binary } = resolveTarget(process.platform, process.arch);
97
+ const asset = `${stem}.${ext}`;
98
+ const base = `https://github.com/${REPO}/releases/download/v${version}`;
99
+
100
+ const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "safehell-"));
101
+ try {
102
+ process.stderr.write(`Downloading ${asset} v${version}\n`);
103
+ const [archive, sums] = await Promise.all([
104
+ download(`${base}/${asset}`),
105
+ download(`${base}/SHA256SUMS`),
106
+ ]);
107
+ verify(sums.toString("utf8"), asset, archive);
108
+
109
+ const archivePath = path.join(scratch, asset);
110
+ fs.writeFileSync(archivePath, archive);
111
+ // bsdtar reads zip as well as tar.gz, and ships with macOS, modern Windows,
112
+ // and every Linux image these binaries target.
113
+ execFileSync("tar", ["-xf", archivePath, "-C", scratch], { stdio: "inherit" });
114
+
115
+ const vendor = path.join(__dirname, "bin");
116
+ fs.mkdirSync(vendor, { recursive: true });
117
+ const destination = path.join(vendor, binary);
118
+ fs.copyFileSync(path.join(scratch, binary), destination);
119
+ fs.chmodSync(destination, 0o755);
120
+ process.stderr.write(`Installed safehell v${version}\n`);
121
+ } finally {
122
+ fs.rmSync(scratch, { recursive: true, force: true });
123
+ }
124
+ }
125
+
126
+ function selftest() {
127
+ const assert = require("assert");
128
+ assert.deepStrictEqual(resolveTarget("darwin", "arm64"), {
129
+ stem: "safehell-macos-aarch64",
130
+ ext: "tar.gz",
131
+ binary: "safehell",
132
+ });
133
+ assert.deepStrictEqual(resolveTarget("win32", "x64"), {
134
+ stem: "safehell-windows-x86_64",
135
+ ext: "zip",
136
+ binary: "safehell.exe",
137
+ });
138
+ assert.throws(() => resolveTarget("sunos", "sparc"), /no prebuilt binary/);
139
+
140
+ // Every mapped asset must be one the release workflow actually produces.
141
+ const built = new Set([
142
+ "safehell-linux-x86_64",
143
+ "safehell-linux-aarch64",
144
+ "safehell-macos-x86_64",
145
+ "safehell-macos-aarch64",
146
+ "safehell-windows-x86_64",
147
+ "safehell-windows-aarch64",
148
+ ]);
149
+ for (const [stem] of Object.values(TARGETS)) {
150
+ assert.ok(built.has(stem), `${stem} is not built by release-build.yml`);
151
+ }
152
+
153
+ const sums = "aa\ncafe" + "0".repeat(60) + " safehell-linux-x86_64.tar.gz\n";
154
+ const body = Buffer.from("x");
155
+ assert.throws(() => verify(sums, "safehell-linux-x86_64.tar.gz", body), /checksum mismatch/);
156
+ assert.throws(() => verify(sums, "safehell-macos-arm64.tar.gz", body), /no entry/);
157
+ const good = crypto.createHash("sha256").update(body).digest("hex");
158
+ verify(`${good} safehell-linux-x86_64.tar.gz\n`, "safehell-linux-x86_64.tar.gz", body);
159
+ verify(`${good} *safehell-linux-x86_64.tar.gz\n`, "safehell-linux-x86_64.tar.gz", body);
160
+
161
+ console.log("selftest ok");
162
+ }
163
+
164
+ if (process.argv.includes("--selftest")) {
165
+ selftest();
166
+ } else {
167
+ main().catch((error) => {
168
+ process.stderr.write(`safehell: ${error.message}\n`);
169
+ process.exit(1);
170
+ });
171
+ }
package/package.json CHANGED
@@ -1,11 +1,33 @@
1
1
  {
2
2
  "name": "@suiflex/safehell",
3
- "version": "0.0.0",
4
- "description": "Placeholder so trusted publishing can be configured. Install 0.2.0 or later.",
3
+ "version": "0.2.0",
4
+ "description": "SafeHell approval-gated SSH broker for AI coding agents",
5
5
  "homepage": "https://github.com/suiflex/SafeHell",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/suiflex/SafeHell.git"
9
9
  },
10
- "license": "MIT"
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "ssh",
13
+ "mcp",
14
+ "agent",
15
+ "security",
16
+ "broker",
17
+ "safehell"
18
+ ],
19
+ "bin": {
20
+ "safehell": "bin/safehell.js"
21
+ },
22
+ "files": [
23
+ "bin/safehell.js",
24
+ "install.js"
25
+ ],
26
+ "scripts": {
27
+ "postinstall": "node install.js",
28
+ "test": "node install.js --selftest"
29
+ },
30
+ "engines": {
31
+ "node": ">=18"
32
+ }
11
33
  }