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 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
+ }