@blamejs/blamejs-shop 0.3.12 → 0.3.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.
Files changed (89) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/lib/admin.js +155 -113
  3. package/lib/asset-manifest.json +1 -1
  4. package/lib/vendor/MANIFEST.json +2 -2
  5. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  6. package/lib/vendor/blamejs/README.md +3 -2
  7. package/lib/vendor/blamejs/SECURITY.md +3 -0
  8. package/lib/vendor/blamejs/api-snapshot.json +14 -2
  9. package/lib/vendor/blamejs/lib/agent-event-bus.js +4 -4
  10. package/lib/vendor/blamejs/lib/agent-idempotency.js +6 -6
  11. package/lib/vendor/blamejs/lib/agent-orchestrator.js +9 -9
  12. package/lib/vendor/blamejs/lib/agent-posture-chain.js +10 -10
  13. package/lib/vendor/blamejs/lib/agent-saga.js +6 -7
  14. package/lib/vendor/blamejs/lib/agent-snapshot.js +8 -8
  15. package/lib/vendor/blamejs/lib/agent-stream.js +3 -3
  16. package/lib/vendor/blamejs/lib/agent-tenant.js +4 -4
  17. package/lib/vendor/blamejs/lib/agent-trace.js +5 -5
  18. package/lib/vendor/blamejs/lib/ai-disclosure.js +3 -3
  19. package/lib/vendor/blamejs/lib/app.js +2 -2
  20. package/lib/vendor/blamejs/lib/archive-read.js +1 -1
  21. package/lib/vendor/blamejs/lib/archive-tar-read.js +1 -1
  22. package/lib/vendor/blamejs/lib/archive-wrap.js +5 -5
  23. package/lib/vendor/blamejs/lib/audit-tools.js +65 -5
  24. package/lib/vendor/blamejs/lib/audit.js +2 -2
  25. package/lib/vendor/blamejs/lib/auth/ciba.js +1 -1
  26. package/lib/vendor/blamejs/lib/auth/dpop.js +1 -1
  27. package/lib/vendor/blamejs/lib/auth/fal.js +1 -1
  28. package/lib/vendor/blamejs/lib/auth/fido-mds3.js +2 -3
  29. package/lib/vendor/blamejs/lib/auth/jwt-external.js +2 -2
  30. package/lib/vendor/blamejs/lib/auth/oauth.js +9 -9
  31. package/lib/vendor/blamejs/lib/auth/oid4vci.js +7 -7
  32. package/lib/vendor/blamejs/lib/auth/oid4vp.js +1 -1
  33. package/lib/vendor/blamejs/lib/auth/openid-federation.js +5 -5
  34. package/lib/vendor/blamejs/lib/auth/passkey.js +6 -6
  35. package/lib/vendor/blamejs/lib/auth/saml.js +1 -1
  36. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -6
  37. package/lib/vendor/blamejs/lib/backup/index.js +18 -18
  38. package/lib/vendor/blamejs/lib/cache.js +4 -4
  39. package/lib/vendor/blamejs/lib/calendar.js +5 -5
  40. package/lib/vendor/blamejs/lib/circuit-breaker.js +1 -1
  41. package/lib/vendor/blamejs/lib/cms-codec.js +2 -2
  42. package/lib/vendor/blamejs/lib/compliance.js +14 -14
  43. package/lib/vendor/blamejs/lib/crypto-field.js +58 -21
  44. package/lib/vendor/blamejs/lib/crypto.js +5 -6
  45. package/lib/vendor/blamejs/lib/db-query.js +131 -9
  46. package/lib/vendor/blamejs/lib/db.js +106 -22
  47. package/lib/vendor/blamejs/lib/external-db.js +64 -16
  48. package/lib/vendor/blamejs/lib/framework-schema.js +4 -4
  49. package/lib/vendor/blamejs/lib/guard-list-id.js +2 -2
  50. package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +1 -2
  51. package/lib/vendor/blamejs/lib/incident-report.js +150 -0
  52. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +1 -1
  53. package/lib/vendor/blamejs/lib/mail-deploy.js +3 -3
  54. package/lib/vendor/blamejs/lib/mail-server-managesieve.js +2 -2
  55. package/lib/vendor/blamejs/lib/mail-server-pop3.js +2 -2
  56. package/lib/vendor/blamejs/lib/mail-store.js +1 -1
  57. package/lib/vendor/blamejs/lib/metrics.js +8 -8
  58. package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +1 -1
  59. package/lib/vendor/blamejs/lib/middleware/dpop.js +5 -5
  60. package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +21 -22
  61. package/lib/vendor/blamejs/lib/middleware/protected-resource-metadata.js +2 -2
  62. package/lib/vendor/blamejs/lib/network-dns-resolver.js +2 -2
  63. package/lib/vendor/blamejs/lib/network-dns.js +1 -2
  64. package/lib/vendor/blamejs/lib/network-tls.js +0 -1
  65. package/lib/vendor/blamejs/lib/outbox.js +1 -1
  66. package/lib/vendor/blamejs/lib/pqc-agent.js +1 -1
  67. package/lib/vendor/blamejs/lib/retention.js +1 -1
  68. package/lib/vendor/blamejs/lib/retry.js +1 -1
  69. package/lib/vendor/blamejs/lib/safe-archive.js +2 -2
  70. package/lib/vendor/blamejs/lib/safe-ical.js +2 -2
  71. package/lib/vendor/blamejs/lib/safe-mime.js +1 -1
  72. package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +1 -1
  73. package/lib/vendor/blamejs/lib/self-update.js +2 -2
  74. package/lib/vendor/blamejs/lib/static.js +1 -1
  75. package/lib/vendor/blamejs/lib/subject.js +2 -2
  76. package/lib/vendor/blamejs/lib/vault/index.js +64 -1
  77. package/lib/vendor/blamejs/lib/vault/rotate.js +19 -0
  78. package/lib/vendor/blamejs/lib/vendor-data.js +1 -1
  79. package/lib/vendor/blamejs/package.json +1 -1
  80. package/lib/vendor/blamejs/release-notes/v0.14.7.json +77 -0
  81. package/lib/vendor/blamejs/scripts/release.js +28 -3
  82. package/lib/vendor/blamejs/test/layer-0-primitives/audit-tools-dual-control.test.js +115 -0
  83. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +163 -3
  84. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +102 -0
  85. package/lib/vendor/blamejs/test/layer-0-primitives/db-column-gate.test.js +150 -0
  86. package/lib/vendor/blamejs/test/layer-0-primitives/db-key-aad.test.js +109 -0
  87. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +53 -0
  88. package/lib/vendor/blamejs/test/layer-0-primitives/incident-report.test.js +65 -0
  89. package/package.json +1 -1
@@ -88,16 +88,41 @@ function _bumpMinor(version) {
88
88
  return parts[0] + "." + (parts[1] + 1) + ".0";
89
89
  }
90
90
 
91
+ // Quote a single argument for a Windows cmd.exe command line. Tokens
92
+ // made only of safe characters pass through unquoted; anything else is
93
+ // double-quoted with embedded quotes doubled.
94
+ function _quoteWinArg(a) {
95
+ a = String(a);
96
+ if (/^[A-Za-z0-9_@.\-/:=]+$/.test(a)) return a;
97
+ return '"' + a.replace(/"/g, '""') + '"';
98
+ }
99
+
91
100
  function _run(cmd, args, opts) {
92
101
  opts = opts || {};
93
- var rv = childProcess.spawnSync(cmd, args, {
102
+ args = args || [];
103
+ var spawnCmd = cmd;
104
+ var spawnArgs = args;
105
+ var useShell = false;
106
+ if (_needsShell(cmd)) {
107
+ // Windows resolves npm / npx through .cmd shims that can only be
108
+ // launched via a shell (the CVE-2024-27980 mitigation refuses to
109
+ // spawn .cmd files without one). Node 26's DEP0190 deprecates
110
+ // pairing an args ARRAY with shell:true because the args would be
111
+ // concatenated onto the command line without escaping — a quoting /
112
+ // injection hazard. Build a single, explicitly-quoted command
113
+ // string and pass NO args array, which is the supported shape.
114
+ spawnCmd = [cmd].concat(args.map(_quoteWinArg)).join(" ");
115
+ spawnArgs = undefined;
116
+ useShell = true;
117
+ }
118
+ var rv = childProcess.spawnSync(spawnCmd, spawnArgs, {
94
119
  cwd: opts.cwd || ROOT,
95
120
  stdio: opts.stdio || "inherit",
96
121
  env: Object.assign({}, process.env, opts.env || {}),
97
- shell: _needsShell(cmd),
122
+ shell: useShell,
98
123
  });
99
124
  if (rv.status !== 0 && !opts.allowFail) {
100
- throw new Error("release: " + cmd + " " + (args || []).join(" ") +
125
+ throw new Error("release: " + cmd + " " + args.join(" ") +
101
126
  " failed with status " + rv.status);
102
127
  }
103
128
  return rv;
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ /**
3
+ * b.auditTools.purge dual-control gate.
4
+ *
5
+ * When audit_log is placed under dual control, physically purging the
6
+ * audit chain requires a consumed m-of-n grant in addition to a
7
+ * verified archive and confirm:true — one operator must not be able to
8
+ * erase the tamper-evident chain alone. The grant's action is also
9
+ * bound, so a grant minted for a different operation can't be replayed
10
+ * against a purge.
11
+ */
12
+
13
+ var helpers = require("../helpers");
14
+ var b = helpers.b;
15
+ var check = helpers.check;
16
+ var fs = helpers.fs;
17
+ var os = helpers.os;
18
+ var path = helpers.path;
19
+ var setupTestDb = helpers.setupTestDb;
20
+ var teardownTestDb = helpers.teardownTestDb;
21
+
22
+ var PASS = Buffer.from("operator-passphrase");
23
+ var PURGE_ACTION = "auditTools.purge";
24
+
25
+ async function _seedAuditRows(count) {
26
+ b.audit.registerNamespace("test");
27
+ for (var i = 0; i < count; i++) {
28
+ await b.audit.record({
29
+ actor: { userId: "u-" + i }, action: "test.seeded", outcome: "success", metadata: { i: i },
30
+ });
31
+ }
32
+ }
33
+
34
+ async function _expectCode(fn, code) {
35
+ var threw = null;
36
+ try { await fn(); } catch (e) { threw = e; }
37
+ return threw && threw.code === code;
38
+ }
39
+
40
+ async function run() {
41
+ var dir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-at-dc-"));
42
+ try {
43
+ await setupTestDb(dir);
44
+ await _seedAuditRows(5);
45
+ await b.audit.checkpoint();
46
+
47
+ var out = path.join(dir, "bundles", "archive-dc");
48
+ await b.auditTools.archive({ before: Date.now() + 1000, out: out, passphrase: PASS });
49
+
50
+ // Simulate audit_log being under dual control (m-of-n). The gate
51
+ // override returns a gate descriptor for the audit_log table; the
52
+ // denial branches all fire before any chain mutation, so the same
53
+ // verified bundle is reused across them.
54
+ var sawTable = null;
55
+ var gate = function (table) { sawTable = table; return { m: 2, n: 3 }; };
56
+
57
+ // No grant → refused.
58
+ check("purge under dual control refuses without a grant",
59
+ await _expectCode(function () {
60
+ return b.auditTools.purge({ archive: out, passphrase: PASS, confirm: true, checkDualControlGate: gate });
61
+ }, "audit-tools/dual-control-required"));
62
+ check("gate was consulted for the audit_log table", sawTable === "audit_log");
63
+
64
+ // Grant not ready (not a consumed m-of-n grant) → refused.
65
+ check("purge refuses a not-ready grant",
66
+ await _expectCode(function () {
67
+ return b.auditTools.purge({
68
+ archive: out, passphrase: PASS, confirm: true, checkDualControlGate: gate,
69
+ dualControlGrant: { ready: false, action: PURGE_ACTION },
70
+ });
71
+ }, "audit-tools/dual-control-grant-not-ready"));
72
+
73
+ // Grant minted for a different action → refused (action binding).
74
+ check("purge refuses a grant bound to a different action",
75
+ await _expectCode(function () {
76
+ return b.auditTools.purge({
77
+ archive: out, passphrase: PASS, confirm: true, checkDualControlGate: gate,
78
+ dualControlGrant: { ready: true, action: "db.eraseHard" },
79
+ });
80
+ }, "audit-tools/dual-control-grant-mismatch"));
81
+
82
+ // Confirm the chain is still intact — no denial mutated it.
83
+ var beforeRows = await b.clusterStorage.executeAll("SELECT COUNT(*) as c FROM audit_log");
84
+ check("audit_log untouched by denied purges", Number(beforeRows[0].c) >= 5);
85
+
86
+ // Valid consumed grant for the purge action → proceeds.
87
+ var pres = await b.auditTools.purge({
88
+ archive: out, passphrase: PASS, confirm: true, checkDualControlGate: gate,
89
+ dualControlGrant: { ready: true, action: PURGE_ACTION },
90
+ });
91
+ check("purge with a valid grant succeeds", pres.purged === true);
92
+ check("purge reports dual-control was consumed", pres.dualControlConsumed === true);
93
+ check("purge deleted rows", pres.rowsDeleted > 0);
94
+
95
+ // No gate (audit_log not under dual control) → confirm + archive
96
+ // alone is sufficient (no grant required) — the gate is opt-in.
97
+ var noGateResult = b.auditTools.purge;
98
+ check("purge surface present", typeof noGateResult === "function");
99
+ } finally {
100
+ await teardownTestDb(dir);
101
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_e) {}
102
+ }
103
+
104
+ console.log("OK — audit-tools dual-control tests");
105
+ }
106
+
107
+ module.exports = { run: run };
108
+ if (require.main === module) {
109
+ // Rethrow on failure so Node surfaces the error and exits non-zero,
110
+ // instead of logging the caught error object — a taint analyzer traces
111
+ // a logged error back to the test passphrase fixture (a non-secret
112
+ // constant) and raises a false clear-text-logging alert.
113
+ run().then(function () { process.exit(0); })
114
+ .catch(function (err) { process.exitCode = 1; throw err; });
115
+ }
@@ -309,6 +309,7 @@ var VALID_ALLOW_CLASSES = {
309
309
  "inline-require": 1,
310
310
  "inline-require-non-empty-string-validation": 1,
311
311
  "internal-binding-in-prose": 1,
312
+ "internal-narrative-comment": 1,
312
313
  "list-without-pagination": 1,
313
314
  "math-random-noncrypto": 1,
314
315
  "no-number-money-arithmetic": 1,
@@ -7951,6 +7952,101 @@ var KNOWN_ANTIPATTERNS = [
7951
7952
  reason: "Codex P1 on v0.11.4 PR #109 — b.audit.useStore({ record }) inlined await on operator-supplied callback. A stalled network call neither resolves nor rejects ⇒ b.audit.record() never returns, emit/safeEmit drains stall behind it; hot-path observability emission is supposed to be drop-silent on hangs. Defense: wrap every external-callback await in safeAsync.withTimeout. Detector catches future operator-supplied async hooks on the same shape (_externalStore / _userStore / _externalSink / _externalCb / _externalCallback / _externalHook).",
7952
7953
  },
7953
7954
 
7955
+ {
7956
+ // v0.14.7 — physically deleting audit-chain rows is a
7957
+ // single-operator-can-erase-evidence risk. `auditTools.purge`
7958
+ // (and the low-level `db.purgeAuditChain` it composes) MUST
7959
+ // consult a dual-control gate — two distinct authorizers — before
7960
+ // the destructive delete runs, so one compromised credential can't
7961
+ // silently truncate the tamper-evident chain. The gate primitive
7962
+ // is `db._checkDualControlGate(table)` (resolved at runtime); the
7963
+ // purge path threads it via `_resolveDualControlGate(opts)` and
7964
+ // refuses with `dual-control-required` when no grant is present.
7965
+ //
7966
+ // Detector: any lib/ call to `purgeAuditChain(` MUST appear in a
7967
+ // file that also names the gate (`_checkDualControlGate` /
7968
+ // `_resolveDualControlGate` / `dualControlGrant`). A new caller
7969
+ // that physically deletes chain rows without routing through the
7970
+ // gate trips here.
7971
+ id: "audit-purge-without-dual-control-gate",
7972
+ primitive: "auditTools.purge({ dualControlGrant }) / db.purgeAuditChain — physical audit-chain deletion MUST consult the dual-control gate (_checkDualControlGate / _resolveDualControlGate); one operator must not be able to erase the tamper-evident chain alone",
7973
+ regex: /\bpurgeAuditChain\s*\(/,
7974
+ requires: /_checkDualControlGate|_resolveDualControlGate|dualControlGrant/,
7975
+ skipCommentLines: true,
7976
+ allowlist: [],
7977
+ reason: "v0.14.7 — audit-chain purge is irreversible and tamper-evidence-destroying. The discipline: the chain may only be truncated under a two-authorizer dual-control grant. db.js defines the gate (_checkDualControlGate) and audit-tools.js purge() resolves+enforces it (_resolveDualControlGate) before calling db().purgeAuditChain; both files satisfy the companion check. A future file that calls purgeAuditChain without naming the gate would let a single operator delete evidence — exactly the shape this blocks.",
7978
+ },
7979
+
7980
+ {
7981
+ // v0.14.7 — raw-SQL fragments passed to `whereRaw` / the
7982
+ // WhereBuilder `.raw(sql, params)` escape hatch must parameterize
7983
+ // values via the `params` array, NOT interpolate or concatenate
7984
+ // them into the SQL string. `_assertRawNoStringLiteral` refuses an
7985
+ // embedded `'...'` literal at runtime (unless the caller opts in
7986
+ // with `allowLiterals`), but the static discipline is stronger:
7987
+ // lib/ must never BUILD the raw SQL by `${...}` template
7988
+ // interpolation or `"..." + value` string concatenation — that is
7989
+ // the injection shape the bound-params API exists to prevent.
7990
+ //
7991
+ // Detector fires on a whereRaw/.raw call whose first argument is a
7992
+ // template literal containing `${` or a string-concat expression.
7993
+ // A no-arg `bodyParser.raw()` (different `.raw`) does not match —
7994
+ // the regex requires an interpolated/concatenated string argument.
7995
+ id: "whereraw-interpolated-or-concatenated-sql",
7996
+ primitive: "whereRaw(sql, params) / qb.raw(sql, params) — pass values through the bound `params` array; never `${...}`-interpolate or `+`-concatenate them into the SQL string (that is the injection shape the params API prevents)",
7997
+ regex: /(?:whereRaw|\.raw)\s*\(\s*(?:`[^`]*\$\{|["'][^"']*["']\s*\+|[A-Za-z_$][\w$]*\s*\+\s*["'`])/,
7998
+ skipCommentLines: true,
7999
+ allowlist: [],
8000
+ reason: "v0.14.7 — b.db whereRaw / WhereBuilder.raw accept an operator SQL fragment plus a bound-params array. Concatenating or template-interpolating a value into the fragment defeats the placeholder binding and reintroduces SQL injection. lib/ has zero such call sites today; the detector keeps it that way (the runtime _assertRawNoStringLiteral gate is the operator-facing backstop, this is the framework-internal one). No-argument `.raw()` mountings (e.g. bodyParser.raw()) don't match — an interpolated/concatenated string argument is required.",
8001
+ },
8002
+
8003
+ {
8004
+ // v0.14.7 — equality-lookup ("derived") hashes for sealed columns
8005
+ // must be computed through cryptoField._computeDerivedHash, which
8006
+ // honours the per-table / per-column mode policy (salted-sha3 by
8007
+ // default, hmac-shake256 opt-in keyed off vault.getDerivedHashMacKey).
8008
+ // Hand-rolling `sha3Hash(vault.getDerivedHashSalt() + ns + value)`
8009
+ // at a new site bypasses the keyed-MAC option AND the mode policy,
8010
+ // and risks a static-salt regression if the salt source is changed
8011
+ // for one caller but not the canonical helper.
8012
+ //
8013
+ // Detector: direct use of `getDerivedHashSalt().toString("hex") +`
8014
+ // outside crypto-field.js (the canonical helper). The helper itself
8015
+ // is allowlisted.
8016
+ id: "derived-hash-handrolled-outside-crypto-field",
8017
+ primitive: "cryptoField derived/lookup hashes — compute via _computeDerivedHash (honours salted-sha3 vs hmac-shake256 mode + vault.getDerivedHashMacKey); do not hand-roll sha3Hash(getDerivedHashSalt() + ns + value) at call sites",
8018
+ regex: /getDerivedHashSalt\s*\(\s*\)\s*\.toString\s*\(\s*["']hex["']\s*\)\s*\+/,
8019
+ skipCommentLines: true,
8020
+ allowlist: [
8021
+ // The canonical helper — _computeDerivedHash branches on mode here.
8022
+ "lib/crypto-field.js",
8023
+ ],
8024
+ reason: "v0.14.7 — derived-hash equality lookups gained a keyed mode (hmac-shake256 off vault.getDerivedHashMacKey) alongside the salted-sha3 default. The mode decision lives in cryptoField._computeDerivedHash. A site that hand-derives the hash with getDerivedHashSalt() bypasses the keyed option and the per-column mode policy; only the canonical helper (crypto-field.js) may name the salt directly.",
8025
+ },
8026
+
8027
+ {
8028
+ // v0.14.7 — a `db.auth.failed` audit row must record WHICH
8029
+ // relation the rejected credential attempted to reach
8030
+ // (`attemptedTable`), so an operator triaging a credential-abuse
8031
+ // event can scope blast radius without correlating to the raw SQL
8032
+ // log. external-db extracts the target relation defensively
8033
+ // (_extractTargetRelation) and stamps both resource.attemptedTable
8034
+ // and the audit metadata. Emitting the auth-failure audit without
8035
+ // it loses the forensic signal.
8036
+ //
8037
+ // Detector: an `action: "db.auth.failed"` audit emit MUST appear in
8038
+ // a file that also names `attemptedTable`. The metric emitter
8039
+ // (_emitMetric("db.auth.failed", ...)) uses a positional shape, not
8040
+ // `action:`, so it doesn't match.
8041
+ id: "db-auth-failed-audit-without-attempted-relation",
8042
+ primitive: "db.auth.failed audit — stamp the attempted relation (attemptedTable, via _extractTargetRelation) on credential-rejection audit rows so blast radius is triageable without the raw SQL log",
8043
+ regex: /action\s*:\s*["']db\.auth\.failed["']/,
8044
+ requires: /attemptedTable/,
8045
+ skipCommentLines: true,
8046
+ allowlist: [],
8047
+ reason: "v0.14.7 — external-db credential-rejection audits (SQLSTATE 28000 / 28P01 / 42501) now carry attemptedTable, the relation the rejected identity tried to touch, extracted defensively from the SQL. The detector requires any file emitting an action:'db.auth.failed' audit to also name attemptedTable so a future emitter can't drop the forensic field.",
8048
+ },
8049
+
7954
8050
  ];
7955
8051
 
7956
8052
  // @example placeholder detection lives in
@@ -10565,9 +10661,9 @@ function testPrimitiveReachability() {
10565
10661
  // onDeny / problemDetails opts. A new deny-path
10566
10662
  // middleware that hardcodes
10567
10663
  // `res.writeHead(<4xx/5xx>, { "Content-Type": ... })`
10568
- // locks consumers out of the response shape — the
10569
- // integration-friction class that blocked downstream
10570
- // adoption (the rate-limit text/plain 429). ----
10664
+ // locks consumers out of the response shape — which is
10665
+ // what pinned rate-limit's 429 to text/plain before this
10666
+ // convention existed. ----
10571
10667
  function testDenyPathComposesDenyResponse() {
10572
10668
  // class: deny-path-hardcoded-response
10573
10669
  var MW_ROOT = path.resolve(LIB_ROOT, "middleware");
@@ -10610,9 +10706,73 @@ function testDenyPathComposesDenyResponse() {
10610
10706
  violations);
10611
10707
  }
10612
10708
 
10709
+ // ---- Pattern: operator-facing source comments must describe the code,
10710
+ // not the internal authoring process. These shapes are
10711
+ // internal slice / bug / plan IDs, code-review-process
10712
+ // residue (Codex P-levels, PR numbers), and dated
10713
+ // decision parentheticals — none of which an operator
10714
+ // reading the shipped source can map to anything in their
10715
+ // own checkout. Genuinely operator-meaningful references
10716
+ // (RFC / CVE / NIST / CWE, "since vX.Y.Z", established
10717
+ // terse markers like D-M4 / AUTH-32) are NOT matched.
10718
+ // Allowlist a false positive with `// allow:internal-
10719
+ // narrative-comment`. ----
10720
+ function testNoInternalNarrativeComments() {
10721
+ // class: internal-narrative-comment
10722
+ var NARRATIVE = [
10723
+ { re: /\b(?:SUBSTRATE|BUG|MAIL)-\d+\b/, what: "internal slice/bug ID" },
10724
+ { re: /\b(?:D-[MLH]\d+|AUTH-\d+|CRYPTO-\d+|SUPPLY-\d+)\b/, what: "internal domain/slice ID" },
10725
+ { re: /\bCodex\s+P\d/, what: "code-review-process reference (Codex P#)" },
10726
+ { re: /\bF-[A-Z]{2,}-\d+\b/, what: "internal feature/plan item ID" },
10727
+ { re: /\bPR\s+#\d+\b/, what: "pull-request number (process residue)" },
10728
+ { re: /\b[Aa]udit\s+\d{4}-\d{2}-\d{2}/, what: "dated audit/decision residue" },
10729
+ { re: /\bReported\s+\d{4}-\d{2}-\d{2}/, what: "dated report residue" },
10730
+ { re: /\bCore Rule\s+§\d/, what: "internal CLAUDE.md rule-number citation" },
10731
+ ];
10732
+ var files = _libFiles();
10733
+ var bad = [];
10734
+ var jsdocLineRe = /^\s*\*/;
10735
+ for (var fi = 0; fi < files.length; fi++) {
10736
+ var rel = _relPath(files[fi]);
10737
+ var src = fs.readFileSync(files[fi], "utf8");
10738
+ var lines = src.split("\n");
10739
+ for (var li = 0; li < lines.length; li++) {
10740
+ var line = lines[li];
10741
+ // Scan the comment portion only: a ` * `-prefixed JSDoc line in
10742
+ // full, or the text after a line's first `//`.
10743
+ var comment = null;
10744
+ if (jsdocLineRe.test(line)) {
10745
+ comment = line;
10746
+ } else {
10747
+ var idx = line.indexOf("//");
10748
+ if (idx !== -1) comment = line.slice(idx);
10749
+ }
10750
+ if (comment === null) continue;
10751
+ for (var p = 0; p < NARRATIVE.length; p++) {
10752
+ var m = comment.match(NARRATIVE[p].re);
10753
+ if (m) {
10754
+ bad.push({
10755
+ file: rel,
10756
+ line: li + 1,
10757
+ content: NARRATIVE[p].what + ": `" + m[0] + "` in an operator-facing comment — " +
10758
+ "describe the change, not the internal process (strip the label / drop the " +
10759
+ "dated parenthetical / cite the public reason instead)",
10760
+ });
10761
+ break;
10762
+ }
10763
+ }
10764
+ }
10765
+ }
10766
+ bad = _filterMarkers(bad, "internal-narrative-comment");
10767
+ _report("operator-facing source comments must not carry internal-process narrative " +
10768
+ "(slice / bug / plan IDs, Codex / PR references, dated decision parentheticals)",
10769
+ bad);
10770
+ }
10771
+
10613
10772
  async function run() {
10614
10773
  testPrimitiveReachability();
10615
10774
  testDenyPathComposesDenyResponse();
10775
+ testNoInternalNarrativeComments();
10616
10776
  testNoOrphanAllowClass();
10617
10777
  testNoRawByteLiterals();
10618
10778
  testNoRawTimeLiterals();
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ /**
3
+ * b.cryptoField derived-hash modes.
4
+ *
5
+ * Equality-lookup ("derived") hashes for sealed columns can be computed
6
+ * two ways:
7
+ * - salted-sha3 (default) — SHA3-512 over a per-deployment salt.
8
+ * - hmac-shake256 (opt-in) — keyed MAC off vault.getDerivedHashMacKey,
9
+ * so the hash is unforgeable without the
10
+ * per-deployment MAC key.
11
+ *
12
+ * The mode is chosen per-table (derivedHashMode) or per-column
13
+ * (spec.mode). The decision lives in _computeDerivedHash; call sites
14
+ * must never hand-roll the hash.
15
+ */
16
+
17
+ var helpers = require("../helpers");
18
+ var b = helpers.b;
19
+ var check = helpers.check;
20
+ var fs = require("fs");
21
+ var os = require("os");
22
+ var path = require("path");
23
+
24
+ async function run() {
25
+ var dir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-cf-dh-"));
26
+ await b.vault.init({ mode: "plaintext", dataDir: dir });
27
+
28
+ // ---- salted-sha3 is the default: SHA3-512 → 128 hex chars ----
29
+ b.cryptoField.registerTable("cf_dh_t1", {
30
+ sealedFields: ["email"],
31
+ derivedHashes: { emailHash: { from: "email" } },
32
+ });
33
+ var saltedH = b.cryptoField.lookupHash("cf_dh_t1", "email", "a@b.com").value;
34
+ check("salted-sha3 default is 128 hex (SHA3-512)", saltedH.length === 128);
35
+ check("salted-sha3 is deterministic",
36
+ saltedH === b.cryptoField.lookupHash("cf_dh_t1", "email", "a@b.com").value);
37
+
38
+ // ---- hmac-shake256 table mode: SHAKE256/32 → 64 hex chars ----
39
+ b.cryptoField.registerTable("cf_dh_t2", {
40
+ sealedFields: ["email"],
41
+ derivedHashes: { emailHash: { from: "email" } },
42
+ derivedHashMode: "hmac-shake256",
43
+ });
44
+ var keyedH = b.cryptoField.lookupHash("cf_dh_t2", "email", "a@b.com").value;
45
+ check("hmac-shake256 is 64 hex (SHAKE256/32)", keyedH.length === 64);
46
+ check("hmac-shake256 is deterministic",
47
+ keyedH === b.cryptoField.lookupHash("cf_dh_t2", "email", "a@b.com").value);
48
+ check("keyed hash differs from salted hash", keyedH !== saltedH);
49
+
50
+ // ---- per-column mode override on a salted-default table ----
51
+ b.cryptoField.registerTable("cf_dh_t3", {
52
+ sealedFields: ["ssn"],
53
+ derivedHashes: { ssnHash: { from: "ssn", mode: "hmac-shake256" } },
54
+ });
55
+ check("per-column hmac-shake256 override → 64 hex",
56
+ b.cryptoField.lookupHash("cf_dh_t3", "ssn", "x").value.length === 64);
57
+
58
+ // ---- MAC key surface: 32 bytes, per-deployment ----
59
+ var macKey = b.vault.getDerivedHashMacKey();
60
+ check("getDerivedHashMacKey is a 32-byte Buffer",
61
+ Buffer.isBuffer(macKey) && macKey.length === 32);
62
+
63
+ // A fresh deployment (new vault dir + MAC key) yields a different
64
+ // keyed hash for the same input — the keyed hash is bound to the MAC
65
+ // key, not just the input.
66
+ var dir2 = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-cf-dh2-"));
67
+ b.vault._resetForTest();
68
+ await b.vault.init({ mode: "plaintext", dataDir: dir2 });
69
+ b.cryptoField.registerTable("cf_dh_t2", {
70
+ sealedFields: ["email"],
71
+ derivedHashes: { emailHash: { from: "email" } },
72
+ derivedHashMode: "hmac-shake256",
73
+ });
74
+ var keyedH2 = b.cryptoField.lookupHash("cf_dh_t2", "email", "a@b.com").value;
75
+ check("new deployment MAC key → different keyed hash", keyedH2 !== keyedH);
76
+
77
+ // ---- input validation: unknown modes are rejected at registerTable ----
78
+ var badTableMode = false;
79
+ try { b.cryptoField.registerTable("cf_dh_bad", { derivedHashMode: "md5" }); }
80
+ catch (e) { badTableMode = /derivedHashMode must be/.test(e.message); }
81
+ check("bad derivedHashMode throws at registerTable", badTableMode);
82
+
83
+ var badColMode = false;
84
+ try {
85
+ b.cryptoField.registerTable("cf_dh_bad2", {
86
+ derivedHashes: { h: { from: "x", mode: "rot13" } },
87
+ });
88
+ } catch (e) { badColMode = /mode must be/.test(e.message); }
89
+ check("bad per-column mode throws at registerTable", badColMode);
90
+
91
+ console.log("OK — crypto-field derived-hash tests");
92
+ }
93
+
94
+ module.exports = { run: run };
95
+ if (require.main === module) {
96
+ // Rethrow on failure so Node surfaces the error and exits non-zero,
97
+ // instead of logging the caught error object — a taint analyzer traces
98
+ // a logged error back to the test passphrase fixture (a non-secret
99
+ // constant) and raises a false clear-text-logging alert.
100
+ run().then(function () { process.exit(0); })
101
+ .catch(function (err) { process.exitCode = 1; throw err; });
102
+ }
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ /**
3
+ * b.db column-membership gate + whereRaw string-literal refusal.
4
+ *
5
+ * Column gate: a Query may only reference columns the table declared in
6
+ * its schema. The default mode is "reject" (throw), so a typo'd or
7
+ * attacker-influenced column name fails closed rather than silently
8
+ * matching nothing (or, for a raw fragment, opening an injection path).
9
+ * Modes: reject (default) | warn (audit, allow) | off (no gate).
10
+ * .allowedColumns([...]) narrows further and is ALWAYS enforced.
11
+ *
12
+ * whereRaw / WhereBuilder.raw refuse an embedded string literal
13
+ * ('...') unless { allowLiterals: true } — every value must bind
14
+ * through the params array.
15
+ */
16
+
17
+ var helpers = require("../helpers");
18
+ var b = helpers.b;
19
+ var check = helpers.check;
20
+ var fs = helpers.fs;
21
+ var os = helpers.os;
22
+ var path = helpers.path;
23
+
24
+ var SCHEMA = [{
25
+ name: "things",
26
+ columns: {
27
+ _id: "TEXT PRIMARY KEY",
28
+ name: "TEXT",
29
+ status: "TEXT DEFAULT 'active'",
30
+ },
31
+ indexes: ["status"],
32
+ }];
33
+
34
+ async function initDb(tmpDir, columnGate) {
35
+ process.env.BLAMEJS_SKIP_NTP_CHECK = "1";
36
+ helpers.setTestPassphraseEnv();
37
+ b.cluster._resetForTest();
38
+ b.audit._resetForTest();
39
+ b.vault._resetForTest();
40
+ b.db._resetForTest();
41
+ await b.vault.init({ dataDir: tmpDir });
42
+ var opts = { dataDir: tmpDir, tmpDir: path.join(tmpDir, "tmpfs"), schema: SCHEMA };
43
+ if (columnGate !== undefined) opts.columnGate = columnGate;
44
+ await b.db.init(opts);
45
+ }
46
+
47
+ function threwMatching(fn, pattern) {
48
+ try { fn(); } catch (e) { return pattern.test(e.message) ? e : null; }
49
+ return null;
50
+ }
51
+
52
+ async function run() {
53
+ var tmp = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-colgate-"));
54
+
55
+ // ---- default mode is reject ----
56
+ await initDb(tmp);
57
+ b.db.from("things").insertOne({ _id: "t1", name: "a", status: "active" });
58
+
59
+ // declared columns pass
60
+ check("declared column where() passes",
61
+ b.db.from("things").where({ name: "a" }).first().status === "active");
62
+ check("declared column select() passes",
63
+ b.db.from("things").select(["name", "status"]).where({ _id: "t1" }).first().name === "a");
64
+
65
+ // undeclared column is rejected (fail-closed)
66
+ check("undeclared where() column rejected",
67
+ !!threwMatching(function () { b.db.from("things").where({ nope: 1 }); },
68
+ /not a declared column of 'things'/));
69
+ check("undeclared orderBy() column rejected",
70
+ !!threwMatching(function () { b.db.from("things").orderBy("bogus"); },
71
+ /not a declared column/));
72
+ check("undeclared select() column rejected",
73
+ !!threwMatching(function () { b.db.from("things").select(["ghost"]); },
74
+ /not a declared column/));
75
+
76
+ // ---- getDeclaredColumns ----
77
+ check("b.db.getDeclaredColumns is fn", typeof b.db.getDeclaredColumns === "function");
78
+ var declared = b.db.getDeclaredColumns("things");
79
+ check("getDeclaredColumns lists schema columns",
80
+ declared.indexOf("_id") !== -1 && declared.indexOf("name") !== -1 && declared.indexOf("status") !== -1);
81
+ check("getDeclaredColumns on unknown table → null",
82
+ b.db.getDeclaredColumns("no_such_table") === null);
83
+
84
+ // ---- allowedColumns() narrows + is always enforced ----
85
+ check("allowedColumns() allows a member",
86
+ b.db.from("things").allowedColumns(["name"]).where({ name: "a" }).first().name === "a");
87
+ check("allowedColumns() rejects a declared-but-not-allowed column",
88
+ !!threwMatching(function () {
89
+ b.db.from("things").allowedColumns(["name"]).where({ status: "active" });
90
+ }, /not in the allowedColumns\(\) set/));
91
+ check("allowedColumns([]) rejected (non-empty array required)",
92
+ !!threwMatching(function () { b.db.from("things").allowedColumns([]); },
93
+ /non-empty array/));
94
+
95
+ // ---- A5: whereRaw refuses embedded string literals ----
96
+ var litErr = threwMatching(function () { b.db.from("things").whereRaw("status = 'active'"); },
97
+ /string literal/);
98
+ check("whereRaw refuses embedded string literal", !!litErr);
99
+ check("whereRaw literal error code is sql/raw-literal", litErr && litErr.code === "sql/raw-literal");
100
+ check("whereRaw with bound params is accepted",
101
+ b.db.from("things").whereRaw("status = ?", ["active"]).first().name === "a");
102
+ check("whereRaw allowLiterals opt-in is accepted",
103
+ b.db.from("things").whereRaw("status = 'active'", [], { allowLiterals: true }).first().name === "a");
104
+
105
+ b.db.close();
106
+ fs.rmSync(tmp, { recursive: true, force: true });
107
+
108
+ // ---- warn mode: audited, not thrown ----
109
+ var tmp2 = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-colgate-warn-"));
110
+ await initDb(tmp2, "warn");
111
+ var warnThrew = false;
112
+ try { b.db.from("things").where({ nope: 1 }); } catch (_e) { warnThrew = true; }
113
+ check("warn mode does not throw on undeclared column", warnThrew === false);
114
+ b.db.close();
115
+ fs.rmSync(tmp2, { recursive: true, force: true });
116
+
117
+ // ---- off mode: no gate ----
118
+ var tmp3 = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-colgate-off-"));
119
+ await initDb(tmp3, "off");
120
+ var offThrew = false;
121
+ try { b.db.from("things").where({ nope: 1 }); } catch (_e) { offThrew = true; }
122
+ check("off mode does not gate undeclared column", offThrew === false);
123
+ b.db.close();
124
+ fs.rmSync(tmp3, { recursive: true, force: true });
125
+
126
+ // ---- bad columnGate value rejected at db.init ----
127
+ var tmp4 = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-colgate-bad-"));
128
+ var initThrew = false;
129
+ try { await initDb(tmp4, "loud"); } catch (e) { initThrew = /columnGate/.test(e.message); }
130
+ check("db.init rejects unknown columnGate mode", initThrew);
131
+ try { b.db.close(); } catch (_e) {}
132
+ fs.rmSync(tmp4, { recursive: true, force: true });
133
+
134
+ b.audit._resetForTest();
135
+ b.db._resetForTest();
136
+ b.vault._resetForTest();
137
+ b.cluster._resetForTest();
138
+
139
+ console.log("OK — db column-gate tests");
140
+ }
141
+
142
+ module.exports = { run: run };
143
+ if (require.main === module) {
144
+ // Rethrow on failure so Node surfaces the error and exits non-zero,
145
+ // instead of logging the caught error object — a taint analyzer traces
146
+ // a logged error back to the test passphrase fixture (a non-secret
147
+ // constant) and raises a false clear-text-logging alert.
148
+ run().then(function () { process.exit(0); })
149
+ .catch(function (err) { process.exitCode = 1; throw err; });
150
+ }