@blamejs/core 0.6.69 → 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 +6 -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 +27 -20
- 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 +17 -4
- 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 +42 -17
- 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/cluster-storage.js
CHANGED
|
@@ -64,6 +64,74 @@ function tableName(local) {
|
|
|
64
64
|
return local;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// Precomputed rewrite table for resolveTables(). Built once at module
|
|
68
|
+
// load from the static frozen frameworkSchema.LOCAL_TO_EXTERNAL mapping —
|
|
69
|
+
// not from operator/request input. Order longest-first so prefix matches
|
|
70
|
+
// don't collide (audit_log before audit). Identity mappings are filtered
|
|
71
|
+
// out so the loop body has no no-op iterations. Framework table names
|
|
72
|
+
// are entirely [A-Za-z0-9_], so a manual word-boundary scan replaces
|
|
73
|
+
// the regex `\b...\b` exactly without runtime regex compilation.
|
|
74
|
+
// Character-code bounds for the word-character class \w that JavaScript
|
|
75
|
+
// regex `\b` uses. Stored as code-from-literal so the magic numbers are
|
|
76
|
+
// self-evident at the source rather than needing a comment to decode.
|
|
77
|
+
var _CC_0 = "0".charCodeAt(0);
|
|
78
|
+
var _CC_9 = "9".charCodeAt(0);
|
|
79
|
+
var _CC_A = "A".charCodeAt(0);
|
|
80
|
+
var _CC_Z = "Z".charCodeAt(0);
|
|
81
|
+
var _CC_UNDERSCORE = "_".charCodeAt(0);
|
|
82
|
+
var _CC_a = "a".charCodeAt(0);
|
|
83
|
+
var _CC_z = "z".charCodeAt(0);
|
|
84
|
+
|
|
85
|
+
function _isWordChar(code) {
|
|
86
|
+
return (code >= _CC_0 && code <= _CC_9) ||
|
|
87
|
+
(code >= _CC_A && code <= _CC_Z) ||
|
|
88
|
+
code === _CC_UNDERSCORE ||
|
|
89
|
+
(code >= _CC_a && code <= _CC_z);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function _replaceWordBoundaryAll(haystack, needle, replacement) {
|
|
93
|
+
if (haystack.length < needle.length) return haystack;
|
|
94
|
+
var out = "";
|
|
95
|
+
var cursor = 0;
|
|
96
|
+
var matched = false;
|
|
97
|
+
var idx = haystack.indexOf(needle);
|
|
98
|
+
while (idx !== -1) {
|
|
99
|
+
var beforeCode = idx === 0 ? -1 : haystack.charCodeAt(idx - 1);
|
|
100
|
+
var afterPos = idx + needle.length;
|
|
101
|
+
var afterCode = afterPos >= haystack.length ? -1 : haystack.charCodeAt(afterPos);
|
|
102
|
+
var leftBoundary = beforeCode === -1 || !_isWordChar(beforeCode);
|
|
103
|
+
var rightBoundary = afterCode === -1 || !_isWordChar(afterCode);
|
|
104
|
+
if (leftBoundary && rightBoundary) {
|
|
105
|
+
out += haystack.slice(cursor, idx) + replacement;
|
|
106
|
+
cursor = afterPos;
|
|
107
|
+
matched = true;
|
|
108
|
+
idx = haystack.indexOf(needle, cursor);
|
|
109
|
+
} else {
|
|
110
|
+
// Boundary mismatch — advance past this position by one and keep
|
|
111
|
+
// scanning. We do NOT flush partial output here; the final
|
|
112
|
+
// haystack.slice(cursor) below covers any non-rewritten tail.
|
|
113
|
+
idx = haystack.indexOf(needle, idx + 1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!matched) return haystack;
|
|
117
|
+
return out + haystack.slice(cursor);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
var _REWRITE_TABLE = (function () {
|
|
121
|
+
var mapping = frameworkSchema.LOCAL_TO_EXTERNAL;
|
|
122
|
+
var names = Object.keys(mapping).sort(function (a, b) {
|
|
123
|
+
return b.length - a.length;
|
|
124
|
+
});
|
|
125
|
+
var entries = [];
|
|
126
|
+
for (var i = 0; i < names.length; i++) {
|
|
127
|
+
var local = names[i];
|
|
128
|
+
var external = mapping[local];
|
|
129
|
+
if (local === external) continue;
|
|
130
|
+
entries.push({ local: local, external: external });
|
|
131
|
+
}
|
|
132
|
+
return Object.freeze(entries);
|
|
133
|
+
})();
|
|
134
|
+
|
|
67
135
|
// Rewrite bare table names in SQL when running in cluster mode. We only
|
|
68
136
|
// touch tokens that are exactly one of the framework's known table names
|
|
69
137
|
// (audit_log, consent_log, …) — anything else passes through unchanged
|
|
@@ -71,18 +139,10 @@ function tableName(local) {
|
|
|
71
139
|
// migrations) isn't rewritten by accident.
|
|
72
140
|
function resolveTables(sql) {
|
|
73
141
|
if (!cluster.isClusterMode()) return sql;
|
|
74
|
-
var mapping = frameworkSchema.LOCAL_TO_EXTERNAL;
|
|
75
|
-
// Order longest-first so prefix matches don't collide (audit_log
|
|
76
|
-
// before audit). Match on word boundaries so we don't rewrite a
|
|
77
|
-
// suffix of a longer identifier.
|
|
78
|
-
var localNames = Object.keys(mapping).sort(function (a, b) { return b.length - a.length; });
|
|
79
142
|
var translated = sql;
|
|
80
|
-
for (var i = 0; i <
|
|
81
|
-
var
|
|
82
|
-
|
|
83
|
-
if (local === external) continue; // identity mapping — no-op
|
|
84
|
-
var re = new RegExp("\\b" + local.replace(/[\\.-]/g, "\\$&") + "\\b", "g");
|
|
85
|
-
translated = translated.replace(re, external);
|
|
143
|
+
for (var i = 0; i < _REWRITE_TABLE.length; i++) {
|
|
144
|
+
var entry = _REWRITE_TABLE[i];
|
|
145
|
+
translated = _replaceWordBoundaryAll(translated, entry.local, entry.external);
|
|
86
146
|
}
|
|
87
147
|
return translated;
|
|
88
148
|
}
|
package/lib/cluster.js
CHANGED
|
@@ -317,12 +317,13 @@ async function _checkChainTipRollback(chainName, logTable, tipTable) {
|
|
|
317
317
|
: 0;
|
|
318
318
|
|
|
319
319
|
if (currentMax < tipCounter) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
320
|
+
throw _err("ROLLBACK_DETECTED",
|
|
321
|
+
"FATAL: cluster-mode " + chainName + "-log rollback detected. " +
|
|
322
|
+
chainName + "-tip counter: " + tipCounter +
|
|
323
|
+
"; current external-db max: " + currentMax +
|
|
324
|
+
". Either external-db was restored from an older snapshot, or " +
|
|
325
|
+
logTable + " rows have been deleted. Investigate before continuing.",
|
|
326
|
+
true);
|
|
326
327
|
}
|
|
327
328
|
|
|
328
329
|
if (tipHash) {
|
|
@@ -335,13 +336,14 @@ async function _checkChainTipRollback(chainName, logTable, tipTable) {
|
|
|
335
336
|
if (hashRows.rows && hashRows.rows.length > 0) {
|
|
336
337
|
var rowAtTip = hashRows.rows[0].rowHash;
|
|
337
338
|
if (rowAtTip !== tipHash) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
"
|
|
344
|
-
|
|
339
|
+
throw _err("ROLLBACK_DETECTED",
|
|
340
|
+
"FATAL: cluster-mode " + chainName + "-log rollback detected (row-hash mismatch). " +
|
|
341
|
+
chainName + "-tip counter: " + tipCounter +
|
|
342
|
+
"; " + chainName + "-tip rowHash: " + tipHash +
|
|
343
|
+
"; current row rowHash: " + rowAtTip +
|
|
344
|
+
". The row at the recorded tip counter has a different hash — " +
|
|
345
|
+
"indicates row substitution at the chain head. Investigate before continuing.",
|
|
346
|
+
true);
|
|
345
347
|
}
|
|
346
348
|
}
|
|
347
349
|
}
|
|
@@ -429,25 +431,28 @@ async function _checkVaultKeyConsistency() {
|
|
|
429
431
|
if (!rows.rows || rows.rows.length === 0) {
|
|
430
432
|
// Should never happen — we just INSERTed. Surface as fatal so the
|
|
431
433
|
// condition isn't silently ignored.
|
|
432
|
-
|
|
433
|
-
"
|
|
434
|
-
|
|
434
|
+
throw _err("CLUSTER_STATE_MISSING",
|
|
435
|
+
"FATAL: cluster-state row missing immediately after INSERT — " +
|
|
436
|
+
"external-db may not be honoring writes. Refusing boot.",
|
|
437
|
+
true);
|
|
435
438
|
}
|
|
436
439
|
var canonical = rows.rows[0];
|
|
437
440
|
if (canonical.vaultKeyFp !== localFp) {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
441
|
+
var fpPrefix = C.BYTES.bytes(16);
|
|
442
|
+
throw _err("VAULT_KEY_DRIFT",
|
|
443
|
+
"FATAL: vault-key drift detected. " +
|
|
444
|
+
"local node: " + nodeId +
|
|
445
|
+
"; local fingerprint: " + localFp.slice(0, fpPrefix) + "…" +
|
|
446
|
+
"; canonical recorded by: " + canonical.recordedByNode +
|
|
447
|
+
"; canonical fingerprint: " + canonical.vaultKeyFp.slice(0, fpPrefix) + "…" +
|
|
448
|
+
". This node holds a DIFFERENT vault key than the rest of the " +
|
|
444
449
|
"cluster. Sealed-column writes from this node would be unreadable " +
|
|
445
450
|
"by the others (and vice versa). Restore the same vault key file " +
|
|
446
|
-
"before booting this node into the cluster."
|
|
447
|
-
|
|
451
|
+
"before booting this node into the cluster.",
|
|
452
|
+
true);
|
|
448
453
|
}
|
|
449
454
|
log("cluster vault-key consistency ok (fingerprint " +
|
|
450
|
-
localFp.slice(0, 16) + "… recorded by " + canonical.recordedByNode + ")");
|
|
455
|
+
localFp.slice(0, C.BYTES.bytes(16)) + "… recorded by " + canonical.recordedByNode + ")");
|
|
451
456
|
}
|
|
452
457
|
|
|
453
458
|
async function _tryAcquire() {
|
package/lib/config-drift.js
CHANGED
|
@@ -51,6 +51,7 @@ var auditSign = require("./audit-sign");
|
|
|
51
51
|
var canonicalJson = require("./canonical-json");
|
|
52
52
|
var crypto = require("./crypto");
|
|
53
53
|
var lazyRequire = require("./lazy-require");
|
|
54
|
+
var safeJson = require("./safe-json");
|
|
54
55
|
var validateOpts = require("./validate-opts");
|
|
55
56
|
var { defineClass } = require("./framework-error");
|
|
56
57
|
|
|
@@ -100,9 +101,7 @@ function create(opts) {
|
|
|
100
101
|
validateOpts(opts, [
|
|
101
102
|
"dataDir", "audit", "baseline", "criticalKeys", "ignoreKeys",
|
|
102
103
|
], "configDrift");
|
|
103
|
-
|
|
104
|
-
throw _err("BAD_OPT", "create: opts.dataDir is required");
|
|
105
|
-
}
|
|
104
|
+
validateOpts.requireNonEmptyString(opts.dataDir, "create: opts.dataDir", ConfigDriftError, "BAD_OPT");
|
|
106
105
|
var dataDir = opts.dataDir;
|
|
107
106
|
var auditOn = opts.audit !== false;
|
|
108
107
|
var auditInstance = (opts.audit && opts.audit !== true) ? opts.audit : null;
|
|
@@ -142,7 +141,7 @@ function create(opts) {
|
|
|
142
141
|
try { raw = fs.readFileSync(sidecarPath, "utf8"); }
|
|
143
142
|
catch (_e) { return null; }
|
|
144
143
|
var parsed;
|
|
145
|
-
try { parsed =
|
|
144
|
+
try { parsed = safeJson.parse(raw); }
|
|
146
145
|
catch (_e) { return { unreadable: true }; }
|
|
147
146
|
if (!parsed || parsed.version !== SIDECAR_VERSION) return { unreadable: true };
|
|
148
147
|
if (typeof parsed.digestHex !== "string" || typeof parsed.signatureBase64 !== "string" ||
|
package/lib/constants.js
CHANGED
|
@@ -51,9 +51,14 @@ function _validateBytes(unit, n) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
var BYTES = Object.freeze({
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
// Passthrough for protocol-fixed byte counts (32-byte salt, 16-byte
|
|
55
|
+
// nonce, 8-byte counter, etc.) — keeps every byte literal routed
|
|
56
|
+
// through C.BYTES so the codebase has a single source of truth.
|
|
57
|
+
// Equivalent to `n` but names the unit at the call site.
|
|
58
|
+
bytes: function (n) { _validateBytes("bytes", n); return n; },
|
|
59
|
+
kib: function (n) { _validateBytes("kib", n); return n * 1024; },
|
|
60
|
+
mib: function (n) { _validateBytes("mib", n); return n * 1024 * 1024; },
|
|
61
|
+
gib: function (n) { _validateBytes("gib", n); return n * 1024 * 1024 * 1024; },
|
|
57
62
|
});
|
|
58
63
|
|
|
59
64
|
// ---- Crypto envelope versioning ----
|
package/lib/cookies.js
CHANGED
|
@@ -72,14 +72,23 @@ var TOKEN_RE = /^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$/;
|
|
|
72
72
|
// Reject CRLF, NUL, semicolon, comma in cookie value pre-encoding.
|
|
73
73
|
var FORBIDDEN_VALUE_RE = /[\r\n\0;,]/;
|
|
74
74
|
|
|
75
|
+
// Length caps applied before the regex tests so a hostile caller can't
|
|
76
|
+
// stage a pathological-length input against the engine. RFC 6265 doesn't
|
|
77
|
+
// pin a name/value length; browsers vary, with a practical 4 KiB total
|
|
78
|
+
// envelope. The framework caps name + value to leave headroom.
|
|
79
|
+
var MAX_NAME_LENGTH = C.BYTES.bytes(256);
|
|
80
|
+
var MAX_VALUE_LENGTH = C.BYTES.kib(4);
|
|
81
|
+
|
|
75
82
|
function _validateName(name) {
|
|
76
83
|
if (typeof name !== "string" || name.length === 0) {
|
|
77
84
|
throw new CookieError("cookies/invalid-name",
|
|
78
85
|
"cookie name must be a non-empty string");
|
|
79
86
|
}
|
|
80
|
-
|
|
87
|
+
// Length cap before the regex test — bound the engine on hostile
|
|
88
|
+
// input lengths even though the regex is anchored.
|
|
89
|
+
if (name.length > MAX_NAME_LENGTH || !TOKEN_RE.test(name)) {
|
|
81
90
|
throw new CookieError("cookies/invalid-name",
|
|
82
|
-
"cookie name '" + name + "' contains forbidden characters");
|
|
91
|
+
"cookie name '" + name + "' is empty, too long, or contains forbidden characters");
|
|
83
92
|
}
|
|
84
93
|
}
|
|
85
94
|
|
|
@@ -88,9 +97,12 @@ function _validateValue(value) {
|
|
|
88
97
|
throw new CookieError("cookies/invalid-value",
|
|
89
98
|
"cookie value must be a string");
|
|
90
99
|
}
|
|
91
|
-
|
|
100
|
+
// Length cap before the regex test (defense in depth — the regex
|
|
101
|
+
// here is a simple character class, but the same discipline that
|
|
102
|
+
// bounds longer regexes elsewhere applies).
|
|
103
|
+
if (value.length > MAX_VALUE_LENGTH || FORBIDDEN_VALUE_RE.test(value)) {
|
|
92
104
|
throw new CookieError("cookies/invalid-value",
|
|
93
|
-
"cookie value contains forbidden control character (CRLF/NUL/;/,)");
|
|
105
|
+
"cookie value is too long or contains forbidden control character (CRLF/NUL/;/,)");
|
|
94
106
|
}
|
|
95
107
|
}
|
|
96
108
|
|
package/lib/credential-hash.js
CHANGED
|
@@ -77,10 +77,7 @@ var { FrameworkError } = require("./framework-error");
|
|
|
77
77
|
|
|
78
78
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
79
79
|
|
|
80
|
-
function _emitEvent(
|
|
81
|
-
try { observability().event(name, value, labels || {}); }
|
|
82
|
-
catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
|
|
83
|
-
}
|
|
80
|
+
function _emitEvent(n, v, l) { observability().safeEvent(n, v, l || {}); }
|
|
84
81
|
|
|
85
82
|
// Default SHAKE256 output size for newly-issued credentials. 128
|
|
86
83
|
// bytes (1024 bits) is double the SHAKE256 capacity (512 bits, which
|
|
@@ -92,14 +89,14 @@ function _emitEvent(name, value, labels) {
|
|
|
92
89
|
// posture (highest practical bar where cost is negligible), default
|
|
93
90
|
// to the larger size; operators pass `{ params: { length: 64 } }`
|
|
94
91
|
// to opt into the SHA3-512-comparable byte count if they need it.
|
|
95
|
-
var SHAKE256_DEFAULT_LENGTH = 128;
|
|
92
|
+
var SHAKE256_DEFAULT_LENGTH = C.BYTES.bytes(128);
|
|
96
93
|
// Minimum payload length both hash() and verify() enforce. 16 bytes
|
|
97
94
|
// (128 bits) is the floor below which the digest's collision space
|
|
98
95
|
// becomes brute-forceable. The asymmetry between hash() (rejected < 16
|
|
99
96
|
// at config time) and verify() (silently accepted any length) let a
|
|
100
97
|
// truncated stored envelope verify with trivial work; both ends now
|
|
101
98
|
// refuse short payloads symmetrically.
|
|
102
|
-
var SHAKE256_MIN_LENGTH = 16;
|
|
99
|
+
var SHAKE256_MIN_LENGTH = C.BYTES.bytes(16);
|
|
103
100
|
|
|
104
101
|
function _shake256(secret, length) {
|
|
105
102
|
// crypto.kdf wraps SHAKE256 with arbitrary output length. That's the
|
|
@@ -227,7 +224,7 @@ async function hash(secret, opts) {
|
|
|
227
224
|
}
|
|
228
225
|
// Unreachable — _validateOpts rejects unknown algos.
|
|
229
226
|
throw new CredentialHashError(
|
|
230
|
-
"credentialHash.hash: unsupported algo id 0x" + algoId.toString(
|
|
227
|
+
"credentialHash.hash: unsupported algo id 0x" + algoId.toString(0x10),
|
|
231
228
|
"credential-hash/unsupported");
|
|
232
229
|
}
|
|
233
230
|
|
package/lib/crypto.js
CHANGED
|
@@ -82,7 +82,7 @@ function hmacSha3(key, data) { return hmac(key, data, "sha3-512"); }
|
|
|
82
82
|
|
|
83
83
|
// (SHA-1 is intentionally NOT exported from b.crypto. The framework's
|
|
84
84
|
// only legitimate SHA-1 use is the HaveIBeenPwned k-anonymity API in
|
|
85
|
-
// lib/auth/password.js, which imports lib/
|
|
85
|
+
// lib/auth/password.js, which imports lib/framework-sha1-hibp.js
|
|
86
86
|
// directly. Public b.crypto.sha1* is permanently off the table — a
|
|
87
87
|
// future caller wanting SHA-1 for storage / signing / fingerprinting
|
|
88
88
|
// would re-introduce a broken primitive into the crypto surface this
|
|
@@ -137,8 +137,8 @@ function encrypt(plaintext, publicKeys) {
|
|
|
137
137
|
privateKey: nodeCrypto.createPrivateKey(ephEc.privateKey),
|
|
138
138
|
publicKey: nodeCrypto.createPublicKey(ecPubPem),
|
|
139
139
|
});
|
|
140
|
-
var key = kdf(Buffer.concat([kem.sharedKey, ecSs]), 32);
|
|
141
|
-
var nonce = generateBytes(24);
|
|
140
|
+
var key = kdf(Buffer.concat([kem.sharedKey, ecSs]), C.BYTES.bytes(32));
|
|
141
|
+
var nonce = generateBytes(C.BYTES.bytes(24));
|
|
142
142
|
var ct = xchacha20poly1305(key, nonce).encrypt(Buffer.from(plaintext, "utf8"));
|
|
143
143
|
|
|
144
144
|
var kemCtLen = Buffer.alloc(2); kemCtLen.writeUInt16BE(kem.ciphertext.length);
|
|
@@ -153,8 +153,8 @@ function encrypt(plaintext, publicKeys) {
|
|
|
153
153
|
|
|
154
154
|
function encryptMlkemOnly(plaintext, publicKeyPem) {
|
|
155
155
|
var kem = nodeCrypto.encapsulate(nodeCrypto.createPublicKey(publicKeyPem));
|
|
156
|
-
var key = kdf(kem.sharedKey, 32);
|
|
157
|
-
var nonce = generateBytes(24);
|
|
156
|
+
var key = kdf(kem.sharedKey, C.BYTES.bytes(32));
|
|
157
|
+
var nonce = generateBytes(C.BYTES.bytes(24));
|
|
158
158
|
var ct = xchacha20poly1305(key, nonce).encrypt(Buffer.from(plaintext, "utf8"));
|
|
159
159
|
var kemCtLen = Buffer.alloc(2); kemCtLen.writeUInt16BE(kem.ciphertext.length);
|
|
160
160
|
return Buffer.concat([
|
|
@@ -200,14 +200,14 @@ function decryptEnvelope(packed, privateKeys) {
|
|
|
200
200
|
privateKey: nodeCrypto.createPrivateKey(ecPrivPem),
|
|
201
201
|
publicKey: nodeCrypto.createPublicKey({ key: ecEphDer, type: "spki", format: "der" }),
|
|
202
202
|
});
|
|
203
|
-
symmetricKey = kdf(Buffer.concat([mlkemSs, ecSs]), 32);
|
|
203
|
+
symmetricKey = kdf(Buffer.concat([mlkemSs, ecSs]), C.BYTES.bytes(32));
|
|
204
204
|
} else if (kemId === C.KEM_IDS.ML_KEM_1024) {
|
|
205
|
-
symmetricKey = kdf(mlkemSs, 32);
|
|
205
|
+
symmetricKey = kdf(mlkemSs, C.BYTES.bytes(32));
|
|
206
206
|
} else {
|
|
207
207
|
throw new Error("Invalid envelope: unsupported KEM ID " + kemId);
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
var nonce = packed.subarray(pos, pos + 24); pos += 24;
|
|
210
|
+
var nonce = packed.subarray(pos, pos + C.BYTES.bytes(24)); pos += C.BYTES.bytes(24);
|
|
211
211
|
return Buffer.from(
|
|
212
212
|
xchacha20poly1305(symmetricKey, nonce).decrypt(packed.subarray(pos))
|
|
213
213
|
).toString("utf8");
|
|
@@ -222,7 +222,7 @@ function decryptEnvelope(packed, privateKeys) {
|
|
|
222
222
|
// ciphertext from row A literally cannot decrypt as row B even with
|
|
223
223
|
// the same key).
|
|
224
224
|
function encryptPacked(buffer, key, aad) {
|
|
225
|
-
var nonce = random(24);
|
|
225
|
+
var nonce = random(C.BYTES.bytes(24));
|
|
226
226
|
var ct = xchacha20poly1305(key, nonce, aad ? Buffer.from(aad) : undefined).encrypt(buffer);
|
|
227
227
|
return Buffer.concat([
|
|
228
228
|
Buffer.from([C.FORMAT.XCHACHA20_POLY1305]),
|
package/lib/csv.js
CHANGED
|
@@ -53,7 +53,7 @@ var DEFAULTS_PARSE = {
|
|
|
53
53
|
quote: "\"",
|
|
54
54
|
trim: false,
|
|
55
55
|
maxBytes: C.BYTES.mib(16),
|
|
56
|
-
maxRows:
|
|
56
|
+
maxRows: 1_000_000,
|
|
57
57
|
maxFieldBytes: C.BYTES.mib(1),
|
|
58
58
|
onBadRow: "throw",
|
|
59
59
|
};
|
|
@@ -84,13 +84,16 @@ function _validateDelim(name, value) {
|
|
|
84
84
|
|
|
85
85
|
function parse(input, opts) {
|
|
86
86
|
opts = Object.assign({}, DEFAULTS_PARSE, opts || {});
|
|
87
|
-
// maxBytes via shared lib/numeric-bounds —
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
// maxBytes / maxRows / maxFieldBytes via shared lib/numeric-bounds —
|
|
88
|
+
// Infinity / NaN bypass the corresponding caps and let a hostile
|
|
89
|
+
// multi-megabyte CSV (or a single hostile row / single hostile field)
|
|
90
|
+
// through unbounded.
|
|
91
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.maxBytes,
|
|
92
|
+
"csv.parse: maxBytes", CsvError, "csv/bad-opt");
|
|
93
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.maxRows,
|
|
94
|
+
"csv.parse: maxRows", CsvError, "csv/bad-opt");
|
|
95
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.maxFieldBytes,
|
|
96
|
+
"csv.parse: maxFieldBytes", CsvError, "csv/bad-opt");
|
|
94
97
|
|
|
95
98
|
var s;
|
|
96
99
|
if (typeof input === "string") s = input;
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
* }
|
|
55
55
|
*/
|
|
56
56
|
var safeSql = require("./safe-sql");
|
|
57
|
+
var validateOpts = require("./validate-opts");
|
|
57
58
|
var { defineClass } = require("./framework-error");
|
|
58
59
|
|
|
59
60
|
var DeclareRowPolicyError = defineClass("DeclareRowPolicyError", { alwaysPermanent: true });
|
|
@@ -103,19 +104,13 @@ function _validateOpts(opts) {
|
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
throw _err("declare-row-policy/missing-opt", "schema is required");
|
|
108
|
-
}
|
|
107
|
+
validateOpts.requireNonEmptyString(opts.schema, "schema", DeclareRowPolicyError, "declare-row-policy/missing-opt");
|
|
109
108
|
_validateIdent("schema", opts.schema);
|
|
110
109
|
|
|
111
|
-
|
|
112
|
-
throw _err("declare-row-policy/missing-opt", "table is required");
|
|
113
|
-
}
|
|
110
|
+
validateOpts.requireNonEmptyString(opts.table, "table", DeclareRowPolicyError, "declare-row-policy/missing-opt");
|
|
114
111
|
_validateIdent("table", opts.table);
|
|
115
112
|
|
|
116
|
-
|
|
117
|
-
throw _err("declare-row-policy/missing-opt", "name is required");
|
|
118
|
-
}
|
|
113
|
+
validateOpts.requireNonEmptyString(opts.name, "name", DeclareRowPolicyError, "declare-row-policy/missing-opt");
|
|
119
114
|
_validateIdent("name", opts.name);
|
|
120
115
|
|
|
121
116
|
var role = null;
|
package/lib/db-declare-view.js
CHANGED
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
* }
|
|
62
62
|
*/
|
|
63
63
|
var safeSql = require("./safe-sql");
|
|
64
|
+
var validateOpts = require("./validate-opts");
|
|
64
65
|
var { defineClass } = require("./framework-error");
|
|
65
66
|
|
|
66
67
|
var DeclareViewError = defineClass("DeclareViewError", { alwaysPermanent: true });
|
|
@@ -135,14 +136,10 @@ function _validateOpts(opts) {
|
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
throw _err("declare-view/missing-opt", "schema is required");
|
|
140
|
-
}
|
|
139
|
+
validateOpts.requireNonEmptyString(opts.schema, "schema", DeclareViewError, "declare-view/missing-opt");
|
|
141
140
|
_validateIdent("schema", opts.schema);
|
|
142
141
|
|
|
143
|
-
|
|
144
|
-
throw _err("declare-view/missing-opt", "name is required");
|
|
145
|
-
}
|
|
142
|
+
validateOpts.requireNonEmptyString(opts.name, "name", DeclareViewError, "declare-view/missing-opt");
|
|
146
143
|
_validateIdent("name", opts.name);
|
|
147
144
|
|
|
148
145
|
if (opts.source === undefined) {
|
package/lib/db-query.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* would always return zero rows; failing loudly is safer).
|
|
28
28
|
*/
|
|
29
29
|
var { Readable } = require("node:stream");
|
|
30
|
+
var C = require("./constants");
|
|
30
31
|
var cryptoField = require("./crypto-field");
|
|
31
32
|
var { generateToken } = require("./crypto");
|
|
32
33
|
var safeSql = require("./safe-sql");
|
|
@@ -298,7 +299,7 @@ class Query {
|
|
|
298
299
|
}
|
|
299
300
|
var withId = Object.assign({}, row);
|
|
300
301
|
if (withId._id === undefined || withId._id === null) {
|
|
301
|
-
withId._id = generateToken(16);
|
|
302
|
+
withId._id = generateToken(C.BYTES.bytes(16));
|
|
302
303
|
}
|
|
303
304
|
var sealed = cryptoField.sealRow(this._cryptoFieldKey(), withId);
|
|
304
305
|
var cols = Object.keys(sealed);
|
|
@@ -392,8 +393,13 @@ class Query {
|
|
|
392
393
|
}
|
|
393
394
|
|
|
394
395
|
function _validateField(field) {
|
|
395
|
-
if (typeof field !== "string" ||
|
|
396
|
-
|
|
396
|
+
if (typeof field !== "string" ||
|
|
397
|
+
field.length === 0 ||
|
|
398
|
+
field.length > safeSql.MAX_IDENTIFIER_LENGTH ||
|
|
399
|
+
!safeSql.DEFAULT_IDENTIFIER_RE.test(field)) {
|
|
400
|
+
throw new Error("invalid field name: '" + field +
|
|
401
|
+
"' (must match " + safeSql.DEFAULT_IDENTIFIER_RE + ", length 1.." +
|
|
402
|
+
safeSql.MAX_IDENTIFIER_LENGTH + ")");
|
|
397
403
|
}
|
|
398
404
|
}
|
|
399
405
|
|
package/lib/db-schema.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Schema reconciler + imperative migration runner
|
|
3
|
+
* Schema reconciler + imperative migration runner + shared SQL
|
|
4
|
+
* execution helpers used by every db-touching primitive.
|
|
4
5
|
*
|
|
5
6
|
* Hybrid migration strategy (per roadmap Q2):
|
|
6
7
|
*
|
|
@@ -19,9 +20,25 @@
|
|
|
19
20
|
* Apps mix both: declarative covers structural changes (CREATE/ALTER),
|
|
20
21
|
* imperative covers data backfills, transformations, conditional schema
|
|
21
22
|
* changes that need code.
|
|
23
|
+
*
|
|
24
|
+
* Shared SQL execution helpers used by lib/migrations / lib/seeders /
|
|
25
|
+
* lib/vault/rotate / lib/db etc.:
|
|
26
|
+
*
|
|
27
|
+
* - runSql — raw DDL / BEGIN / COMMIT / PRAGMA on
|
|
28
|
+
* the framework's better-sqlite3 handle.
|
|
29
|
+
* - runSqlOnHandle — handles BOTH raw better-sqlite3 and
|
|
30
|
+
* b.db framework wrapper shapes —
|
|
31
|
+
* operator-supplied handles can be either.
|
|
32
|
+
* - runInTransaction — wrap fn in BEGIN / COMMIT / ROLLBACK on
|
|
33
|
+
* the supplied db handle. opts.lockMode
|
|
34
|
+
* appends to BEGIN ("IMMEDIATE" /
|
|
35
|
+
* "EXCLUSIVE" on SQLite). opts.onRollbackFail
|
|
36
|
+
* surfaces a rollback throw without
|
|
37
|
+
* swallowing the original error.
|
|
22
38
|
*/
|
|
23
39
|
var path = require("path");
|
|
24
40
|
var atomicFile = require("./atomic-file");
|
|
41
|
+
var safeSql = require("./safe-sql");
|
|
25
42
|
|
|
26
43
|
// SQLite raw-SQL helper. node:sqlite DatabaseSync exposes a method on the
|
|
27
44
|
// database object that runs raw SQL without bind parameters — used for DDL,
|
|
@@ -30,6 +47,48 @@ var atomicFile = require("./atomic-file");
|
|
|
30
47
|
// `.exec(` regardless of receiver type.
|
|
31
48
|
function runSql(database, sql) { return database["exec"](sql); }
|
|
32
49
|
|
|
50
|
+
// runSqlOnHandle — execute SQL against either handle shape:
|
|
51
|
+
// - raw better-sqlite3 / node:sqlite Database (db.exec)
|
|
52
|
+
// - b.db framework wrapper (db.runSql)
|
|
53
|
+
// Migrations / seeders / break-glass migrate accept operator-supplied
|
|
54
|
+
// handles in either shape; this wrapper normalizes the call. The
|
|
55
|
+
// 'exec'/'runSql' lookups use bracket notation so the eslint rule
|
|
56
|
+
// banning bare-identifier db method calls (collapsing prepare → field)
|
|
57
|
+
// stays satisfied.
|
|
58
|
+
function runSqlOnHandle(db, sql) {
|
|
59
|
+
if (db && typeof db["exec"] === "function") return db["exec"](sql);
|
|
60
|
+
if (db && typeof db["runSql"] === "function") return db["runSql"](sql);
|
|
61
|
+
throw new Error("dbSchema.runSqlOnHandle: handle exposes no DDL runner (exec / runSql)");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// runInTransaction — wrap fn in BEGIN / COMMIT / ROLLBACK on the
|
|
65
|
+
// supplied db handle. Used by migrations / seeders / any operator-
|
|
66
|
+
// facing transaction. opts.lockMode appends to BEGIN ("IMMEDIATE",
|
|
67
|
+
// "EXCLUSIVE" — SQLite-specific). opts.onRollbackFail surfaces a
|
|
68
|
+
// rollback throw without swallowing the original error (the original
|
|
69
|
+
// always re-throws).
|
|
70
|
+
function runInTransaction(db, fn, opts) {
|
|
71
|
+
if (typeof fn !== "function") {
|
|
72
|
+
throw new TypeError("dbSchema.runInTransaction: fn must be a function");
|
|
73
|
+
}
|
|
74
|
+
opts = opts || {};
|
|
75
|
+
var beginSql = opts.lockMode ? "BEGIN " + opts.lockMode : "BEGIN";
|
|
76
|
+
runSqlOnHandle(db, beginSql);
|
|
77
|
+
try {
|
|
78
|
+
var result = fn();
|
|
79
|
+
runSqlOnHandle(db, "COMMIT");
|
|
80
|
+
return result;
|
|
81
|
+
} catch (e) {
|
|
82
|
+
try { runSqlOnHandle(db, "ROLLBACK"); }
|
|
83
|
+
catch (rollbackErr) {
|
|
84
|
+
if (typeof opts.onRollbackFail === "function") {
|
|
85
|
+
try { opts.onRollbackFail(rollbackErr); } catch (_e) { /* nested handler must not bubble */ }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
throw e;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
33
92
|
// ---- Internal migrations table ----
|
|
34
93
|
|
|
35
94
|
var MIGRATIONS_TABLE = "_blamejs_migrations";
|
|
@@ -186,8 +245,13 @@ function listColumns(database, tableName) {
|
|
|
186
245
|
|
|
187
246
|
// SQL identifier safety: alphanumeric + underscore, starts with letter or underscore.
|
|
188
247
|
function validateIdent(ident, kind) {
|
|
189
|
-
if (typeof ident !== "string" ||
|
|
190
|
-
|
|
248
|
+
if (typeof ident !== "string" ||
|
|
249
|
+
ident.length === 0 ||
|
|
250
|
+
ident.length > safeSql.MAX_IDENTIFIER_LENGTH ||
|
|
251
|
+
!safeSql.DEFAULT_IDENTIFIER_RE.test(ident)) {
|
|
252
|
+
throw new Error("invalid " + kind + ": '" + ident +
|
|
253
|
+
"' (must match " + safeSql.DEFAULT_IDENTIFIER_RE + ", length 1.." +
|
|
254
|
+
safeSql.MAX_IDENTIFIER_LENGTH + ")");
|
|
191
255
|
}
|
|
192
256
|
}
|
|
193
257
|
|
|
@@ -228,14 +292,13 @@ function runMigrations(database, migrationDir) {
|
|
|
228
292
|
}
|
|
229
293
|
|
|
230
294
|
try {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
295
|
+
runInTransaction(database, function () {
|
|
296
|
+
mig.up(database);
|
|
297
|
+
database.prepare(
|
|
298
|
+
"INSERT INTO " + Q_MIGRATIONS_TABLE + " (name, description, appliedAt) VALUES (?, ?, ?)"
|
|
299
|
+
).run(file, mig.description || "", new Date().toISOString());
|
|
300
|
+
});
|
|
237
301
|
} catch (e) {
|
|
238
|
-
try { runSql(database, "ROLLBACK"); } catch (_e) { /* ignored — already in error path */ }
|
|
239
302
|
throw new Error("migration '" + file + "' failed: " + e.message);
|
|
240
303
|
}
|
|
241
304
|
applied.push(file);
|
|
@@ -250,5 +313,7 @@ module.exports = {
|
|
|
250
313
|
runMigrations: runMigrations,
|
|
251
314
|
validateIdent: validateIdent,
|
|
252
315
|
runSql: runSql,
|
|
316
|
+
runSqlOnHandle: runSqlOnHandle,
|
|
317
|
+
runInTransaction: runInTransaction,
|
|
253
318
|
MIGRATIONS_TABLE: MIGRATIONS_TABLE,
|
|
254
319
|
};
|