@blamejs/blamejs-shop 0.3.11 → 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.
- package/CHANGELOG.md +4 -0
- package/lib/admin.js +157 -117
- package/lib/asset-manifest.json +1 -1
- package/lib/currency-rounding.js +2 -14
- package/lib/index.js +1 -0
- package/lib/text-guard.js +227 -0
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/README.md +3 -2
- package/lib/vendor/blamejs/SECURITY.md +3 -0
- package/lib/vendor/blamejs/api-snapshot.json +14 -2
- package/lib/vendor/blamejs/lib/agent-event-bus.js +4 -4
- package/lib/vendor/blamejs/lib/agent-idempotency.js +6 -6
- package/lib/vendor/blamejs/lib/agent-orchestrator.js +9 -9
- package/lib/vendor/blamejs/lib/agent-posture-chain.js +10 -10
- package/lib/vendor/blamejs/lib/agent-saga.js +6 -7
- package/lib/vendor/blamejs/lib/agent-snapshot.js +8 -8
- package/lib/vendor/blamejs/lib/agent-stream.js +3 -3
- package/lib/vendor/blamejs/lib/agent-tenant.js +4 -4
- package/lib/vendor/blamejs/lib/agent-trace.js +5 -5
- package/lib/vendor/blamejs/lib/ai-disclosure.js +3 -3
- package/lib/vendor/blamejs/lib/app.js +2 -2
- package/lib/vendor/blamejs/lib/archive-read.js +1 -1
- package/lib/vendor/blamejs/lib/archive-tar-read.js +1 -1
- package/lib/vendor/blamejs/lib/archive-wrap.js +5 -5
- package/lib/vendor/blamejs/lib/audit-tools.js +65 -5
- package/lib/vendor/blamejs/lib/audit.js +2 -2
- package/lib/vendor/blamejs/lib/auth/ciba.js +1 -1
- package/lib/vendor/blamejs/lib/auth/dpop.js +1 -1
- package/lib/vendor/blamejs/lib/auth/fal.js +1 -1
- package/lib/vendor/blamejs/lib/auth/fido-mds3.js +2 -3
- package/lib/vendor/blamejs/lib/auth/jwt-external.js +2 -2
- package/lib/vendor/blamejs/lib/auth/oauth.js +9 -9
- package/lib/vendor/blamejs/lib/auth/oid4vci.js +7 -7
- package/lib/vendor/blamejs/lib/auth/oid4vp.js +1 -1
- package/lib/vendor/blamejs/lib/auth/openid-federation.js +5 -5
- package/lib/vendor/blamejs/lib/auth/passkey.js +6 -6
- package/lib/vendor/blamejs/lib/auth/saml.js +1 -1
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -6
- package/lib/vendor/blamejs/lib/backup/index.js +18 -18
- package/lib/vendor/blamejs/lib/cache.js +4 -4
- package/lib/vendor/blamejs/lib/calendar.js +5 -5
- package/lib/vendor/blamejs/lib/circuit-breaker.js +1 -1
- package/lib/vendor/blamejs/lib/cms-codec.js +2 -2
- package/lib/vendor/blamejs/lib/compliance.js +14 -14
- package/lib/vendor/blamejs/lib/crypto-field.js +58 -21
- package/lib/vendor/blamejs/lib/crypto.js +5 -6
- package/lib/vendor/blamejs/lib/db-query.js +131 -9
- package/lib/vendor/blamejs/lib/db.js +106 -22
- package/lib/vendor/blamejs/lib/external-db.js +64 -16
- package/lib/vendor/blamejs/lib/framework-schema.js +4 -4
- package/lib/vendor/blamejs/lib/guard-list-id.js +2 -2
- package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +1 -2
- package/lib/vendor/blamejs/lib/incident-report.js +150 -0
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +1 -1
- package/lib/vendor/blamejs/lib/mail-deploy.js +3 -3
- package/lib/vendor/blamejs/lib/mail-server-managesieve.js +2 -2
- package/lib/vendor/blamejs/lib/mail-server-pop3.js +2 -2
- package/lib/vendor/blamejs/lib/mail-store.js +1 -1
- package/lib/vendor/blamejs/lib/metrics.js +8 -8
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/dpop.js +5 -5
- package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +21 -22
- package/lib/vendor/blamejs/lib/middleware/protected-resource-metadata.js +2 -2
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +2 -2
- package/lib/vendor/blamejs/lib/network-dns.js +1 -2
- package/lib/vendor/blamejs/lib/network-tls.js +0 -1
- package/lib/vendor/blamejs/lib/outbox.js +1 -1
- package/lib/vendor/blamejs/lib/pqc-agent.js +1 -1
- package/lib/vendor/blamejs/lib/retention.js +1 -1
- package/lib/vendor/blamejs/lib/retry.js +1 -1
- package/lib/vendor/blamejs/lib/safe-archive.js +2 -2
- package/lib/vendor/blamejs/lib/safe-ical.js +2 -2
- package/lib/vendor/blamejs/lib/safe-mime.js +1 -1
- package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +1 -1
- package/lib/vendor/blamejs/lib/self-update.js +2 -2
- package/lib/vendor/blamejs/lib/static.js +1 -1
- package/lib/vendor/blamejs/lib/subject.js +2 -2
- package/lib/vendor/blamejs/lib/vault/index.js +64 -1
- package/lib/vendor/blamejs/lib/vault/rotate.js +19 -0
- package/lib/vendor/blamejs/lib/vendor-data.js +1 -1
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.7.json +77 -0
- package/lib/vendor/blamejs/scripts/release.js +28 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-tools-dual-control.test.js +115 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +163 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +102 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-column-gate.test.js +150 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-key-aad.test.js +109 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +53 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/incident-report.test.js +65 -0
- package/lib/webhook-subscriptions.js +11 -24
- package/lib/webhooks.js +12 -33
- package/package.json +1 -1
|
@@ -102,11 +102,12 @@ function resolvePaths(dataDir) {
|
|
|
102
102
|
plaintext: nodePath.join(dataDir, "vault.key"),
|
|
103
103
|
sealed: nodePath.join(dataDir, "vault.key.sealed"),
|
|
104
104
|
derivedHashSalt: nodePath.join(dataDir, "vault.derived-hash-salt"),
|
|
105
|
+
derivedHashMacKey: nodePath.join(dataDir, "vault.derived-hash-mac.sealed"),
|
|
105
106
|
};
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
// derivedHashSalt — per-deployment salt for crypto-field
|
|
109
|
-
// derivedHashes
|
|
110
|
+
// derivedHashes. Pre-v0.8.42 the deterministic
|
|
110
111
|
// sha3(namespace + plaintext) shape allowed cross-deployment
|
|
111
112
|
// rainbow + cross-table correlation; binding a 32-byte
|
|
112
113
|
// per-deployment salt closes that class without breaking
|
|
@@ -175,6 +176,66 @@ function getDerivedHashSalt() {
|
|
|
175
176
|
return _cachedDerivedHashSalt;
|
|
176
177
|
}
|
|
177
178
|
|
|
179
|
+
// derivedHashMacKey — per-deployment SECRET key for crypto-field's
|
|
180
|
+
// keyed (hmac-shake256) derived-hash mode. Unlike the salt, this is
|
|
181
|
+
// SEALED at rest (vault.derived-hash-mac.sealed), so an attacker with
|
|
182
|
+
// disk access alone cannot recompute the keyed digest and correlate
|
|
183
|
+
// low-entropy plaintexts. Like the salt, it is keypair-bound and
|
|
184
|
+
// survives a passphrase-only rotation; an ENVELOPE rotation re-seals it
|
|
185
|
+
// because it is registered in rotate's additionalSealed sweep.
|
|
186
|
+
function _readOrCreateDerivedHashMacKey() {
|
|
187
|
+
if (!paths) {
|
|
188
|
+
throw new VaultError("vault/not-initialized",
|
|
189
|
+
"vault.getDerivedHashMacKey() requires init()");
|
|
190
|
+
}
|
|
191
|
+
if (nodeFs.existsSync(paths.derivedHashMacKey)) {
|
|
192
|
+
var sealed = atomicFile.readSync(paths.derivedHashMacKey, { encoding: "utf8" }).trim();
|
|
193
|
+
var b64 = unseal(sealed);
|
|
194
|
+
var key = Buffer.from(b64, "base64");
|
|
195
|
+
if (key.length !== 32) { // 32-byte (256-bit) MAC key
|
|
196
|
+
throw new VaultError("vault/derived-hash-mac-key-corrupted",
|
|
197
|
+
"vault.derived-hash-mac key must unseal to exactly 32 bytes; got " + key.length);
|
|
198
|
+
}
|
|
199
|
+
return key;
|
|
200
|
+
}
|
|
201
|
+
var nodeCrypto = require("node:crypto");
|
|
202
|
+
var raw = nodeCrypto.randomBytes(32); // 32-byte MAC key
|
|
203
|
+
atomicFile.writeSync(paths.derivedHashMacKey, seal(raw.toString("base64")), { fileMode: 0o600 });
|
|
204
|
+
log("generated per-deployment derivedHash MAC key at " + paths.derivedHashMacKey);
|
|
205
|
+
return raw;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
var _cachedDerivedHashMacKey = null;
|
|
209
|
+
/**
|
|
210
|
+
* @primitive b.vault.getDerivedHashMacKey
|
|
211
|
+
* @signature b.vault.getDerivedHashMacKey()
|
|
212
|
+
* @since 0.14.7
|
|
213
|
+
* @related b.vault.getDerivedHashSalt, b.cryptoField.registerTable
|
|
214
|
+
*
|
|
215
|
+
* Returns the 32-byte per-deployment SECRET key that backs crypto-
|
|
216
|
+
* field's keyed (`hmac-shake256`) derived-hash mode. Generated once on
|
|
217
|
+
* first use, SEALED at rest (`vault.derived-hash-mac.sealed`, mode
|
|
218
|
+
* `0o600`) so disk access alone does not expose it, and re-sealed by an
|
|
219
|
+
* envelope vault rotation. Distinct from `getDerivedHashSalt`, which is
|
|
220
|
+
* a non-secret salt stored in plaintext.
|
|
221
|
+
*
|
|
222
|
+
* Throws `VaultError("vault/not-initialized")` before `init()`, or
|
|
223
|
+
* `vault/derived-hash-mac-key-corrupted` if the sealed file does not
|
|
224
|
+
* unseal to exactly 32 bytes.
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* await b.vault.init({ dataDir: "/var/lib/blamejs", mode: "plaintext" });
|
|
228
|
+
* var k = b.vault.getDerivedHashMacKey();
|
|
229
|
+
* k.length; // → 32
|
|
230
|
+
* Buffer.isBuffer(k); // → true
|
|
231
|
+
*/
|
|
232
|
+
function getDerivedHashMacKey() {
|
|
233
|
+
if (_cachedDerivedHashMacKey === null) {
|
|
234
|
+
_cachedDerivedHashMacKey = _readOrCreateDerivedHashMacKey();
|
|
235
|
+
}
|
|
236
|
+
return _cachedDerivedHashMacKey;
|
|
237
|
+
}
|
|
238
|
+
|
|
178
239
|
// ---- Init dispatch ----
|
|
179
240
|
|
|
180
241
|
/**
|
|
@@ -620,6 +681,7 @@ module.exports = {
|
|
|
620
681
|
seal: seal,
|
|
621
682
|
unseal: unseal,
|
|
622
683
|
getDerivedHashSalt: getDerivedHashSalt,
|
|
684
|
+
getDerivedHashMacKey: getDerivedHashMacKey,
|
|
623
685
|
_zeroizeAndReplace: _zeroizeAndReplace,
|
|
624
686
|
aad: vaultAad,
|
|
625
687
|
getKeysJson: getKeysJson,
|
|
@@ -633,6 +695,7 @@ module.exports = {
|
|
|
633
695
|
_resetForTest: function () {
|
|
634
696
|
if (currentPassphrase) safeBuffer.secureZero(currentPassphrase);
|
|
635
697
|
keys = null; initialized = false; currentPassphrase = null; paths = null; currentMode = null;
|
|
698
|
+
_cachedDerivedHashSalt = null; _cachedDerivedHashMacKey = null;
|
|
636
699
|
},
|
|
637
700
|
_getKeysForTest: function () { return keys; },
|
|
638
701
|
_getPathsForTest: function () { return paths; },
|
|
@@ -651,6 +651,25 @@ async function rotate(opts) {
|
|
|
651
651
|
_reSealValue(current, oldKeys, newKeys), { mode: 0o600 });
|
|
652
652
|
}
|
|
653
653
|
|
|
654
|
+
// 3b. Framework-managed crypto-field derived-hash files — always
|
|
655
|
+
// rotated regardless of operator opts.paths, so the staging copy is
|
|
656
|
+
// complete. The plaintext salt is copied verbatim; the SEALED MAC key
|
|
657
|
+
// (keyed hmac-shake256 mode) is re-sealed under the new keypair so an
|
|
658
|
+
// envelope rotation doesn't orphan it (a passphrase-only rotation
|
|
659
|
+
// re-seals to the same value since the keypair is unchanged).
|
|
660
|
+
var saltSrc = nodePath.join(dataDir, "vault.derived-hash-salt");
|
|
661
|
+
if (nodeFs.existsSync(saltSrc)) {
|
|
662
|
+
nodeFs.copyFileSync(saltSrc, nodePath.join(stagingDir, "vault.derived-hash-salt"));
|
|
663
|
+
}
|
|
664
|
+
var macSrc = nodePath.join(dataDir, "vault.derived-hash-mac.sealed");
|
|
665
|
+
if (nodeFs.existsSync(macSrc)) {
|
|
666
|
+
var macCurrent = nodeFs.readFileSync(macSrc, "utf8").trim();
|
|
667
|
+
if (macCurrent.indexOf(C.VAULT_PREFIX) === 0) {
|
|
668
|
+
nodeFs.writeFileSync(nodePath.join(stagingDir, "vault.derived-hash-mac.sealed"),
|
|
669
|
+
_reSealValue(macCurrent, oldKeys, newKeys), { mode: 0o600 });
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
654
673
|
// 4. decrypt + rotate + re-encrypt db.enc
|
|
655
674
|
_emit(progress, { phase: "rotate_db" });
|
|
656
675
|
var encDbPath = nodePath.join(dataDir, paths.encryptedDb);
|
|
@@ -306,7 +306,7 @@ function _loadAndVerify(name) {
|
|
|
306
306
|
// Memoized — "sha256:" + sha256(pemToRaw(PUBKEY_PEM)). Matches the
|
|
307
307
|
// canonical fingerprint shape `scripts/vendor-data-gen.js` writes into
|
|
308
308
|
// each .data.js's `metadata.publicKeyFingerprint`. Computed lazily on
|
|
309
|
-
// first verify (also lazily by verifyAll at boot).
|
|
309
|
+
// first verify (also lazily by verifyAll at boot). Every
|
|
310
310
|
// per-entry verify cross-checks this against the entry's declared
|
|
311
311
|
// `meta.publicKeyFingerprint` so a pubkey-swap attack fails before
|
|
312
312
|
// signature verify even runs.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.14.7",
|
|
4
|
+
"date": "2026-05-30",
|
|
5
|
+
"headline": "Storage and audit-trail hardening: queries are gated to declared columns, raw SQL refuses embedded literals, the database key is bound to its location, sealed-column lookup hashes gain a keyed mode, audit-chain purges can require dual control, and breach deadlines ship a running clock",
|
|
6
|
+
"summary": "This release tightens the data and audit layers against a set of failure modes that were previously reachable. Database queries are now checked against the columns a table declared in its schema: a reference to an undeclared column fails closed by default instead of silently matching nothing, and the `whereRaw` escape hatch refuses an embedded string literal so values bind through placeholders. The database encryption key is sealed with its purpose, data directory, and key path as additional authenticated data, so a key file cannot be relocated to another deployment and unsealed there; an older key without that binding upgrades itself on first load. Sealed-column equality-lookup hashes can now be computed as a keyed MAC (HMAC-SHAKE256) off a per-deployment key, making the lookup hash unforgeable without that key, while the salted-SHA3 default is unchanged. Purging the tamper-evident audit chain can be placed under a two-authorizer dual-control grant so one operator cannot erase it alone, and database credential-rejection audits now record which relation the rejected credential tried to reach. Finally, breach-notification deadlines get a running clock that raises approaching and passed alerts as each regime's window elapses. One behavior change to note: the column gate defaults to reject — if a service issues queries against columns it did not declare in its schema, set `db.init({ columnGate: \"warn\" })` (audited, allowed) or `\"off\"` while the schema is reconciled.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "Column-membership gate on every query",
|
|
13
|
+
"body": "`b.db.from(table)` now checks each referenced column against the table's declared schema. The mode is set with `db.init({ columnGate: \"reject\" | \"warn\" | \"off\" })` (default `reject`), and `query.allowedColumns([...])` narrows a single query to an explicit allowlist that is always enforced. `b.db.getDeclaredColumns(table)` returns a table's declared column names (or `null` for an unknown table). This is defense in depth against typo'd or caller-influenced column names reaching the SQL layer (CWE-89)."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "Keyed mode for sealed-column lookup hashes",
|
|
17
|
+
"body": "Equality-lookup (\"derived\") hashes for sealed columns can be computed as `hmac-shake256` — a keyed MAC over a per-deployment key — instead of the default `salted-sha3`. Set it per table with `cryptoField.registerTable(name, { derivedHashMode: \"hmac-shake256\" })` or per column with `{ from, mode: \"hmac-shake256\" }`. The keyed hash is unforgeable and un-correlatable without the deployment's MAC key, which raises the bar against offline lookup-table attacks on low-entropy sealed values (CWE-916). `b.vault.getDerivedHashMacKey()` exposes the 32-byte per-deployment key; it is created on first use and re-sealed across key rotation automatically."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"title": "Dual-control gate on audit-chain purge",
|
|
21
|
+
"body": "`b.auditTools.purge` accepts `dualControlGrant`. When `audit_log` is placed under dual control, a verified archive and `confirm: true` are no longer sufficient: the purge additionally requires a consumed m-of-n grant whose action is bound to the purge, so a grant minted for another operation cannot be replayed and one operator cannot erase the tamper-evident chain alone (NIST SP 800-53 AU-9, separation of duties)."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"title": "Running clock for breach-notification deadlines",
|
|
25
|
+
"body": "`b.incident.report.createDeadlineClock({ notify, approachThresholds })` tracks open incidents and raises `deadline_approaching` and `deadline_passed` alerts as each regime's window elapses (GDPR 72h, DORA, NIS2, and the rest of the registry). Alerts are deduplicated per incident and stage, suppressed once a submission stage is acknowledged, and the clock can run on an interval or be ticked manually."
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"heading": "Changed",
|
|
31
|
+
"items": [
|
|
32
|
+
{
|
|
33
|
+
"title": "Queries against undeclared columns now fail closed by default",
|
|
34
|
+
"body": "The column gate defaults to `reject`: a query that references a column the table did not declare throws rather than silently matching nothing. A service that intentionally queries undeclared columns can set `db.init({ columnGate: \"warn\" })` to audit and allow, or `\"off\"` to disable the gate, while its schema is reconciled. Framework-declared columns (including `_id` and derived-hash columns) are always members."
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"heading": "Security",
|
|
40
|
+
"items": [
|
|
41
|
+
{
|
|
42
|
+
"title": "Database encryption key bound to its location",
|
|
43
|
+
"body": "`db.key.enc` is sealed with additional authenticated data over its purpose, resolved data directory, and resolved key path. A sealed key copied to a different deployment or path no longer unseals there — the AEAD authentication fails — which prevents silent key relocation. A legacy key sealed without this binding is detected and re-sealed in the bound format on first load, with no operator action required."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"title": "`whereRaw` refuses embedded string literals",
|
|
47
|
+
"body": "`whereRaw(sql, params)` and `WhereBuilder.raw(sql, params)` reject a raw fragment containing a string literal (`'...'`); values must bind through the `params` array. A static, operator-controlled literal can opt in with `{ allowLiterals: true }`. This closes a path where a value concatenated into a raw fragment would reintroduce SQL injection (CWE-89)."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"title": "Credential-rejection audits record the attempted relation",
|
|
51
|
+
"body": "A `db.auth.failed` audit row (SQLSTATE 28000 / 28P01 / 42501) now carries `attemptedTable`, the relation the rejected credential tried to reach, extracted defensively from the statement. Triage can scope the blast radius of a credential-abuse event without correlating back to the raw SQL log (CWE-778)."
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"heading": "Detectors",
|
|
57
|
+
"items": [
|
|
58
|
+
{
|
|
59
|
+
"title": "Audit-purge dual-control gate",
|
|
60
|
+
"body": "A new check fails the build if a call to `purgeAuditChain` appears in a file that does not also route through the dual-control gate, so a future caller cannot physically delete chain rows without two-authorizer enforcement."
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"title": "Raw-SQL literal/interpolation guard",
|
|
64
|
+
"body": "A new check fails the build on a `whereRaw` / `.raw` call whose SQL argument is built by template interpolation or string concatenation, keeping the bound-params discipline enforceable in framework code."
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"title": "Hand-rolled lookup-hash guard",
|
|
68
|
+
"body": "A new check fails the build if a sealed-column lookup hash is derived from the per-deployment salt outside the canonical helper, so call sites cannot bypass the keyed-mode and per-column mode policy."
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"title": "Auth-audit attempted-relation guard",
|
|
72
|
+
"body": "A new check fails the build if a `db.auth.failed` audit is emitted in a file that does not name `attemptedTable`, so the forensic field cannot be dropped from a future emitter."
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
@@ -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
|
-
|
|
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:
|
|
122
|
+
shell: useShell,
|
|
98
123
|
});
|
|
99
124
|
if (rv.status !== 0 && !opts.allowFail) {
|
|
100
|
-
throw new Error("release: " + cmd + " " +
|
|
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 —
|
|
10569
|
-
//
|
|
10570
|
-
//
|
|
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();
|