@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.
- package/CHANGELOG.md +2 -0
- package/lib/admin.js +155 -113
- package/lib/asset-manifest.json +1 -1
- 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/package.json +1 -1
|
@@ -175,7 +175,7 @@ function memoryStore(opts) {
|
|
|
175
175
|
* headers. Requires `b.vault.init(...)` to have run; falls back to
|
|
176
176
|
* plain-text with a one-shot audit warning when vault isn't ready,
|
|
177
177
|
* so test-fixture / boot-script callers still work.
|
|
178
|
-
* - `aad: true` (since 0.9.58
|
|
178
|
+
* - `aad: true` (since 0.9.58) — sealed columns are bound
|
|
179
179
|
* via Additional Authenticated Data to (table, k, column,
|
|
180
180
|
* schemaVersion) so a DB-write attacker can't copy a sealed
|
|
181
181
|
* header/body cell from one row to another (which previously
|
|
@@ -184,12 +184,12 @@ function memoryStore(opts) {
|
|
|
184
184
|
* detects the envelope shape; lazy re-seal on next `set()` upgrades
|
|
185
185
|
* each row to AAD form. Operators wanting a one-shot migration
|
|
186
186
|
* call `b.middleware.idempotencyKey.resealMigrate(store)`.
|
|
187
|
-
* - `fingerprintSeal: true` (since 0.9.58
|
|
187
|
+
* - `fingerprintSeal: true` (since 0.9.58) — the request
|
|
188
188
|
* `fingerprint` column carries an HMAC under a vault-derived
|
|
189
189
|
* secret instead of a bare SHA3-256 of method+path+body. The
|
|
190
190
|
* compare path is constant-time so the column doubles as a
|
|
191
191
|
* mismatch oracle without offline-brute-force exposure.
|
|
192
|
-
* - `bodyFingerprintFallback: "deny"` (since 0.9.58
|
|
192
|
+
* - `bodyFingerprintFallback: "deny"` (since 0.9.58) —
|
|
193
193
|
* when neither `bodyFingerprint` nor `req._rawBody`/`req.body` is
|
|
194
194
|
* populated for a body-bearing method, the middleware previously
|
|
195
195
|
* silently degraded the fingerprint to method+path. Set to
|
|
@@ -228,8 +228,8 @@ function memoryStore(opts) {
|
|
|
228
228
|
* init?: boolean, // default true — run CREATE TABLE IF NOT EXISTS at construction
|
|
229
229
|
* hashKeys?: boolean, // default true — store sha3-512 namespace-hash of the key, not the raw key
|
|
230
230
|
* seal?: boolean, // default true — seal headers + body via b.cryptoField when vault is ready
|
|
231
|
-
* aad?: boolean, // default true — AAD-bind seal to (table,k,column) so a DB-write attacker can't cross-row swap
|
|
232
|
-
* fingerprintSeal?: boolean, // default true — HMAC fingerprint under a vault-derived secret instead of bare sha3-256
|
|
231
|
+
* aad?: boolean, // default true — AAD-bind seal to (table,k,column) so a DB-write attacker can't cross-row swap
|
|
232
|
+
* fingerprintSeal?: boolean, // default true — HMAC fingerprint under a vault-derived secret instead of bare sha3-256
|
|
233
233
|
*
|
|
234
234
|
* @example
|
|
235
235
|
* // single-process daemon, framework's internal sqlite, both defaults on:
|
|
@@ -264,11 +264,11 @@ function dbStore(opts) {
|
|
|
264
264
|
var doInit = opts.init !== false;
|
|
265
265
|
var hashKeys = opts.hashKeys !== false;
|
|
266
266
|
var sealReq = opts.seal !== false;
|
|
267
|
-
//
|
|
267
|
+
// AAD-bind sealing to (table, k, column) by default.
|
|
268
268
|
// Forms a defense-in-depth pair with seal: cross-row swap fails
|
|
269
269
|
// Poly1305 even when the attacker controls the DB layer.
|
|
270
270
|
var aadOn = opts.aad !== false;
|
|
271
|
-
//
|
|
271
|
+
// HMAC the fingerprint under a vault-derived secret by
|
|
272
272
|
// default. Bare SHA3-256 of method+path+body is offline-brute-
|
|
273
273
|
// forceable for any DB-dump attacker; HMAC under a vault secret
|
|
274
274
|
// forces them to break the vault first.
|
|
@@ -297,7 +297,7 @@ function dbStore(opts) {
|
|
|
297
297
|
|
|
298
298
|
// Register the table with cryptoField. registerTable is idempotent
|
|
299
299
|
// — subsequent dbStore() calls with the same tableName re-declare
|
|
300
|
-
// the same sealedFields and no-op.
|
|
300
|
+
// the same sealedFields and no-op. When aad is on,
|
|
301
301
|
// (table, k, column) is threaded into the AEAD AAD so a DB-write
|
|
302
302
|
// attacker can't copy a sealed value between rows.
|
|
303
303
|
if (sealEnabled) {
|
|
@@ -309,7 +309,7 @@ function dbStore(opts) {
|
|
|
309
309
|
});
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
//
|
|
312
|
+
// Derive a per-vault HMAC secret for fingerprint sealing.
|
|
313
313
|
// The vault root key is the trust root; without it the secret is
|
|
314
314
|
// unrecoverable. Lazy: only derived when fpSealOn is enabled AND the
|
|
315
315
|
// vault is ready, so test fixtures that haven't initialized the
|
|
@@ -349,7 +349,7 @@ function dbStore(opts) {
|
|
|
349
349
|
// so audit/forensic SELECTs don't have to unseal-everything. The
|
|
350
350
|
// `k` column is selected even when not strictly needed for read
|
|
351
351
|
// because cryptoField.unsealRow uses it as the rowId in AAD when
|
|
352
|
-
// the table is AAD-bound
|
|
352
|
+
// the table is AAD-bound.
|
|
353
353
|
var stmtGet = db.prepare(
|
|
354
354
|
"SELECT k, fingerprint, status_code, headers, body, expires_at FROM " +
|
|
355
355
|
qTable + " WHERE k = ?");
|
|
@@ -372,7 +372,7 @@ function dbStore(opts) {
|
|
|
372
372
|
return bCrypto.namespaceHash("idempotency-key", rawKey);
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
//
|
|
375
|
+
// Emit / compare HMAC-shape fingerprints. The store
|
|
376
376
|
// round-trips the column as plain text (no transformation per-get);
|
|
377
377
|
// sealing happens at MINT time (when the middleware builds the
|
|
378
378
|
// fingerprint and hands it to set()). The store's responsibility is
|
|
@@ -391,7 +391,7 @@ function dbStore(opts) {
|
|
|
391
391
|
if (sealEnabled) {
|
|
392
392
|
try { liveRow = cryptoField.unsealRow(tableNameRaw, row); }
|
|
393
393
|
catch (_unsealErr) {
|
|
394
|
-
//
|
|
394
|
+
// Decryption failure used to delete the row,
|
|
395
395
|
// which let an attacker probe key presence via a "tamper +
|
|
396
396
|
// observe subsequent SELECT" oracle. The fix: emit audit,
|
|
397
397
|
// return null, do NOT delete. TTL sweeps stale rows out
|
|
@@ -407,7 +407,7 @@ function dbStore(opts) {
|
|
|
407
407
|
}
|
|
408
408
|
var headersObj;
|
|
409
409
|
try {
|
|
410
|
-
//
|
|
410
|
+
// Route through C.BYTES.mib(4); raw `4 * 1024 * 1024`
|
|
411
411
|
// was a drift smell flagged by codebase-patterns. 4 MiB ceiling
|
|
412
412
|
// unchanged.
|
|
413
413
|
headersObj = safeJson.parse(liveRow.headers, { maxBytes: C.BYTES.mib(4) });
|
|
@@ -421,7 +421,6 @@ function dbStore(opts) {
|
|
|
421
421
|
// DELETING it would clobber another process's cache and
|
|
422
422
|
// turn a hit into a miss with potential side-effect re-
|
|
423
423
|
// execution. Treat as miss + LEAVE the row in place.
|
|
424
|
-
// Per Codex P1 on PR #45.
|
|
425
424
|
var lookedSealed = typeof liveRow.headers === "string" &&
|
|
426
425
|
(liveRow.headers.indexOf("vault:") === 0 ||
|
|
427
426
|
liveRow.headers.indexOf("vault.aad:") === 0);
|
|
@@ -456,7 +455,7 @@ function dbStore(opts) {
|
|
|
456
455
|
delete: function (rawKey) {
|
|
457
456
|
stmtDelete.run(_k(rawKey));
|
|
458
457
|
},
|
|
459
|
-
//
|
|
458
|
+
// The middleware consults this hook to HMAC the
|
|
460
459
|
// method+path+body digest under a vault-derived secret before
|
|
461
460
|
// insert + compare. Returns null when fpSeal is disabled OR the
|
|
462
461
|
// vault wasn't ready at construction; the middleware then falls
|
|
@@ -466,7 +465,7 @@ function dbStore(opts) {
|
|
|
466
465
|
return nodeCrypto.createHmac("sha3-256", fpHmacSecret)
|
|
467
466
|
.update(preimageBytes).digest("hex");
|
|
468
467
|
},
|
|
469
|
-
//
|
|
468
|
+
// Operator helper: walk the table and reseal every row
|
|
470
469
|
// under the AAD form. Existing v0.9.15-v0.9.57 rows continue to
|
|
471
470
|
// read on a per-row basis (unsealRow auto-detects shape), but
|
|
472
471
|
// operators wanting an explicit migration step call this once.
|
|
@@ -526,7 +525,7 @@ function _fingerprintRequest(req, bodyBytes, store) {
|
|
|
526
525
|
// Fingerprint preimage = method + path + body. Per the draft §4.3,
|
|
527
526
|
// a key+body mismatch is a client-side mistake; our preimage covers
|
|
528
527
|
// method + path so a client reusing a key across different
|
|
529
|
-
// endpoints is also caught.
|
|
528
|
+
// endpoints is also caught. When the store exposes a
|
|
530
529
|
// `fingerprintHmac` hook (dbStore with fingerprintSeal:true + vault
|
|
531
530
|
// ready), the preimage is HMAC'd under a vault-derived secret so a
|
|
532
531
|
// DB dump leaks neither the preimage nor a brute-forceable digest.
|
|
@@ -602,7 +601,7 @@ function _emitAudit(action, metadata, outcome) {
|
|
|
602
601
|
* requireIdempotencyKey: boolean, // default: false — refuse missing-key
|
|
603
602
|
* bodyFingerprint: function, // (req) => Buffer|string|object|null — operator-supplied body extractor
|
|
604
603
|
* maxBodyBytes: number, // default: 1 MiB — replay-cache body cap
|
|
605
|
-
* bodyFingerprintFallback: string, // default "deny"
|
|
604
|
+
* bodyFingerprintFallback: string, // default "deny" — when neither
|
|
606
605
|
* // bodyFingerprint nor req._rawBody / req.body is
|
|
607
606
|
* // available for POST/PUT/PATCH, refuse with HTTP 400
|
|
608
607
|
* // idempotency/missing-body-fingerprint instead of
|
|
@@ -669,7 +668,7 @@ function create(opts) {
|
|
|
669
668
|
opts.bodyFingerprint, "idempotencyKey.bodyFingerprint",
|
|
670
669
|
IdempotencyError, "idempotency/bad-body-fingerprint"
|
|
671
670
|
) || null;
|
|
672
|
-
//
|
|
671
|
+
// Default "deny" refuses body-bearing requests that
|
|
673
672
|
// arrive with neither req._rawBody / req.body NOR an operator-
|
|
674
673
|
// supplied bodyFingerprint hook. The silent-degrade-to-method+path
|
|
675
674
|
// path was a §4.3 violation (same key + different body returned
|
|
@@ -762,7 +761,7 @@ function create(opts) {
|
|
|
762
761
|
// Misordered-mount detector — body-bearing method reached us
|
|
763
762
|
// with neither a parsed body nor a raw-body buffer. Most likely
|
|
764
763
|
// body-parser hasn't run yet, which used to silently degrade the
|
|
765
|
-
// fingerprint to method+path;
|
|
764
|
+
// fingerprint to method+path; v0.9.58 makes that
|
|
766
765
|
// case refuse with HTTP 400 by default. The audit emit fires in
|
|
767
766
|
// both fallback modes so operator review surfaces the
|
|
768
767
|
// misconfiguration regardless of the chosen fallback.
|
|
@@ -929,8 +928,8 @@ function _redactKey(key) {
|
|
|
929
928
|
* @related b.middleware.idempotencyKey.dbStore
|
|
930
929
|
*
|
|
931
930
|
* One-shot operator helper that walks a dbStore's table and reseals
|
|
932
|
-
* every row under the AAD-bound envelope shape introduced in v0.9.58
|
|
933
|
-
*
|
|
931
|
+
* every row under the AAD-bound envelope shape introduced in v0.9.58.
|
|
932
|
+
* Existing v0.9.15-v0.9.57 rows continue to read on a
|
|
934
933
|
* per-row basis (unsealRow auto-detects shape) so a deploy without
|
|
935
934
|
* this call is correct, but operators who want to upgrade in bulk
|
|
936
935
|
* call this once after upgrading.
|
|
@@ -94,7 +94,7 @@ function create(opts) {
|
|
|
94
94
|
"middleware/protected-resource-metadata/no-as",
|
|
95
95
|
"authorizationServers must be a non-empty array of issuer URLs");
|
|
96
96
|
}
|
|
97
|
-
//
|
|
97
|
+
// RFC 9728 §3 + RFC 8414 §3.1: authorizationServers entries
|
|
98
98
|
// are issuer URLs and MUST be https://. Pre-v0.9.x only required
|
|
99
99
|
// non-empty string, so an operator typo could ship `http://idp.test`
|
|
100
100
|
// (or, worse, `javascript:` / `data:`) to clients via the well-known
|
|
@@ -156,7 +156,7 @@ function create(opts) {
|
|
|
156
156
|
if (opts.dpopBoundAccessTokensRequired === true) doc.dpop_bound_access_tokens_required = true;
|
|
157
157
|
if (opts.mtlsBoundAccessTokensRequired === true) doc.tls_client_certificate_bound_access_tokens = true;
|
|
158
158
|
|
|
159
|
-
//
|
|
159
|
+
// RFC 9728 §3.2 signed_metadata. Operators with an
|
|
160
160
|
// anti-tamper requirement pass `signMetadata: { key, alg, kid }`;
|
|
161
161
|
// the middleware emits `application/jwt` carrying the JWS-signed
|
|
162
162
|
// metadata. Default output remains cleartext `application/json`.
|
|
@@ -126,7 +126,7 @@ var DEFAULT_MAX_TTL_MS = C.TIME.hours(24);
|
|
|
126
126
|
var DEFAULT_MIN_TTL_MS = C.TIME.seconds(60);
|
|
127
127
|
var DEFAULT_STALE_WINDOW = C.TIME.hours(6);
|
|
128
128
|
var DEFAULT_PROFILE = "strict";
|
|
129
|
-
//
|
|
129
|
+
// CWE-400/770. Bound the cache so a hostile peer
|
|
130
130
|
// that can drive query-name selection (e.g. inbound SMTP forwarding
|
|
131
131
|
// DKIM `s=` / `d=` tag-controlled lookups) cannot inflate the Map to
|
|
132
132
|
// OOM. Default 5000 entries: a parsed-response object ~100 bytes ×
|
|
@@ -216,7 +216,7 @@ function create(opts) {
|
|
|
216
216
|
|
|
217
217
|
var cache = new Map(); // key → { response, parsed, ttl, expiresAt, staleUntil }
|
|
218
218
|
|
|
219
|
-
// CWE-400/770
|
|
219
|
+
// CWE-400/770. LRU eviction on insert when the cache is at
|
|
220
220
|
// capacity. v8 Map preserves insertion order; oldest key is the
|
|
221
221
|
// first entry returned by Map.keys().next().
|
|
222
222
|
function _evictIfFull() {
|
|
@@ -39,8 +39,7 @@ var STATE = {
|
|
|
39
39
|
// Default-on secure DNS (DoH via Cloudflare) when neither doh nor dot
|
|
40
40
|
// is operator-configured AND no opt-out env var is set. Operators
|
|
41
41
|
// who explicitly want the system resolver call useSystemResolver()
|
|
42
|
-
// or set BLAMEJS_DNS_TRANSPORT=system. Default-on
|
|
43
|
-
// ("security defaults are not opt-in").
|
|
42
|
+
// or set BLAMEJS_DNS_TRANSPORT=system. Default-on (security defaults are not opt-in).
|
|
44
43
|
systemResolver: false,
|
|
45
44
|
};
|
|
46
45
|
|
|
@@ -1136,7 +1136,6 @@ function evaluateOcspResponse(ocspDer, opts) {
|
|
|
1136
1136
|
// length inputs but fast-paths on length mismatch; not security-
|
|
1137
1137
|
// critical here (the OCSP response is CA-signed and signature
|
|
1138
1138
|
// already verified) but matches the project discipline.
|
|
1139
|
-
// (Audit 2026-05-11.)
|
|
1140
1139
|
if (!bCrypto.timingSafeEqual(parsed.basic.nonce, opts.expectedNonce)) {
|
|
1141
1140
|
return { ok: false, status: parsed.status, signatureValid: true,
|
|
1142
1141
|
errors: ["OCSP nonce mismatch — possible replay or wrong responder"] };
|
|
@@ -342,7 +342,7 @@ function create(opts) {
|
|
|
342
342
|
var stopping = false;
|
|
343
343
|
var inFlight = null;
|
|
344
344
|
|
|
345
|
-
//
|
|
345
|
+
// `FOR UPDATE SKIP LOCKED` is Postgres / MySQL 8+ only.
|
|
346
346
|
// SQLite (single-writer at the DB level, but WAL mode lets multiple
|
|
347
347
|
// processes share the file with concurrent SELECTs) doesn't support
|
|
348
348
|
// SKIP LOCKED — feeding it Postgres syntax silently double-publishes
|
|
@@ -322,7 +322,7 @@ function _getDefaultAgent() {
|
|
|
322
322
|
* logger.info("pqc-agent reloaded", res);
|
|
323
323
|
*/
|
|
324
324
|
function reload() {
|
|
325
|
-
//
|
|
325
|
+
// Null the cached agent BEFORE calling destroy. The
|
|
326
326
|
// previous order let a concurrent _getDefaultAgent() see the
|
|
327
327
|
// destroyed-not-null agent and hand it to a caller; the caller
|
|
328
328
|
// then tries to issue a request through a torn-down keep-alive
|
|
@@ -569,7 +569,7 @@ function complianceFloor(posture, candidateTtlMs) {
|
|
|
569
569
|
return candidateTtlMs > floor ? candidateTtlMs : floor;
|
|
570
570
|
}
|
|
571
571
|
|
|
572
|
-
// applyPosture —
|
|
572
|
+
// applyPosture — cascade hook. b.compliance.set(posture)
|
|
573
573
|
// calls this to merge posture defaults into retention's state. The
|
|
574
574
|
// retention module itself doesn't carry per-instance global defaults;
|
|
575
575
|
// the cascade's job here is to surface the posture's audit-log
|
|
@@ -275,7 +275,7 @@ function backoffDelay(attempt, opts) {
|
|
|
275
275
|
opts = opts || DEFAULT_RETRY;
|
|
276
276
|
var base = opts.baseDelayMs * Math.pow(2, attempt - 1);
|
|
277
277
|
var capped = Math.min(base, opts.maxDelayMs);
|
|
278
|
-
//
|
|
278
|
+
// Jitter exists to spread retry storms across the
|
|
279
279
|
// millisecond window so N peer clients waking from the same
|
|
280
280
|
// upstream outage don't all hit the recovering service at the same
|
|
281
281
|
// tick. The value is observable to every client by construction
|
|
@@ -225,7 +225,7 @@ async function extract(opts) {
|
|
|
225
225
|
}
|
|
226
226
|
inner = await archiveWrap().unwrapWithPassphrase(sealedBytes, { passphrase: opts.passphrase });
|
|
227
227
|
}
|
|
228
|
-
//
|
|
228
|
+
// Close the original source
|
|
229
229
|
// adapter BEFORE replacing it. When opts.source was a string
|
|
230
230
|
// path, the fs adapter opened a file descriptor; overwriting
|
|
231
231
|
// `source` loses the close reference and the descriptor
|
|
@@ -236,7 +236,7 @@ async function extract(opts) {
|
|
|
236
236
|
if (typeof source.close === "function" && typeof opts.source === "string") {
|
|
237
237
|
try { source.close(); } catch (_e) { /* drop-silent */ }
|
|
238
238
|
}
|
|
239
|
-
//
|
|
239
|
+
// Forward opts.signal to the
|
|
240
240
|
// inner buffer adapter so abort propagation stays intact
|
|
241
241
|
// across the unwrap boundary. Without it, an abort raised
|
|
242
242
|
// after unwrapping would no longer cancel inner range()
|
|
@@ -251,9 +251,9 @@ function parse(text, opts) {
|
|
|
251
251
|
var vcal = consumed.component;
|
|
252
252
|
// RFC 5545 §3.4 — a stream may carry multiple VCALENDAR objects.
|
|
253
253
|
// Walk the remainder so trailing objects are validated under the
|
|
254
|
-
// same caps + control-char + property allowlist
|
|
254
|
+
// same caps + control-char + property allowlist; without
|
|
255
255
|
// this, CalDAV ingest can pass validation on the first object while
|
|
256
|
-
// trailing malformed objects ride through untouched
|
|
256
|
+
// trailing malformed objects ride through untouched.
|
|
257
257
|
var vcalendars = [_shapeVcalendar(vcal)];
|
|
258
258
|
var cursor = consumed.nextIdx;
|
|
259
259
|
while (cursor < lines.length) {
|
|
@@ -549,7 +549,7 @@ function _splitMultipart(buf, boundary) {
|
|
|
549
549
|
// Per RFC 2046 §5.1.1 a boundary delimiter is `--<value>` preceded
|
|
550
550
|
// by CRLF (or LF) — OR at the very start of the body. A boundary-
|
|
551
551
|
// shaped sequence elsewhere in a part's body MUST NOT be treated
|
|
552
|
-
// as a delimiter.
|
|
552
|
+
// as a delimiter.
|
|
553
553
|
var idx = _findBoundaryAtLineStart(buf, delimiter, pos);
|
|
554
554
|
if (idx < 0) break;
|
|
555
555
|
if (buf[idx + delimiter.length] === 0x2D && buf[idx + delimiter.length + 1] === 0x2D) {
|
|
@@ -208,7 +208,7 @@ function verify(assetPath, signaturePath, pubkeyPem) {
|
|
|
208
208
|
// produces. 64 KiB chunks match the framework's hash-while-streaming
|
|
209
209
|
// convention elsewhere.
|
|
210
210
|
//
|
|
211
|
-
//
|
|
211
|
+
// Hardening (v0.9.58): fstat the asset BEFORE the read loop
|
|
212
212
|
// for every alg path, clamp every readSync to (assetStat.size -
|
|
213
213
|
// fullOff), and reject if the final fullOff diverges from
|
|
214
214
|
// assetStat.size. A grow-during-read race (writer appends as we
|
|
@@ -115,7 +115,7 @@ function _normalizeTag(tag) {
|
|
|
115
115
|
* Missing numeric components on either side are treated as `"0"` so
|
|
116
116
|
* `"1.0"` and `"1.0.0"` compare equal.
|
|
117
117
|
*
|
|
118
|
-
*
|
|
118
|
+
* Hardening (v0.9.58) — pre-v0.9.58 the pre-release segment fell back
|
|
119
119
|
* to lexicographic comparison, which silently misordered `"1.0.0-alpha.10"`
|
|
120
120
|
* (the strict-§11 LARGER pre-release) and `"1.0.0-alpha.9"`: as strings
|
|
121
121
|
* "10" < "9" so `alpha.10 < alpha.9`, and a downstream consumer polling
|
|
@@ -294,7 +294,7 @@ function _matchAsset(name, pattern, fallback) {
|
|
|
294
294
|
* timeoutMs: number, // request timeout (default 15s)
|
|
295
295
|
* headers: object, // additional request headers
|
|
296
296
|
* etag: string, // last-seen etag for If-None-Match
|
|
297
|
-
* // (
|
|
297
|
+
* // (etags are RFC 9110 §13.1.1
|
|
298
298
|
* // per-resource; an etag captured for
|
|
299
299
|
* // releasesUrl=A is meaningless against
|
|
300
300
|
* // releasesUrl=B. Operators rotating
|
|
@@ -216,7 +216,7 @@ function _resolveSafe(root, requestedPath) {
|
|
|
216
216
|
// deposited disk content: shell-exec extensions (.exe / .bin / .so /
|
|
217
217
|
// legitimate `<name>.<hash>.js` bundler output) are valid here. The
|
|
218
218
|
// other balanced checks still reject the traversal + smuggling
|
|
219
|
-
// surface
|
|
219
|
+
// surface.
|
|
220
220
|
var fname = nodePath.basename(resolved);
|
|
221
221
|
var rv = guardFilename().validate(fname, {
|
|
222
222
|
profile: "balanced",
|
|
@@ -362,7 +362,7 @@ function erase(subjectId, opts) {
|
|
|
362
362
|
|
|
363
363
|
// ---- Crypto-shred erase (Art. 17 + WAL/replica residual closure) ----
|
|
364
364
|
//
|
|
365
|
-
//
|
|
365
|
+
// When a table opts into per-row keying via
|
|
366
366
|
// b.cryptoField.declarePerRowKey, this primitive deletes the
|
|
367
367
|
// per-row K_row entries from _blamejs_per_row_keys, leaving any
|
|
368
368
|
// residual ciphertext in WAL / replica / backup storage
|
|
@@ -477,7 +477,7 @@ function eraseHard(subjectId, opts) {
|
|
|
477
477
|
totalDeleted += deleted;
|
|
478
478
|
perTable[spec.name] = deleted;
|
|
479
479
|
// REINDEX the table so B-tree pages holding the deleted row's
|
|
480
|
-
// index entries are rebuilt — closes the
|
|
480
|
+
// index entries are rebuilt — closes the erase-vacuum residual class.
|
|
481
481
|
try { db().runSql('REINDEX "' + spec.name + '"'); } // table name comes from FRAMEWORK_SCHEMA
|
|
482
482
|
catch (_e) { /* cluster mode / unsupported dialect */ }
|
|
483
483
|
}
|
|
@@ -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
|
+
}
|