@dalyem/hopkey 0.0.1 → 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 +21 -0
- package/README.md +23 -2
- package/bin/hop +8 -0
- package/install.js +54 -0
- package/lib/platform.js +34 -0
- package/lib/shim.js +87 -0
- package/package.json +40 -3
- package/vendor/darwin-arm64/hop +0 -0
- package/vendor/darwin-x64/hop +0 -0
- package/vendor/linux-arm64/hop +0 -0
- package/vendor/linux-x64/hop +0 -0
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
|
-
# hopkey
|
|
1
|
+
# @dalyem/hopkey
|
|
2
2
|
|
|
3
|
-
|
|
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
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
|
+
}
|
package/lib/platform.js
ADDED
|
@@ -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,10 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dalyem/hopkey",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
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
|
+
},
|
|
6
15
|
"repository": {
|
|
7
16
|
"type": "git",
|
|
8
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"
|
|
9
46
|
}
|
|
10
47
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|