@blamejs/core 0.17.11 → 0.17.13

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/lib/redact.js CHANGED
@@ -1055,8 +1055,62 @@ function installForPosture(posture, primitives) {
1055
1055
  });
1056
1056
  }
1057
1057
 
1058
+ var TEXT_REDACT_MARKER = "[redacted]";
1059
+
1060
+ /**
1061
+ * @primitive b.redact.redactText
1062
+ * @signature b.redact.redactText(str)
1063
+ * @since 0.17.13
1064
+ * @status stable
1065
+ * @related b.redact.redact
1066
+ *
1067
+ * Scrub credentials EMBEDDED in a free-text string in place, keeping the
1068
+ * surrounding prose — for log messages and other operator-facing text where a
1069
+ * secret may be interpolated mid-sentence. Unlike `redact` (structured,
1070
+ * whole-value, anchored), this uses word-boundary fragment replacement so
1071
+ * "login failed: <jwt> for bob" keeps everything but the jwt. Detects PEM
1072
+ * blocks, JWTs, AWS access keys, URL-userinfo passwords, bearer tokens,
1073
+ * `key=secret` assignments, SSN/EIN, and Luhn-valid PANs. The high-entropy
1074
+ * api-key-shape detector is deliberately excluded (on free text it eats
1075
+ * ordinary IDs / hashes / base64). Drop-safe: never throws (it runs on the
1076
+ * hot-path log-emit sink); on any error it returns a fully-masked marker rather
1077
+ * than the raw input. Every quantifier is length-capped (ReDoS backstop).
1078
+ *
1079
+ * @example
1080
+ * b.redact.redactText("token=AKIAIOSFODNN7EXAMPLE ok"); // → "token=[redacted] ok"
1081
+ */
1082
+ function redactText(str) {
1083
+ if (typeof str !== "string" || str.length === 0) return str;
1084
+ try {
1085
+ return str
1086
+ // PEM / OpenSSH private-key blocks (multi-line).
1087
+ .replace(/-----BEGIN [A-Z0-9 ]{1,40}-----[\s\S]{0,8192}?-----END [A-Z0-9 ]{1,40}-----/g, TEXT_REDACT_MARKER)
1088
+ // JWT — three base64url segments, word-boundary anchored for embedding.
1089
+ .replace(/\beyJ[A-Za-z0-9_-]{1,4096}\.[A-Za-z0-9_-]{1,8192}\.[A-Za-z0-9_-]{1,4096}\b/g, TEXT_REDACT_MARKER)
1090
+ // AWS access-key IDs.
1091
+ .replace(/\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[A-Z0-9]{16}\b/g, TEXT_REDACT_MARKER)
1092
+ // Credentials in a URL userinfo: scheme://user:secret@host.
1093
+ .replace(/([a-z][a-z0-9+.-]{0,32}:\/\/[^\s:@/]{1,256}:)[^\s@/]{1,256}(@)/gi, "$1" + TEXT_REDACT_MARKER + "$2")
1094
+ // Bearer tokens.
1095
+ .replace(/\b([Bb]earer\s{1,4})[A-Za-z0-9._~+/-]{8,4096}=*/g, "$1" + TEXT_REDACT_MARKER)
1096
+ // key=secret / password: secret style assignments.
1097
+ .replace(/\b((?:api[_-]?key|access[_-]?token|secret|password|passwd|pwd|token)\s{0,4}[=:]\s{0,4})[^\s&;"']{6,4096}/gi, "$1" + TEXT_REDACT_MARKER)
1098
+ // SSN / EIN.
1099
+ .replace(/\b\d{3}-\d{2}-\d{4}\b/g, TEXT_REDACT_MARKER)
1100
+ .replace(/\b\d{2}-\d{7}\b/g, TEXT_REDACT_MARKER)
1101
+ // Credit-card / PAN — Luhn-validated so ordinary long digit runs survive.
1102
+ .replace(/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{1,7}\b/g, function (m) {
1103
+ var inner = m.replace(/[\s-]/g, "");
1104
+ return (inner.length >= 13 && inner.length <= 19 && _luhnCheck(inner)) ? TEXT_REDACT_MARKER : m;
1105
+ });
1106
+ } catch (_e) {
1107
+ return TEXT_REDACT_MARKER;
1108
+ }
1109
+ }
1110
+
1058
1111
  module.exports = {
1059
1112
  redact: redact,
1113
+ redactText: redactText,
1060
1114
  registerFieldRule: registerFieldRule,
1061
1115
  registerValueDetector: registerValueDetector,
1062
1116
  classifyDefaults: classifyDefaults,
@@ -0,0 +1,80 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module b.safeObject
6
+ * @nav Validation
7
+ * @title Safe object access
8
+ * @order 100
9
+ *
10
+ * @intro
11
+ * Prototype-pollution-safe own-property access. Interpolators, template
12
+ * engines, and structured readers that look up a key by name on an
13
+ * attacker-influenced object must treat an inherited / prototype-chain key
14
+ * (`__proto__`, `constructor`, `toString`) as ABSENT — never read it into
15
+ * rendered output. This composes that guard into one primitive so every
16
+ * consumer routes through the same hardened read instead of hand-rolling
17
+ * `Object.prototype.hasOwnProperty.call(o, k) ? o[k] : undefined` (which a
18
+ * `__proto__` accessor property can still defeat).
19
+ *
20
+ * @card
21
+ * Prototype-pollution-safe own-property get/set — the single guard every
22
+ * interpolator composes.
23
+ */
24
+
25
+ /**
26
+ * @primitive b.safeObject.ownProp
27
+ * @signature b.safeObject.ownProp(obj, key)
28
+ * @since 0.17.13
29
+ * @status stable
30
+ * @related b.safeObject.ownSet
31
+ *
32
+ * Returns the value of `obj`'s OWN property `key`, or `undefined` when `key`
33
+ * is not an own property (inherited / prototype-chain keys read as absent).
34
+ * Uses `Object.getOwnPropertyDescriptor` rather than `hasOwnProperty` + index
35
+ * read, so a `__proto__`-injected accessor property cannot run on read. A
36
+ * defined-but-`undefined` own value returns `undefined` (callers treat that as
37
+ * absent, matching the prior guarded reads).
38
+ *
39
+ * @example
40
+ * b.safeObject.ownProp({ a: 1 }, "a"); // → 1
41
+ * b.safeObject.ownProp({}, "toString"); // → undefined (inherited)
42
+ * b.safeObject.ownProp({}, "__proto__"); // → undefined
43
+ */
44
+ function ownProp(obj, key) {
45
+ if (obj === null || obj === undefined) return undefined;
46
+ var d = Object.getOwnPropertyDescriptor(obj, key);
47
+ return d ? d.value : undefined;
48
+ }
49
+
50
+ /**
51
+ * @primitive b.safeObject.ownSet
52
+ * @signature b.safeObject.ownSet(obj, key, value)
53
+ * @since 0.17.13
54
+ * @status stable
55
+ * @related b.safeObject.ownProp
56
+ *
57
+ * Sets `obj[key] = value` as a plain own data property via
58
+ * `Object.defineProperty`, so a `__proto__` / `constructor` / accessor key on
59
+ * the prototype chain cannot intercept the write or pollute the prototype.
60
+ * Returns `obj`.
61
+ *
62
+ * @example
63
+ * var o = {};
64
+ * b.safeObject.ownSet(o, "__proto__", { polluted: 1 });
65
+ * ({}).polluted; // → undefined (Object.prototype not touched)
66
+ */
67
+ function ownSet(obj, key, value) {
68
+ Object.defineProperty(obj, key, {
69
+ value: value,
70
+ writable: true,
71
+ enumerable: true,
72
+ configurable: true,
73
+ });
74
+ return obj;
75
+ }
76
+
77
+ module.exports = {
78
+ ownProp: ownProp,
79
+ ownSet: ownSet,
80
+ };
@@ -88,6 +88,17 @@
88
88
  var nodeCrypto = require("node:crypto");
89
89
  var nodeFs = require("node:fs");
90
90
 
91
+ // _svErr — throw shape for this module. A plain Error (zero-dep — no framework
92
+ // error class) carrying a machine-readable `.kind` so a framework caller
93
+ // (b.selfUpdate.verify) can map the failure to its own typed error class
94
+ // without parsing English message text. Operators consuming the module
95
+ // standalone still get the human-readable `.message` unchanged.
96
+ function _svErr(kind, message) {
97
+ var e = new Error(message);
98
+ e.kind = kind;
99
+ return e;
100
+ }
101
+
91
102
  // _streamHashAndVerify — read the asset in 64 KiB chunks, feed each
92
103
  // chunk into sha256, sha3-512, AND the signature verifier in parallel.
93
104
  // Single pass over the file; no in-memory copy. node:crypto's
@@ -103,18 +114,18 @@ function _detectAlg(pubkeyPem) {
103
114
  try {
104
115
  key = nodeCrypto.createPublicKey(pubkeyPem);
105
116
  } catch (e) {
106
- throw new Error("standalone-verifier: pubkey PEM did not parse: " +
117
+ throw _svErr("bad-pubkey", "standalone-verifier: pubkey PEM did not parse: " +
107
118
  (e && e.message ? e.message : String(e)));
108
119
  }
109
120
  var t = key.asymmetricKeyType;
110
121
  if (t === "ec") {
111
122
  var curve = key.asymmetricKeyDetails && key.asymmetricKeyDetails.namedCurve;
112
123
  if (curve === "P-384" || curve === "secp384r1") return { alg: "ecdsa-p384", key: key };
113
- throw new Error("standalone-verifier: unsupported EC curve '" + curve + "' (need P-384)");
124
+ throw _svErr("unsupported-key", "standalone-verifier: unsupported EC curve '" + curve + "' (need P-384)");
114
125
  }
115
126
  if (t === "ed25519") return { alg: "ed25519", key: key };
116
127
  if (t === "ml-dsa-87" || t === "ml-dsa") return { alg: "ml-dsa-87", key: key };
117
- throw new Error("standalone-verifier: unrecognized pubkey type '" + t + "' " +
128
+ throw _svErr("unsupported-key", "standalone-verifier: unrecognized pubkey type '" + t + "' " +
118
129
  "(need ecdsa-p384, ed25519, or ml-dsa-87)");
119
130
  }
120
131
 
@@ -163,7 +174,7 @@ function _looksLikeDerEcdsa(sig) {
163
174
 
164
175
  /**
165
176
  * @primitive b.selfUpdate.standaloneVerifier.verify
166
- * @signature b.selfUpdate.standaloneVerifier.verify(assetPath, signaturePath, pubkeyPem)
177
+ * @signature b.selfUpdate.standaloneVerifier.verify(assetPath, signaturePath, pubkeyPem, opts?)
167
178
  * @since 0.9.13
168
179
  * @status stable
169
180
  * @related b.selfUpdate.verify
@@ -175,12 +186,21 @@ function _looksLikeDerEcdsa(sig) {
175
186
  * Streams the asset in 64 KiB chunks through SHA-256 + SHA-3-512 + the
176
187
  * signature verifier in parallel — single allocation peak (one buffer
177
188
  * sized to fstat(asset).size for Ed25519 / ML-DSA-87, ECDSA P-384 needs
178
- * no buffer because createVerify is incremental).
189
+ * no buffer because createVerify is incremental). The signature commits
190
+ * to a SHA3-512 digest and the ECDSA encoding is dispatched by structure
191
+ * (DER SEQUENCE vs raw IEEE-P1363), so both encodings of a SHA3-512-signed
192
+ * P-384 sidecar verify.
179
193
  *
180
- * Returns `{ ok, sha3_512, sha256, alg }` on success; throws on
181
- * unrecognized pubkey shape, missing files, or signature mismatch.
182
- * `alg` is one of `"ecdsa-p384"`, `"ed25519"`, `"ml-dsa-87"` (auto-
183
- * detected from the pubkey PEM).
194
+ * Returns `{ ok, sha3_512, sha256, alg, bytes, digests }` on success;
195
+ * throws on unrecognized pubkey shape, missing files, or signature
196
+ * mismatch. `alg` is one of `"ecdsa-p384"`, `"ed25519"`, `"ml-dsa-87"`
197
+ * (auto-detected from the pubkey PEM). `bytes` is the verified asset byte
198
+ * count; `digests` maps each requested `opts.extraDigests` name to its
199
+ * hex digest (computed in the same single pass).
200
+ *
201
+ * @opts
202
+ * maxAssetBytes: number, // asset-size ceiling (default 2 GiB); refuse a larger asset before hashing
203
+ * extraDigests: array, // additional node:crypto digest names to compute in the same stream
184
204
  *
185
205
  * @example
186
206
  * var verifier = require("./standalone-verifier");
@@ -193,15 +213,26 @@ function _looksLikeDerEcdsa(sig) {
193
213
  * if (!result.ok) process.exit(1);
194
214
  * process.stdout.write("verified " + result.alg + " sha3-512=" + result.sha3_512 + "\n");
195
215
  */
196
- function verify(assetPath, signaturePath, pubkeyPem) {
216
+ function verify(assetPath, signaturePath, pubkeyPem, opts) {
217
+ opts = opts || {};
218
+ // Asset-size ceiling — override the default via opts.maxAssetBytes (the
219
+ // framework caller passes its own maxBytes). A self-update bundle (SEA) is
220
+ // intentionally large, so the default ceiling is generous (2 GiB), but it
221
+ // stops a signaturePath/assetPath pointed at an unbounded file from OOM-ing.
222
+ var maxAssetBytes = (typeof opts.maxAssetBytes === "number" && isFinite(opts.maxAssetBytes) &&
223
+ opts.maxAssetBytes > 0)
224
+ ? opts.maxAssetBytes
225
+ : (2 * 1024 * 1024 * 1024); // allow:raw-byte-literal — zero-dep module, 2 GiB asset ceiling
226
+ var extraDigests = Array.isArray(opts.extraDigests) ? opts.extraDigests : [];
227
+
197
228
  if (typeof assetPath !== "string" || assetPath.length === 0) {
198
- throw new Error("standalone-verifier.verify: assetPath must be a non-empty string");
229
+ throw _svErr("bad-input", "standalone-verifier.verify: assetPath must be a non-empty string");
199
230
  }
200
231
  if (typeof signaturePath !== "string" || signaturePath.length === 0) {
201
- throw new Error("standalone-verifier.verify: signaturePath must be a non-empty string");
232
+ throw _svErr("bad-input", "standalone-verifier.verify: signaturePath must be a non-empty string");
202
233
  }
203
234
  if (typeof pubkeyPem !== "string" || pubkeyPem.indexOf("-----BEGIN ") !== 0) {
204
- throw new Error("standalone-verifier.verify: pubkeyPem must be a PEM-encoded public key string");
235
+ throw _svErr("bad-input", "standalone-verifier.verify: pubkeyPem must be a PEM-encoded public key string");
205
236
  }
206
237
 
207
238
  // Open both files BEFORE parsing the pubkey so we own stable fds
@@ -212,7 +243,7 @@ function verify(assetPath, signaturePath, pubkeyPem) {
212
243
  try {
213
244
  assetFd = nodeFs.openSync(assetPath, "r");
214
245
  } catch (e) {
215
- throw new Error("standalone-verifier.verify: asset not found at " + assetPath +
246
+ throw _svErr("asset-not-found", "standalone-verifier.verify: asset not found at " + assetPath +
216
247
  " — " + (e && e.message ? e.message : String(e)));
217
248
  }
218
249
  var sigFd;
@@ -220,7 +251,7 @@ function verify(assetPath, signaturePath, pubkeyPem) {
220
251
  sigFd = nodeFs.openSync(signaturePath, "r");
221
252
  } catch (e) {
222
253
  nodeFs.closeSync(assetFd);
223
- throw new Error("standalone-verifier.verify: signature not found at " + signaturePath +
254
+ throw _svErr("sig-not-found", "standalone-verifier.verify: signature not found at " + signaturePath +
224
255
  " — " + (e && e.message ? e.message : String(e)));
225
256
  }
226
257
  var signature;
@@ -231,7 +262,7 @@ function verify(assetPath, signaturePath, pubkeyPem) {
231
262
  // from OOM-ing if signaturePath is pointed at a giant file. Zero-dep by
232
263
  // contract — inline literal, cannot import C.BYTES.
233
264
  if (sigStat.size > 64 * 1024) { // allow:raw-byte-literal — zero-dep module
234
- throw new Error("standalone-verifier.verify: signature file implausibly large (" +
265
+ throw _svErr("sig-too-large", "standalone-verifier.verify: signature file implausibly large (" +
235
266
  sigStat.size + " bytes)");
236
267
  }
237
268
  signature = Buffer.allocUnsafe(sigStat.size);
@@ -241,7 +272,7 @@ function verify(assetPath, signaturePath, pubkeyPem) {
241
272
  }
242
273
  if (signature.length === 0) {
243
274
  nodeFs.closeSync(assetFd);
244
- throw new Error("standalone-verifier.verify: signature file is empty");
275
+ throw _svErr("sig-empty", "standalone-verifier.verify: signature file is empty");
245
276
  }
246
277
 
247
278
  var detected;
@@ -249,7 +280,7 @@ function verify(assetPath, signaturePath, pubkeyPem) {
249
280
  detected = _detectAlg(pubkeyPem);
250
281
  } catch (e) {
251
282
  nodeFs.closeSync(assetFd);
252
- throw e;
283
+ throw e; // carries _detectAlg's .kind (bad-pubkey / unsupported-key)
253
284
  }
254
285
  var alg = detected.alg;
255
286
  var key = detected.key;
@@ -273,15 +304,24 @@ function verify(assetPath, signaturePath, pubkeyPem) {
273
304
  // time.
274
305
  var assetStat = nodeFs.fstatSync(assetFd);
275
306
  // Bound the asset alloc before Buffer.allocUnsafe(assetStat.size): a self-update
276
- // bundle (SEA) is intentionally large, so the ceiling is generous (2 GiB), but
277
- // it stops a signaturePath/assetPath pointed at an unbounded file from OOM-ing
278
- // the verifier before any byte is hashed. Zero-dep by contract inline literal.
279
- if (assetStat.size > 2 * 1024 * 1024 * 1024) { // allow:raw-byte-literal — zero-dep module, 2 GiB asset ceiling
280
- throw new Error("standalone-verifier.verify: asset implausibly large (" +
281
- assetStat.size + " bytes) — exceeds the 2 GiB self-update ceiling");
307
+ // bundle (SEA) is intentionally large, so the ceiling is generous (2 GiB by
308
+ // default, or opts.maxAssetBytes), but it stops a signaturePath/assetPath
309
+ // pointed at an unbounded file from OOM-ing the verifier before any byte is
310
+ // hashed.
311
+ if (assetStat.size > maxAssetBytes) {
312
+ nodeFs.closeSync(assetFd);
313
+ throw _svErr("asset-too-large", "standalone-verifier.verify: asset implausibly large (" +
314
+ assetStat.size + " bytes) — exceeds the " + maxAssetBytes + "-byte asset ceiling");
282
315
  }
283
316
  var sha256 = nodeCrypto.createHash("sha256");
284
317
  var sha3 = nodeCrypto.createHash("sha3-512");
318
+ // Additional operator-requested digests, computed in the same single pass so
319
+ // a framework caller can report an audit digest (sha-512 / shake256 / …)
320
+ // without a second read of the asset.
321
+ var extraHashers = [];
322
+ for (var xd = 0; xd < extraDigests.length; xd += 1) {
323
+ extraHashers.push({ name: extraDigests[xd], hash: nodeCrypto.createHash(extraDigests[xd]) });
324
+ }
285
325
  var verifier = (alg === "ecdsa-p384") ? nodeCrypto.createVerify("sha3-512") : null;
286
326
  var fullBuf = null;
287
327
  var fullOff = 0;
@@ -305,6 +345,7 @@ function verify(assetPath, signaturePath, pubkeyPem) {
305
345
  var slice = chunk.subarray(0, n);
306
346
  sha256.update(slice);
307
347
  sha3.update(slice);
348
+ for (var xh = 0; xh < extraHashers.length; xh += 1) extraHashers[xh].hash.update(slice);
308
349
  if (verifier) verifier.update(slice);
309
350
  if (fullBuf) {
310
351
  slice.copy(fullBuf, fullOff);
@@ -319,7 +360,7 @@ function verify(assetPath, signaturePath, pubkeyPem) {
319
360
  // beyond what the clamp let through. Both cases mean the hashers
320
361
  // and verifier saw a different byte set than the on-disk file.
321
362
  if (fullOff !== assetStat.size) {
322
- throw new Error("standalone-verifier.verify: asset '" + assetPath +
363
+ throw _svErr("size-race", "standalone-verifier.verify: asset '" + assetPath +
323
364
  "' changed size during read (expected " + assetStat.size +
324
365
  " bytes per fstat, read " + fullOff +
325
366
  " bytes) — refusing to return a hash that may not match the on-disk file");
@@ -327,6 +368,10 @@ function verify(assetPath, signaturePath, pubkeyPem) {
327
368
 
328
369
  var sha256Hex = sha256.digest("hex");
329
370
  var sha3Hex = sha3.digest("hex");
371
+ var digests = {};
372
+ for (var dh = 0; dh < extraHashers.length; dh += 1) {
373
+ digests[extraHashers[dh].name] = extraHashers[dh].hash.digest("hex");
374
+ }
330
375
 
331
376
  var ok = false;
332
377
  if (alg === "ecdsa-p384") {
@@ -351,7 +396,7 @@ function verify(assetPath, signaturePath, pubkeyPem) {
351
396
  } else {
352
397
  // assetFd was already closed by the read loop's `finally`; this is a
353
398
  // pure fail-closed refusal, same as the `if (!ok)` path below.
354
- throw new Error("standalone-verifier.verify: ecdsa-p384 signature is neither a " +
399
+ throw _svErr("bad-sig-encoding", "standalone-verifier.verify: ecdsa-p384 signature is neither a " +
355
400
  "well-formed DER SEQUENCE nor a raw " + (coordLen * 2) +
356
401
  "-byte IEEE-P1363 pair (length " + signature.length +
357
402
  ") — refusing to guess the encoding");
@@ -366,7 +411,7 @@ function verify(assetPath, signaturePath, pubkeyPem) {
366
411
  }
367
412
 
368
413
  if (!ok) {
369
- throw new Error("standalone-verifier.verify: " + alg + " signature INVALID for " +
414
+ throw _svErr("verify-failed", "standalone-verifier.verify: " + alg + " signature INVALID for " +
370
415
  assetPath + " (sha3-512=" + sha3Hex.slice(0, 16) + "...). " + // 16-char hex prefix for forensic display, not bytes
371
416
  "Either the asset was tampered with after signing, the signature " +
372
417
  "doesn't match this asset, or the pubkey doesn't match the signing key.");
@@ -377,6 +422,8 @@ function verify(assetPath, signaturePath, pubkeyPem) {
377
422
  sha3_512: sha3Hex,
378
423
  sha256: sha256Hex,
379
424
  alg: alg,
425
+ bytes: fullOff,
426
+ digests: digests,
380
427
  };
381
428
  }
382
429