@dalyem/hopkey 0.0.0-stage → 0.1.0-rc.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daly Mauldin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,24 @@
1
- # Temporary Holding Version
1
+ # @dalyem/hopkey
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The `hop` command of [HopKey](https://github.com/dalyem/HopKey), an end-to-end encrypted vault for SSH hosts and keys. `hop` is a Go program; this package is only a way to get it.
4
+
5
+ ```
6
+ npm i -g @dalyem/hopkey # a release
7
+ npm i -g @dalyem/hopkey@next # the latest prerelease
8
+ ```
9
+
10
+ Linux and macOS, on x64 and arm64, with Node 18 or later for the install. The package holds all four builds and downloads nothing when it installs. The server, `hop-server`, is a Docker image: `docker pull ghcr.io/dalyem/hop-server:VERSION`.
11
+
12
+ ## What `hop` is after the install
13
+
14
+ The package's postinstall copies the build for your platform over `bin/hop`, so `hop` on your PATH is the native binary: no Node when it runs, and its signals and exit codes are its own. The package contains a small Node launcher at `bin/hop` for installs that skip postinstall scripts (`npm i -g --ignore-scripts`, pnpm and bun by default). The launcher starts `vendor/<os>-<arch>/hop`, passes stdin, stdout and stderr straight through, and exits with hop's exit code, or with hop's signal. It forwards SIGTERM. It does not forward SIGINT or SIGHUP: the terminal already sends those to hop, and a second one makes hop quit at once.
15
+
16
+ One thing the launcher cannot do: a `nohup hop ...` that should ignore hangups does not, because Node starts its child with default signal handling. If that matters, run the native binary: reinstall without `--ignore-scripts`, or run `vendor/<os>-<arch>/hop` from the package directory (`$(npm root -g)/@dalyem/hopkey`).
17
+
18
+ If the postinstall cannot replace `bin/hop` for any reason, it says nothing, the install still succeeds, and the launcher stays.
19
+
20
+ ## Upgrading
21
+
22
+ A running `hop daemon` keeps running the code it started with. After an upgrade, run `hop daemon stop`; the next hop command starts the new one. The login service (`hop setup --service`) and the MCP client entries (`hop mcp install`) name the path of the `hop` file. If an upgrade or a different Node version (nvm, say) changes that path, run `hop setup --service` and `hop mcp install` again.
23
+
24
+ More in the [HopKey README](https://github.com/dalyem/HopKey#readme).
package/bin/hop ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Fallback for installs whose postinstall did not run; see lib/shim.js.
4
+ require('../lib/shim.js').run({
5
+ platform: process.platform,
6
+ arch: process.arch,
7
+ argv: process.argv.slice(2),
8
+ });
package/install.js ADDED
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ // postinstall: put the native hop binary where bin/hop is, so that `hop` on
4
+ // PATH is the Go binary itself and not a Node launcher: exact signals, exact
5
+ // exit codes, no Node at runtime, and the same file hop finds for itself
6
+ // (os.Executable, os.SameFile) when it starts its daemon or writes a service
7
+ // unit or an MCP client entry.
8
+ //
9
+ // This is an optimisation, and never a reason for the install to fail. If
10
+ // anything goes wrong it leaves the shim (lib/shim.js) in place, prints
11
+ // nothing and exits 0. It makes no network request: the binaries are in the
12
+ // package, under vendor/.
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { resolveBinary } = require('./lib/platform.js');
17
+
18
+ // install copies the binary for this platform over bin/hop, through a temp
19
+ // file in the same directory and a rename. Overwriting bin/hop in place would
20
+ // keep its inode, and on macOS arm64 a changed signed binary on an old inode
21
+ // can get the process killed. It returns whether bin/hop was replaced.
22
+ function install({ root, platform, arch }) {
23
+ const found = resolveBinary({ root, platform, arch });
24
+ if (found.error) {
25
+ return false;
26
+ }
27
+ const bin = path.join(root, 'bin');
28
+ const dest = path.join(bin, 'hop');
29
+ const tmp = path.join(bin, `.hop.${process.pid}.tmp`);
30
+ try {
31
+ fs.copyFileSync(found.path, tmp);
32
+ fs.chmodSync(tmp, 0o755);
33
+ fs.renameSync(tmp, dest);
34
+ return true;
35
+ } catch (e) {
36
+ try {
37
+ fs.unlinkSync(tmp);
38
+ } catch (e2) {
39
+ // It may not have been made.
40
+ }
41
+ return false;
42
+ }
43
+ }
44
+
45
+ module.exports = { install };
46
+
47
+ if (require.main === module) {
48
+ try {
49
+ install({ root: __dirname, platform: process.platform, arch: process.arch });
50
+ } catch (e) {
51
+ // Leave the shim.
52
+ }
53
+ process.exit(0);
54
+ }
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ // Where the hop binary for a platform lives inside this package. The shim
4
+ // (lib/shim.js) and the postinstall (install.js) both use it.
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const PLATFORMS = ['linux', 'darwin'];
10
+ const ARCHES = ['x64', 'arm64'];
11
+
12
+ // resolveBinary returns {path} for the binary that fits platform and arch
13
+ // (as process.platform and process.arch name them), or {error}, one line
14
+ // of text for stderr, when there is none.
15
+ function resolveBinary({ root, platform, arch }) {
16
+ if (!PLATFORMS.includes(platform) || !ARCHES.includes(arch)) {
17
+ return {
18
+ error: `hop: no hop binary for ${platform}-${arch}; the @dalyem/hopkey package ships linux and darwin, on x64 and arm64`,
19
+ };
20
+ }
21
+ const file = path.join(root, 'vendor', `${platform}-${arch}`, 'hop');
22
+ let st;
23
+ try {
24
+ st = fs.statSync(file);
25
+ } catch (e) {
26
+ st = null;
27
+ }
28
+ if (!st || !st.isFile()) {
29
+ return { error: `hop: the binary for ${platform}-${arch} is missing from this install (${file}); reinstall the @dalyem/hopkey package` };
30
+ }
31
+ return { path: file };
32
+ }
33
+
34
+ module.exports = { resolveBinary };
package/lib/shim.js ADDED
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ // The fallback launcher behind bin/hop. A normal install never reaches it:
4
+ // install.js (postinstall) replaces bin/hop with the native binary, so hop
5
+ // runs with no Node in between. The shim is what runs when the postinstall
6
+ // did not: npm --ignore-scripts, pnpm and bun, which skip it by default.
7
+ //
8
+ // It stays out of hop's way:
9
+ // - stdio is inherited, so the shim never reads or writes hop's stdin or
10
+ // stdout (hop mcp speaks MCP on them, and --json must stay pure), and
11
+ // it writes to stderr only to say it cannot start hop.
12
+ // - hop's exit code is passed on as it is; a child killed by a signal is
13
+ // reported by killing this process with the same signal.
14
+ // - SIGTERM is forwarded. SIGINT and SIGHUP are not: the terminal sends
15
+ // them to the whole foreground process group, hop included, and hop
16
+ // exits at once on a second one without restoring the terminal. They
17
+ // are caught here only so that Node does not die first and leave hop
18
+ // running in the shell's background.
19
+ //
20
+ // Known limit: libuv resets every signal to its default in the child, so a
21
+ // SIGHUP that was ignored when the shim started (nohup hop ...) is not
22
+ // ignored in hop, which respects an ignored SIGHUP only when it starts
23
+ // that way. Under the shim, a nohup'd hop dies on hangup. Run the native
24
+ // binary (a normal install, or the file in vendor/) when that matters.
25
+
26
+ const fs = require('fs');
27
+ const os = require('os');
28
+ const path = require('path');
29
+ const { spawn } = require('child_process');
30
+ const { resolveBinary } = require('./platform.js');
31
+
32
+ const EXIT_FAILURE = 255; // hop's own code for its failures
33
+
34
+ // say writes one line to stderr. It uses the file descriptor directly:
35
+ // touching process.stderr could change how the descriptor, which hop shares,
36
+ // is set up.
37
+ function say(line) {
38
+ try {
39
+ fs.writeSync(2, line + '\n');
40
+ } catch (e) {
41
+ // Nobody to tell.
42
+ }
43
+ }
44
+
45
+ function run({ root = path.resolve(__dirname, '..'), platform, arch, argv }) {
46
+ const found = resolveBinary({ root, platform, arch });
47
+ if (found.error) {
48
+ say(found.error);
49
+ process.exit(EXIT_FAILURE);
50
+ return;
51
+ }
52
+
53
+ const child = spawn(found.path, argv, { stdio: 'inherit' });
54
+
55
+ const forward = () => child.kill('SIGTERM');
56
+ const ignore = () => {};
57
+ process.on('SIGTERM', forward);
58
+ process.on('SIGINT', ignore);
59
+ process.on('SIGHUP', ignore);
60
+
61
+ child.on('error', (err) => {
62
+ say(`hop: cannot run ${found.path}: ${err.code || err.message}`);
63
+ process.exit(EXIT_FAILURE);
64
+ });
65
+
66
+ child.on('exit', (code, signal) => {
67
+ if (signal === null) {
68
+ process.exit(code);
69
+ return;
70
+ }
71
+ // Hand the signal back to ourselves with our handlers gone, so that
72
+ // whoever waits for us sees the same death as hop's.
73
+ process.removeListener('SIGTERM', forward);
74
+ process.removeListener('SIGINT', ignore);
75
+ process.removeListener('SIGHUP', ignore);
76
+ try {
77
+ process.kill(process.pid, signal);
78
+ } catch (e) {
79
+ // Fall through to the exit code the shell would report.
80
+ }
81
+ // The signal ends us at once; this runs only if it did not (one whose
82
+ // default action is to be ignored).
83
+ setTimeout(() => process.exit(128 + (os.constants.signals[signal] || 0)), 200);
84
+ });
85
+ }
86
+
87
+ module.exports = { run };
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@dalyem/hopkey",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-rc.3",
4
+ "description": "HopKey: an end-to-end encrypted vault for SSH hosts and keys (the hop CLI)",
5
+ "keywords": [
6
+ "ssh",
7
+ "vault",
8
+ "ssh-agent",
9
+ "hop"
10
+ ],
11
+ "homepage": "https://github.com/dalyem/HopKey#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/dalyem/HopKey/issues"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/dalyem/HopKey.git"
18
+ },
19
+ "license": "MIT",
20
+ "bin": {
21
+ "hop": "bin/hop"
22
+ },
23
+ "scripts": {
24
+ "postinstall": "node install.js"
25
+ },
26
+ "files": [
27
+ "bin/hop",
28
+ "install.js",
29
+ "lib/",
30
+ "vendor/",
31
+ "README.md"
32
+ ],
33
+ "os": [
34
+ "linux",
35
+ "darwin"
36
+ ],
37
+ "cpu": [
38
+ "x64",
39
+ "arm64"
40
+ ],
41
+ "engines": {
42
+ "node": ">=18"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ }
47
+ }
Binary file
Binary file
Binary file
Binary file