@blamejs/core 0.6.70 → 0.7.0
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/README.md +11 -1
- package/index.js +2 -5
- package/lib/api-key.js +24 -55
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +20 -16
- package/lib/app.js +6 -5
- package/lib/archive.js +58 -50
- package/lib/atomic-file.js +11 -5
- package/lib/audit-chain.js +12 -4
- package/lib/audit-sign.js +38 -13
- package/lib/audit-tools.js +4 -4
- package/lib/audit.js +8 -4
- package/lib/auth/jwt.js +3 -2
- package/lib/auth/lockout.js +3 -13
- package/lib/auth/oauth.js +27 -17
- package/lib/auth/passkey.js +1 -5
- package/lib/auth/password.js +23 -13
- package/lib/backup/bundle.js +2 -4
- package/lib/backup/crypto.js +8 -6
- package/lib/backup/index.js +18 -14
- package/lib/backup/manifest.js +6 -2
- package/lib/break-glass.js +12 -7
- package/lib/bundler.js +32 -21
- package/lib/cache-redis.js +8 -3
- package/lib/cache.js +39 -59
- package/lib/canonical-json.js +17 -2
- package/lib/chain-writer.js +2 -2
- package/lib/cli-helpers.js +9 -4
- package/lib/cli.js +27 -5
- package/lib/cluster-provider-db.js +2 -1
- package/lib/cluster-storage.js +71 -11
- package/lib/cluster.js +30 -25
- package/lib/config-drift.js +3 -4
- package/lib/constants.js +8 -3
- package/lib/cookies.js +16 -4
- package/lib/credential-hash.js +4 -7
- package/lib/crypto.js +9 -9
- package/lib/csv.js +11 -8
- package/lib/db-declare-row-policy.js +4 -9
- package/lib/db-declare-view.js +3 -6
- package/lib/db-query.js +9 -3
- package/lib/db-schema.js +75 -10
- package/lib/db.js +116 -70
- package/lib/deprecate.js +3 -8
- package/lib/dev.js +30 -19
- package/lib/dual-control.js +2 -4
- package/lib/error-page.js +32 -39
- package/lib/external-db-migrate.js +23 -26
- package/lib/external-db.js +11 -3
- package/lib/file-type.js +16 -8
- package/lib/forms.js +48 -12
- package/lib/framework-error.js +1 -2
- package/lib/{internal-sha1-hibp.js → framework-sha1-hibp.js} +1 -1
- package/lib/handlers.js +1 -1
- package/lib/http-client-cookie-jar.js +25 -18
- package/lib/http-client.js +46 -23
- package/lib/i18n.js +6 -16
- package/lib/jobs.js +5 -1
- package/lib/log-stream-cloudwatch.js +31 -30
- package/lib/log-stream-local.js +12 -3
- package/lib/log-stream-otlp-grpc.js +29 -23
- package/lib/log-stream-otlp.js +13 -19
- package/lib/log-stream-syslog.js +27 -22
- package/lib/log-stream-webhook.js +8 -17
- package/lib/log-stream.js +13 -2
- package/lib/log.js +83 -5
- package/lib/mail-bounce.js +22 -15
- package/lib/mail-dkim.js +3 -6
- package/lib/mail.js +62 -34
- package/lib/metrics.js +61 -25
- package/lib/middleware/api-encrypt.js +15 -13
- package/lib/middleware/body-parser.js +46 -39
- package/lib/middleware/bot-guard.js +25 -4
- package/lib/middleware/compression.js +26 -11
- package/lib/middleware/cors.js +6 -5
- package/lib/middleware/csp-nonce.js +4 -3
- package/lib/middleware/csrf-protect.js +1 -1
- package/lib/middleware/db-role-for.js +7 -30
- package/lib/middleware/health.js +42 -8
- package/lib/middleware/index.js +55 -36
- package/lib/middleware/rate-limit.js +11 -8
- package/lib/middleware/request-id.js +10 -2
- package/lib/middleware/request-log.js +3 -2
- package/lib/middleware/require-auth.js +5 -2
- package/lib/middleware/sse.js +5 -4
- package/lib/migration-files.js +37 -0
- package/lib/migrations.js +26 -15
- package/lib/mtls-ca.js +38 -17
- package/lib/mtls-engine-default.js +15 -7
- package/lib/network-dns.js +49 -20
- package/lib/network-heartbeat.js +7 -7
- package/lib/network-nts.js +70 -48
- package/lib/network-proxy.js +34 -15
- package/lib/network-tls.js +7 -5
- package/lib/network.js +3 -3
- package/lib/notify.js +14 -36
- package/lib/ntp-check.js +7 -6
- package/lib/numeric-bounds.js +35 -3
- package/lib/object-store/azure-blob-bucket-ops.js +73 -15
- package/lib/object-store/azure-blob.js +31 -24
- package/lib/object-store/gcs-bucket-ops.js +41 -17
- package/lib/object-store/gcs.js +41 -34
- package/lib/object-store/http-put.js +2 -11
- package/lib/object-store/http-request.js +38 -0
- package/lib/object-store/index.js +1 -1
- package/lib/object-store/sigv4-bucket-ops.js +33 -37
- package/lib/object-store/sigv4.js +50 -30
- package/lib/observability.js +14 -3
- package/lib/otel-export.js +16 -14
- package/lib/pagination.js +23 -8
- package/lib/parsers/index.js +11 -5
- package/lib/parsers/safe-env.js +43 -14
- package/lib/parsers/safe-ini.js +29 -10
- package/lib/parsers/safe-toml.js +55 -25
- package/lib/parsers/safe-xml.js +22 -11
- package/lib/parsers/safe-yaml.js +55 -17
- package/lib/permissions.js +24 -33
- package/lib/pqc-agent.js +4 -2
- package/lib/pqc-gate.js +28 -15
- package/lib/protobuf-encoder.js +9 -3
- package/lib/protocol-dispatcher.js +29 -12
- package/lib/pubsub-redis.js +25 -8
- package/lib/pubsub.js +84 -12
- package/lib/queue-local.js +13 -4
- package/lib/queue-redis.js +2 -2
- package/lib/queue-sqs.js +21 -16
- package/lib/queue.js +8 -4
- package/lib/redis-client.js +13 -10
- package/lib/request-helpers.js +33 -2
- package/lib/restore-bundle.js +2 -4
- package/lib/restore-rollback.js +4 -1
- package/lib/restore.js +12 -10
- package/lib/retry.js +58 -9
- package/lib/router.js +96 -27
- package/lib/safe-async.js +97 -4
- package/lib/safe-buffer.js +47 -0
- package/lib/safe-json.js +18 -9
- package/lib/safe-schema.js +54 -18
- package/lib/safe-url.js +6 -2
- package/lib/scheduler.js +25 -14
- package/lib/security-assert.js +31 -26
- package/lib/seeders.js +71 -60
- package/lib/session.js +13 -7
- package/lib/slug.js +35 -8
- package/lib/ssrf-guard.js +58 -43
- package/lib/static.js +11 -7
- package/lib/storage.js +2 -1
- package/lib/template.js +17 -7
- package/lib/testing.js +28 -8
- package/lib/time.js +26 -5
- package/lib/totp.js +18 -9
- package/lib/tracing.js +15 -5
- package/lib/uuid.js +47 -20
- package/lib/validate-opts.js +213 -0
- package/lib/vault/index.js +36 -27
- package/lib/vault/rotate.js +41 -22
- package/lib/vault/wrap.js +26 -19
- package/lib/webhook.js +37 -68
- package/lib/websocket.js +60 -46
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
- package/lib/auth/totp.js +0 -17
- package/lib/object-store/retry.js +0 -15
package/lib/backup/manifest.js
CHANGED
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
*/
|
|
70
70
|
|
|
71
71
|
var C = require("../constants");
|
|
72
|
+
var safeBuffer = require("../safe-buffer");
|
|
72
73
|
var safeJson = require("../safe-json");
|
|
73
74
|
var { FrameworkError } = require("../framework-error");
|
|
74
75
|
|
|
@@ -84,7 +85,10 @@ class BackupManifestError extends FrameworkError {
|
|
|
84
85
|
var FORMAT_VERSION = 1;
|
|
85
86
|
var FRAMEWORK_NAME = "blamejs";
|
|
86
87
|
var VALID_KINDS = { "raw": 1, "vault-sealed": 1, "plaintext": 1 };
|
|
87
|
-
|
|
88
|
+
// SHA3-512 produces 64 raw bytes — each byte serializes as 2 hex chars
|
|
89
|
+
// in the manifest, so the hex string is 128 chars long.
|
|
90
|
+
var SHA3_512_HEX_LENGTH = 128;
|
|
91
|
+
var HEX_RE = safeBuffer.HEX_RE;
|
|
88
92
|
var BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
89
93
|
|
|
90
94
|
function _isHex(s, evenLength) {
|
|
@@ -124,7 +128,7 @@ function _validateFileEntry(f, idx, errors) {
|
|
|
124
128
|
if (typeof f.encryptedSize !== "number" || !Number.isInteger(f.encryptedSize) || f.encryptedSize < 0) {
|
|
125
129
|
errors.push("files[" + idx + "].encryptedSize: required non-negative integer");
|
|
126
130
|
}
|
|
127
|
-
if (!_isHex(f.checksum, true) || f.checksum.length !==
|
|
131
|
+
if (!_isHex(f.checksum, true) || f.checksum.length !== SHA3_512_HEX_LENGTH) {
|
|
128
132
|
errors.push("files[" + idx + "].checksum: required 128-char hex string (sha3-512)");
|
|
129
133
|
}
|
|
130
134
|
if (!_isHex(f.salt, true)) {
|
package/lib/break-glass.js
CHANGED
|
@@ -66,6 +66,14 @@ var BreakGlassError = defineClass("BreakGlassError", { alwaysPermanent: false })
|
|
|
66
66
|
|
|
67
67
|
// ---- Defaults (matched to operator-locked decisions) ----
|
|
68
68
|
|
|
69
|
+
// Symmetric byte counts: 32-byte cell-key (kdf output, XChaCha20 key
|
|
70
|
+
// length); 32-byte DEK (AEAD key); 16-byte grant-id randomness (128-bit
|
|
71
|
+
// uniqueness for cross-table grant lookups). All routed through C.BYTES
|
|
72
|
+
// so the framework's byte math has a single source of truth.
|
|
73
|
+
var CELL_KEY_BYTES = C.BYTES.bytes(32);
|
|
74
|
+
var DEK_BYTES = C.BYTES.bytes(32);
|
|
75
|
+
var GRANT_ID_BYTES = C.BYTES.bytes(16);
|
|
76
|
+
|
|
69
77
|
var DEFAULT_GRANT_TTL_MS = C.TIME.minutes(15);
|
|
70
78
|
var DEFAULT_MAX_ROWS = 1; // operator-locked: row-by-row auth
|
|
71
79
|
var DEFAULT_REASON_MIN_LEN = 12;
|
|
@@ -137,7 +145,7 @@ function _kCell(dek, table, rowId, column) {
|
|
|
137
145
|
return kdf(Buffer.concat([
|
|
138
146
|
Buffer.isBuffer(dek) ? dek : Buffer.from(dek, "base64"),
|
|
139
147
|
Buffer.from("breakglass.cell|" + table + "|" + String(rowId) + "|" + column, "utf8"),
|
|
140
|
-
]),
|
|
148
|
+
]), CELL_KEY_BYTES);
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
async function _ensureDek(table) {
|
|
@@ -158,7 +166,7 @@ async function _ensureDek(table) {
|
|
|
158
166
|
if (sealed) {
|
|
159
167
|
dek = Buffer.from(vault().unseal(sealed), "base64");
|
|
160
168
|
} else {
|
|
161
|
-
dek = generateBytes(
|
|
169
|
+
dek = generateBytes(DEK_BYTES);
|
|
162
170
|
var sealedDek = vault().seal(dek.toString("base64"));
|
|
163
171
|
await clusterStorage.execute(
|
|
164
172
|
"UPDATE _blamejs_break_glass_policies SET dekSealed = ? WHERE tableName = ?",
|
|
@@ -415,10 +423,7 @@ function _validatePolicySet(table, opts) {
|
|
|
415
423
|
// CLI to convert existing rows). At unseal time, the row's
|
|
416
424
|
// glass-locked columns are decrypted via decryptCell with encryption
|
|
417
425
|
// context binding (table, rowId, column).
|
|
418
|
-
|
|
419
|
-
throw new BreakGlassError("breakglass/bad-policy",
|
|
420
|
-
"policy.set: cryptographic must be a boolean");
|
|
421
|
-
}
|
|
426
|
+
validateOpts.optionalBoolean(opts.cryptographic, "policy.set: cryptographic", BreakGlassError, "breakglass/bad-policy");
|
|
422
427
|
var grantTtl = opts.grantTtl != null ? opts.grantTtl : DEFAULT_GRANT_TTL_MS;
|
|
423
428
|
if (typeof grantTtl !== "number" || !isFinite(grantTtl) || grantTtl <= 0) {
|
|
424
429
|
throw new BreakGlassError("breakglass/bad-policy",
|
|
@@ -733,7 +738,7 @@ async function grant(opts) {
|
|
|
733
738
|
|
|
734
739
|
// Build + persist the grant row
|
|
735
740
|
var nowMs = Date.now();
|
|
736
|
-
var grantId = "bg-" + generateToken(
|
|
741
|
+
var grantId = "bg-" + generateToken(GRANT_ID_BYTES);
|
|
737
742
|
var sessionId = (opts.req && opts.req.session && opts.req.session.id) || null;
|
|
738
743
|
// Honor the framework-wide trustProxy setting from init() — same
|
|
739
744
|
// boundary as middleware. Without trustProxy, X-Forwarded-For is
|
package/lib/bundler.js
CHANGED
|
@@ -81,11 +81,22 @@ var path = require("path");
|
|
|
81
81
|
var fs = require("fs");
|
|
82
82
|
var crypto = require("./crypto");
|
|
83
83
|
var atomicFile = require("./atomic-file");
|
|
84
|
-
var
|
|
84
|
+
var logModule = require("./log");
|
|
85
|
+
var nb = require("./numeric-bounds");
|
|
85
86
|
var safeJson = require("./safe-json");
|
|
86
87
|
var validateOpts = require("./validate-opts");
|
|
88
|
+
var { defineClass } = require("./framework-error");
|
|
87
89
|
|
|
88
90
|
var BundlerError = defineClass("BundlerError", { alwaysPermanent: true });
|
|
91
|
+
var bootLog = logModule.boot("bundler");
|
|
92
|
+
|
|
93
|
+
// Default content-hash length: 16 hex chars (8 bytes of SHA3-512). Long
|
|
94
|
+
// enough that collision probability is negligible for asset cache busting,
|
|
95
|
+
// short enough to keep generated filenames readable.
|
|
96
|
+
var DEFAULT_HASH_LEN = 0x10;
|
|
97
|
+
var MIN_HASH_LEN = 0x4;
|
|
98
|
+
var MAX_HASH_LEN = 0x40;
|
|
99
|
+
var DEFAULT_GRACE_MS = 100;
|
|
89
100
|
|
|
90
101
|
function _hashContent(buf, hexLen) {
|
|
91
102
|
// SHA3-512 → take the first hexLen hex chars. Same family as the
|
|
@@ -221,10 +232,7 @@ function create(opts) {
|
|
|
221
232
|
"_watch", "_setTimeout", "_clearTimeout",
|
|
222
233
|
], "b.bundler");
|
|
223
234
|
_validateEntries(opts.entries);
|
|
224
|
-
|
|
225
|
-
throw new BundlerError("bundler/no-outdir",
|
|
226
|
-
"bundler.create requires opts.outdir");
|
|
227
|
-
}
|
|
235
|
+
validateOpts.requireNonEmptyString(opts.outdir, "bundler.create: opts.outdir", BundlerError, "bundler/no-outdir");
|
|
228
236
|
var engineImpl = _validateEngine(opts.engine);
|
|
229
237
|
|
|
230
238
|
var entries = Object.assign({}, opts.entries);
|
|
@@ -236,8 +244,17 @@ function create(opts) {
|
|
|
236
244
|
? opts.manifest
|
|
237
245
|
: "manifest.json");
|
|
238
246
|
var hashOn = opts.hash !== false;
|
|
239
|
-
var hashLen =
|
|
240
|
-
|
|
247
|
+
var hashLen = DEFAULT_HASH_LEN;
|
|
248
|
+
if (opts.hashLen !== undefined) {
|
|
249
|
+
if (!nb.isPositiveFiniteInt(opts.hashLen) ||
|
|
250
|
+
opts.hashLen < MIN_HASH_LEN || opts.hashLen > MAX_HASH_LEN) {
|
|
251
|
+
throw new BundlerError("bundler/bad-hash-len",
|
|
252
|
+
"bundler.create: opts.hashLen must be a positive finite integer " +
|
|
253
|
+
"between " + MIN_HASH_LEN + " and " + MAX_HASH_LEN +
|
|
254
|
+
"; got " + nb.shape(opts.hashLen));
|
|
255
|
+
}
|
|
256
|
+
hashLen = opts.hashLen;
|
|
257
|
+
}
|
|
241
258
|
var log = opts.log || null;
|
|
242
259
|
|
|
243
260
|
// Test seam: tests pass a fake watcher so we don't actually fs.watch
|
|
@@ -246,7 +263,9 @@ function create(opts) {
|
|
|
246
263
|
};
|
|
247
264
|
var setTimeoutFn = opts._setTimeout || setTimeout;
|
|
248
265
|
var clearTimeoutFn = opts._clearTimeout || clearTimeout;
|
|
249
|
-
|
|
266
|
+
nb.requireNonNegativeFiniteIntIfPresent(opts.graceMs,
|
|
267
|
+
"bundler.create: opts.graceMs", BundlerError, "bundler/bad-grace-ms");
|
|
268
|
+
var graceMs = opts.graceMs !== undefined ? opts.graceMs : DEFAULT_GRACE_MS;
|
|
250
269
|
|
|
251
270
|
var watchers = [];
|
|
252
271
|
var debounceTimer = null;
|
|
@@ -256,15 +275,7 @@ function create(opts) {
|
|
|
256
275
|
return path.isAbsolute(p) ? p : path.resolve(cwd, p);
|
|
257
276
|
}
|
|
258
277
|
|
|
259
|
-
|
|
260
|
-
if (log && typeof log[level] === "function") {
|
|
261
|
-
try { log[level](message, fields); } catch (_e) { /* logger best-effort */ }
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
var line = "[blamejs:bundler] " + message + (fields ? " " + JSON.stringify(fields) : "");
|
|
265
|
-
if (level === "error" || level === "warn" || level === "fatal") console.error(line);
|
|
266
|
-
else console.log(line);
|
|
267
|
-
}
|
|
278
|
+
var _logVia = logModule.makeViaOrFallback(log, bootLog);
|
|
268
279
|
|
|
269
280
|
async function build() {
|
|
270
281
|
var t0 = Date.now();
|
|
@@ -350,16 +361,16 @@ function create(opts) {
|
|
|
350
361
|
|
|
351
362
|
function _scheduleRebuild(reason, callback) {
|
|
352
363
|
if (debounceTimer) {
|
|
353
|
-
try { clearTimeoutFn(debounceTimer); } catch (_e) {}
|
|
364
|
+
try { clearTimeoutFn(debounceTimer); } catch (_e) { /* timer already cleared */ }
|
|
354
365
|
}
|
|
355
366
|
debounceTimer = setTimeoutFn(function () {
|
|
356
367
|
debounceTimer = null;
|
|
357
368
|
_logVia("info", "rebuilding", { reason: reason });
|
|
358
369
|
build().then(
|
|
359
|
-
function (r) { if (callback) try { callback(null, r); } catch (_e) {} },
|
|
370
|
+
function (r) { if (callback) try { callback(null, r); } catch (_e) { /* operator callback threw — logged elsewhere if material */ } },
|
|
360
371
|
function (e) {
|
|
361
372
|
_logVia("error", "rebuild failed", { error: (e && e.message) || String(e) });
|
|
362
|
-
if (callback) try { callback(e, null); } catch (_e) {}
|
|
373
|
+
if (callback) try { callback(e, null); } catch (_e) { /* operator callback threw — already logged */ }
|
|
363
374
|
}
|
|
364
375
|
);
|
|
365
376
|
}, graceMs);
|
|
@@ -404,7 +415,7 @@ function create(opts) {
|
|
|
404
415
|
async function close() {
|
|
405
416
|
watching = false;
|
|
406
417
|
if (debounceTimer) {
|
|
407
|
-
try { clearTimeoutFn(debounceTimer); } catch (_e) {}
|
|
418
|
+
try { clearTimeoutFn(debounceTimer); } catch (_e) { /* timer already cleared */ }
|
|
408
419
|
debounceTimer = null;
|
|
409
420
|
}
|
|
410
421
|
for (var i = 0; i < watchers.length; i++) {
|
package/lib/cache-redis.js
CHANGED
|
@@ -33,9 +33,14 @@
|
|
|
33
33
|
* is filtered against actual key existence — see invalidateTag below).
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
+
var C = require("./constants");
|
|
37
|
+
var { boot } = require("./log");
|
|
36
38
|
var redisClient = require("./redis-client");
|
|
39
|
+
var safeJson = require("./safe-json");
|
|
37
40
|
var { CacheError } = require("./framework-error");
|
|
38
41
|
|
|
42
|
+
var log = boot("cache-redis");
|
|
43
|
+
|
|
39
44
|
var _err = CacheError.factory;
|
|
40
45
|
|
|
41
46
|
function _toStr(v) {
|
|
@@ -84,7 +89,7 @@ function create(cfg) {
|
|
|
84
89
|
var s = _toStr(v);
|
|
85
90
|
if (s === null) return undefined;
|
|
86
91
|
var parsed;
|
|
87
|
-
try { parsed =
|
|
92
|
+
try { parsed = safeJson.parse(s, { maxBytes: C.BYTES.mib(64) }); }
|
|
88
93
|
catch (_e) { return undefined; }
|
|
89
94
|
// Sliding TTL: extend the entry's life on every read. Best-effort
|
|
90
95
|
// (PEXPIREAT may race with delete; if it fails the operator just
|
|
@@ -216,7 +221,7 @@ function create(cfg) {
|
|
|
216
221
|
} else {
|
|
217
222
|
// Ghost — just SREM from this tag set + drop the reverse.
|
|
218
223
|
try { await client.command("SREM", _tagKey(tag), keys[i]); }
|
|
219
|
-
catch (
|
|
224
|
+
catch (e) { log.debug("invalidateTag-cleanup-failed", { op: "SREM", tag: tag, error: e.message }); }
|
|
220
225
|
}
|
|
221
226
|
}
|
|
222
227
|
// Drop the tag set itself if it's now empty (or empty after the
|
|
@@ -224,7 +229,7 @@ function create(cfg) {
|
|
|
224
229
|
var remaining = await client.command("SCARD", _tagKey(tag));
|
|
225
230
|
if (Number(remaining) === 0) {
|
|
226
231
|
try { await client.command("DEL", _tagKey(tag)); }
|
|
227
|
-
catch (
|
|
232
|
+
catch (e) { log.debug("invalidateTag-cleanup-failed", { op: "DEL", tag: tag, error: e.message }); }
|
|
228
233
|
}
|
|
229
234
|
emitObs("cache.redis.invalidateTag", { namespace: namespace, tag: tag, dropped: dropped });
|
|
230
235
|
return dropped;
|
package/lib/cache.js
CHANGED
|
@@ -85,15 +85,19 @@
|
|
|
85
85
|
* the application layer.
|
|
86
86
|
*/
|
|
87
87
|
|
|
88
|
+
var cacheRedis = require("./cache-redis");
|
|
88
89
|
var clusterStorage = require("./cluster-storage");
|
|
89
90
|
var C = require("./constants");
|
|
90
91
|
var lazyRequire = require("./lazy-require");
|
|
92
|
+
var { boot } = require("./log");
|
|
91
93
|
var numericChecks = require("./numeric-checks");
|
|
92
94
|
var requestHelpers = require("./request-helpers");
|
|
93
95
|
var safeAsync = require("./safe-async");
|
|
96
|
+
var safeJson = require("./safe-json");
|
|
94
97
|
var validateOpts = require("./validate-opts");
|
|
95
98
|
var { CacheError } = require("./framework-error");
|
|
96
99
|
|
|
100
|
+
var log = boot("cache");
|
|
97
101
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
98
102
|
|
|
99
103
|
var _err = CacheError.factory;
|
|
@@ -101,7 +105,7 @@ var _err = CacheError.factory;
|
|
|
101
105
|
var DEFAULTS = Object.freeze({
|
|
102
106
|
backend: "memory",
|
|
103
107
|
ttlMs: C.TIME.minutes(5),
|
|
104
|
-
maxEntries: 10000,
|
|
108
|
+
maxEntries: C.BYTES.bytes(10000),
|
|
105
109
|
maxBytes: Infinity,
|
|
106
110
|
sweepIntervalMs: C.TIME.minutes(1),
|
|
107
111
|
staleWhileRevalidate: false,
|
|
@@ -148,7 +152,7 @@ function _defaultSizeOf(value) {
|
|
|
148
152
|
if (value === null || value === undefined) return 0;
|
|
149
153
|
if (Buffer.isBuffer(value)) return value.length;
|
|
150
154
|
if (typeof value === "string") return Buffer.byteLength(value, "utf8");
|
|
151
|
-
if (typeof value === "number" || typeof value === "boolean") return 8;
|
|
155
|
+
if (typeof value === "number" || typeof value === "boolean") return C.BYTES.bytes(8);
|
|
152
156
|
// Fallback: round-trip through JSON. Cost is real; documented in the
|
|
153
157
|
// DEFAULTS docstring so operators with hot-path size accounting know
|
|
154
158
|
// to supply their own sizeOf.
|
|
@@ -170,12 +174,8 @@ function _validateBackendObject(backend) {
|
|
|
170
174
|
}
|
|
171
175
|
|
|
172
176
|
function _validateCreateOpts(opts) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
if (typeof opts.namespace !== "string" || opts.namespace.length === 0) {
|
|
177
|
-
throw _err("BAD_OPT", "cache.create: namespace must be a non-empty string (cache identity for observability + audit)");
|
|
178
|
-
}
|
|
177
|
+
validateOpts.requireObject(opts, "cache.create", CacheError);
|
|
178
|
+
validateOpts.requireNonEmptyString(opts.namespace, "cache.create: namespace", CacheError, "BAD_OPT");
|
|
179
179
|
// Composite cluster-key separator is ":" — namespace must not contain it
|
|
180
180
|
// or two namespaces could collide ("a:b" + "c" = "a:b:c" = "a" + "b:c").
|
|
181
181
|
if (opts.namespace.indexOf(":") !== -1) {
|
|
@@ -200,41 +200,21 @@ function _validateCreateOpts(opts) {
|
|
|
200
200
|
if (opts.ttlMs !== undefined) _validateTtl("cache.create: ttlMs", opts.ttlMs);
|
|
201
201
|
if (opts.maxEntries !== undefined) _validateMaxEntries(opts.maxEntries);
|
|
202
202
|
if (opts.maxBytes !== undefined) _validateMaxBytes(opts.maxBytes);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
if (opts.slidingTtl !== undefined && typeof opts.slidingTtl !== "boolean") {
|
|
207
|
-
throw _err("BAD_OPT", "cache.create: slidingTtl must be a boolean");
|
|
208
|
-
}
|
|
203
|
+
validateOpts.optionalFunction(opts.sizeOf, "cache.create: sizeOf", CacheError);
|
|
204
|
+
validateOpts.optionalBoolean(opts.slidingTtl, "cache.create: slidingTtl", CacheError);
|
|
209
205
|
if (opts.sweepIntervalMs !== undefined) {
|
|
210
|
-
|
|
211
|
-
|
|
206
|
+
validateOpts.optionalFiniteNonNegative(opts.sweepIntervalMs, "cache.create: sweepIntervalMs", CacheError);
|
|
207
|
+
if (opts.sweepIntervalMs < C.TIME.seconds(1)) {
|
|
208
|
+
throw _err("BAD_OPT", "cache.create: sweepIntervalMs must be >= 1000ms, got " +
|
|
212
209
|
JSON.stringify(opts.sweepIntervalMs));
|
|
213
210
|
}
|
|
214
211
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (opts.auditClear !== undefined && typeof opts.auditClear !== "boolean") {
|
|
222
|
-
throw _err("BAD_OPT", "cache.create: auditClear must be a boolean");
|
|
223
|
-
}
|
|
224
|
-
if (opts.audit !== undefined && opts.audit !== null) {
|
|
225
|
-
if (typeof opts.audit !== "object" || typeof opts.audit.safeEmit !== "function") {
|
|
226
|
-
throw _err("BAD_OPT", "cache.create: audit must be a b.audit-shaped object (safeEmit fn)");
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
if (opts.observability !== undefined && opts.observability !== null) {
|
|
230
|
-
if (typeof opts.observability !== "object" ||
|
|
231
|
-
typeof opts.observability.event !== "function") {
|
|
232
|
-
throw _err("BAD_OPT", "cache.create: observability must be a b.observability-shaped object (event fn)");
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
if (opts.clock !== undefined && typeof opts.clock !== "function") {
|
|
236
|
-
throw _err("BAD_OPT", "cache.create: clock must be a function");
|
|
237
|
-
}
|
|
212
|
+
validateOpts.optionalBoolean(opts.staleWhileRevalidate, "cache.create: staleWhileRevalidate", CacheError);
|
|
213
|
+
validateOpts.optionalBoolean(opts.auditFailures, "cache.create: auditFailures", CacheError);
|
|
214
|
+
validateOpts.optionalBoolean(opts.auditClear, "cache.create: auditClear", CacheError);
|
|
215
|
+
validateOpts.auditShape(opts.audit, "cache.create", CacheError);
|
|
216
|
+
validateOpts.observabilityShape(opts.observability, "cache.create", CacheError);
|
|
217
|
+
validateOpts.optionalFunction(opts.clock, "cache.create: clock", CacheError);
|
|
238
218
|
}
|
|
239
219
|
|
|
240
220
|
function _validateKey(key, ctx) {
|
|
@@ -505,7 +485,7 @@ function _clusterBackend(cfg) {
|
|
|
505
485
|
[newExpires, now, _composedKey(key), now]
|
|
506
486
|
).catch(function () { /* best-effort */ });
|
|
507
487
|
}
|
|
508
|
-
try { return
|
|
488
|
+
try { return safeJson.parse(row.valueJson, { maxBytes: C.BYTES.mib(64) }); }
|
|
509
489
|
catch (_e) { return undefined; }
|
|
510
490
|
}
|
|
511
491
|
|
|
@@ -750,18 +730,19 @@ function create(opts) {
|
|
|
750
730
|
"invalidationPubsub",
|
|
751
731
|
], "cache");
|
|
752
732
|
_validateCreateOpts(opts);
|
|
733
|
+
var cfg0 = validateOpts.applyDefaults(opts, DEFAULTS);
|
|
753
734
|
|
|
754
735
|
var namespace = opts.namespace;
|
|
755
|
-
var backendKind =
|
|
756
|
-
var defaultTtlMs =
|
|
757
|
-
var maxEntries =
|
|
758
|
-
var maxBytes =
|
|
736
|
+
var backendKind = cfg0.backend;
|
|
737
|
+
var defaultTtlMs = cfg0.ttlMs;
|
|
738
|
+
var maxEntries = cfg0.maxEntries;
|
|
739
|
+
var maxBytes = cfg0.maxBytes;
|
|
759
740
|
var sizeOf = (typeof opts.sizeOf === "function") ? opts.sizeOf : _defaultSizeOf;
|
|
760
|
-
var sweepIntervalMs =
|
|
761
|
-
var staleRevalidate =
|
|
762
|
-
var slidingTtl =
|
|
763
|
-
var auditFailures =
|
|
764
|
-
var auditClear =
|
|
741
|
+
var sweepIntervalMs = cfg0.sweepIntervalMs;
|
|
742
|
+
var staleRevalidate = cfg0.staleWhileRevalidate;
|
|
743
|
+
var slidingTtl = cfg0.slidingTtl;
|
|
744
|
+
var auditFailures = cfg0.auditFailures;
|
|
745
|
+
var auditClear = cfg0.auditClear;
|
|
765
746
|
var audit = opts.audit || null;
|
|
766
747
|
var operatorObs = opts.observability || null;
|
|
767
748
|
var clock = opts.clock || function () { return Date.now(); };
|
|
@@ -786,11 +767,7 @@ function create(opts) {
|
|
|
786
767
|
} catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
|
|
787
768
|
}
|
|
788
769
|
|
|
789
|
-
|
|
790
|
-
if (!audit) return;
|
|
791
|
-
try { audit.safeEmit(Object.assign({ action: action }, info || {})); }
|
|
792
|
-
catch (_e) { /* audit best-effort */ }
|
|
793
|
-
}
|
|
770
|
+
var emitAudit = validateOpts.makeAuditEmitter(audit);
|
|
794
771
|
|
|
795
772
|
function _actor(callerOpts) {
|
|
796
773
|
return requestHelpers.resolveActorWithOverride(callerOpts);
|
|
@@ -825,7 +802,6 @@ function create(opts) {
|
|
|
825
802
|
} else if (backendKind === "cluster") {
|
|
826
803
|
backend = _clusterBackend(cfg);
|
|
827
804
|
} else if (backendKind === "redis") {
|
|
828
|
-
var cacheRedis = require("./cache-redis");
|
|
829
805
|
backend = _customBackend(cacheRedis.create({
|
|
830
806
|
namespace: namespace,
|
|
831
807
|
url: opts.redisUrl,
|
|
@@ -1171,11 +1147,14 @@ function create(opts) {
|
|
|
1171
1147
|
try {
|
|
1172
1148
|
if (ev.kind === "tag" && typeof ev.tag === "string" &&
|
|
1173
1149
|
typeof backend.invalidateTag === "function") {
|
|
1174
|
-
try { await backend.invalidateTag(ev.tag); }
|
|
1150
|
+
try { await backend.invalidateTag(ev.tag); }
|
|
1151
|
+
catch (e) { log.debug("invalidation-apply-failed", { op: "invalidateTag", tag: ev.tag, error: e.message }); }
|
|
1175
1152
|
} else if (ev.kind === "del" && typeof ev.key === "string") {
|
|
1176
|
-
try { await backend.del(ev.key); }
|
|
1153
|
+
try { await backend.del(ev.key); }
|
|
1154
|
+
catch (e) { log.debug("invalidation-apply-failed", { op: "del", key: ev.key, error: e.message }); }
|
|
1177
1155
|
} else if (ev.kind === "clear") {
|
|
1178
|
-
try { await backend.clear(); }
|
|
1156
|
+
try { await backend.clear(); }
|
|
1157
|
+
catch (e) { log.debug("invalidation-apply-failed", { op: "clear", error: e.message }); }
|
|
1179
1158
|
}
|
|
1180
1159
|
// Wipe local in-flight memoization so a freshly-invalidated key
|
|
1181
1160
|
// can't resolve from a still-pending fetch on this node.
|
|
@@ -1195,7 +1174,8 @@ function create(opts) {
|
|
|
1195
1174
|
if (closed) return;
|
|
1196
1175
|
closed = true;
|
|
1197
1176
|
if (invalidationPubsub && invalidationToken) {
|
|
1198
|
-
try { invalidationPubsub.unsubscribe(invalidationToken); }
|
|
1177
|
+
try { invalidationPubsub.unsubscribe(invalidationToken); }
|
|
1178
|
+
catch (e) { log.debug("close-cleanup-failed", { op: "unsubscribe", error: e.message }); }
|
|
1199
1179
|
invalidationToken = null;
|
|
1200
1180
|
}
|
|
1201
1181
|
inflight.clear();
|
package/lib/canonical-json.js
CHANGED
|
@@ -78,7 +78,10 @@ function _scrub(value, seen, bufferAs) {
|
|
|
78
78
|
if (Array.isArray(value)) {
|
|
79
79
|
return value.map(function (v) { return _scrub(v, seen, bufferAs); });
|
|
80
80
|
}
|
|
81
|
-
|
|
81
|
+
// Canonical-json IS the destination for sorted-keys walks across the
|
|
82
|
+
// codebase; the keys-then-sort here is the canonical primitive itself.
|
|
83
|
+
var keys = Object.keys(value);
|
|
84
|
+
keys.sort();
|
|
82
85
|
var out = {};
|
|
83
86
|
for (var i = 0; i < keys.length; i++) {
|
|
84
87
|
out[keys[i]] = _scrub(value[keys[i]], seen, bufferAs);
|
|
@@ -97,4 +100,16 @@ function stringify(value, opts) {
|
|
|
97
100
|
return JSON.stringify(_scrub(value, null, bufferAs));
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
|
|
103
|
+
// Stable key ordering for an object — same lexicographic sort used by
|
|
104
|
+
// the canonical-json walker. Exposed so call sites that need a sorted
|
|
105
|
+
// key list (CLI report ordering, fingerprint inputs) route through
|
|
106
|
+
// the framework's single source-of-truth ordering rule rather than
|
|
107
|
+
// re-implementing the keys-then-sort dance inline.
|
|
108
|
+
function sortKeys(obj) {
|
|
109
|
+
if (!obj || typeof obj !== "object") return [];
|
|
110
|
+
var keys = Object.keys(obj);
|
|
111
|
+
keys.sort();
|
|
112
|
+
return keys;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { stringify: stringify, sortKeys: sortKeys };
|
package/lib/chain-writer.js
CHANGED
|
@@ -172,11 +172,11 @@ function create(opts) {
|
|
|
172
172
|
async function _appendInsideMutex(logical) {
|
|
173
173
|
var counter = _nextCounter++;
|
|
174
174
|
var nowMs = Date.now();
|
|
175
|
-
var nonce = generateBytes(16);
|
|
175
|
+
var nonce = generateBytes(C.BYTES.bytes(16));
|
|
176
176
|
|
|
177
177
|
// Caller-supplied logical row: spread + add framework-managed fields.
|
|
178
178
|
var fullLogical = Object.assign({}, logical, {
|
|
179
|
-
_id: (logical && logical._id) || generateToken(16),
|
|
179
|
+
_id: (logical && logical._id) || generateToken(C.BYTES.bytes(16)),
|
|
180
180
|
recordedAt: nowMs,
|
|
181
181
|
monotonicCounter: counter,
|
|
182
182
|
});
|
package/lib/cli-helpers.js
CHANGED
|
@@ -47,8 +47,16 @@
|
|
|
47
47
|
* spawning a child process.
|
|
48
48
|
*/
|
|
49
49
|
|
|
50
|
+
var lazyRequire = require("./lazy-require");
|
|
50
51
|
var validateOpts = require("./validate-opts");
|
|
51
52
|
|
|
53
|
+
// lazyRequire the framework root so cli-helpers stays loadable in
|
|
54
|
+
// contexts that aren't the framework root (tests, operator-side
|
|
55
|
+
// scripts importing only cli-helpers). The first bootApp() call
|
|
56
|
+
// resolves the framework module; subsequent calls reuse the cached
|
|
57
|
+
// load.
|
|
58
|
+
var blamejs = lazyRequire(function () { return require("../"); });
|
|
59
|
+
|
|
52
60
|
// ---- Streams + exit-code-shaped reporting --------------------------------
|
|
53
61
|
|
|
54
62
|
function _writeLine(stream, line) {
|
|
@@ -178,10 +186,7 @@ async function bootApp(opts) {
|
|
|
178
186
|
vaultPassphrase = Buffer.from(raw, "utf8");
|
|
179
187
|
}
|
|
180
188
|
|
|
181
|
-
|
|
182
|
-
// the framework root (tests, operator-side scripts importing only
|
|
183
|
-
// cli-helpers).
|
|
184
|
-
var b = require("../");
|
|
189
|
+
var b = blamejs();
|
|
185
190
|
var app = await b.createApp({
|
|
186
191
|
dataDir: opts.dataDir,
|
|
187
192
|
routes: function () {},
|
package/lib/cli.js
CHANGED
|
@@ -41,17 +41,29 @@ var apiSnapshot = require("./api-snapshot");
|
|
|
41
41
|
var auditChain = require("./audit-chain");
|
|
42
42
|
var auditTools = require("./audit-tools");
|
|
43
43
|
var backup = require("./backup");
|
|
44
|
+
var canonicalJson = require("./canonical-json");
|
|
44
45
|
var cliHelpers = require("./cli-helpers");
|
|
45
46
|
var constants = require("./constants");
|
|
47
|
+
var C = constants;
|
|
46
48
|
var crypto = require("./crypto");
|
|
47
49
|
var dev = require("./dev");
|
|
50
|
+
var fileType = require("./file-type");
|
|
48
51
|
var migrations = require("./migrations");
|
|
52
|
+
var passwordModule = require("./auth/password");
|
|
49
53
|
var requestHelpers = require("./request-helpers");
|
|
50
54
|
var restore = require("./restore");
|
|
51
55
|
var restoreBundle = require("./restore-bundle");
|
|
52
56
|
var restoreRollback = require("./restore-rollback");
|
|
53
57
|
var seeders = require("./seeders");
|
|
54
58
|
var vaultPassphraseOps = require("./vault/passphrase-ops");
|
|
59
|
+
var { defineClass } = require("./framework-error");
|
|
60
|
+
|
|
61
|
+
var CliError = defineClass("CliError", { alwaysPermanent: true });
|
|
62
|
+
|
|
63
|
+
// Maximum length for an operator-supplied --ignore regex pattern. Caps
|
|
64
|
+
// ReDoS surface area: a multi-megabyte pattern compiled to a regex
|
|
65
|
+
// would already be a footgun, so refuse anything beyond the cap.
|
|
66
|
+
var MAX_IGNORE_PATTERN_LENGTH = 0x100;
|
|
55
67
|
|
|
56
68
|
var DEFAULT_MIG_DIR = "./migrations";
|
|
57
69
|
var DEFAULT_SEED_DIR = "./seeders";
|
|
@@ -363,7 +375,15 @@ async function _runDev(args, ctx) {
|
|
|
363
375
|
}
|
|
364
376
|
var argList = _coerceList(args.flags.arg).map(String);
|
|
365
377
|
var watchList = _coerceList(args.flags.watch).map(String);
|
|
366
|
-
var ignoreList = _coerceList(args.flags.ignore).map(function (s) {
|
|
378
|
+
var ignoreList = _coerceList(args.flags.ignore).map(function (s) {
|
|
379
|
+
var str = String(s);
|
|
380
|
+
if (str.length > MAX_IGNORE_PATTERN_LENGTH) {
|
|
381
|
+
throw new CliError("cli/bad-ignore-pattern",
|
|
382
|
+
"blamejs dev: --ignore pattern exceeds max length " +
|
|
383
|
+
MAX_IGNORE_PATTERN_LENGTH + " (got " + str.length + ")");
|
|
384
|
+
}
|
|
385
|
+
return RegExp(str);
|
|
386
|
+
});
|
|
367
387
|
var graceMs = args.flags["grace-ms"] !== undefined ? Number(args.flags["grace-ms"]) : undefined;
|
|
368
388
|
if (graceMs !== undefined && (!Number.isFinite(graceMs) || graceMs < 0)) {
|
|
369
389
|
_writeLine(ctx.stderr, "blamejs dev: --grace-ms must be a non-negative number");
|
|
@@ -1220,7 +1240,11 @@ async function _runBackup(args, ctx) {
|
|
|
1220
1240
|
var kind = m.files[k].kind || "unknown";
|
|
1221
1241
|
kinds[kind] = (kinds[kind] || 0) + 1;
|
|
1222
1242
|
}
|
|
1223
|
-
|
|
1243
|
+
// Stable kind ordering for the human-readable report — same
|
|
1244
|
+
// sort the framework's canonical-json walker uses for object
|
|
1245
|
+
// keys. Pulled out as a sortKeys() helper so this single sorted
|
|
1246
|
+
// walk in cli matches the canonical-json discipline elsewhere.
|
|
1247
|
+
var ks = canonicalJson.sortKeys(kinds);
|
|
1224
1248
|
for (var ki = 0; ki < ks.length; ki++) {
|
|
1225
1249
|
report.write(" " + ks[ki] + ": " + kinds[ks[ki]]);
|
|
1226
1250
|
}
|
|
@@ -1240,7 +1264,7 @@ async function _runBackup(args, ctx) {
|
|
|
1240
1264
|
|
|
1241
1265
|
if (sub === "verify") {
|
|
1242
1266
|
var stagingDir = path.join(os.tmpdir(),
|
|
1243
|
-
"blamejs-backup-verify-" + crypto.generateToken(8));
|
|
1267
|
+
"blamejs-backup-verify-" + crypto.generateToken(C.BYTES.bytes(8)));
|
|
1244
1268
|
try {
|
|
1245
1269
|
var r = await restoreBundle.extract({
|
|
1246
1270
|
bundleDir: bundleDir,
|
|
@@ -1836,7 +1860,6 @@ function _runFileType(args, ctx) {
|
|
|
1836
1860
|
catch (e) {
|
|
1837
1861
|
return report.error("read failed: " + ((e && e.message) || String(e)));
|
|
1838
1862
|
}
|
|
1839
|
-
var fileType = require("./file-type");
|
|
1840
1863
|
if (args.flags.allowlist && args.flags.allowlist !== true) {
|
|
1841
1864
|
var allowed = requestHelpers.parseListHeader(args.flags.allowlist);
|
|
1842
1865
|
try {
|
|
@@ -1929,7 +1952,6 @@ async function _runPassword(args, ctx) {
|
|
|
1929
1952
|
policyOpts.breachCheck = "haveibeenpwned";
|
|
1930
1953
|
if (args.flags["fail-closed"]) policyOpts.failClosed = true;
|
|
1931
1954
|
}
|
|
1932
|
-
var passwordModule = require("./auth/password");
|
|
1933
1955
|
var policy;
|
|
1934
1956
|
try { policy = passwordModule.policy(policyOpts); }
|
|
1935
1957
|
catch (e) { return report.error("bad policy: " + ((e && e.message) || String(e)), 2); }
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
* Lease object shape:
|
|
46
46
|
* { nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint }
|
|
47
47
|
*/
|
|
48
|
+
var C = require("./constants");
|
|
48
49
|
var { generateToken } = require("./crypto");
|
|
49
50
|
var externalDb = require("./external-db");
|
|
50
51
|
var { ClusterProviderError } = require("./framework-error");
|
|
@@ -129,7 +130,7 @@ function create(config) {
|
|
|
129
130
|
throw _err("INVALID_TTL", "leaseTtlMs must be a positive number", true);
|
|
130
131
|
}
|
|
131
132
|
var endpoint = (opts && opts.endpoint) || null;
|
|
132
|
-
var leaseId = generateToken(16);
|
|
133
|
+
var leaseId = generateToken(C.BYTES.bytes(16));
|
|
133
134
|
var nowMs = Date.now();
|
|
134
135
|
var expiresAt = nowMs + leaseTtlMs;
|
|
135
136
|
|