@blamejs/blamejs-shop 0.3.12 → 0.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/lib/admin.js +155 -113
- package/lib/asset-manifest.json +1 -1
- package/lib/security-middleware.js +28 -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/package.json +1 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.db encryption-key AAD binding.
|
|
4
|
+
*
|
|
5
|
+
* The database encryption key (db.key.enc) is sealed with additional
|
|
6
|
+
* authenticated data bound to its purpose, data directory, and key
|
|
7
|
+
* path. That binding means a sealed key blob cannot be silently
|
|
8
|
+
* relocated to a different deployment / path and unsealed there — the
|
|
9
|
+
* AEAD authentication fails. A legacy plain-sealed key (pre-binding)
|
|
10
|
+
* is transparently upgraded to the AAD-sealed format on next load.
|
|
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
|
+
|
|
21
|
+
var DB_KEY_PURPOSE = "blamejs/db-encryption-key/v1";
|
|
22
|
+
var SCHEMA = [{
|
|
23
|
+
name: "things",
|
|
24
|
+
columns: { _id: "TEXT PRIMARY KEY", v: "TEXT" },
|
|
25
|
+
}];
|
|
26
|
+
|
|
27
|
+
function dbKeyAad(tmp, keyPath) {
|
|
28
|
+
return b.vault.aad.buildContextAad({
|
|
29
|
+
purpose: DB_KEY_PURPOSE,
|
|
30
|
+
dataDir: path.resolve(tmp),
|
|
31
|
+
keyPath: path.resolve(keyPath),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function reinitDbOnly(tmp) {
|
|
36
|
+
helpers.setTestPassphraseEnv();
|
|
37
|
+
b.db._resetForTest();
|
|
38
|
+
await b.db.init({ dataDir: tmp, tmpDir: path.join(tmp, "tmpfs"), schema: SCHEMA });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function run() {
|
|
42
|
+
var tmp = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-dbkey-aad-"));
|
|
43
|
+
// setupTestDb resets + inits vault and db; vault stays initialized
|
|
44
|
+
// through every phase below (we only reset db between phases).
|
|
45
|
+
await setupTestDb(tmp, SCHEMA);
|
|
46
|
+
b.db.from("things").insertOne({ _id: "x", v: "secret" });
|
|
47
|
+
|
|
48
|
+
var keyPath = path.join(tmp, "db.key.enc");
|
|
49
|
+
var sealed = fs.readFileSync(keyPath, "utf8");
|
|
50
|
+
|
|
51
|
+
// ---- on-disk key is AAD-sealed ----
|
|
52
|
+
check("db.key.enc is AAD-sealed", b.vault.aad.isAadSealed(sealed) === true);
|
|
53
|
+
|
|
54
|
+
// ---- correct AAD unseals to a non-empty key ----
|
|
55
|
+
var b64 = b.vault.aad.unseal(sealed, dbKeyAad(tmp, keyPath));
|
|
56
|
+
check("correct AAD unseals db key to non-empty base64", typeof b64 === "string" && b64.length > 0);
|
|
57
|
+
|
|
58
|
+
// ---- binding: a different keyPath / dataDir fails to unseal ----
|
|
59
|
+
var bindKeyPathThrew = false;
|
|
60
|
+
try { b.vault.aad.unseal(sealed, dbKeyAad(tmp, path.join(tmp, "elsewhere.enc"))); }
|
|
61
|
+
catch (_e) { bindKeyPathThrew = true; }
|
|
62
|
+
check("db key won't unseal under a different keyPath", bindKeyPathThrew);
|
|
63
|
+
|
|
64
|
+
var bindDirThrew = false;
|
|
65
|
+
try {
|
|
66
|
+
b.vault.aad.unseal(sealed, b.vault.aad.buildContextAad({
|
|
67
|
+
purpose: DB_KEY_PURPOSE,
|
|
68
|
+
dataDir: path.resolve(tmp, "..", "some-other-deployment"),
|
|
69
|
+
keyPath: path.resolve(keyPath),
|
|
70
|
+
}));
|
|
71
|
+
} catch (_e) { bindDirThrew = true; }
|
|
72
|
+
check("db key won't unseal under a different dataDir", bindDirThrew);
|
|
73
|
+
|
|
74
|
+
// ---- round-trip: re-init at the same dir decrypts the row ----
|
|
75
|
+
b.db.close();
|
|
76
|
+
await reinitDbOnly(tmp);
|
|
77
|
+
check("re-init at same dir decrypts row through AAD-sealed key",
|
|
78
|
+
b.db.from("things").where({ _id: "x" }).first().v === "secret");
|
|
79
|
+
|
|
80
|
+
// ---- legacy plain-sealed key is upgraded to AAD on next load ----
|
|
81
|
+
b.db.close();
|
|
82
|
+
fs.writeFileSync(keyPath, b.vault.seal(b64), { mode: 0o600 });
|
|
83
|
+
check("legacy seed: db.key.enc is NOT AAD-sealed",
|
|
84
|
+
b.vault.aad.isAadSealed(fs.readFileSync(keyPath, "utf8")) === false);
|
|
85
|
+
await reinitDbOnly(tmp);
|
|
86
|
+
check("legacy plain-sealed key upgraded to AAD on load",
|
|
87
|
+
b.vault.aad.isAadSealed(fs.readFileSync(keyPath, "utf8")) === true);
|
|
88
|
+
check("row still decrypts after legacy key upgrade",
|
|
89
|
+
b.db.from("things").where({ _id: "x" }).first().v === "secret");
|
|
90
|
+
|
|
91
|
+
b.db.close();
|
|
92
|
+
b.audit._resetForTest();
|
|
93
|
+
b.db._resetForTest();
|
|
94
|
+
b.vault._resetForTest();
|
|
95
|
+
b.cluster._resetForTest();
|
|
96
|
+
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (_e) {}
|
|
97
|
+
|
|
98
|
+
console.log("OK — db-key AAD tests");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { run: run };
|
|
102
|
+
if (require.main === module) {
|
|
103
|
+
// Rethrow on failure so Node surfaces the error and exits non-zero,
|
|
104
|
+
// instead of logging the caught error object — a taint analyzer traces
|
|
105
|
+
// a logged error back to the test passphrase fixture (a non-secret
|
|
106
|
+
// constant) and raises a false clear-text-logging alert.
|
|
107
|
+
run().then(function () { process.exit(0); })
|
|
108
|
+
.catch(function (err) { process.exitCode = 1; throw err; });
|
|
109
|
+
}
|
|
@@ -168,6 +168,59 @@ async function run() {
|
|
|
168
168
|
await new Promise(function (r) { setImmediate(r); });
|
|
169
169
|
check("D-M2: 42501 emit completes without crashing", true);
|
|
170
170
|
|
|
171
|
+
// ---- attempted-relation extraction for auth-failure audits ----
|
|
172
|
+
// A rejected credential's audit row records WHICH relation it tried
|
|
173
|
+
// to reach, so triage can scope blast radius without the raw SQL log.
|
|
174
|
+
// _extractTargetRelation is the defensive parser behind that field.
|
|
175
|
+
var xr = b.externalDb._extractTargetRelation;
|
|
176
|
+
check("extractRelation: bare table after FROM",
|
|
177
|
+
xr("SELECT * FROM accounts WHERE id = $1") === "accounts");
|
|
178
|
+
check("extractRelation: INTO target",
|
|
179
|
+
xr("INSERT INTO audit_log (a) VALUES ($1)") === "audit_log");
|
|
180
|
+
check("extractRelation: UPDATE target",
|
|
181
|
+
xr("UPDATE users SET x = 1") === "users");
|
|
182
|
+
check("extractRelation: schema-qualified",
|
|
183
|
+
xr("SELECT * FROM public.secrets") === "public.secrets");
|
|
184
|
+
check("extractRelation: double-quoted identifier (quotes stripped)",
|
|
185
|
+
xr('SELECT * FROM "Order Items"') === "Order Items");
|
|
186
|
+
check("extractRelation: backtick-quoted identifier (ticks stripped)",
|
|
187
|
+
xr("SELECT * FROM `weird table`") === "weird table");
|
|
188
|
+
check("extractRelation: JOIN target picks first relation",
|
|
189
|
+
xr("SELECT * FROM a JOIN b ON a.id = b.id") === "a");
|
|
190
|
+
// Defensive: unparseable / control-char input returns null rather
|
|
191
|
+
// than leaking a partial fragment into the audit metadata.
|
|
192
|
+
check("extractRelation: no relation keyword returns null", xr("SELECT 1") === null);
|
|
193
|
+
check("extractRelation: non-SQL input returns null",
|
|
194
|
+
xr("just some words here") === null);
|
|
195
|
+
check("extractRelation: empty input returns null", xr("") === null);
|
|
196
|
+
|
|
197
|
+
// The audit hook stamps attemptedTable from the parser. Drive a
|
|
198
|
+
// rejection through the query path and confirm the field is present
|
|
199
|
+
// on the captured audit row.
|
|
200
|
+
var auditRows = [];
|
|
201
|
+
var origEmit = b.audit && b.audit.safeEmit;
|
|
202
|
+
if (origEmit) {
|
|
203
|
+
b.audit.safeEmit = function (rec) {
|
|
204
|
+
if (rec && rec.action === "db.auth.failed") auditRows.push(rec);
|
|
205
|
+
return origEmit.apply(b.audit, arguments);
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
var d6 = _instrumentingDriver({
|
|
210
|
+
failOnce: { match: /^SELECT \* FROM payroll/i, code: "28000", message: "auth failed" },
|
|
211
|
+
});
|
|
212
|
+
b.externalDb._resetForTest();
|
|
213
|
+
b.externalDb.init({ backends: { main: { connect: d6.connect, query: d6.query, close: d6.close } } });
|
|
214
|
+
try { await b.externalDb.query("SELECT * FROM payroll WHERE id = $1", [1]); } catch (_e) { /* expected */ }
|
|
215
|
+
await new Promise(function (r) { setImmediate(r); });
|
|
216
|
+
} finally {
|
|
217
|
+
if (origEmit) b.audit.safeEmit = origEmit;
|
|
218
|
+
}
|
|
219
|
+
var payrollRow = auditRows.filter(function (r) {
|
|
220
|
+
return r.metadata && r.metadata.attemptedTable === "payroll";
|
|
221
|
+
});
|
|
222
|
+
check("auth-failure audit carries attemptedTable", payrollRow.length >= 1);
|
|
223
|
+
|
|
171
224
|
b.externalDb._resetForTest();
|
|
172
225
|
}
|
|
173
226
|
|
|
@@ -25,5 +25,70 @@ var check = helpers.check;
|
|
|
25
25
|
try { await ir2.open({}); } catch (e) { threwBadSpec = e.code === "incident-report/bad-regime"; }
|
|
26
26
|
check("incident.open refuses bad spec", threwBadSpec);
|
|
27
27
|
|
|
28
|
+
// ---- createDeadlineClock — running-clock approaching/passed alerts ----
|
|
29
|
+
// Manual-tick mode (autoStart:false) keeps the test deterministic and
|
|
30
|
+
// sleep-free; thresholds fire as the injected "now" crosses fractions
|
|
31
|
+
// of each stage deadline.
|
|
32
|
+
check("createDeadlineClock is fn", typeof b.incident.report.createDeadlineClock === "function");
|
|
33
|
+
|
|
34
|
+
var sent = [];
|
|
35
|
+
var clk = b.incident.report.createDeadlineClock({
|
|
36
|
+
notify: { send: function (m) { sent.push(m); } },
|
|
37
|
+
autoStart: false,
|
|
38
|
+
approachThresholds: [0.5, 0.9],
|
|
39
|
+
});
|
|
40
|
+
// detectedAt=0; stage deadlines initial=100, intermediate=200, final=300.
|
|
41
|
+
clk.track({ id: "inc-1", detectedAt: 0, regime: "gdpr", dueBy: { initial: 100, intermediate: 200, final: 300 } });
|
|
42
|
+
check("deadline clock: tracked=1", clk.status().tracked === 1);
|
|
43
|
+
|
|
44
|
+
clk.tick(50); // 50% of initial → approaching@0.5
|
|
45
|
+
check("tick@50: one approaching@0.5 (initial)",
|
|
46
|
+
sent.length === 1 && sent[0].kind === "deadline_approaching" &&
|
|
47
|
+
sent[0].stage === "initial" && sent[0].threshold === 0.5);
|
|
48
|
+
|
|
49
|
+
clk.tick(55); // still <0.9 of initial → dedupe, no new alert
|
|
50
|
+
check("tick@55: dedupe (no new alert)", sent.length === 1);
|
|
51
|
+
|
|
52
|
+
clk.tick(95); // 95% of initial → approaching@0.9
|
|
53
|
+
check("tick@95: approaching@0.9 fires", sent.length === 2 && sent[1].threshold === 0.9);
|
|
54
|
+
|
|
55
|
+
clk.tick(150); // initial(100) passed
|
|
56
|
+
check("tick@150: initial passed fires once",
|
|
57
|
+
sent.filter(function (m) { return m.kind === "deadline_passed" && m.stage === "initial"; }).length === 1);
|
|
58
|
+
|
|
59
|
+
clk.tick(160); // no duplicate passed for initial
|
|
60
|
+
check("tick@160: no duplicate passed",
|
|
61
|
+
sent.filter(function (m) { return m.kind === "deadline_passed" && m.stage === "initial"; }).length === 1);
|
|
62
|
+
|
|
63
|
+
// Acknowledging a stage suppresses its further alerts.
|
|
64
|
+
clk.acknowledgeSubmission("inc-1", "intermediate");
|
|
65
|
+
clk.tick(250); // intermediate(200) would be passed, but acked → suppressed
|
|
66
|
+
check("acknowledged stage: no passed alert",
|
|
67
|
+
sent.filter(function (m) { return m.stage === "intermediate" && m.kind === "deadline_passed"; }).length === 0);
|
|
68
|
+
|
|
69
|
+
// Construction-time + track-time input validation.
|
|
70
|
+
var badThresh = false;
|
|
71
|
+
try { b.incident.report.createDeadlineClock({ approachThresholds: [1.5] }); }
|
|
72
|
+
catch (e) { badThresh = /between 0 and 1/.test(e.message); }
|
|
73
|
+
check("createDeadlineClock rejects threshold > 1", badThresh);
|
|
74
|
+
|
|
75
|
+
var badTrack = false;
|
|
76
|
+
try { clk.track({ id: "x" }); } catch (e) { badTrack = /dueBy/.test(e.message); }
|
|
77
|
+
check("deadline clock: track without dueBy rejected", badTrack);
|
|
78
|
+
|
|
79
|
+
// Faster regime crosses before slower one on the same tick
|
|
80
|
+
// (registry windows differ — DORA vs GDPR here).
|
|
81
|
+
var sent2 = [];
|
|
82
|
+
var clk2 = b.incident.report.createDeadlineClock({
|
|
83
|
+
notify: { send: function (m) { sent2.push(m); } }, autoStart: false,
|
|
84
|
+
});
|
|
85
|
+
clk2.track({ id: "dora-1", detectedAt: 0, regime: "dora", dueBy: { initial: 4, intermediate: 8, final: 12 } });
|
|
86
|
+
clk2.track({ id: "gdpr-1", detectedAt: 0, regime: "gdpr", dueBy: { initial: 72, intermediate: 100, final: 200 } });
|
|
87
|
+
clk2.tick(5); // DORA initial(4) passed; GDPR initial(72) only ~7% → nothing
|
|
88
|
+
check("deadline clock: faster regime crosses first",
|
|
89
|
+
sent2.filter(function (m) { return m.incidentId === "dora-1" && m.kind === "deadline_passed"; }).length === 1 &&
|
|
90
|
+
sent2.filter(function (m) { return m.incidentId === "gdpr-1"; }).length === 0);
|
|
91
|
+
clk2.stop();
|
|
92
|
+
|
|
28
93
|
console.log("OK — incident.report tests");
|
|
29
94
|
})().catch(function (e) { console.error(e); process.exit(1); });
|
package/package.json
CHANGED