@blamejs/core 0.9.12 → 0.9.14

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.
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.selfUpdate.standaloneVerifier
4
+ * @nav Production
5
+ * @title Self-Update Standalone Verifier
6
+ * @order 640
7
+ *
8
+ * @intro
9
+ * Zero-dep companion to `b.selfUpdate.verify` for install-pipeline
10
+ * contexts that run BEFORE the framework itself is installed —
11
+ * Dockerfile build stages, `install.sh`, `update.sh`, SEA-bundle
12
+ * verification at deploy time. The full `b.selfUpdate.verify`
13
+ * chain reaches into `b.crypto`, `b.httpClient`, `b.audit`, vendor
14
+ * imports, etc.; none of those exist yet when an operator's
15
+ * install script runs `node verify-release.js` against the
16
+ * downloaded artifact.
17
+ *
18
+ * This module is intentionally hermetic — `node:crypto` + `node:fs`
19
+ * only, no framework imports, no third-party modules. Operators
20
+ * physically copy the file into their install pipeline alongside a
21
+ * public-key module they own. Both go into version control on the
22
+ * operator's side; neither updates without their explicit action.
23
+ *
24
+ * Surface (single function):
25
+ *
26
+ * verify(assetPath, signaturePath, pubkeyPem, opts?) → {
27
+ * ok: boolean,
28
+ * sha3_512: string, // hex digest of asset bytes (SBOM correlation)
29
+ * sha256: string, // hex digest of asset bytes (defense-in-depth)
30
+ * alg: string, // detected algorithm: "ecdsa-p384" | "ed25519" | "ml-dsa-87"
31
+ * }
32
+ *
33
+ * The function refuses to load the asset into memory in one go;
34
+ * it streams the bytes through both hashers + the signature
35
+ * verifier so multi-GB SEA bundles don't OOM the install runner.
36
+ *
37
+ * Throws on:
38
+ * - missing asset / signature / pubkey file
39
+ * - unrecognized pubkey PEM shape
40
+ * - signature length mismatch with the algorithm
41
+ * - cryptographic verify failure
42
+ *
43
+ * Per the operator's request that surfaced this primitive
44
+ * (hermitstash-sync 2026-05-13): the install pipeline needs P-384
45
+ * ECDSA + SHA3-512 as the baseline cross-check. ML-DSA-87 is also
46
+ * supported when the operator's pubkey carries the corresponding
47
+ * OID (Node 22+ via the FIPS 204 OIDs in node:crypto).
48
+ *
49
+ * ## How operators consume this
50
+ *
51
+ * ```sh
52
+ * # one-time copy at framework-install time:
53
+ * cp "$(node -p "require('@blamejs/core').selfUpdate.standaloneVerifier.path")" \
54
+ * install/standalone-verifier.js
55
+ * ```
56
+ *
57
+ * ```js
58
+ * // install/verify-release.js (operator-owned, in their repo):
59
+ * var verifier = require("./standalone-verifier");
60
+ * var pubkey = require("./release-pubkey"); // operator-owned PEM
61
+ *
62
+ * var result = verifier.verify(
63
+ * "/tmp/blamejs-sea-bundle",
64
+ * "/tmp/blamejs-sea-bundle.sig",
65
+ * pubkey,
66
+ * );
67
+ * if (!result.ok) {
68
+ * process.stderr.write("release verification FAILED\n");
69
+ * process.exit(1);
70
+ * }
71
+ * process.stdout.write("verified " + result.alg + " sha3-512=" + result.sha3_512 + "\n");
72
+ * ```
73
+ *
74
+ * The module is also reachable as `b.selfUpdate.standaloneVerifier.verify`
75
+ * from inside a fully-installed framework process — useful for tests
76
+ * that exercise the same code path the operator's install pipeline
77
+ * does, without forking a subprocess.
78
+ *
79
+ * @card
80
+ * Zero-dep verifier for use BEFORE the framework is installed.
81
+ * Install-pipeline scripts copy this file alongside an operator-owned
82
+ * pubkey to verify signed release artifacts during Dockerfile build
83
+ * or systemd `install.sh`. node:crypto + node:fs only.
84
+ */
85
+
86
+ var nodeCrypto = require("crypto");
87
+ var nodeFs = require("fs");
88
+
89
+ // _streamHashAndVerify — read the asset in 64 KiB chunks, feed each
90
+ // chunk into sha256, sha3-512, AND the signature verifier in parallel.
91
+ // Single pass over the file; no in-memory copy. node:crypto's
92
+ // `createVerify` consumes streaming input via `.update()` for ECDSA +
93
+ // EdDSA; ML-DSA's `crypto.verify` requires the full payload, so we
94
+ // also accumulate to a buffer ONLY when the alg requires it.
95
+ function _detectAlg(pubkeyPem) {
96
+ // Inspect the PEM header / SPKI for a recognizable curve / OID. The
97
+ // pubkey PEM carries the algorithm identifier in the SPKI ASN.1; we
98
+ // load it via createPublicKey() and read `asymmetricKeyType` +
99
+ // `asymmetricKeyDetails.namedCurve`.
100
+ var key;
101
+ try {
102
+ key = nodeCrypto.createPublicKey(pubkeyPem);
103
+ } catch (e) {
104
+ throw new Error("standalone-verifier: pubkey PEM did not parse: " +
105
+ (e && e.message ? e.message : String(e)));
106
+ }
107
+ var t = key.asymmetricKeyType;
108
+ if (t === "ec") {
109
+ var curve = key.asymmetricKeyDetails && key.asymmetricKeyDetails.namedCurve;
110
+ if (curve === "P-384" || curve === "secp384r1") return { alg: "ecdsa-p384", key: key };
111
+ throw new Error("standalone-verifier: unsupported EC curve '" + curve + "' (need P-384)");
112
+ }
113
+ if (t === "ed25519") return { alg: "ed25519", key: key };
114
+ if (t === "ml-dsa-87" || t === "ml-dsa") return { alg: "ml-dsa-87", key: key };
115
+ throw new Error("standalone-verifier: unrecognized pubkey type '" + t + "' " +
116
+ "(need ecdsa-p384, ed25519, or ml-dsa-87)");
117
+ }
118
+
119
+ /**
120
+ * @primitive b.selfUpdate.standaloneVerifier.verify
121
+ * @signature b.selfUpdate.standaloneVerifier.verify(assetPath, signaturePath, pubkeyPem)
122
+ * @since 0.9.13
123
+ * @status stable
124
+ * @related b.selfUpdate.verify
125
+ *
126
+ * Verify a signed release asset using only `node:crypto` + `node:fs`
127
+ * (no framework imports). For install-pipeline contexts where the
128
+ * framework itself is not yet installed.
129
+ *
130
+ * Streams the asset in 64 KiB chunks through SHA-256 + SHA-3-512 + the
131
+ * signature verifier in parallel — single allocation peak (one buffer
132
+ * sized to fstat(asset).size for Ed25519 / ML-DSA-87, ECDSA P-384 needs
133
+ * no buffer because createVerify is incremental).
134
+ *
135
+ * Returns `{ ok, sha3_512, sha256, alg }` on success; throws on
136
+ * unrecognized pubkey shape, missing files, or signature mismatch.
137
+ * `alg` is one of `"ecdsa-p384"`, `"ed25519"`, `"ml-dsa-87"` (auto-
138
+ * detected from the pubkey PEM).
139
+ *
140
+ * @example
141
+ * var verifier = require("./standalone-verifier");
142
+ * var pubkey = require("./release-pubkey");
143
+ * var result = verifier.verify(
144
+ * "/tmp/blamejs-sea-bundle",
145
+ * "/tmp/blamejs-sea-bundle.sig",
146
+ * pubkey,
147
+ * );
148
+ * if (!result.ok) process.exit(1);
149
+ * process.stdout.write("verified " + result.alg + " sha3-512=" + result.sha3_512 + "\n");
150
+ */
151
+ function verify(assetPath, signaturePath, pubkeyPem) {
152
+ if (typeof assetPath !== "string" || assetPath.length === 0) {
153
+ throw new Error("standalone-verifier.verify: assetPath must be a non-empty string");
154
+ }
155
+ if (typeof signaturePath !== "string" || signaturePath.length === 0) {
156
+ throw new Error("standalone-verifier.verify: signaturePath must be a non-empty string");
157
+ }
158
+ if (typeof pubkeyPem !== "string" || pubkeyPem.indexOf("-----BEGIN ") !== 0) {
159
+ throw new Error("standalone-verifier.verify: pubkeyPem must be a PEM-encoded public key string");
160
+ }
161
+
162
+ // Open both files BEFORE parsing the pubkey so we own stable fds
163
+ // against TOCTOU races (CodeQL js/file-system-race) — checking
164
+ // existsSync before readFileSync leaves a swap window. Asset opens
165
+ // first so a missing-asset path surfaces before a missing-sig path.
166
+ var assetFd;
167
+ try {
168
+ assetFd = nodeFs.openSync(assetPath, "r");
169
+ } catch (e) {
170
+ throw new Error("standalone-verifier.verify: asset not found at " + assetPath +
171
+ " — " + (e && e.message ? e.message : String(e)));
172
+ }
173
+ var sigFd;
174
+ try {
175
+ sigFd = nodeFs.openSync(signaturePath, "r");
176
+ } catch (e) {
177
+ nodeFs.closeSync(assetFd);
178
+ throw new Error("standalone-verifier.verify: signature not found at " + signaturePath +
179
+ " — " + (e && e.message ? e.message : String(e)));
180
+ }
181
+ var signature;
182
+ try {
183
+ var sigStat = nodeFs.fstatSync(sigFd);
184
+ signature = Buffer.allocUnsafe(sigStat.size);
185
+ if (sigStat.size > 0) nodeFs.readSync(sigFd, signature, 0, sigStat.size, 0);
186
+ } finally {
187
+ nodeFs.closeSync(sigFd);
188
+ }
189
+ if (signature.length === 0) {
190
+ nodeFs.closeSync(assetFd);
191
+ throw new Error("standalone-verifier.verify: signature file is empty");
192
+ }
193
+
194
+ var detected;
195
+ try {
196
+ detected = _detectAlg(pubkeyPem);
197
+ } catch (e) {
198
+ nodeFs.closeSync(assetFd);
199
+ throw e;
200
+ }
201
+ var alg = detected.alg;
202
+ var key = detected.key;
203
+
204
+ // Stream the asset through both hashers. For ECDSA we stream through
205
+ // createVerify (incremental). For Ed25519 / ML-DSA we pre-allocate
206
+ // ONE buffer of stat.size and stream-fill it at increasing offsets —
207
+ // single allocation peak, not the 2× peak that Buffer.concat([...chunks])
208
+ // produces. 64 KiB chunks match the framework's hash-while-streaming
209
+ // convention elsewhere.
210
+ var sha256 = nodeCrypto.createHash("sha256");
211
+ var sha3 = nodeCrypto.createHash("sha3-512");
212
+ var verifier = (alg === "ecdsa-p384") ? nodeCrypto.createVerify("sha3-512") : null;
213
+ var fullBuf = null;
214
+ var fullOff = 0;
215
+ if (verifier === null) {
216
+ var assetStat = nodeFs.fstatSync(assetFd);
217
+ fullBuf = Buffer.allocUnsafe(assetStat.size);
218
+ }
219
+
220
+ try {
221
+ var chunk = Buffer.allocUnsafe(64 * 1024); // allow:raw-byte-literal — module is zero-dep by contract; cannot import C.BYTES
222
+ while (true) {
223
+ var n = nodeFs.readSync(assetFd, chunk, 0, chunk.length, null);
224
+ if (n === 0) break;
225
+ var slice = chunk.subarray(0, n);
226
+ sha256.update(slice);
227
+ sha3.update(slice);
228
+ if (verifier) verifier.update(slice);
229
+ if (fullBuf) {
230
+ slice.copy(fullBuf, fullOff);
231
+ fullOff += n;
232
+ }
233
+ }
234
+ } finally {
235
+ nodeFs.closeSync(assetFd);
236
+ }
237
+
238
+ var sha256Hex = sha256.digest("hex");
239
+ var sha3Hex = sha3.digest("hex");
240
+
241
+ var ok = false;
242
+ if (alg === "ecdsa-p384") {
243
+ // P-384 IEEE-P1363 sigs are exactly 96 bytes (48-byte r || 48-byte s).
244
+ // P-384 DER sigs are variable (~100-104 bytes — ASN.1 SEQUENCE
245
+ // wrapping two INTEGERs). Detect by length so we only call
246
+ // verifier.verify ONCE — calling it a second time after a failed
247
+ // verify returns stale state and silently passes tampered assets.
248
+ // 96 = P-384 IEEE-P1363 signature length; protocol constant, not a byte-size.
249
+ var dsaEncoding = signature.length === 96 ? "ieee-p1363" : "der"; // allow:raw-byte-literal — IEEE-P1363 P-384 signature length
250
+ ok = verifier.verify({ key: key, dsaEncoding: dsaEncoding }, signature);
251
+ } else if (alg === "ed25519") {
252
+ // fullBuf may be shorter than allocated (sparse files / size-races);
253
+ // slice to fullOff so verify sees only the bytes we actually read.
254
+ ok = nodeCrypto.verify(null, fullBuf.subarray(0, fullOff), key, signature);
255
+ } else if (alg === "ml-dsa-87") {
256
+ ok = nodeCrypto.verify(null, fullBuf.subarray(0, fullOff), key, signature);
257
+ }
258
+
259
+ if (!ok) {
260
+ throw new Error("standalone-verifier.verify: " + alg + " signature INVALID for " +
261
+ assetPath + " (sha3-512=" + sha3Hex.slice(0, 16) + "...). " + // allow:raw-byte-literal — 16-char hex prefix for forensic display, not bytes
262
+ "Either the asset was tampered with after signing, the signature " +
263
+ "doesn't match this asset, or the pubkey doesn't match the signing key.");
264
+ }
265
+
266
+ return {
267
+ ok: true,
268
+ sha3_512: sha3Hex,
269
+ sha256: sha256Hex,
270
+ alg: alg,
271
+ };
272
+ }
273
+
274
+ module.exports = {
275
+ verify: verify,
276
+ // Absolute path to this module file. Operators copy it via:
277
+ // cp "$(node -p "require('@blamejs/core').selfUpdate.standaloneVerifier.path")" \
278
+ // install/standalone-verifier.js
279
+ path: __filename,
280
+ };
@@ -59,6 +59,7 @@ var safeJson = require("./safe-json");
59
59
  var { URL: NodeUrl } = require("url");
60
60
  var lazyRequire = require("./lazy-require");
61
61
  var C = require("./constants");
62
+ var standaloneVerifier = require("./self-update-standalone-verifier");
62
63
  var { boot } = require("./log");
63
64
  var { defineClass } = require("./framework-error");
64
65
 
@@ -635,13 +636,18 @@ async function rollback(opts) {
635
636
  }
636
637
 
637
638
  module.exports = {
638
- poll: poll,
639
- verify: verify,
640
- swap: swap,
641
- rollback: rollback,
642
- SelfUpdateError: SelfUpdateError,
643
- ALLOWED_HASH_ALGS: ALLOWED_HASH_ALGS,
644
- DEFAULT_HASH_ALG: DEFAULT_HASH_ALG,
639
+ poll: poll,
640
+ verify: verify,
641
+ swap: swap,
642
+ rollback: rollback,
643
+ // Standalone verifier — zero-dep companion for install-pipeline
644
+ // contexts that run BEFORE the framework is installed (Dockerfile
645
+ // build stages, install.sh, update.sh). See the module's intro for
646
+ // the copy-this-file workflow.
647
+ standaloneVerifier: standaloneVerifier,
648
+ SelfUpdateError: SelfUpdateError,
649
+ ALLOWED_HASH_ALGS: ALLOWED_HASH_ALGS,
650
+ DEFAULT_HASH_ALG: DEFAULT_HASH_ALG,
645
651
  // Internal — exposed for the layer-0 test suite only.
646
- _compareTags: _compareTags,
652
+ _compareTags: _compareTags,
647
653
  };
@@ -52,6 +52,7 @@ var fs = require("fs");
52
52
  var path = require("path");
53
53
  var { DatabaseSync } = require("node:sqlite");
54
54
  var atomicFile = require("../atomic-file");
55
+ var safeSql = require("../safe-sql");
55
56
  var C = require("../constants");
56
57
  var cryptoField = require("../crypto-field");
57
58
  var cryptoLib = require("../crypto");
@@ -437,8 +438,11 @@ function _walkAndReSeal(node, oldKeys, newKeys) {
437
438
  function _runStmt(db, sql) { db.prepare(sql).run(); }
438
439
 
439
440
  function _rotateColumn(db, table, column, oldKeys, newKeys, batchSize, progress) {
440
- var qt = '"' + table.replace(/"/g, '""') + '"';
441
- var qc = '"' + column.replace(/"/g, '""') + '"';
441
+ // Identifiers reach SQL through safeSql.quoteIdentifier runs
442
+ // validateIdentifier (rejects bad shape / reserved words /
443
+ // sqlite_-prefix) + emits the dialect-correct quoted form.
444
+ var qt = safeSql.quoteIdentifier(table, "sqlite");
445
+ var qc = safeSql.quoteIdentifier(column, "sqlite");
442
446
  var total = db.prepare("SELECT COUNT(*) AS n FROM " + qt + " WHERE " + qc + " IS NOT NULL").get().n;
443
447
  if (total === 0) return 0;
444
448
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.9.12",
3
+ "version": "0.9.14",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.6",
5
- "serialNumber": "urn:uuid:d4b77800-e22b-4c36-be2e-cecdcb0c34da",
5
+ "serialNumber": "urn:uuid:cc380387-6002-4e34-863f-7bb3090533eb",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-13T16:54:10.092Z",
8
+ "timestamp": "2026-05-13T20:27:26.640Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.9.12",
22
+ "bom-ref": "@blamejs/core@0.9.14",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.9.12",
25
+ "version": "0.9.14",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.9.12",
29
+ "purl": "pkg:npm/%40blamejs/core@0.9.14",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.9.12",
57
+ "ref": "@blamejs/core@0.9.14",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]