qnsqy 7.2.19 → 7.2.21
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 +8 -6
- package/README.md +64 -55
- package/bin/integrity.json +5 -0
- package/bin/qnsqy.js +161 -18
- package/package.json +5 -16
- package/lib/manifest.json +0 -23
- package/postinstall.js +0 -443
- package/uninstall.js +0 -24
package/LICENSE
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
QNSQY is proprietary software. (c) 2026 Quantum Sequrity.
|
|
2
2
|
|
|
3
|
-
The binary
|
|
4
|
-
end-user license agreement,
|
|
3
|
+
The QNSQY binary, delivered as a platform-specific optional dependency of
|
|
4
|
+
this package, is licensed under the QNSQY end-user license agreement,
|
|
5
|
+
available at https://quantumsequrity.com/terms.
|
|
5
6
|
|
|
6
|
-
This npm wrapper package (the JavaScript
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
This npm wrapper package (the JavaScript shim, README, and LICENSE) is
|
|
8
|
+
licensed for use solely to facilitate installation of the QNSQY binary,
|
|
9
|
+
which is delivered as a platform-specific optional dependency. Redistribution
|
|
10
|
+
of the wrapper without the binary, or modification of the wrapper to run a
|
|
11
|
+
different binary, is not permitted.
|
|
10
12
|
|
|
11
13
|
Source-available for security audit on request: security@quantumsequrity.com.
|
package/README.md
CHANGED
|
@@ -24,10 +24,13 @@ Run once without installing:
|
|
|
24
24
|
npx qnsqy --help
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
The matching prebuilt binary for your platform is installed automatically
|
|
28
|
+
by npm as an optional dependency (`@quantumsequrity/qnsqy-linux-x64` or
|
|
29
|
+
`@quantumsequrity/qnsqy-win32-x64`), selected by its `os` / `cpu` fields.
|
|
30
|
+
There is no install script and no network download of our own: npm fetches
|
|
31
|
+
only the one platform package that matches your machine, from the npm
|
|
32
|
+
registry. Because no install script is involved,
|
|
33
|
+
`npm install --ignore-scripts` works too.
|
|
31
34
|
|
|
32
35
|
## Quick start
|
|
33
36
|
|
|
@@ -64,81 +67,87 @@ https://quantumsequrity.com/docs.html.
|
|
|
64
67
|
|-----------------|----------------------------------------------------------------------------------|
|
|
65
68
|
| Linux x86_64 | Supported. Requires glibc 2.35+ (Ubuntu 22.04+, Debian 12+, Fedora 40+, AlmaLinux 10). |
|
|
66
69
|
| Windows x86_64 | Supported. Windows 10 1809+ and Windows 11. |
|
|
67
|
-
| macOS | Not shipping yet. Target Q3 2026. npm install
|
|
68
|
-
| ARM (any OS) | Not shipping yet. npm install
|
|
70
|
+
| macOS | Not shipping yet. Target Q3 2026. `npm install` succeeds; `qnsqy` prints a not-yet-shipping message at run time. |
|
|
71
|
+
| ARM (any OS) | Not shipping yet. `npm install` succeeds; `qnsqy` prints a not-yet-shipping message at run time. |
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
On an unsupported platform, `npm install qnsqy` still succeeds (the main
|
|
74
|
+
package installs everywhere), but no platform binary matches your machine,
|
|
75
|
+
so running `qnsqy` prints a clear message that the platform is not yet
|
|
76
|
+
shipping. If instead you see that message on a supported platform, you
|
|
77
|
+
likely installed with optional dependencies disabled: reinstall with
|
|
78
|
+
`npm install qnsqy --include=optional`.
|
|
73
79
|
|
|
74
80
|
## How this package works
|
|
75
81
|
|
|
76
|
-
The
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
82
|
+
The main `qnsqy` package is a thin JavaScript shim. No binary is bundled
|
|
83
|
+
in it and it runs no install script. The binary ships in two
|
|
84
|
+
platform-specific packages that the main package lists as optional
|
|
85
|
+
dependencies:
|
|
86
|
+
|
|
87
|
+
| Package | For |
|
|
88
|
+
|------------------------------------|----------------|
|
|
89
|
+
| `@quantumsequrity/qnsqy-linux-x64` | Linux x86_64 |
|
|
90
|
+
| `@quantumsequrity/qnsqy-win32-x64` | Windows x86_64 |
|
|
91
|
+
|
|
92
|
+
1. `npm install -g qnsqy` reads the main package's `optionalDependencies`.
|
|
93
|
+
2. npm evaluates each platform package's `os` / `cpu` fields and downloads
|
|
94
|
+
only the one that matches your machine, from the npm registry. The
|
|
95
|
+
others are skipped. No install script and no network fetch of our own
|
|
96
|
+
are involved, so this works even under `npm install --ignore-scripts`.
|
|
97
|
+
3. `node_modules/.bin/qnsqy` is a Node shim (`bin/qnsqy.js`) that resolves
|
|
98
|
+
the binary out of the installed platform package and execs it with your
|
|
99
|
+
arguments, propagating exit codes and signals.
|
|
100
|
+
|
|
101
|
+
The wrapper has zero third-party npm dependencies and uses Node 18+ stdlib
|
|
102
|
+
only (`fs`, `path`, `child_process`). The platform packages contain only
|
|
103
|
+
the raw binary plus metadata: no code, no scripts.
|
|
104
|
+
|
|
105
|
+
On the npm channel, binary integrity rests on the npm registry's tarball
|
|
106
|
+
checksums (recorded in your lockfile). The same binary bytes are published
|
|
107
|
+
on the CDN with SHA-256 checksums and an ML-DSA-87 signature logged to the
|
|
108
|
+
Sigstore Rekor transparency log (see the download page), and the QNSQY
|
|
109
|
+
binary self-verifies its embedded integrity hash at startup.
|
|
100
110
|
|
|
101
111
|
## Air-gapped install
|
|
102
112
|
|
|
103
|
-
If your machine cannot reach
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
If your machine cannot reach the npm registry for the platform package, or
|
|
114
|
+
you want to run a specific pre-staged binary, set the `QNSQY_BINARY_PATH`
|
|
115
|
+
env var. The shim honours it at run time, ahead of the platform package:
|
|
106
116
|
|
|
107
117
|
```
|
|
108
|
-
# 1. On a machine
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
118
|
+
# 1. On a connected machine, download the binary for the target platform
|
|
119
|
+
# from https://quantumsequrity.com/download and verify its SHA-256.
|
|
120
|
+
# 2. Copy it to the air-gapped machine (USB, internal mirror, etc.).
|
|
121
|
+
# 3. Install the wrapper. The platform package may be skipped if the
|
|
122
|
+
# registry is unreachable; that is fine, the env var takes over:
|
|
113
123
|
|
|
114
|
-
|
|
115
|
-
npm install -g --ignore-scripts qnsqy
|
|
124
|
+
npm install -g qnsqy # or: npm install -g --ignore-scripts qnsqy
|
|
116
125
|
|
|
117
|
-
# 4.
|
|
126
|
+
# 4. Run qnsqy with the env var pointing at your pre-staged binary:
|
|
118
127
|
|
|
119
|
-
QNSQY_BINARY_PATH=/path/to/qnsqy
|
|
120
|
-
node $(npm root -g)/qnsqy/postinstall.js
|
|
128
|
+
QNSQY_BINARY_PATH=/path/to/qnsqy qnsqy version
|
|
121
129
|
```
|
|
122
130
|
|
|
123
|
-
|
|
124
|
-
performed
|
|
125
|
-
out-of-band before staging it.
|
|
131
|
+
`QNSQY_BINARY_PATH` must be a real regular file; symlinks are rejected. No
|
|
132
|
+
SHA-256 check is performed on that path, so you are responsible for
|
|
133
|
+
verifying the binary out-of-band before staging it. To make it permanent,
|
|
134
|
+
export `QNSQY_BINARY_PATH` from your shell profile or service unit.
|
|
126
135
|
|
|
127
136
|
## Verification
|
|
128
137
|
|
|
129
|
-
You can verify the
|
|
130
|
-
|
|
138
|
+
You can verify the binary manually against the official checksums on the
|
|
139
|
+
download page.
|
|
131
140
|
|
|
132
141
|
```
|
|
133
142
|
# Linux DEB
|
|
134
|
-
sha256sum qnsqy_7.2.
|
|
143
|
+
sha256sum qnsqy_7.2.20-1_amd64.deb
|
|
135
144
|
# Expected:
|
|
136
|
-
#
|
|
145
|
+
# 93caee47f8af7c09f73373771ab116019d04903d6958369e865c77638e600afc
|
|
137
146
|
|
|
138
147
|
# Windows standalone
|
|
139
|
-
certutil -hashfile qnsqy-7.2.
|
|
148
|
+
certutil -hashfile qnsqy-7.2.20-x86_64.exe SHA256
|
|
140
149
|
# Expected:
|
|
141
|
-
#
|
|
150
|
+
# 1383df812dff16cc593b0caab6bbe6092184a42f212aac8d13e42ed6a52b8f38
|
|
142
151
|
```
|
|
143
152
|
|
|
144
153
|
The canonical hash list is published at
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "SHA-256 of each platform binary, baked into the main qnsqy package (the trust root the user installs). The shim verifies the resolved platform binary against these before exec. Stamped by scripts/stage-platform-packages.sh at release time; empty values are placeholders that make the shim fail closed.",
|
|
3
|
+
"linux-x64": "f40dc28c70d49670e39537b020780481fdf46a535f11e9f65131bd81249eadc0",
|
|
4
|
+
"win32-x64": "1383df812dff16cc593b0caab6bbe6092184a42f212aac8d13e42ed6a52b8f38"
|
|
5
|
+
}
|
package/bin/qnsqy.js
CHANGED
|
@@ -1,31 +1,171 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
// QNSQY shim. Resolves the platform binary
|
|
5
|
-
//
|
|
4
|
+
// QNSQY shim. Resolves the platform binary that npm installed as an
|
|
5
|
+
// optional dependency (@quantumsequrity/qnsqy-<platform>-<arch>) and execs
|
|
6
|
+
// it with the user's argv. Exit code and signals propagate.
|
|
7
|
+
//
|
|
8
|
+
// There is NO install script and NO network access. The binary arrives
|
|
9
|
+
// purely through npm dependency resolution, so it is present even under
|
|
10
|
+
// `npm install --ignore-scripts`.
|
|
11
|
+
//
|
|
12
|
+
// Integrity: the SHA-256 of each platform binary is pinned in
|
|
13
|
+
// ./integrity.json, which ships INSIDE this (main) package, the package the
|
|
14
|
+
// user explicitly installed. Before exec, the shim hashes the resolved
|
|
15
|
+
// platform binary and refuses to run on mismatch. This anchors trust in the
|
|
16
|
+
// main package, so a substituted or name-squatted platform dependency cannot
|
|
17
|
+
// ship runnable bytes. The QNSQY_BINARY_PATH escape hatch is exempt (the
|
|
18
|
+
// operator vouches for that binary out-of-band).
|
|
6
19
|
|
|
7
20
|
const fs = require('node:fs');
|
|
8
21
|
const path = require('node:path');
|
|
22
|
+
const crypto = require('node:crypto');
|
|
9
23
|
const { spawn } = require('node:child_process');
|
|
10
24
|
|
|
11
|
-
|
|
12
|
-
|
|
25
|
+
// PLATFORM_PACKAGES maps `${platform}-${arch}` to the optional dependency
|
|
26
|
+
// that carries the prebuilt binary and the binary's filename inside it.
|
|
27
|
+
const PLATFORM_PACKAGES = {
|
|
28
|
+
'linux-x64': { pkg: '@quantumsequrity/qnsqy-linux-x64', bin: 'qnsqy' },
|
|
29
|
+
'win32-x64': { pkg: '@quantumsequrity/qnsqy-win32-x64', bin: 'qnsqy.exe' },
|
|
30
|
+
};
|
|
13
31
|
|
|
14
|
-
|
|
15
|
-
|
|
32
|
+
function fail(msg) {
|
|
33
|
+
process.stderr.write('qnsqy: ' + msg + '\n');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// loadIntegrity: read the pinned per-platform hashes that ship with this
|
|
38
|
+
// package. Fail closed if the file is missing or unreadable.
|
|
39
|
+
function loadIntegrity() {
|
|
40
|
+
try {
|
|
41
|
+
return require('./integrity.json');
|
|
42
|
+
} catch (err) {
|
|
43
|
+
fail(
|
|
44
|
+
'integrity manifest (integrity.json) could not be loaded: ' +
|
|
45
|
+
(err && err.message ? err.message : String(err)) +
|
|
46
|
+
'. This qnsqy install is corrupt; reinstall it.'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// resolveFromEnv: honour the QNSQY_BINARY_PATH escape hatch. Returns a
|
|
52
|
+
// real binary path or null. Rejects symlinks (a hostile value must not
|
|
53
|
+
// point the runtime at an arbitrary file) and requires a regular,
|
|
54
|
+
// executable file. No SHA-256 check applies here: the operator vouches for
|
|
55
|
+
// the binary out-of-band. (A residual lstat->spawn TOCTOU exists, but it is
|
|
56
|
+
// only exploitable by someone who already has write access to that exact
|
|
57
|
+
// path, i.e. who can already run code as this user.)
|
|
58
|
+
function resolveFromEnv() {
|
|
59
|
+
const envPath = process.env.QNSQY_BINARY_PATH;
|
|
60
|
+
if (!envPath) return null;
|
|
61
|
+
let st;
|
|
62
|
+
try {
|
|
63
|
+
st = fs.lstatSync(envPath);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
fail(
|
|
66
|
+
'QNSQY_BINARY_PATH was set to "' + envPath + '" but lstat failed: ' +
|
|
67
|
+
(err && err.message ? err.message : String(err))
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (st.isSymbolicLink()) {
|
|
71
|
+
fail('QNSQY_BINARY_PATH must not be a symlink. Resolve it to a real path and retry.');
|
|
72
|
+
}
|
|
73
|
+
if (!st.isFile()) {
|
|
74
|
+
fail(
|
|
75
|
+
'QNSQY_BINARY_PATH must be a regular file (got ' +
|
|
76
|
+
(st.isDirectory() ? 'directory' : 'special file') + ').'
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (process.platform !== 'win32' && !(st.mode & 0o111)) {
|
|
80
|
+
fail('QNSQY_BINARY_PATH points to a non-executable file. Run: chmod +x ' + envPath);
|
|
81
|
+
}
|
|
16
82
|
process.stderr.write(
|
|
17
|
-
'qnsqy:
|
|
18
|
-
|
|
19
|
-
'or the postinstall step failed.\n' +
|
|
20
|
-
'qnsqy: fetch the binary by running:\n' +
|
|
21
|
-
' node ' + postinstall + '\n' +
|
|
22
|
-
'qnsqy: or, for air-gapped machines, set QNSQY_BINARY_PATH to a pre-staged ' +
|
|
23
|
-
'binary and re-run the postinstall:\n' +
|
|
24
|
-
' QNSQY_BINARY_PATH=/path/to/qnsqy node ' + postinstall + '\n'
|
|
83
|
+
'qnsqy: using QNSQY_BINARY_PATH=' + envPath +
|
|
84
|
+
' (integrity check skipped; you are responsible for verifying this binary out-of-band).\n'
|
|
25
85
|
);
|
|
26
|
-
|
|
86
|
+
return path.resolve(envPath);
|
|
27
87
|
}
|
|
28
88
|
|
|
89
|
+
// verifyIntegrity: hash the resolved platform binary and compare it against
|
|
90
|
+
// the value pinned in integrity.json. Fail closed on any mismatch or if the
|
|
91
|
+
// build was never provisioned with a real hash.
|
|
92
|
+
function verifyIntegrity(key, binPath) {
|
|
93
|
+
const integrity = loadIntegrity();
|
|
94
|
+
const expected = (integrity && typeof integrity[key] === 'string') ? integrity[key].toLowerCase() : '';
|
|
95
|
+
if (!/^[0-9a-f]{64}$/.test(expected)) {
|
|
96
|
+
fail(
|
|
97
|
+
'no integrity hash is provisioned for ' + key + ' in integrity.json. This qnsqy ' +
|
|
98
|
+
'build was not prepared for release (run scripts/stage-platform-packages.sh). ' +
|
|
99
|
+
'Refusing to run an unverified binary.'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const actual = crypto.createHash('sha256').update(fs.readFileSync(binPath)).digest('hex');
|
|
103
|
+
let match = false;
|
|
104
|
+
try {
|
|
105
|
+
match = crypto.timingSafeEqual(Buffer.from(actual, 'hex'), Buffer.from(expected, 'hex'));
|
|
106
|
+
} catch (_) {
|
|
107
|
+
match = false;
|
|
108
|
+
}
|
|
109
|
+
if (!match) {
|
|
110
|
+
fail(
|
|
111
|
+
'binary integrity check FAILED for ' + PLATFORM_PACKAGES[key].pkg + '.\n' +
|
|
112
|
+
' expected sha256: ' + expected + '\n' +
|
|
113
|
+
' actual sha256: ' + actual + '\n' +
|
|
114
|
+
'The installed platform binary does not match the hash pinned in the qnsqy ' +
|
|
115
|
+
'package. This can indicate a tampered or substituted dependency. Refusing to ' +
|
|
116
|
+
'run. Report to security@quantumsequrity.com.'
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// resolvePlatformBinary: find the binary inside the installed optional
|
|
122
|
+
// dependency. Resolving the package's package.json (always resolvable when
|
|
123
|
+
// the package is present, regardless of `exports`) and joining the binary
|
|
124
|
+
// name is the robust pattern esbuild and @swc/core use. Verifies integrity
|
|
125
|
+
// before returning.
|
|
126
|
+
function resolvePlatformBinary() {
|
|
127
|
+
const key = process.platform + '-' + process.arch;
|
|
128
|
+
const entry = PLATFORM_PACKAGES[key];
|
|
129
|
+
if (!entry) {
|
|
130
|
+
fail(
|
|
131
|
+
'no prebuilt QNSQY binary is published for ' + process.platform + '/' +
|
|
132
|
+
process.arch + '.\n' +
|
|
133
|
+
'qnsqy: shipping targets today are Linux x86_64 and Windows x86_64. ' +
|
|
134
|
+
'macOS is targeted for Q3 2026; ARM is not yet shipping. ' +
|
|
135
|
+
'See https://quantumsequrity.com/download for status.\n' +
|
|
136
|
+
'qnsqy: for a pre-staged binary, set QNSQY_BINARY_PATH=/path/to/qnsqy and re-run.'
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
let pkgJson;
|
|
140
|
+
try {
|
|
141
|
+
pkgJson = require.resolve(entry.pkg + '/package.json');
|
|
142
|
+
} catch (_) {
|
|
143
|
+
fail(
|
|
144
|
+
'the platform package "' + entry.pkg + '" is not installed.\n' +
|
|
145
|
+
'qnsqy: this happens when npm skipped optional dependencies. Reinstall with:\n' +
|
|
146
|
+
' npm install qnsqy --include=optional\n' +
|
|
147
|
+
'qnsqy: (also check you did not pass --no-optional / --omit=optional, and that ' +
|
|
148
|
+
'any lockfile was generated with optional dependencies enabled).\n' +
|
|
149
|
+
'qnsqy: or set QNSQY_BINARY_PATH=/path/to/qnsqy for an air-gapped install.'
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const binPath = path.join(path.dirname(pkgJson), entry.bin);
|
|
153
|
+
if (!fs.existsSync(binPath)) {
|
|
154
|
+
fail(
|
|
155
|
+
'platform package "' + entry.pkg + '" is installed but its binary is missing at ' +
|
|
156
|
+
binPath + '.\n' +
|
|
157
|
+
'qnsqy: if you use Yarn in PnP (plug-n-play) mode, ensure the platform package is ' +
|
|
158
|
+
'unplugged (it sets preferUnplugged: true; rerun `yarn install`) or set ' +
|
|
159
|
+
'nodeLinker: node-modules in .yarnrc.yml.\n' +
|
|
160
|
+
'qnsqy: otherwise reinstall qnsqy, or report at https://quantumsequrity.com/contact.'
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
verifyIntegrity(key, binPath);
|
|
164
|
+
return binPath;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const binPath = resolveFromEnv() || resolvePlatformBinary();
|
|
168
|
+
|
|
29
169
|
const child = spawn(binPath, process.argv.slice(2), { stdio: 'inherit' });
|
|
30
170
|
|
|
31
171
|
function forwardSignal(sig) {
|
|
@@ -52,7 +192,7 @@ child.on('error', function (err) {
|
|
|
52
192
|
detachSignalListeners();
|
|
53
193
|
process.stderr.write(
|
|
54
194
|
'qnsqy: failed to spawn ' + binPath + ': ' +
|
|
55
|
-
|
|
195
|
+
(err && err.message ? err.message : String(err)) + '\n'
|
|
56
196
|
);
|
|
57
197
|
process.exit(1);
|
|
58
198
|
});
|
|
@@ -60,12 +200,15 @@ child.on('error', function (err) {
|
|
|
60
200
|
child.on('exit', function (code, signal) {
|
|
61
201
|
detachSignalListeners();
|
|
62
202
|
if (signal) {
|
|
203
|
+
// Re-raise so the parent shell sees "killed by signal". A short fallback
|
|
204
|
+
// timer guarantees we still exit even if the signal is intercepted or is
|
|
205
|
+
// non-fatal (e.g. on Windows, where Unix signals do not terminate).
|
|
206
|
+
setTimeout(function () { process.exit(1); }, 100).unref();
|
|
63
207
|
try {
|
|
64
208
|
process.kill(process.pid, signal);
|
|
65
209
|
} catch (_) {
|
|
66
|
-
|
|
210
|
+
process.exit(1);
|
|
67
211
|
}
|
|
68
|
-
process.exit(1);
|
|
69
212
|
return;
|
|
70
213
|
}
|
|
71
214
|
process.exit(typeof code === 'number' ? code : 1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qnsqy",
|
|
3
|
-
"version": "7.2.
|
|
3
|
+
"version": "7.2.21",
|
|
4
4
|
"description": "Post-quantum cryptography tool. NIST FIPS 203 / 204 / 205 algorithms hybridized with classical X25519, Ed25519, AES-256-GCM. Local-only execution. 84 MCP tools for AI agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"post-quantum",
|
|
@@ -25,33 +25,22 @@
|
|
|
25
25
|
"bugs": "https://quantumsequrity.com/contact",
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|
|
28
|
-
"url": "https://github.com/quantumsequrity/qnsqy.git"
|
|
28
|
+
"url": "git+https://github.com/quantumsequrity/qnsqy.git"
|
|
29
29
|
},
|
|
30
30
|
"license": "SEE LICENSE IN LICENSE",
|
|
31
31
|
"author": "Quantum Sequrity",
|
|
32
32
|
"bin": {
|
|
33
33
|
"qnsqy": "bin/qnsqy.js"
|
|
34
34
|
},
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"preuninstall": "node uninstall.js"
|
|
35
|
+
"optionalDependencies": {
|
|
36
|
+
"@quantumsequrity/qnsqy-linux-x64": "7.2.21",
|
|
37
|
+
"@quantumsequrity/qnsqy-win32-x64": "7.2.21"
|
|
39
38
|
},
|
|
40
39
|
"engines": {
|
|
41
40
|
"node": ">=18.0.0"
|
|
42
41
|
},
|
|
43
|
-
"os": [
|
|
44
|
-
"linux",
|
|
45
|
-
"win32"
|
|
46
|
-
],
|
|
47
|
-
"cpu": [
|
|
48
|
-
"x64"
|
|
49
|
-
],
|
|
50
42
|
"files": [
|
|
51
43
|
"bin/",
|
|
52
|
-
"lib/",
|
|
53
|
-
"postinstall.js",
|
|
54
|
-
"uninstall.js",
|
|
55
44
|
"README.md",
|
|
56
45
|
"LICENSE"
|
|
57
46
|
]
|
package/lib/manifest.json
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"qnsqy_version": "7.2.19",
|
|
3
|
-
"generated_at": "2026-05-25T16:09Z",
|
|
4
|
-
"source": "freshly built from qs-ultra/ source tree 2026-05-25; matches website-YC/download.md SHA-256 Checksums (v7.2.19)",
|
|
5
|
-
"platforms": {
|
|
6
|
-
"linux-x64": {
|
|
7
|
-
"url": "https://cdn.quantumsequrity.com/linux/qnsqy_7.2.19-1_amd64.deb",
|
|
8
|
-
"filename": "qnsqy_7.2.19-1_amd64.deb",
|
|
9
|
-
"format": "deb",
|
|
10
|
-
"extract": "deb",
|
|
11
|
-
"sha256": "983bfed387a969ecf9983f65fa27ee3025ed6edb5c32a38b91acd78a04d561a9",
|
|
12
|
-
"binary_path_in_archive": "usr/bin/qnsqy"
|
|
13
|
-
},
|
|
14
|
-
"win32-x64": {
|
|
15
|
-
"url": "https://cdn.quantumsequrity.com/windows/qnsqy-7.2.19-x86_64.exe",
|
|
16
|
-
"filename": "qnsqy-7.2.19-x86_64.exe",
|
|
17
|
-
"format": "exe",
|
|
18
|
-
"extract": "passthrough",
|
|
19
|
-
"sha256": "a6aaabdca0864dd843b8f238471a7c5d15517b05ffd6b322eac6ca37a570c090",
|
|
20
|
-
"binary_path_in_archive": "qnsqy-7.2.19-x86_64.exe"
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
package/postinstall.js
DELETED
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
// QNSQY npm wrapper postinstall.
|
|
5
|
-
// Responsibilities:
|
|
6
|
-
// 1. Detect platform.
|
|
7
|
-
// 2. Honour QNSQY_BINARY_PATH escape hatch (rejects symlinks).
|
|
8
|
-
// 3. Download the matching binary archive from cdn.quantumsequrity.com
|
|
9
|
-
// with a 2-minute timeout and post-redirect host pinning.
|
|
10
|
-
// 4. Verify SHA-256 (constant-time) against the value pinned in
|
|
11
|
-
// lib/manifest.json.
|
|
12
|
-
// 5. Extract (DEB on Linux via in-process ar parser + system tar,
|
|
13
|
-
// passthrough on Windows).
|
|
14
|
-
// 6. Atomic install into bin/qnsqy-bin (or qnsqy-bin.exe on Windows).
|
|
15
|
-
//
|
|
16
|
-
// Zero npm dependencies. Node 18+ stdlib only.
|
|
17
|
-
|
|
18
|
-
const fs = require('node:fs');
|
|
19
|
-
const path = require('node:path');
|
|
20
|
-
const os = require('node:os');
|
|
21
|
-
const crypto = require('node:crypto');
|
|
22
|
-
const { spawnSync } = require('node:child_process');
|
|
23
|
-
|
|
24
|
-
const PKG_VERSION = '7.2.19';
|
|
25
|
-
const PKG_DIR = __dirname;
|
|
26
|
-
const BIN_DIR = path.join(PKG_DIR, 'bin');
|
|
27
|
-
const MANIFEST_PATH = path.join(PKG_DIR, 'lib', 'manifest.json');
|
|
28
|
-
const CDN_HOSTNAME = 'cdn.quantumsequrity.com';
|
|
29
|
-
const DOWNLOAD_TIMEOUT_MS = 600000;
|
|
30
|
-
const MAX_DOWNLOAD_BYTES = 200 * 1024 * 1024;
|
|
31
|
-
|
|
32
|
-
function info(msg) {
|
|
33
|
-
process.stdout.write('qnsqy postinstall: ' + msg + '\n');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function die(msg, code) {
|
|
37
|
-
process.stderr.write('qnsqy postinstall error: ' + msg + '\n');
|
|
38
|
-
process.exit(typeof code === 'number' ? code : 1);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function platformKey() {
|
|
42
|
-
const plat = process.platform;
|
|
43
|
-
const arch = process.arch;
|
|
44
|
-
if (plat === 'linux' && arch === 'x64') return 'linux-x64';
|
|
45
|
-
if (plat === 'win32' && arch === 'x64') return 'win32-x64';
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function ensureBinDir() {
|
|
50
|
-
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function binTargetPath() {
|
|
54
|
-
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
55
|
-
return path.join(BIN_DIR, 'qnsqy-bin' + ext);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// atomicWrite: write to a PID-suffixed tmp file (collision-safe under
|
|
59
|
-
// concurrent installs), fsync, chmod 0755 BEFORE rename so the published
|
|
60
|
-
// file is atomically executable, then rename.
|
|
61
|
-
function atomicWrite(targetPath, buffer) {
|
|
62
|
-
const tmp = targetPath + '.' + process.pid + '.tmp';
|
|
63
|
-
fs.writeFileSync(tmp, buffer);
|
|
64
|
-
const fd = fs.openSync(tmp, 'r+');
|
|
65
|
-
try {
|
|
66
|
-
fs.fsyncSync(fd);
|
|
67
|
-
} finally {
|
|
68
|
-
fs.closeSync(fd);
|
|
69
|
-
}
|
|
70
|
-
if (process.platform !== 'win32') {
|
|
71
|
-
try {
|
|
72
|
-
fs.chmodSync(tmp, 0o755);
|
|
73
|
-
} catch (err) {
|
|
74
|
-
try { fs.unlinkSync(tmp); } catch (_) {}
|
|
75
|
-
die(
|
|
76
|
-
'failed to chmod 755 on ' + tmp + ': ' +
|
|
77
|
-
(err && err.message ? err.message : String(err)) +
|
|
78
|
-
'. Check ownership of node_modules.'
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
fs.renameSync(tmp, targetPath);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function copyEscapeHatch(envPath) {
|
|
86
|
-
if (!envPath) return false;
|
|
87
|
-
let stat;
|
|
88
|
-
try {
|
|
89
|
-
stat = fs.lstatSync(envPath);
|
|
90
|
-
} catch (err) {
|
|
91
|
-
die(
|
|
92
|
-
'QNSQY_BINARY_PATH was set to "' + envPath + '" but lstat failed: ' +
|
|
93
|
-
(err && err.message ? err.message : String(err))
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
if (stat.isSymbolicLink()) {
|
|
97
|
-
die(
|
|
98
|
-
'QNSQY_BINARY_PATH must not be a symlink. Symlinks are rejected to ' +
|
|
99
|
-
'prevent a hostile env var from pointing at sensitive files. ' +
|
|
100
|
-
'Resolve the symlink to a real path and retry.'
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
if (!stat.isFile()) {
|
|
104
|
-
die('QNSQY_BINARY_PATH must be a regular file (got ' + (stat.isDirectory() ? 'directory' : 'special') + ').');
|
|
105
|
-
}
|
|
106
|
-
ensureBinDir();
|
|
107
|
-
const target = binTargetPath();
|
|
108
|
-
// Open with O_NOFOLLOW on POSIX as defense-in-depth against a symlink
|
|
109
|
-
// race between lstat and open.
|
|
110
|
-
let fd;
|
|
111
|
-
try {
|
|
112
|
-
if (process.platform !== 'win32') {
|
|
113
|
-
fd = fs.openSync(envPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
114
|
-
} else {
|
|
115
|
-
fd = fs.openSync(envPath, 'r');
|
|
116
|
-
}
|
|
117
|
-
const data = fs.readFileSync(fd);
|
|
118
|
-
atomicWrite(target, data);
|
|
119
|
-
} finally {
|
|
120
|
-
if (fd !== undefined) {
|
|
121
|
-
try { fs.closeSync(fd); } catch (_) {}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
process.stderr.write(
|
|
125
|
-
'qnsqy postinstall WARN: SHA-256 verification SKIPPED because ' +
|
|
126
|
-
'QNSQY_BINARY_PATH was used. You are responsible for verifying the ' +
|
|
127
|
-
'binary out-of-band (sha256sum against https://quantumsequrity.com/download).\n'
|
|
128
|
-
);
|
|
129
|
-
info(
|
|
130
|
-
'qnsqy ' + PKG_VERSION + ' installed at ' + target +
|
|
131
|
-
' (source: QNSQY_BINARY_PATH, hash check skipped).'
|
|
132
|
-
);
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function fetchOnce(url, attemptLabel) {
|
|
137
|
-
const ac = new AbortController();
|
|
138
|
-
const timer = setTimeout(function () { ac.abort(); }, DOWNLOAD_TIMEOUT_MS);
|
|
139
|
-
try {
|
|
140
|
-
const res = await fetch(url, { redirect: 'follow', signal: ac.signal });
|
|
141
|
-
if (!res.ok) {
|
|
142
|
-
throw new Error('HTTP ' + res.status);
|
|
143
|
-
}
|
|
144
|
-
let finalUrl;
|
|
145
|
-
try {
|
|
146
|
-
finalUrl = new URL(res.url);
|
|
147
|
-
} catch (_) {
|
|
148
|
-
throw new Error('invalid response URL: ' + res.url);
|
|
149
|
-
}
|
|
150
|
-
if (finalUrl.protocol !== 'https:') {
|
|
151
|
-
throw new Error('response URL is not HTTPS: ' + res.url);
|
|
152
|
-
}
|
|
153
|
-
if (finalUrl.hostname !== CDN_HOSTNAME) {
|
|
154
|
-
throw new Error('response URL is not on ' + CDN_HOSTNAME + ': ' + res.url);
|
|
155
|
-
}
|
|
156
|
-
const contentLengthHeader = res.headers.get('content-length');
|
|
157
|
-
if (contentLengthHeader != null) {
|
|
158
|
-
const cl = parseInt(contentLengthHeader, 10);
|
|
159
|
-
if (Number.isFinite(cl) && cl > MAX_DOWNLOAD_BYTES) {
|
|
160
|
-
throw new Error('Content-Length ' + cl + ' exceeds ceiling ' + MAX_DOWNLOAD_BYTES);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
const arrayBuffer = await res.arrayBuffer();
|
|
164
|
-
if (arrayBuffer.byteLength > MAX_DOWNLOAD_BYTES) {
|
|
165
|
-
throw new Error('body ' + arrayBuffer.byteLength + ' bytes exceeds ceiling ' + MAX_DOWNLOAD_BYTES);
|
|
166
|
-
}
|
|
167
|
-
return Buffer.from(arrayBuffer);
|
|
168
|
-
} finally {
|
|
169
|
-
clearTimeout(timer);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// fetchToBuffer: download with one retry on transient failures. The CDN
|
|
174
|
-
// can be slow on cold-cache reads (10 MB sometimes takes >60 s through
|
|
175
|
-
// Cloudflare). Hospital / military deployments often run on slow links;
|
|
176
|
-
// a single retry on AbortError / network reset turns a brittle install
|
|
177
|
-
// into a robust one.
|
|
178
|
-
async function fetchToBuffer(url) {
|
|
179
|
-
const maxAttempts = 3;
|
|
180
|
-
let lastErr = null;
|
|
181
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
182
|
-
try {
|
|
183
|
-
if (attempt > 1) {
|
|
184
|
-
info('retry ' + (attempt - 1) + '/' + (maxAttempts - 1) + ' for ' + url);
|
|
185
|
-
}
|
|
186
|
-
return await fetchOnce(url, attempt);
|
|
187
|
-
} catch (err) {
|
|
188
|
-
lastErr = err;
|
|
189
|
-
const reason = (err && err.name === 'AbortError')
|
|
190
|
-
? 'timed out after ' + (DOWNLOAD_TIMEOUT_MS / 1000) + ' seconds'
|
|
191
|
-
: (err && err.message ? err.message : String(err));
|
|
192
|
-
if (attempt < maxAttempts) {
|
|
193
|
-
process.stderr.write(
|
|
194
|
-
'qnsqy postinstall: download attempt ' + attempt + ' failed (' + reason +
|
|
195
|
-
'); retrying...\n'
|
|
196
|
-
);
|
|
197
|
-
// small backoff: 2s, 5s
|
|
198
|
-
await new Promise(function (r) { setTimeout(r, attempt === 1 ? 2000 : 5000); });
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
die(
|
|
202
|
-
'network error downloading ' + url + ' after ' + maxAttempts +
|
|
203
|
-
' attempts: ' + reason +
|
|
204
|
-
'. Retry, or set QNSQY_BINARY_PATH to a pre-staged binary.'
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
// Unreachable; satisfies lint
|
|
209
|
-
die('download loop exited unexpectedly: ' + (lastErr && lastErr.message ? lastErr.message : 'unknown'));
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function sha256OfBuffer(buf) {
|
|
213
|
-
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function constantTimeHexEqual(a, b) {
|
|
217
|
-
if (typeof a !== 'string' || typeof b !== 'string') return false;
|
|
218
|
-
if (a.length !== b.length || a.length === 0) return false;
|
|
219
|
-
const ab = Buffer.from(a, 'hex');
|
|
220
|
-
const bb = Buffer.from(b, 'hex');
|
|
221
|
-
if (ab.length === 0 || ab.length !== bb.length) return false;
|
|
222
|
-
return crypto.timingSafeEqual(ab, bb);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function toolExists(cmd) {
|
|
226
|
-
try {
|
|
227
|
-
// BusyBox tar exits 0 for `--help`; GNU and BSD tar also exit 0.
|
|
228
|
-
// `--version` is rejected by BusyBox, so it must not be used here.
|
|
229
|
-
const probe = spawnSync(cmd, ['--help'], { stdio: 'ignore' });
|
|
230
|
-
return probe.error == null;
|
|
231
|
-
} catch (_) {
|
|
232
|
-
return false;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function validateSlot(key, slot) {
|
|
237
|
-
if (slot == null) {
|
|
238
|
-
die('manifest has no entry for platform ' + key);
|
|
239
|
-
}
|
|
240
|
-
const requiredString = ['url', 'filename', 'sha256', 'binary_path_in_archive', 'format'];
|
|
241
|
-
for (let i = 0; i < requiredString.length; i++) {
|
|
242
|
-
const field = requiredString[i];
|
|
243
|
-
if (typeof slot[field] !== 'string' || slot[field].length === 0) {
|
|
244
|
-
die('manifest entry for ' + key + ' is missing required string field: ' + field);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
if (!/^[0-9a-fA-F]{64}$/.test(slot.sha256)) {
|
|
248
|
-
die(
|
|
249
|
-
'manifest sha256 for ' + key + ' is not a 64-char hex string ' +
|
|
250
|
-
'(got ' + slot.sha256.length + ' chars: ' + slot.sha256 + ').'
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
if (!slot.url.startsWith('https://')) {
|
|
254
|
-
die('manifest url for ' + key + ' must be HTTPS: ' + slot.url);
|
|
255
|
-
}
|
|
256
|
-
try {
|
|
257
|
-
const u = new URL(slot.url);
|
|
258
|
-
if (u.hostname !== CDN_HOSTNAME) {
|
|
259
|
-
die('manifest url for ' + key + ' must be on ' + CDN_HOSTNAME + ': ' + slot.url);
|
|
260
|
-
}
|
|
261
|
-
} catch (_) {
|
|
262
|
-
die('manifest url for ' + key + ' is not a valid URL: ' + slot.url);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Parse a .deb (BSD ar archive) and return the data.tar.* member as a
|
|
267
|
-
// Buffer. .deb member names are short (data.tar.gz / xz / zst) so the
|
|
268
|
-
// BSD/GNU long-name extensions are not needed.
|
|
269
|
-
function readDebDataMember(debBuf) {
|
|
270
|
-
const magic = debBuf.slice(0, 8).toString('ascii');
|
|
271
|
-
if (magic !== '!<arch>\n') {
|
|
272
|
-
die('downloaded DEB is not a valid ar archive (magic = ' + JSON.stringify(magic) + ').');
|
|
273
|
-
}
|
|
274
|
-
let off = 8;
|
|
275
|
-
while (off + 60 <= debBuf.length) {
|
|
276
|
-
const name = debBuf.slice(off, off + 16).toString('ascii').replace(/[\s\/]+$/, '');
|
|
277
|
-
const sizeStr = debBuf.slice(off + 48, off + 58).toString('ascii').trim();
|
|
278
|
-
const size = parseInt(sizeStr, 10);
|
|
279
|
-
if (!Number.isFinite(size) || size < 0 || size > MAX_DOWNLOAD_BYTES) {
|
|
280
|
-
die('invalid ar member size at offset ' + off + ': ' + JSON.stringify(sizeStr));
|
|
281
|
-
}
|
|
282
|
-
const dataStart = off + 60;
|
|
283
|
-
const dataEnd = dataStart + size;
|
|
284
|
-
if (dataEnd > debBuf.length) {
|
|
285
|
-
die(
|
|
286
|
-
'ar member "' + name + '" declares size ' + size + ' but only ' +
|
|
287
|
-
(debBuf.length - dataStart) + ' bytes remain. Archive is malformed.'
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
if (name.indexOf('data.tar') === 0) {
|
|
291
|
-
return { name: name, data: debBuf.slice(dataStart, dataEnd) };
|
|
292
|
-
}
|
|
293
|
-
off = dataEnd + (size % 2);
|
|
294
|
-
}
|
|
295
|
-
die('no data.tar member found in DEB archive.');
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function extractBinaryFromTar(tarPath, innerPath) {
|
|
300
|
-
// Defense in depth: reject path-traversal in the manifest-supplied innerPath.
|
|
301
|
-
if (innerPath.split('/').some(function (p) { return p === '..' || p === ''; })) {
|
|
302
|
-
die('binary_path_in_archive contains forbidden segment: ' + innerPath);
|
|
303
|
-
}
|
|
304
|
-
const list = spawnSync('tar', ['-tf', tarPath], {
|
|
305
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
306
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
307
|
-
});
|
|
308
|
-
if (list.error) {
|
|
309
|
-
die(
|
|
310
|
-
'tar invocation failed: ' + list.error.message +
|
|
311
|
-
'. Install tar via your package manager and retry.'
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
if (list.status !== 0 || list.stdout == null) {
|
|
315
|
-
const sig = list.signal ? ' (killed by signal ' + list.signal + ')' : '';
|
|
316
|
-
die(
|
|
317
|
-
'tar -tf failed on ' + tarPath + sig + ': ' +
|
|
318
|
-
(list.stderr ? list.stderr.toString().slice(0, 500) : 'no stderr')
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
const entries = list.stdout.toString().split('\n');
|
|
322
|
-
let match = null;
|
|
323
|
-
for (let i = 0; i < entries.length; i++) {
|
|
324
|
-
const norm = entries[i].replace(/^\.\//, '').replace(/^\//, '');
|
|
325
|
-
if (norm === innerPath) {
|
|
326
|
-
match = entries[i];
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
if (!match) {
|
|
331
|
-
die(
|
|
332
|
-
'expected binary ' + innerPath + ' was not found inside the data tarball. ' +
|
|
333
|
-
'Archive layout may have changed; report at https://quantumsequrity.com/contact.'
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
const extract = spawnSync('tar', ['-xf', tarPath, '-O', match], {
|
|
337
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
338
|
-
maxBuffer: 256 * 1024 * 1024,
|
|
339
|
-
});
|
|
340
|
-
if (extract.error) {
|
|
341
|
-
die('tar extract spawn failed: ' + extract.error.message);
|
|
342
|
-
}
|
|
343
|
-
if (extract.status !== 0 || extract.stdout == null || extract.stdout.length === 0) {
|
|
344
|
-
const sig = extract.signal ? ' (killed by signal ' + extract.signal + ')' : '';
|
|
345
|
-
die(
|
|
346
|
-
'tar extract failed for ' + match + sig + ': ' +
|
|
347
|
-
(extract.stderr ? extract.stderr.toString().slice(0, 500) : 'no stderr or empty output')
|
|
348
|
-
);
|
|
349
|
-
}
|
|
350
|
-
return extract.stdout;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
async function main() {
|
|
354
|
-
if (copyEscapeHatch(process.env.QNSQY_BINARY_PATH)) return;
|
|
355
|
-
|
|
356
|
-
const key = platformKey();
|
|
357
|
-
if (!key) {
|
|
358
|
-
die(
|
|
359
|
-
'platform ' + process.platform + '/' + process.arch + ' is not supported ' +
|
|
360
|
-
'by the QNSQY npm wrapper today. Linux x86_64 and Windows x86_64 are the ' +
|
|
361
|
-
'shipping targets. macOS is targeted for Q3 2026; ARM is not yet shipping. ' +
|
|
362
|
-
'See https://quantumsequrity.com/download for current status. ' +
|
|
363
|
-
'For pre-staged binaries set QNSQY_BINARY_PATH and reinstall.'
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
if (!fs.existsSync(MANIFEST_PATH)) {
|
|
368
|
-
die('manifest missing at ' + MANIFEST_PATH);
|
|
369
|
-
}
|
|
370
|
-
let manifest;
|
|
371
|
-
try {
|
|
372
|
-
manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
|
|
373
|
-
} catch (err) {
|
|
374
|
-
die('manifest at ' + MANIFEST_PATH + ' is not valid JSON: ' + (err && err.message ? err.message : err));
|
|
375
|
-
}
|
|
376
|
-
const slot = manifest.platforms && manifest.platforms[key];
|
|
377
|
-
validateSlot(key, slot);
|
|
378
|
-
|
|
379
|
-
if (slot.format === 'deb' && !toolExists('tar')) {
|
|
380
|
-
die(
|
|
381
|
-
'tar is required to unpack the QNSQY DEB but was not found on PATH. ' +
|
|
382
|
-
'Install it: apt install tar / dnf install tar / apk add tar / pacman -S tar.'
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
ensureBinDir();
|
|
387
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'qnsqy-install-'));
|
|
388
|
-
|
|
389
|
-
function cleanupTmp() {
|
|
390
|
-
try {
|
|
391
|
-
const entries = fs.readdirSync(tmpDir);
|
|
392
|
-
for (let i = 0; i < entries.length; i++) {
|
|
393
|
-
try { fs.unlinkSync(path.join(tmpDir, entries[i])); } catch (_) {}
|
|
394
|
-
}
|
|
395
|
-
fs.rmdirSync(tmpDir);
|
|
396
|
-
} catch (_) {
|
|
397
|
-
// best-effort cleanup; never fatal
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
process.once('exit', cleanupTmp);
|
|
401
|
-
|
|
402
|
-
try {
|
|
403
|
-
info('downloading ' + slot.url);
|
|
404
|
-
const downloadedBuf = await fetchToBuffer(slot.url);
|
|
405
|
-
|
|
406
|
-
const actualHash = sha256OfBuffer(downloadedBuf);
|
|
407
|
-
if (!constantTimeHexEqual(actualHash, slot.sha256)) {
|
|
408
|
-
die(
|
|
409
|
-
'SHA-256 mismatch for ' + slot.filename + '\n' +
|
|
410
|
-
' expected: ' + slot.sha256 + '\n' +
|
|
411
|
-
' actual: ' + actualHash + '\n' +
|
|
412
|
-
'The binary on the CDN may have been rotated, or the download was tampered with. ' +
|
|
413
|
-
'Verify the hash against https://quantumsequrity.com/download and report any ' +
|
|
414
|
-
'genuine mismatch to security@quantumsequrity.com.'
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
info('verified SHA-256 (' + downloadedBuf.length + ' bytes).');
|
|
418
|
-
|
|
419
|
-
const target = binTargetPath();
|
|
420
|
-
|
|
421
|
-
if (slot.format === 'exe' || slot.extract === 'passthrough') {
|
|
422
|
-
atomicWrite(target, downloadedBuf);
|
|
423
|
-
} else if (slot.format === 'deb' || slot.extract === 'deb') {
|
|
424
|
-
const dataMember = readDebDataMember(downloadedBuf);
|
|
425
|
-
const dataTarPath = path.join(tmpDir, dataMember.name);
|
|
426
|
-
fs.writeFileSync(dataTarPath, dataMember.data);
|
|
427
|
-
const binBuf = extractBinaryFromTar(dataTarPath, slot.binary_path_in_archive);
|
|
428
|
-
atomicWrite(target, binBuf);
|
|
429
|
-
} else if (slot.format === 'appimage' || slot.extract === 'appimage') {
|
|
430
|
-
atomicWrite(target, downloadedBuf);
|
|
431
|
-
} else {
|
|
432
|
-
die('unknown extract format in manifest: ' + slot.format + ' / ' + slot.extract);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
info('qnsqy ' + PKG_VERSION + ' installed at ' + target);
|
|
436
|
-
} finally {
|
|
437
|
-
cleanupTmp();
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
main().catch(function (err) {
|
|
442
|
-
die(err && err.message ? err.message : String(err));
|
|
443
|
-
});
|
package/uninstall.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
// QNSQY npm wrapper uninstall hook.
|
|
5
|
-
// Removes the downloaded binary on `npm uninstall qnsqy`. Idempotent: never errors.
|
|
6
|
-
|
|
7
|
-
const fs = require('node:fs');
|
|
8
|
-
const path = require('node:path');
|
|
9
|
-
|
|
10
|
-
const BIN_DIR = path.join(__dirname, 'bin');
|
|
11
|
-
const candidates = [
|
|
12
|
-
path.join(BIN_DIR, 'qnsqy-bin'),
|
|
13
|
-
path.join(BIN_DIR, 'qnsqy-bin.exe'),
|
|
14
|
-
path.join(BIN_DIR, 'qnsqy-bin.tmp'),
|
|
15
|
-
path.join(BIN_DIR, 'qnsqy-bin.exe.tmp'),
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
for (let i = 0; i < candidates.length; i++) {
|
|
19
|
-
try {
|
|
20
|
-
fs.unlinkSync(candidates[i]);
|
|
21
|
-
} catch (_) {
|
|
22
|
-
// already gone, ignore.
|
|
23
|
-
}
|
|
24
|
-
}
|