@blamejs/core 0.6.13 → 0.6.21
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 +8 -0
- package/NOTICE +16 -0
- package/README.md +27 -18
- package/index.js +12 -0
- package/lib/archive.js +8 -7
- package/lib/audit.js +4 -0
- package/lib/auth/password.js +449 -4
- package/lib/bundler.js +8 -8
- package/lib/cache.js +105 -20
- package/lib/cli.js +598 -4
- package/lib/config-drift.js +309 -0
- package/lib/crypto-field.js +37 -0
- package/lib/crypto.js +8 -0
- package/lib/db-query.js +21 -2
- package/lib/db.js +32 -2
- package/lib/dual-control.js +475 -0
- package/lib/file-type.js +265 -0
- package/lib/framework-schema.js +38 -6
- package/lib/http-client-cookie-jar.js +117 -17
- package/lib/http-client.js +81 -3
- package/lib/internal-sha1-hibp.js +34 -0
- package/lib/mail.js +5 -4
- package/lib/middleware/csp-nonce.js +7 -4
- package/lib/middleware/index.js +2 -0
- package/lib/middleware/network-allowlist.js +199 -0
- package/lib/network-dns.js +564 -0
- package/lib/network-heartbeat.js +290 -0
- package/lib/network-nts.js +552 -0
- package/lib/network-proxy.js +246 -0
- package/lib/network-tls.js +326 -0
- package/lib/network.js +233 -0
- package/lib/ntp-check.js +50 -4
- package/lib/object-store/azure-blob.js +16 -42
- package/lib/pagination.js +136 -76
- package/lib/parsers/index.js +16 -2
- package/lib/parsers/safe-ini.js +273 -0
- package/lib/permissions.js +223 -9
- package/lib/pqc-agent.js +4 -4
- package/lib/retention.js +439 -0
- package/lib/security-assert.js +368 -0
- package/lib/session.js +138 -8
- package/lib/ssrf-guard.js +9 -0
- package/lib/vault/index.js +3 -3
- package/lib/vendor/MANIFEST.json +12 -0
- package/lib/vendor/common-passwords-top-10000.txt +10000 -0
- package/package.json +3 -2
- package/sbom.cyclonedx.json +61 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* config-drift — boot-time config-baseline capture + signed sidecar +
|
|
4
|
+
* next-boot drift detection.
|
|
5
|
+
*
|
|
6
|
+
* The framework's audit chain captures DATA writes (every row, every
|
|
7
|
+
* key change). It does NOT capture RUNTIME CONFIG (the operator
|
|
8
|
+
* silently changed `allowedOrigins` 3 weeks ago and we have no
|
|
9
|
+
* signal). config-drift fills that gap: at every boot, the operator
|
|
10
|
+
* passes the baseline config snapshot they want tracked. The
|
|
11
|
+
* primitive hashes it with SHA3-512, signs the digest with the audit-
|
|
12
|
+
* signing key, and writes the result to a sidecar at
|
|
13
|
+
* `<dataDir>/config-baseline.sig`. On the next boot, the sidecar is
|
|
14
|
+
* loaded + verified + diffed against the new snapshot. Drift surfaces
|
|
15
|
+
* as an audit event (`config.drift.detected`); no boot block — the
|
|
16
|
+
* operator may have a legitimate reason to change config and the
|
|
17
|
+
* framework's job is to make the change auditable, not to refuse to
|
|
18
|
+
* start.
|
|
19
|
+
*
|
|
20
|
+
* var configDrift = b.configDrift.create({
|
|
21
|
+
* dataDir: "/data",
|
|
22
|
+
* audit: b.audit,
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* await configDrift.checkpoint({
|
|
26
|
+
* // operator decides what's tracked. JSON-stringifiable.
|
|
27
|
+
* allowedOrigins: ["https://app.example.com"],
|
|
28
|
+
* csp: "default-src 'self'",
|
|
29
|
+
* auditMode: b.audit.getMode(),
|
|
30
|
+
* vaultMode: b.vault.getMode(),
|
|
31
|
+
* dbAtRest: b.db.getAtRestMode(),
|
|
32
|
+
* });
|
|
33
|
+
* // → { signed: true, drifted: false, previousAt: 1730000000000 }
|
|
34
|
+
*
|
|
35
|
+
* The signed sidecar uses b.auditSign — same SLH-DSA-SHAKE-256f keypair
|
|
36
|
+
* the audit chain anchors on. An attacker who flips a config value
|
|
37
|
+
* would also need to forge the signing key to update the sidecar
|
|
38
|
+
* cleanly; otherwise next-boot verify catches the tamper.
|
|
39
|
+
*
|
|
40
|
+
* Validation:
|
|
41
|
+
* - create() opts: throw at boot on bad shape
|
|
42
|
+
* - checkpoint() snapshot: must be a JSON-serialisable object
|
|
43
|
+
* - sidecar verify failure (tampered, key rotated, missing pubkey)
|
|
44
|
+
* surfaces as `config.baseline.tamper` audit event AND the call
|
|
45
|
+
* returns { tamper: true } so the operator can decide whether
|
|
46
|
+
* to refuse boot
|
|
47
|
+
*/
|
|
48
|
+
var fs = require("node:fs");
|
|
49
|
+
var path = require("node:path");
|
|
50
|
+
var auditSign = require("./audit-sign");
|
|
51
|
+
var crypto = require("./crypto");
|
|
52
|
+
var lazyRequire = require("./lazy-require");
|
|
53
|
+
var validateOpts = require("./validate-opts");
|
|
54
|
+
var { defineClass } = require("./framework-error");
|
|
55
|
+
|
|
56
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
57
|
+
|
|
58
|
+
var ConfigDriftError = defineClass("ConfigDriftError", { alwaysPermanent: true });
|
|
59
|
+
var _err = ConfigDriftError.factory;
|
|
60
|
+
|
|
61
|
+
var SIDECAR_NAME = "config-baseline.sig";
|
|
62
|
+
var SIDECAR_VERSION = 1;
|
|
63
|
+
|
|
64
|
+
// Stable JSON serialization: deterministic key order so the same
|
|
65
|
+
// snapshot always hashes to the same digest. Without this, an object
|
|
66
|
+
// reordered between boots would falsely flag as drift.
|
|
67
|
+
function _stableStringify(value) {
|
|
68
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
return "[" + value.map(_stableStringify).join(",") + "]";
|
|
71
|
+
}
|
|
72
|
+
var keys = Object.keys(value).sort();
|
|
73
|
+
var pairs = [];
|
|
74
|
+
for (var i = 0; i < keys.length; i++) {
|
|
75
|
+
pairs.push(JSON.stringify(keys[i]) + ":" + _stableStringify(value[keys[i]]));
|
|
76
|
+
}
|
|
77
|
+
return "{" + pairs.join(",") + "}";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function _hashSnapshot(snapshot) {
|
|
81
|
+
return crypto.sha3Hash(_stableStringify(snapshot));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function _diffShallow(prev, next) {
|
|
85
|
+
// For drift reporting only — names the keys that changed without
|
|
86
|
+
// dumping the full snapshot (which may carry sensitive values like
|
|
87
|
+
// a CSP nonce-derivation key). Operators reading the drift event
|
|
88
|
+
// get "these keys changed" + a hash on each side.
|
|
89
|
+
var changed = [];
|
|
90
|
+
var added = [];
|
|
91
|
+
var removed = [];
|
|
92
|
+
var allKeys = {};
|
|
93
|
+
Object.keys(prev || {}).forEach(function (k) { allKeys[k] = true; });
|
|
94
|
+
Object.keys(next || {}).forEach(function (k) { allKeys[k] = true; });
|
|
95
|
+
Object.keys(allKeys).forEach(function (k) {
|
|
96
|
+
var inPrev = Object.prototype.hasOwnProperty.call(prev || {}, k);
|
|
97
|
+
var inNext = Object.prototype.hasOwnProperty.call(next || {}, k);
|
|
98
|
+
if (inPrev && !inNext) { removed.push(k); return; }
|
|
99
|
+
if (!inPrev && inNext) { added.push(k); return; }
|
|
100
|
+
if (_stableStringify(prev[k]) !== _stableStringify(next[k])) changed.push(k);
|
|
101
|
+
});
|
|
102
|
+
return { changed: changed, added: added, removed: removed };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function create(opts) {
|
|
106
|
+
opts = opts || {};
|
|
107
|
+
validateOpts(opts, [
|
|
108
|
+
"dataDir", "audit", "baseline", "criticalKeys", "ignoreKeys",
|
|
109
|
+
], "configDrift");
|
|
110
|
+
if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
|
|
111
|
+
throw _err("BAD_OPT", "create: opts.dataDir is required");
|
|
112
|
+
}
|
|
113
|
+
var dataDir = opts.dataDir;
|
|
114
|
+
var auditOn = opts.audit !== false;
|
|
115
|
+
var auditInstance = (opts.audit && opts.audit !== true) ? opts.audit : null;
|
|
116
|
+
// Multi-baseline support — each operator-named baseline lives in its
|
|
117
|
+
// own sidecar so production / staging / disaster-recovery deploys
|
|
118
|
+
// each track their own drift independently.
|
|
119
|
+
var baselineName = (typeof opts.baseline === "string" && opts.baseline.length > 0)
|
|
120
|
+
? opts.baseline : "default";
|
|
121
|
+
// Critical-keys allowlist: drift in these keys raises severity to
|
|
122
|
+
// "high" in the audit emission so SIEM rules can page on them
|
|
123
|
+
// separately from cosmetic drift. null = every key is treated as
|
|
124
|
+
// high severity (the safer default).
|
|
125
|
+
var criticalKeys = Array.isArray(opts.criticalKeys) ? opts.criticalKeys.slice() : null;
|
|
126
|
+
// Ignore-keys: drift in these keys is excluded from drift detection
|
|
127
|
+
// (e.g. operator-tracked metadata that legitimately changes per
|
|
128
|
+
// boot). Captured in the snapshot but never flagged.
|
|
129
|
+
var ignoreKeys = Array.isArray(opts.ignoreKeys) ? opts.ignoreKeys.slice() : [];
|
|
130
|
+
var sidecarPath = path.join(dataDir,
|
|
131
|
+
baselineName === "default" ? SIDECAR_NAME : ("config-baseline-" + baselineName + ".sig"));
|
|
132
|
+
|
|
133
|
+
function _emit(action, info, outcome) {
|
|
134
|
+
if (!auditOn) return;
|
|
135
|
+
var sink = auditInstance || audit();
|
|
136
|
+
try {
|
|
137
|
+
sink.safeEmit({
|
|
138
|
+
action: action,
|
|
139
|
+
outcome: outcome,
|
|
140
|
+
metadata: info || {},
|
|
141
|
+
reason: info && info.reason ? info.reason : null,
|
|
142
|
+
});
|
|
143
|
+
} catch (_e) { /* audit best-effort */ }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _readSidecar() {
|
|
147
|
+
if (!fs.existsSync(sidecarPath)) return null;
|
|
148
|
+
var raw;
|
|
149
|
+
try { raw = fs.readFileSync(sidecarPath, "utf8"); }
|
|
150
|
+
catch (_e) { return null; }
|
|
151
|
+
var parsed;
|
|
152
|
+
try { parsed = JSON.parse(raw); }
|
|
153
|
+
catch (_e) { return { unreadable: true }; }
|
|
154
|
+
if (!parsed || parsed.version !== SIDECAR_VERSION) return { unreadable: true };
|
|
155
|
+
if (typeof parsed.digestHex !== "string" || typeof parsed.signatureBase64 !== "string" ||
|
|
156
|
+
typeof parsed.publicKeyPem !== "string" || typeof parsed.snapshot !== "object") {
|
|
157
|
+
return { unreadable: true };
|
|
158
|
+
}
|
|
159
|
+
return parsed;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function _writeSidecar(snapshot, digestHex) {
|
|
163
|
+
// Sign over the digest (not the snapshot bytes directly) so the
|
|
164
|
+
// sidecar stays small even when the snapshot is large.
|
|
165
|
+
var signature = auditSign.sign(digestHex);
|
|
166
|
+
var payload = {
|
|
167
|
+
version: SIDECAR_VERSION,
|
|
168
|
+
capturedAt: Date.now(),
|
|
169
|
+
digestHex: digestHex,
|
|
170
|
+
signatureBase64: Buffer.from(signature).toString("base64"),
|
|
171
|
+
publicKeyPem: auditSign.getPublicKey(),
|
|
172
|
+
snapshot: snapshot,
|
|
173
|
+
};
|
|
174
|
+
var tmp = sidecarPath + ".tmp";
|
|
175
|
+
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2));
|
|
176
|
+
fs.renameSync(tmp, sidecarPath);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function _verifySidecar(parsed) {
|
|
180
|
+
// Verify the recorded signature against the recorded public key
|
|
181
|
+
// first — this catches tampering with the digest+sig pair on its
|
|
182
|
+
// own. Then verify the digest matches a re-hash of the recorded
|
|
183
|
+
// snapshot — catches tampering with the snapshot field.
|
|
184
|
+
var sigBuf = Buffer.from(parsed.signatureBase64, "base64");
|
|
185
|
+
if (!auditSign.verify(parsed.digestHex, sigBuf, parsed.publicKeyPem)) {
|
|
186
|
+
return { ok: false, reason: "signature-invalid" };
|
|
187
|
+
}
|
|
188
|
+
if (_hashSnapshot(parsed.snapshot) !== parsed.digestHex) {
|
|
189
|
+
return { ok: false, reason: "digest-mismatch" };
|
|
190
|
+
}
|
|
191
|
+
return { ok: true };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function checkpoint(snapshot) {
|
|
195
|
+
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
|
|
196
|
+
throw _err("BAD_OPT", "checkpoint: snapshot must be a plain object");
|
|
197
|
+
}
|
|
198
|
+
var newDigest = _hashSnapshot(snapshot);
|
|
199
|
+
|
|
200
|
+
var existing = _readSidecar();
|
|
201
|
+
if (existing && existing.unreadable) {
|
|
202
|
+
_emit("config.baseline.unreadable",
|
|
203
|
+
{ sidecar: sidecarPath, reason: "sidecar present but malformed or wrong version" },
|
|
204
|
+
"warning");
|
|
205
|
+
_writeSidecar(snapshot, newDigest);
|
|
206
|
+
return { signed: true, drifted: false, tamper: false, previousAt: null, reason: "sidecar-unreadable-rewritten" };
|
|
207
|
+
}
|
|
208
|
+
if (!existing) {
|
|
209
|
+
_writeSidecar(snapshot, newDigest);
|
|
210
|
+
_emit("config.baseline.captured",
|
|
211
|
+
{ digestHex: newDigest, capturedAt: Date.now() }, "success");
|
|
212
|
+
return { signed: true, drifted: false, tamper: false, previousAt: null };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
var verified = _verifySidecar(existing);
|
|
216
|
+
if (!verified.ok) {
|
|
217
|
+
_emit("config.baseline.tamper",
|
|
218
|
+
{ sidecar: sidecarPath, reason: verified.reason, previousAt: existing.capturedAt },
|
|
219
|
+
"failure");
|
|
220
|
+
// DO NOT auto-rewrite on tamper — let the operator inspect first.
|
|
221
|
+
return { signed: false, drifted: false, tamper: true, reason: verified.reason, previousAt: existing.capturedAt };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (existing.digestHex === newDigest) {
|
|
225
|
+
// No drift; refresh capturedAt so the sidecar timestamp moves
|
|
226
|
+
// forward each successful boot (operator can see "last verified
|
|
227
|
+
// clean at <T>" in the file).
|
|
228
|
+
_writeSidecar(snapshot, newDigest);
|
|
229
|
+
return { signed: true, drifted: false, tamper: false, previousAt: existing.capturedAt };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
var diff = _diffShallow(existing.snapshot, snapshot);
|
|
233
|
+
// Filter ignore-keys out of the drift report.
|
|
234
|
+
function _stripIgnored(arr) {
|
|
235
|
+
return arr.filter(function (k) { return ignoreKeys.indexOf(k) === -1; });
|
|
236
|
+
}
|
|
237
|
+
diff.changed = _stripIgnored(diff.changed);
|
|
238
|
+
diff.added = _stripIgnored(diff.added);
|
|
239
|
+
diff.removed = _stripIgnored(diff.removed);
|
|
240
|
+
if (diff.changed.length === 0 && diff.added.length === 0 && diff.removed.length === 0) {
|
|
241
|
+
// All drift was in ignore-keys — refresh sidecar without
|
|
242
|
+
// emitting a drift event.
|
|
243
|
+
_writeSidecar(snapshot, newDigest);
|
|
244
|
+
return { signed: true, drifted: false, tamper: false,
|
|
245
|
+
previousAt: existing.capturedAt, ignoredOnly: true };
|
|
246
|
+
}
|
|
247
|
+
// Severity classification: HIGH when any drifted key is in
|
|
248
|
+
// criticalKeys (or no allowlist is configured — every key is
|
|
249
|
+
// critical by default). LOW when criticalKeys is set and none
|
|
250
|
+
// of the drifted keys are in it.
|
|
251
|
+
var severity = "high";
|
|
252
|
+
if (criticalKeys !== null) {
|
|
253
|
+
var anyCritical = false;
|
|
254
|
+
var allDrifted = diff.changed.concat(diff.added, diff.removed);
|
|
255
|
+
for (var di = 0; di < allDrifted.length; di++) {
|
|
256
|
+
if (criticalKeys.indexOf(allDrifted[di]) !== -1) { anyCritical = true; break; }
|
|
257
|
+
}
|
|
258
|
+
severity = anyCritical ? "high" : "low";
|
|
259
|
+
}
|
|
260
|
+
_emit("config.drift.detected",
|
|
261
|
+
{
|
|
262
|
+
baseline: baselineName,
|
|
263
|
+
previousDigestHex: existing.digestHex,
|
|
264
|
+
currentDigestHex: newDigest,
|
|
265
|
+
previousAt: existing.capturedAt,
|
|
266
|
+
keysChanged: diff.changed,
|
|
267
|
+
keysAdded: diff.added,
|
|
268
|
+
keysRemoved: diff.removed,
|
|
269
|
+
severity: severity,
|
|
270
|
+
},
|
|
271
|
+
severity === "high" ? "failure" : "warning");
|
|
272
|
+
_writeSidecar(snapshot, newDigest);
|
|
273
|
+
return {
|
|
274
|
+
signed: true,
|
|
275
|
+
drifted: true,
|
|
276
|
+
tamper: false,
|
|
277
|
+
severity: severity,
|
|
278
|
+
previousAt: existing.capturedAt,
|
|
279
|
+
diff: diff,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function read() {
|
|
284
|
+
var existing = _readSidecar();
|
|
285
|
+
if (!existing || existing.unreadable) return null;
|
|
286
|
+
var verified = _verifySidecar(existing);
|
|
287
|
+
return {
|
|
288
|
+
capturedAt: existing.capturedAt,
|
|
289
|
+
digestHex: existing.digestHex,
|
|
290
|
+
snapshot: existing.snapshot,
|
|
291
|
+
verified: verified.ok,
|
|
292
|
+
tamperReason: verified.ok ? null : verified.reason,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
checkpoint: checkpoint,
|
|
298
|
+
read: read,
|
|
299
|
+
sidecarPath: sidecarPath,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports = {
|
|
304
|
+
create: create,
|
|
305
|
+
ConfigDriftError: ConfigDriftError,
|
|
306
|
+
// Test-only export for hashing — operators don't need this directly.
|
|
307
|
+
_hashSnapshot: _hashSnapshot,
|
|
308
|
+
_stableStringify: _stableStringify,
|
|
309
|
+
};
|
package/lib/crypto-field.js
CHANGED
|
@@ -126,6 +126,42 @@ function unsealRow(table, row) {
|
|
|
126
126
|
return out;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// ---- Erasure (GDPR Art. 17 / "right to be forgotten") ----
|
|
130
|
+
//
|
|
131
|
+
// eraseRow(table, row) returns a tombstoned copy of the row: every
|
|
132
|
+
// sealed column is replaced with NULL, every derived hash column
|
|
133
|
+
// (computed from a sealed source) is replaced with NULL, and a
|
|
134
|
+
// `__erasedAt` field is added carrying the erasure timestamp. The
|
|
135
|
+
// row itself stays in the table (referential integrity), but the
|
|
136
|
+
// sealed cleartext is unrecoverable — even with the vault key, NULL
|
|
137
|
+
// decrypts to NULL.
|
|
138
|
+
//
|
|
139
|
+
// Callers that need the row removed entirely should DELETE; eraseRow
|
|
140
|
+
// is for the case where downstream FKs / audit references make
|
|
141
|
+
// outright deletion infeasible.
|
|
142
|
+
function eraseRow(table, row) {
|
|
143
|
+
if (!row) return row;
|
|
144
|
+
var s = schemas[table];
|
|
145
|
+
if (!s) return row;
|
|
146
|
+
var out = Object.assign({}, row);
|
|
147
|
+
// Erase sealed columns — set to null. After this, unsealRow on the
|
|
148
|
+
// erased row returns null for these columns; no key recovers them
|
|
149
|
+
// because there's no ciphertext to decrypt.
|
|
150
|
+
for (var i = 0; i < s.sealedFields.length; i++) {
|
|
151
|
+
out[s.sealedFields[i]] = null;
|
|
152
|
+
}
|
|
153
|
+
// Erase derived hashes — they're indexed lookup mirrors of sealed
|
|
154
|
+
// sources and would otherwise let an attacker reverse the cleartext
|
|
155
|
+
// via dictionary enumeration of the hash.
|
|
156
|
+
if (s.derivedHashes) {
|
|
157
|
+
for (var derivedField in s.derivedHashes) {
|
|
158
|
+
out[derivedField] = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
out.__erasedAt = Date.now();
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
129
165
|
// ---- Lookup translation ----
|
|
130
166
|
|
|
131
167
|
// where({ email: 'x' }) → where({ emailHash: hash(...) }).
|
|
@@ -153,6 +189,7 @@ module.exports = {
|
|
|
153
189
|
getSealedFields: getSealedFields,
|
|
154
190
|
sealRow: sealRow,
|
|
155
191
|
unsealRow: unsealRow,
|
|
192
|
+
eraseRow: eraseRow,
|
|
156
193
|
computeDerived: computeDerived,
|
|
157
194
|
lookupHash: lookupHash,
|
|
158
195
|
clearForTest: clearForTest,
|
package/lib/crypto.js
CHANGED
|
@@ -80,6 +80,14 @@ function timingSafeEqual(a, b) {
|
|
|
80
80
|
function sha3Hash(data) { return hash(data, "sha3-512").toString("hex"); }
|
|
81
81
|
function hmacSha3(key, data) { return hmac(key, data, "sha3-512"); }
|
|
82
82
|
|
|
83
|
+
// (SHA-1 is intentionally NOT exported from b.crypto. The framework's
|
|
84
|
+
// only legitimate SHA-1 use is the HaveIBeenPwned k-anonymity API in
|
|
85
|
+
// lib/auth/password.js, which imports lib/internal-sha1-hibp.js
|
|
86
|
+
// directly. Public b.crypto.sha1* is permanently off the table — a
|
|
87
|
+
// future caller wanting SHA-1 for storage / signing / fingerprinting
|
|
88
|
+
// would re-introduce a broken primitive into the crypto surface this
|
|
89
|
+
// framework spent every other line keeping out.)
|
|
90
|
+
|
|
83
91
|
// ---- KDF ----
|
|
84
92
|
function kdf(input, outputLength) { return hash(input, "shake256", outputLength); }
|
|
85
93
|
|
package/lib/db-query.js
CHANGED
|
@@ -148,7 +148,21 @@ class Query {
|
|
|
148
148
|
if (direction !== "asc" && direction !== "desc") {
|
|
149
149
|
throw new Error("orderBy direction must be 'asc' or 'desc'");
|
|
150
150
|
}
|
|
151
|
-
|
|
151
|
+
var entry = { field: field, direction: direction.toUpperCase() };
|
|
152
|
+
if (this._orderBy === null) {
|
|
153
|
+
// First call — keep the back-compat single-object shape so any
|
|
154
|
+
// legacy reader that does `query._orderBy.field` keeps working.
|
|
155
|
+
this._orderBy = entry;
|
|
156
|
+
return this;
|
|
157
|
+
}
|
|
158
|
+
// Second-or-later call — promote to an array. Multi-column ORDER BY
|
|
159
|
+
// is the keyset-pagination tiebreaker pattern: ORDER BY createdAt
|
|
160
|
+
// DESC, _id DESC means same-second rows still have a total order.
|
|
161
|
+
if (Array.isArray(this._orderBy)) {
|
|
162
|
+
this._orderBy.push(entry);
|
|
163
|
+
} else {
|
|
164
|
+
this._orderBy = [this._orderBy, entry];
|
|
165
|
+
}
|
|
152
166
|
return this;
|
|
153
167
|
}
|
|
154
168
|
|
|
@@ -173,7 +187,12 @@ class Query {
|
|
|
173
187
|
_orderLimitOffset() {
|
|
174
188
|
var s = "";
|
|
175
189
|
if (this._orderBy) {
|
|
176
|
-
|
|
190
|
+
var entries = Array.isArray(this._orderBy) ? this._orderBy : [this._orderBy];
|
|
191
|
+
var fragments = [];
|
|
192
|
+
for (var i = 0; i < entries.length; i++) {
|
|
193
|
+
fragments.push('"' + entries[i].field + '" ' + entries[i].direction);
|
|
194
|
+
}
|
|
195
|
+
s += " ORDER BY " + fragments.join(", ");
|
|
177
196
|
}
|
|
178
197
|
if (this._limit !== null) s += " LIMIT " + this._limit;
|
|
179
198
|
if (this._offset !== null) s += " OFFSET " + this._offset;
|
package/lib/db.js
CHANGED
|
@@ -399,6 +399,21 @@ var FRAMEWORK_SCHEMA = [
|
|
|
399
399
|
indexes: ["expiresAt"],
|
|
400
400
|
sealedFields: [],
|
|
401
401
|
},
|
|
402
|
+
{
|
|
403
|
+
// _blamejs_cache_tags — junction table for tag→cacheKey lookup
|
|
404
|
+
// backing b.cache.invalidateTag(t) on the cluster backend. Composite
|
|
405
|
+
// PK (cacheKey, tag) lets one cacheKey carry many tags; index on
|
|
406
|
+
// tag makes invalidation a single indexed scan. Cleared together
|
|
407
|
+
// with the matching _blamejs_cache rows on del / clear / sweep.
|
|
408
|
+
name: "_blamejs_cache_tags",
|
|
409
|
+
columns: {
|
|
410
|
+
cacheKey: "TEXT NOT NULL",
|
|
411
|
+
tag: "TEXT NOT NULL",
|
|
412
|
+
},
|
|
413
|
+
primaryKey: ["cacheKey", "tag"],
|
|
414
|
+
indexes: ["tag"],
|
|
415
|
+
sealedFields: [],
|
|
416
|
+
},
|
|
402
417
|
{
|
|
403
418
|
// _blamejs_seeders — registry of applied seed files for the
|
|
404
419
|
// b.seeders primitive (lib/seeders.js). Composite PK (env, name)
|
|
@@ -1037,11 +1052,26 @@ async function _runNtpBootCheck(opts) {
|
|
|
1037
1052
|
try { ntpCheck = require("./ntp-check"); }
|
|
1038
1053
|
catch (_e) { return; /* module not present — skip silently */ }
|
|
1039
1054
|
|
|
1055
|
+
var envServersRaw = safeEnv.readVar("BLAMEJS_NTP_SERVERS", { default: "" });
|
|
1056
|
+
var envTimeout = safeEnv.readVar("BLAMEJS_NTP_TIMEOUT_MS", { default: "" });
|
|
1057
|
+
var envWarn = safeEnv.readVar("BLAMEJS_NTP_DRIFT_WARN_MS", { default: "" });
|
|
1058
|
+
var envFatal = safeEnv.readVar("BLAMEJS_NTP_DRIFT_FATAL_MS", { default: "" });
|
|
1059
|
+
var resolvedServers = (opts && opts.ntpServers) ||
|
|
1060
|
+
(envServersRaw ? envServersRaw.split(",").map(function (s) { return s.trim(); }).filter(Boolean) : undefined);
|
|
1061
|
+
var resolvedTimeout = (opts && opts.ntpTimeoutMs) ||
|
|
1062
|
+
(envTimeout ? parseInt(envTimeout, 10) : undefined);
|
|
1063
|
+
if (envWarn || envFatal) {
|
|
1064
|
+
var thr = {};
|
|
1065
|
+
if (envWarn) thr.warnMs = parseInt(envWarn, 10);
|
|
1066
|
+
if (envFatal) thr.fatalMs = parseInt(envFatal, 10);
|
|
1067
|
+
try { ntpCheck.setThresholds(thr); } catch (_e) {}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1040
1070
|
var result;
|
|
1041
1071
|
try {
|
|
1042
1072
|
result = await ntpCheck.bootCheck({
|
|
1043
|
-
servers:
|
|
1044
|
-
timeoutMs:
|
|
1073
|
+
servers: resolvedServers,
|
|
1074
|
+
timeoutMs: resolvedTimeout,
|
|
1045
1075
|
});
|
|
1046
1076
|
} catch (e) {
|
|
1047
1077
|
log.error("ntp boot check threw unexpectedly: " + e.message + " (continuing)");
|