qnsqy 7.2.19
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 +11 -0
- package/README.md +195 -0
- package/bin/qnsqy.js +72 -0
- package/lib/manifest.json +23 -0
- package/package.json +58 -0
- package/postinstall.js +443 -0
- package/uninstall.js +24 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
QNSQY is proprietary software. (c) 2026 Quantum Sequrity.
|
|
2
|
+
|
|
3
|
+
The binary downloaded by this npm package is licensed under the QNSQY
|
|
4
|
+
end-user license agreement, available at https://quantumsequrity.com/terms.
|
|
5
|
+
|
|
6
|
+
This npm wrapper package (the JavaScript scaffolding, postinstall script,
|
|
7
|
+
README, and LICENSE) is licensed for use solely to facilitate installation
|
|
8
|
+
of the QNSQY binary. Redistribution of the wrapper without the binary, or
|
|
9
|
+
modification of the wrapper to fetch a different binary, is not permitted.
|
|
10
|
+
|
|
11
|
+
Source-available for security audit on request: security@quantumsequrity.com.
|
package/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# QNSQY
|
|
2
|
+
|
|
3
|
+
Post-quantum cryptography tool, NIST FIPS 203 / 204 / 205.
|
|
4
|
+
|
|
5
|
+
QNSQY ships ML-KEM (FIPS 203), ML-DSA (FIPS 204), and SLH-DSA (FIPS 205)
|
|
6
|
+
hybridized with X25519, Ed25519, and AES-256-GCM. The single binary
|
|
7
|
+
covers encrypt, decrypt, sign, verify, hash, keygen, threshold, and
|
|
8
|
+
escrow operations. File content, passwords, and private keys never leave
|
|
9
|
+
the machine. Network access is restricted to billing metadata only,
|
|
10
|
+
enforced by seccomp-bpf on Linux. 84 MCP tools are exposed for AI agents
|
|
11
|
+
over JSON-RPC 2.0 stdio.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
Install globally:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
npm install -g qnsqy
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Run once without installing:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
npx qnsqy --help
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
On install, this package downloads the QNSQY 7.2.19 binary for your
|
|
28
|
+
platform from `cdn.quantumsequrity.com`, verifies its SHA-256 hash
|
|
29
|
+
against the value pinned in `lib/manifest.json`, and places it under
|
|
30
|
+
`node_modules/.bin/qnsqy`. No binary is bundled in the npm tarball.
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
# Encrypt a file. The default algorithm is ML-KEM-512 + X25519 hybrid.
|
|
36
|
+
# The output is secret.pdf.qs.
|
|
37
|
+
qnsqy encrypt -i secret.pdf
|
|
38
|
+
|
|
39
|
+
# Decrypt.
|
|
40
|
+
qnsqy decrypt -i secret.pdf.qs
|
|
41
|
+
|
|
42
|
+
# Generate a hybrid signing keypair (ML-DSA-44 + Ed25519).
|
|
43
|
+
qnsqy keygen-sign -o mykey -n "My Signing Key"
|
|
44
|
+
|
|
45
|
+
# Sign a file.
|
|
46
|
+
qnsqy sign -i report.txt -k mykey
|
|
47
|
+
|
|
48
|
+
# Verify a signature.
|
|
49
|
+
qnsqy verify -i report.txt -k mykey.pub
|
|
50
|
+
|
|
51
|
+
# BLAKE3 hash a file.
|
|
52
|
+
qnsqy hash -i secret.pdf
|
|
53
|
+
|
|
54
|
+
# Show version and tier.
|
|
55
|
+
qnsqy version
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For the full command list, run `qnsqy --help` or see
|
|
59
|
+
https://quantumsequrity.com/docs.html.
|
|
60
|
+
|
|
61
|
+
## Platforms
|
|
62
|
+
|
|
63
|
+
| Platform | Status |
|
|
64
|
+
|-----------------|----------------------------------------------------------------------------------|
|
|
65
|
+
| Linux x86_64 | Supported. Requires glibc 2.35+ (Ubuntu 22.04+, Debian 12+, Fedora 40+, AlmaLinux 10). |
|
|
66
|
+
| Windows x86_64 | Supported. Windows 10 1809+ and Windows 11. |
|
|
67
|
+
| macOS | Not shipping yet. Target Q3 2026. npm install exits 1 on darwin with a friendly message. |
|
|
68
|
+
| ARM (any OS) | Not shipping yet. npm install exits 1 with a friendly message. |
|
|
69
|
+
|
|
70
|
+
If `npm install` refuses to install on your platform with an
|
|
71
|
+
`EBADPLATFORM` error, that is the npm `os` / `cpu` field acting as a
|
|
72
|
+
guard rail. It is not a bug in your toolchain.
|
|
73
|
+
|
|
74
|
+
## How this package works
|
|
75
|
+
|
|
76
|
+
The npm package is a thin wrapper. The actual QNSQY binary is not
|
|
77
|
+
bundled.
|
|
78
|
+
|
|
79
|
+
1. `npm install -g qnsqy` triggers `postinstall.js`.
|
|
80
|
+
2. `postinstall.js` selects the right artifact for your platform from
|
|
81
|
+
`lib/manifest.json`.
|
|
82
|
+
3. The artifact is downloaded from `cdn.quantumsequrity.com` over HTTPS,
|
|
83
|
+
using Node 18+ built-in `fetch`. `HTTPS_PROXY` and `HTTP_PROXY` env
|
|
84
|
+
vars are honoured via the standard agent.
|
|
85
|
+
4. The download is verified against the SHA-256 hash pinned in the
|
|
86
|
+
manifest. On mismatch, install fails and both hashes are printed.
|
|
87
|
+
5. On Linux the DEB is unpacked in-process (the wrapper parses the `ar`
|
|
88
|
+
archive itself, no `dpkg` required) and `usr/bin/qnsqy` is extracted
|
|
89
|
+
with the system `tar`.
|
|
90
|
+
6. On Windows the standalone .exe is dropped directly into the package's
|
|
91
|
+
`bin/` directory.
|
|
92
|
+
7. The binary is installed atomically (write to `.tmp`, fsync, rename)
|
|
93
|
+
and chmod 755 on POSIX.
|
|
94
|
+
8. `node_modules/.bin/qnsqy` is a Node shim (`bin/qnsqy.js`) that execs
|
|
95
|
+
the platform binary with your arguments and propagates exit codes and
|
|
96
|
+
signals.
|
|
97
|
+
|
|
98
|
+
The wrapper has zero npm dependencies. It uses Node 18+ stdlib only
|
|
99
|
+
(`fs`, `path`, `os`, `crypto`, `child_process`, built-in `fetch`).
|
|
100
|
+
|
|
101
|
+
## Air-gapped install
|
|
102
|
+
|
|
103
|
+
If your machine cannot reach `cdn.quantumsequrity.com`, pre-stage the
|
|
104
|
+
QNSQY binary on disk and point the postinstall at it with the
|
|
105
|
+
`QNSQY_BINARY_PATH` env var:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
# 1. On a machine with network access, download the binary for the
|
|
109
|
+
# target platform from https://quantumsequrity.com/download.
|
|
110
|
+
# Verify its SHA-256 manually.
|
|
111
|
+
# 2. Copy the binary to the air-gapped machine (USB, internal mirror, etc.).
|
|
112
|
+
# 3. Install with scripts disabled so the wrapper does not try to fetch:
|
|
113
|
+
|
|
114
|
+
QNSQY_BINARY_PATH=/path/to/qnsqy \
|
|
115
|
+
npm install -g --ignore-scripts qnsqy
|
|
116
|
+
|
|
117
|
+
# 4. Re-run the postinstall by hand so QNSQY_BINARY_PATH is honoured:
|
|
118
|
+
|
|
119
|
+
QNSQY_BINARY_PATH=/path/to/qnsqy \
|
|
120
|
+
node $(npm root -g)/qnsqy/postinstall.js
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The escape hatch copies the binary verbatim. No SHA-256 check is
|
|
124
|
+
performed in that path: you are responsible for verifying the binary
|
|
125
|
+
out-of-band before staging it.
|
|
126
|
+
|
|
127
|
+
## Verification
|
|
128
|
+
|
|
129
|
+
You can verify the downloaded binary manually against the official
|
|
130
|
+
checksums on the download page.
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
# Linux DEB
|
|
134
|
+
sha256sum qnsqy_7.2.19-1_amd64.deb
|
|
135
|
+
# Expected:
|
|
136
|
+
# 983bfed387a969ecf9983f65fa27ee3025ed6edb5c32a38b91acd78a04d561a9
|
|
137
|
+
|
|
138
|
+
# Windows standalone
|
|
139
|
+
certutil -hashfile qnsqy-7.2.19-x86_64.exe SHA256
|
|
140
|
+
# Expected:
|
|
141
|
+
# a6aaabdca0864dd843b8f238471a7c5d15517b05ffd6b322eac6ca37a570c090
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The canonical hash list is published at
|
|
145
|
+
https://quantumsequrity.com/download under "SHA-256 Checksums".
|
|
146
|
+
|
|
147
|
+
QNSQY releases are also signed with ML-DSA-87 (NIST FIPS 204) and logged
|
|
148
|
+
to the Sigstore Rekor transparency log. See the download page for the
|
|
149
|
+
post-quantum signature verification flow.
|
|
150
|
+
|
|
151
|
+
## Tier model
|
|
152
|
+
|
|
153
|
+
QNSQY has four tiers: Free, Pro, Business, Enterprise. Tier is determined
|
|
154
|
+
at runtime by the billing API on first run, not at install time.
|
|
155
|
+
Installing via npm does not grant Pro, Business, or Enterprise access.
|
|
156
|
+
|
|
157
|
+
- Free covers ML-KEM-512 + ML-DSA-44 with no file size limit and works
|
|
158
|
+
without an account. Advanced algorithms have a 100 MB per-file limit.
|
|
159
|
+
- Pro unlocks ML-KEM-768/1024, ML-DSA-65/87, SLH-DSA, 25 GB file limit,
|
|
160
|
+
batch operations, the encrypted password vault, audit logging, and
|
|
161
|
+
password rekey.
|
|
162
|
+
- Business adds HQC, FN-DSA, LMS, pure KEM mode, M-of-N threshold
|
|
163
|
+
encryption, Shamir secret sharing, time-lock, steganography,
|
|
164
|
+
deniable encryption, polyglot files, PQ migration scanning, encryption
|
|
165
|
+
policy management, recipient groups, and key escrow.
|
|
166
|
+
- Enterprise adds air-gap license bundles, HSM integration, SLA, and a
|
|
167
|
+
dedicated engineering channel.
|
|
168
|
+
|
|
169
|
+
See https://quantumsequrity.com/pricing.html for current pricing.
|
|
170
|
+
|
|
171
|
+
## Disclaimers
|
|
172
|
+
|
|
173
|
+
QNSQY uses NIST-standardized algorithms (FIPS 203 ML-KEM, FIPS 204
|
|
174
|
+
ML-DSA, FIPS 205 SLH-DSA, FIPS 206 draft FN-DSA, SP 800-208 LMS, RFC 9106
|
|
175
|
+
Argon2id). The product itself holds no CMVP certificate (so is not a
|
|
176
|
+
FIPS 140 module), no SOC 2 attestation, no U.S. federal cloud ATO under
|
|
177
|
+
the standard government program, and no external HIPAA audit on file.
|
|
178
|
+
The vendor does not sign HIPAA Business Associate Agreements.
|
|
179
|
+
|
|
180
|
+
The distinction matters: "uses NIST algorithms" is a property of the
|
|
181
|
+
cryptographic primitives; "FIPS 140 validated" is a property of a
|
|
182
|
+
CMVP-tested cryptographic module. QNSQY is the former, not the latter.
|
|
183
|
+
|
|
184
|
+
Compliance documentation packages (control mapping, algorithm usage,
|
|
185
|
+
data flow) are available on request for Business tier customers as a
|
|
186
|
+
starting point for your internal or third-party audit.
|
|
187
|
+
|
|
188
|
+
## Links
|
|
189
|
+
|
|
190
|
+
- Homepage: https://quantumsequrity.com
|
|
191
|
+
- Documentation: https://quantumsequrity.com/docs.html
|
|
192
|
+
- Pricing: https://quantumsequrity.com/pricing.html
|
|
193
|
+
- Download page: https://quantumsequrity.com/download
|
|
194
|
+
- Contact: https://quantumsequrity.com/contact
|
|
195
|
+
- Security disclosure: security@quantumsequrity.com
|
package/bin/qnsqy.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// QNSQY shim. Resolves the platform binary placed by postinstall.js and
|
|
5
|
+
// execs it with the user's argv. Exit code and signals propagate.
|
|
6
|
+
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const { spawn } = require('node:child_process');
|
|
10
|
+
|
|
11
|
+
const isWin = process.platform === 'win32';
|
|
12
|
+
const binPath = path.join(__dirname, 'qnsqy-bin' + (isWin ? '.exe' : ''));
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(binPath)) {
|
|
15
|
+
const postinstall = path.join(__dirname, '..', 'postinstall.js');
|
|
16
|
+
process.stderr.write(
|
|
17
|
+
'qnsqy: binary not found at ' + binPath + '\n' +
|
|
18
|
+
'qnsqy: this usually means `npm install` ran with --ignore-scripts, ' +
|
|
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'
|
|
25
|
+
);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const child = spawn(binPath, process.argv.slice(2), { stdio: 'inherit' });
|
|
30
|
+
|
|
31
|
+
function forwardSignal(sig) {
|
|
32
|
+
if (child && child.pid && !child.killed) {
|
|
33
|
+
try {
|
|
34
|
+
child.kill(sig);
|
|
35
|
+
} catch (_) {
|
|
36
|
+
// child may have already exited
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function detachSignalListeners() {
|
|
42
|
+
try { process.removeAllListeners('SIGINT'); } catch (_) {}
|
|
43
|
+
try { process.removeAllListeners('SIGTERM'); } catch (_) {}
|
|
44
|
+
try { process.removeAllListeners('SIGHUP'); } catch (_) {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
process.on('SIGINT', function onSigint() { forwardSignal('SIGINT'); });
|
|
48
|
+
process.on('SIGTERM', function onSigterm() { forwardSignal('SIGTERM'); });
|
|
49
|
+
process.on('SIGHUP', function onSighup() { forwardSignal('SIGHUP'); });
|
|
50
|
+
|
|
51
|
+
child.on('error', function (err) {
|
|
52
|
+
detachSignalListeners();
|
|
53
|
+
process.stderr.write(
|
|
54
|
+
'qnsqy: failed to spawn ' + binPath + ': ' +
|
|
55
|
+
(err && err.message ? err.message : String(err)) + '\n'
|
|
56
|
+
);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
child.on('exit', function (code, signal) {
|
|
61
|
+
detachSignalListeners();
|
|
62
|
+
if (signal) {
|
|
63
|
+
try {
|
|
64
|
+
process.kill(process.pid, signal);
|
|
65
|
+
} catch (_) {
|
|
66
|
+
// Some signals are not supported on Windows; fall through.
|
|
67
|
+
}
|
|
68
|
+
process.exit(1);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
process.exit(typeof code === 'number' ? code : 1);
|
|
72
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
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/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qnsqy",
|
|
3
|
+
"version": "7.2.19",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"post-quantum",
|
|
7
|
+
"cryptography",
|
|
8
|
+
"encryption",
|
|
9
|
+
"ml-kem",
|
|
10
|
+
"ml-dsa",
|
|
11
|
+
"slh-dsa",
|
|
12
|
+
"fips-203",
|
|
13
|
+
"fips-204",
|
|
14
|
+
"fips-205",
|
|
15
|
+
"hybrid",
|
|
16
|
+
"pqc",
|
|
17
|
+
"quantum-safe",
|
|
18
|
+
"quantum-resistant",
|
|
19
|
+
"harvest-now-decrypt-later",
|
|
20
|
+
"hndl",
|
|
21
|
+
"mcp",
|
|
22
|
+
"model-context-protocol"
|
|
23
|
+
],
|
|
24
|
+
"homepage": "https://quantumsequrity.com",
|
|
25
|
+
"bugs": "https://quantumsequrity.com/contact",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/quantumsequrity/qnsqy.git"
|
|
29
|
+
},
|
|
30
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
31
|
+
"author": "Quantum Sequrity",
|
|
32
|
+
"bin": {
|
|
33
|
+
"qnsqy": "bin/qnsqy.js"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"postinstall": "node postinstall.js",
|
|
37
|
+
"uninstall": "node uninstall.js",
|
|
38
|
+
"preuninstall": "node uninstall.js"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
},
|
|
43
|
+
"os": [
|
|
44
|
+
"linux",
|
|
45
|
+
"win32"
|
|
46
|
+
],
|
|
47
|
+
"cpu": [
|
|
48
|
+
"x64"
|
|
49
|
+
],
|
|
50
|
+
"files": [
|
|
51
|
+
"bin/",
|
|
52
|
+
"lib/",
|
|
53
|
+
"postinstall.js",
|
|
54
|
+
"uninstall.js",
|
|
55
|
+
"README.md",
|
|
56
|
+
"LICENSE"
|
|
57
|
+
]
|
|
58
|
+
}
|
package/postinstall.js
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
}
|