qnsqy 7.2.20 → 7.2.22

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 CHANGED
@@ -1,11 +1,13 @@
1
1
  QNSQY is proprietary software. (c) 2026 Quantum Sequrity.
2
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.
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 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.
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
- 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.
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 exits 1 on darwin with a friendly message. |
68
- | ARM (any OS) | Not shipping yet. npm install exits 1 with a friendly message. |
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
- 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
+ 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 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`).
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 `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:
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 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:
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
- QNSQY_BINARY_PATH=/path/to/qnsqy \
115
- npm install -g --ignore-scripts qnsqy
124
+ npm install -g qnsqy # or: npm install -g --ignore-scripts qnsqy
116
125
 
117
- # 4. Re-run the postinstall by hand so QNSQY_BINARY_PATH is honoured:
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
- 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.
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 downloaded binary manually against the official
130
- checksums on the download page.
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.19-1_amd64.deb
143
+ sha256sum qnsqy_7.2.20-1_amd64.deb
135
144
  # Expected:
136
- # 983bfed387a969ecf9983f65fa27ee3025ed6edb5c32a38b91acd78a04d561a9
145
+ # 93caee47f8af7c09f73373771ab116019d04903d6958369e865c77638e600afc
137
146
 
138
147
  # Windows standalone
139
- certutil -hashfile qnsqy-7.2.19-x86_64.exe SHA256
148
+ certutil -hashfile qnsqy-7.2.20-x86_64.exe SHA256
140
149
  # Expected:
141
- # a6aaabdca0864dd843b8f238471a7c5d15517b05ffd6b322eac6ca37a570c090
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 placed by postinstall.js and
5
- // execs it with the user's argv. Exit code and signals propagate.
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
- const isWin = process.platform === 'win32';
12
- const binPath = path.join(__dirname, 'qnsqy-bin' + (isWin ? '.exe' : ''));
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
- if (!fs.existsSync(binPath)) {
15
- const postinstall = path.join(__dirname, '..', 'postinstall.js');
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: 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'
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
- process.exit(1);
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
- (err && err.message ? err.message : String(err)) + '\n'
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
- // Some signals are not supported on Windows; fall through.
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.20",
3
+ "version": "7.2.22",
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",
@@ -23,35 +23,20 @@
23
23
  ],
24
24
  "homepage": "https://quantumsequrity.com",
25
25
  "bugs": "https://quantumsequrity.com/contact",
26
- "repository": {
27
- "type": "git",
28
- "url": "git+https://github.com/quantumsequrity/qnsqy.git"
29
- },
30
26
  "license": "SEE LICENSE IN LICENSE",
31
27
  "author": "Quantum Sequrity",
32
28
  "bin": {
33
29
  "qnsqy": "bin/qnsqy.js"
34
30
  },
35
- "scripts": {
36
- "postinstall": "node postinstall.js",
37
- "uninstall": "node uninstall.js",
38
- "preuninstall": "node uninstall.js"
31
+ "optionalDependencies": {
32
+ "@quantumsequrity/qnsqy-linux-x64": "7.2.22",
33
+ "@quantumsequrity/qnsqy-win32-x64": "7.2.22"
39
34
  },
40
35
  "engines": {
41
36
  "node": ">=18.0.0"
42
37
  },
43
- "os": [
44
- "linux",
45
- "win32"
46
- ],
47
- "cpu": [
48
- "x64"
49
- ],
50
38
  "files": [
51
39
  "bin/",
52
- "lib/",
53
- "postinstall.js",
54
- "uninstall.js",
55
40
  "README.md",
56
41
  "LICENSE"
57
42
  ]
package/lib/manifest.json DELETED
@@ -1,23 +0,0 @@
1
- {
2
- "qnsqy_version": "7.2.20",
3
- "generated_at": "2026-06-01T11:00Z",
4
- "source": "freshly built from qs-ultra/ source tree 2026-06-01 (Ubuntu 22.04 / glibc 2.35 container build); matches website-YC/download.md SHA-256 Checksums (v7.2.20)",
5
- "platforms": {
6
- "linux-x64": {
7
- "url": "https://cdn.quantumsequrity.com/linux/qnsqy_7.2.20-1_amd64.deb",
8
- "filename": "qnsqy_7.2.20-1_amd64.deb",
9
- "format": "deb",
10
- "extract": "deb",
11
- "sha256": "93caee47f8af7c09f73373771ab116019d04903d6958369e865c77638e600afc",
12
- "binary_path_in_archive": "usr/bin/qnsqy"
13
- },
14
- "win32-x64": {
15
- "url": "https://cdn.quantumsequrity.com/windows/qnsqy-7.2.20-x86_64.exe",
16
- "filename": "qnsqy-7.2.20-x86_64.exe",
17
- "format": "exe",
18
- "extract": "passthrough",
19
- "sha256": "1383df812dff16cc593b0caab6bbe6092184a42f212aac8d13e42ed6a52b8f38",
20
- "binary_path_in_archive": "qnsqy-7.2.20-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.20';
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
- }