@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/uuid.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* column (jobs queue, audit chain extensions, anything where insertion
|
|
29
29
|
* order matters for index locality).
|
|
30
30
|
*/
|
|
31
|
+
var C = require("./constants");
|
|
31
32
|
var { generateBytes } = require("./crypto");
|
|
32
33
|
|
|
33
34
|
// Canonical UUID layout: 8-4-4-4-12 hex digits, version nibble at byte
|
|
@@ -38,21 +39,40 @@ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-7][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a
|
|
|
38
39
|
// gating use `parse()`.
|
|
39
40
|
var UUID_LOOSE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
40
41
|
|
|
42
|
+
// RFC 4122 §4.1 byte counts and field positions. UUID is 16 bytes; the
|
|
43
|
+
// canonical hex form is 32 chars with dashes between the time-low (4 B),
|
|
44
|
+
// time-mid (2 B), time-hi+version (2 B), clock-seq+variant (2 B) and
|
|
45
|
+
// node (6 B) fields. Maximum length of the canonical form is 36 chars.
|
|
46
|
+
var UUID_BYTE_LEN = C.BYTES.bytes(16);
|
|
47
|
+
var UUID_STR_MAX_LEN = 36;
|
|
48
|
+
// Hex-string slice offsets (each pair = 1 byte); routed through C.BYTES
|
|
49
|
+
// so the framework's byte math stays in one place.
|
|
50
|
+
var HEX_TIME_LOW_END = C.BYTES.bytes(8); // chars 0..8 (bytes 0-3)
|
|
51
|
+
var HEX_TIME_MID_END = C.BYTES.bytes(12); // chars 8..12 (bytes 4-5)
|
|
52
|
+
var HEX_TIME_HI_VER_END = C.BYTES.bytes(16); // chars 12..16 (bytes 6-7)
|
|
53
|
+
var HEX_CLOCK_SEQ_END = C.BYTES.bytes(20); // chars 16..20 (bytes 8-9)
|
|
54
|
+
var HEX_NODE_END = C.BYTES.bytes(32); // chars 20..32 (bytes 10-15)
|
|
55
|
+
// Byte-index positions for version + variant manipulation. Variant
|
|
56
|
+
// index value (8) is routed through C.BYTES so every byte literal in
|
|
57
|
+
// the file routes through one helper.
|
|
58
|
+
var BYTE_VERSION_IDX = 6;
|
|
59
|
+
var BYTE_VARIANT_IDX = C.BYTES.bytes(8);
|
|
60
|
+
|
|
41
61
|
function _bytesToString(bytes) {
|
|
42
62
|
var hex = bytes.toString("hex");
|
|
43
|
-
return hex.slice(0,
|
|
44
|
-
hex.slice(
|
|
45
|
-
hex.slice(
|
|
46
|
-
hex.slice(
|
|
47
|
-
hex.slice(
|
|
63
|
+
return hex.slice(0, HEX_TIME_LOW_END) + "-" +
|
|
64
|
+
hex.slice(HEX_TIME_LOW_END, HEX_TIME_MID_END) + "-" +
|
|
65
|
+
hex.slice(HEX_TIME_MID_END, HEX_TIME_HI_VER_END) + "-" +
|
|
66
|
+
hex.slice(HEX_TIME_HI_VER_END, HEX_CLOCK_SEQ_END) + "-" +
|
|
67
|
+
hex.slice(HEX_CLOCK_SEQ_END, HEX_NODE_END);
|
|
48
68
|
}
|
|
49
69
|
|
|
50
70
|
function v4() {
|
|
51
|
-
var b = generateBytes(
|
|
71
|
+
var b = generateBytes(UUID_BYTE_LEN);
|
|
52
72
|
// version = 4 (0100): high nibble of byte 6
|
|
53
|
-
b[
|
|
73
|
+
b[BYTE_VERSION_IDX] = (b[BYTE_VERSION_IDX] & 0x0f) | 0x40;
|
|
54
74
|
// variant = RFC 4122 (10xx): top two bits of byte 8
|
|
55
|
-
b[
|
|
75
|
+
b[BYTE_VARIANT_IDX] = (b[BYTE_VARIANT_IDX] & 0x3f) | 0x80;
|
|
56
76
|
return _bytesToString(b);
|
|
57
77
|
}
|
|
58
78
|
|
|
@@ -62,39 +82,46 @@ function v7(opts) {
|
|
|
62
82
|
// bytes 6-7 : version nibble (7) + 12 bits random_a
|
|
63
83
|
// bytes 8-15 : variant bits + 62 bits random_b
|
|
64
84
|
var ms = (opts && typeof opts.now === "number") ? opts.now : Date.now();
|
|
65
|
-
var b = generateBytes(
|
|
66
|
-
// 48-bit ms timestamp (big-endian) into bytes 0-5
|
|
67
|
-
// ms can exceed 2^32 (we're in 2026, ms is ~1.78e12), so
|
|
68
|
-
|
|
69
|
-
var
|
|
85
|
+
var b = generateBytes(UUID_BYTE_LEN);
|
|
86
|
+
// 48-bit ms timestamp (big-endian) into bytes 0-5.
|
|
87
|
+
// ms can exceed 2^32 (we're in 2026, ms is ~1.78e12), so split via
|
|
88
|
+
// Math.floor + unsigned shift instead of relying on 32-bit bit-ops.
|
|
89
|
+
var msHi = Math.floor(ms / 0x100000000);
|
|
90
|
+
var msLo = ms >>> 0;
|
|
70
91
|
b[0] = (msHi >> 8) & 0xff;
|
|
71
92
|
b[1] = msHi & 0xff;
|
|
72
93
|
b[2] = (msLo >>> 24) & 0xff;
|
|
73
94
|
b[3] = (msLo >>> 16) & 0xff;
|
|
74
|
-
b[4] = (msLo >>>
|
|
95
|
+
b[4] = (msLo >>> 8) & 0xff;
|
|
75
96
|
b[5] = msLo & 0xff;
|
|
76
97
|
// version = 7 (0111) in high nibble of byte 6, random_a in low nibble + byte 7
|
|
77
|
-
b[
|
|
98
|
+
b[BYTE_VERSION_IDX] = (b[BYTE_VERSION_IDX] & 0x0f) | 0x70;
|
|
78
99
|
// variant = RFC 4122 (10xx) in top two bits of byte 8
|
|
79
|
-
b[
|
|
100
|
+
b[BYTE_VARIANT_IDX] = (b[BYTE_VARIANT_IDX] & 0x3f) | 0x80;
|
|
80
101
|
return _bytesToString(b);
|
|
81
102
|
}
|
|
82
103
|
|
|
83
104
|
function parse(str) {
|
|
84
105
|
if (typeof str !== "string") return { ok: false, reason: "not-a-string" };
|
|
85
|
-
|
|
106
|
+
// Length cap before regex — RFC 4122 canonical form is exactly 36
|
|
107
|
+
// chars; capping defends the regex engine against pathological-length
|
|
108
|
+
// inputs even though UUID_RE is anchored.
|
|
109
|
+
if (str.length > UUID_STR_MAX_LEN) return { ok: false, reason: "malformed" };
|
|
110
|
+
if (!UUID_RE.test(str)) return { ok: false, reason: "malformed" };
|
|
86
111
|
var hex = str.replace(/-/g, "");
|
|
87
112
|
var bytes = Buffer.from(hex, "hex");
|
|
88
113
|
// Version is the high nibble of byte 6.
|
|
89
|
-
var version = (bytes[
|
|
114
|
+
var version = (bytes[BYTE_VERSION_IDX] >> 4) & 0x0f;
|
|
90
115
|
// Variant: top two bits of byte 8 must be 10 for RFC 4122 / 9562.
|
|
91
|
-
var variant = (bytes[
|
|
116
|
+
var variant = (bytes[BYTE_VARIANT_IDX] >> 6) & 0x03;
|
|
92
117
|
if (variant !== 0b10) return { ok: false, reason: "bad-variant" };
|
|
93
118
|
return { ok: true, version: version, bytes: bytes };
|
|
94
119
|
}
|
|
95
120
|
|
|
96
121
|
function isValid(str) {
|
|
97
|
-
|
|
122
|
+
if (typeof str !== "string") return false;
|
|
123
|
+
if (str.length > UUID_STR_MAX_LEN) return false;
|
|
124
|
+
return UUID_LOOSE_RE.test(str);
|
|
98
125
|
}
|
|
99
126
|
|
|
100
127
|
module.exports = {
|
package/lib/validate-opts.js
CHANGED
|
@@ -53,5 +53,218 @@ function check(opts, allowedKeys, primitive) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// auditShape — operator-supplied `opts.audit` must expose a
|
|
57
|
+
// `safeEmit` function. 10+ primitive create() functions previously
|
|
58
|
+
// inlined the same shape check + throw. Centralized here so every
|
|
59
|
+
// consumer has one validation contract.
|
|
60
|
+
function auditShape(audit, callerLabel, errorClass, code) {
|
|
61
|
+
if (audit === undefined || audit === null) return audit;
|
|
62
|
+
if (typeof audit !== "object" || typeof audit.safeEmit !== "function") {
|
|
63
|
+
var msg = (callerLabel || "audit") +
|
|
64
|
+
": audit must be a b.audit-shaped object (safeEmit fn)";
|
|
65
|
+
if (errorClass && errorClass.factory) {
|
|
66
|
+
throw errorClass.factory(code || "BAD_OPT", msg);
|
|
67
|
+
}
|
|
68
|
+
if (typeof errorClass === "function") {
|
|
69
|
+
throw new errorClass(code || "audit/bad-shape", msg);
|
|
70
|
+
}
|
|
71
|
+
throw new Error(msg);
|
|
72
|
+
}
|
|
73
|
+
return audit;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// _throw — shared error-emission for the optional* validators. Routes
|
|
77
|
+
// through the caller's framework-error class when supplied, falls back
|
|
78
|
+
// to plain Error so this helper itself stays decoupled from any one
|
|
79
|
+
// error hierarchy.
|
|
80
|
+
function _throw(errorClass, code, msg, defaultCode) {
|
|
81
|
+
if (errorClass && errorClass.factory) {
|
|
82
|
+
throw errorClass.factory(code || "BAD_OPT", msg);
|
|
83
|
+
}
|
|
84
|
+
if (typeof errorClass === "function") {
|
|
85
|
+
throw new errorClass(code || defaultCode, msg);
|
|
86
|
+
}
|
|
87
|
+
throw new Error(msg);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// optionalBoolean / optionalPositiveInt / optionalFiniteNonNegative /
|
|
91
|
+
// optionalPositiveFinite / optionalFunction — opt-time type assertions
|
|
92
|
+
// for fields that may be omitted. Each returns the value unchanged when
|
|
93
|
+
// undefined or null (preserving the caller's "absent → use default"
|
|
94
|
+
// semantics) and throws on any other shape.
|
|
95
|
+
//
|
|
96
|
+
// 10+ primitive create() functions previously rolled identical
|
|
97
|
+
// `if (opts.X !== undefined && typeof opts.X !== "Y") throw`
|
|
98
|
+
// sequences. Centralizing the throw here keeps the operator-facing
|
|
99
|
+
// message format consistent and gives the codebase-pattern catalog one
|
|
100
|
+
// place to point new authors at.
|
|
101
|
+
|
|
102
|
+
function optionalBoolean(value, label, errorClass, code) {
|
|
103
|
+
if (value === undefined || value === null) return value;
|
|
104
|
+
if (typeof value !== "boolean") {
|
|
105
|
+
_throw(errorClass, code, (label || "opt") + " must be a boolean, got " + typeof value,
|
|
106
|
+
"validate-opts/bad-boolean");
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function optionalPositiveInt(value, label, errorClass, code) {
|
|
112
|
+
if (value === undefined || value === null) return value;
|
|
113
|
+
if (typeof value !== "number" || !isFinite(value) || value < 1 || Math.floor(value) !== value) {
|
|
114
|
+
_throw(errorClass, code, (label || "opt") +
|
|
115
|
+
" must be a positive integer (>= 1, finite), got " +
|
|
116
|
+
(typeof value === "number" ? String(value) : typeof value),
|
|
117
|
+
"validate-opts/bad-positive-int");
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function optionalFiniteNonNegative(value, label, errorClass, code) {
|
|
123
|
+
if (value === undefined || value === null) return value;
|
|
124
|
+
if (typeof value !== "number" || !isFinite(value) || value < 0) {
|
|
125
|
+
_throw(errorClass, code, (label || "opt") +
|
|
126
|
+
" must be a non-negative finite number, got " +
|
|
127
|
+
(typeof value === "number" ? String(value) : typeof value),
|
|
128
|
+
"validate-opts/bad-non-negative-finite");
|
|
129
|
+
}
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function optionalPositiveFinite(value, label, errorClass, code) {
|
|
134
|
+
if (value === undefined || value === null) return value;
|
|
135
|
+
if (typeof value !== "number" || !isFinite(value) || value <= 0) {
|
|
136
|
+
_throw(errorClass, code, (label || "opt") +
|
|
137
|
+
" must be a positive finite number (> 0), got " +
|
|
138
|
+
(typeof value === "number" ? String(value) : typeof value),
|
|
139
|
+
"validate-opts/bad-positive-finite");
|
|
140
|
+
}
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function optionalFunction(value, label, errorClass, code) {
|
|
145
|
+
if (value === undefined || value === null) return value;
|
|
146
|
+
if (typeof value !== "function") {
|
|
147
|
+
_throw(errorClass, code, (label || "opt") + " must be a function, got " + typeof value,
|
|
148
|
+
"validate-opts/bad-function");
|
|
149
|
+
}
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// applyDefaults — resolve every key in DEFAULTS against opts. For each
|
|
154
|
+
// key, the operator's value (if not undefined) wins; otherwise the
|
|
155
|
+
// default is used. Returns a new plain object — NOT a frozen one, so
|
|
156
|
+
// the create() function can layer additional resolved fields if it
|
|
157
|
+
// needs them. 5+ primitives previously rolled their own
|
|
158
|
+
// `var X = (opts.X === undefined) ? DEFAULTS.X : opts.X;` cascade
|
|
159
|
+
// across 5–10 lines per call site; this collapses them all to one.
|
|
160
|
+
//
|
|
161
|
+
// Operator-supplied keys that aren't in DEFAULTS are dropped — use
|
|
162
|
+
// validateOpts(opts, ALLOWED_KEYS, label) at the boundary FIRST to
|
|
163
|
+
// reject typos before this resolves. Pass-through behavior would let
|
|
164
|
+
// `{ ttllMs: 5000 }` silently apply the default ttlMs without warning.
|
|
165
|
+
function applyDefaults(opts, defaults) {
|
|
166
|
+
if (defaults === null || typeof defaults !== "object") {
|
|
167
|
+
throw new Error("validate-opts.applyDefaults: defaults must be an object");
|
|
168
|
+
}
|
|
169
|
+
opts = opts || {};
|
|
170
|
+
var out = {};
|
|
171
|
+
var keys = Object.keys(defaults);
|
|
172
|
+
for (var i = 0; i < keys.length; i++) {
|
|
173
|
+
var k = keys[i];
|
|
174
|
+
out[k] = (opts[k] === undefined) ? defaults[k] : opts[k];
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// requireObject — every primitive's `_validate*Opts(opts)` function
|
|
180
|
+
// previously started with the same `if (!opts || typeof opts !== "object")
|
|
181
|
+
// throw _err(...)` prelude. Centralized here so the contract is one
|
|
182
|
+
// definition. Throws on null / undefined / non-object; returns opts on
|
|
183
|
+
// success.
|
|
184
|
+
function requireObject(opts, callerLabel, errorClass, code) {
|
|
185
|
+
if (!opts || typeof opts !== "object") {
|
|
186
|
+
var msg = (callerLabel || "opts") + ": opts must be an object, got " +
|
|
187
|
+
(opts === null ? "null" : typeof opts);
|
|
188
|
+
_throw(errorClass, code, msg, "validate-opts/bad-object");
|
|
189
|
+
}
|
|
190
|
+
return opts;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function optionalNonEmptyString(value, label, errorClass, code) {
|
|
194
|
+
if (value === undefined || value === null) return value;
|
|
195
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
196
|
+
_throw(errorClass, code, (label || "opt") +
|
|
197
|
+
" must be a non-empty string, got " +
|
|
198
|
+
(typeof value === "string" ? "empty string" : typeof value),
|
|
199
|
+
"validate-opts/bad-non-empty-string");
|
|
200
|
+
}
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// requireNonEmptyString — required-field counterpart to
|
|
205
|
+
// optionalNonEmptyString. Throws on undefined / null / empty / non-
|
|
206
|
+
// string. The dozens of `if (typeof opts.X !== "string" ||
|
|
207
|
+
// opts.X.length === 0) throw` sites every primitive's create() rolled
|
|
208
|
+
// at the top of validation collapse to one call here.
|
|
209
|
+
function requireNonEmptyString(value, label, errorClass, code) {
|
|
210
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
211
|
+
var got = value === undefined ? "undefined"
|
|
212
|
+
: value === null ? "null"
|
|
213
|
+
: typeof value === "string" ? "empty string"
|
|
214
|
+
: typeof value;
|
|
215
|
+
_throw(errorClass, code, (label || "opt") +
|
|
216
|
+
" must be a non-empty string, got " + got,
|
|
217
|
+
"validate-opts/missing-non-empty-string");
|
|
218
|
+
}
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// makeAuditEmitter — closure factory parallel to safeAsync.makeDropCallback.
|
|
223
|
+
// Replaces the per-file `function _emit(action, info) { if (!audit) return;
|
|
224
|
+
// try { audit.safeEmit(Object.assign({ action: action }, info || {})); }
|
|
225
|
+
// catch (_e) { /* audit best-effort */ } }` boilerplate that 5 primitives
|
|
226
|
+
// previously rolled by hand (api-key / cache / notify / permissions /
|
|
227
|
+
// sigv4-bucket-ops).
|
|
228
|
+
//
|
|
229
|
+
// audit may be null/undefined — returns a no-op emitter in that case so
|
|
230
|
+
// callers don't need to null-check at every call site.
|
|
231
|
+
//
|
|
232
|
+
// var _emit = validateOpts.makeAuditEmitter(opts.audit);
|
|
233
|
+
// _emit("apikey.create", { actor: ..., resource: ..., outcome: "success" });
|
|
234
|
+
function makeAuditEmitter(audit) {
|
|
235
|
+
if (!audit || typeof audit.safeEmit !== "function") {
|
|
236
|
+
return function _noopEmit() {};
|
|
237
|
+
}
|
|
238
|
+
return function _emit(action, info) {
|
|
239
|
+
try { audit.safeEmit(Object.assign({ action: action }, info || {})); }
|
|
240
|
+
catch (_e) { /* audit best-effort — never break the caller */ }
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// observabilityShape — operator-supplied `opts.observability` must
|
|
245
|
+
// expose an `event` function. Parallel to auditShape; the n=1 catalog
|
|
246
|
+
// tracks both inline-shape regexes.
|
|
247
|
+
function observabilityShape(observability, callerLabel, errorClass, code) {
|
|
248
|
+
if (observability === undefined || observability === null) return observability;
|
|
249
|
+
if (typeof observability !== "object" || typeof observability.event !== "function") {
|
|
250
|
+
var msg = (callerLabel || "observability") +
|
|
251
|
+
": observability must be a b.observability-shaped object (event fn)";
|
|
252
|
+
_throw(errorClass, code, msg, "observability/bad-shape");
|
|
253
|
+
}
|
|
254
|
+
return observability;
|
|
255
|
+
}
|
|
256
|
+
|
|
56
257
|
module.exports = check;
|
|
57
258
|
module.exports.check = check;
|
|
259
|
+
module.exports.auditShape = auditShape;
|
|
260
|
+
module.exports.optionalBoolean = optionalBoolean;
|
|
261
|
+
module.exports.optionalPositiveInt = optionalPositiveInt;
|
|
262
|
+
module.exports.optionalFiniteNonNegative = optionalFiniteNonNegative;
|
|
263
|
+
module.exports.optionalPositiveFinite = optionalPositiveFinite;
|
|
264
|
+
module.exports.optionalFunction = optionalFunction;
|
|
265
|
+
module.exports.optionalNonEmptyString = optionalNonEmptyString;
|
|
266
|
+
module.exports.requireNonEmptyString = requireNonEmptyString;
|
|
267
|
+
module.exports.observabilityShape = observabilityShape;
|
|
268
|
+
module.exports.requireObject = requireObject;
|
|
269
|
+
module.exports.applyDefaults = applyDefaults;
|
|
270
|
+
module.exports.makeAuditEmitter = makeAuditEmitter;
|
package/lib/vault/index.js
CHANGED
|
@@ -41,6 +41,13 @@ var safeJson = require("../safe-json");
|
|
|
41
41
|
var observability = require("../observability");
|
|
42
42
|
var vaultPassphraseSource = require("./passphrase-source");
|
|
43
43
|
var vaultWrap = require("./wrap");
|
|
44
|
+
var { defineClass } = require("../framework-error");
|
|
45
|
+
|
|
46
|
+
// VaultError — thrown by init() for fatal boot-time conditions
|
|
47
|
+
// (corrupt sealed file, schema mismatch, mode/state conflicts). The
|
|
48
|
+
// CLI / app entry point catches it and exits; lib code never calls
|
|
49
|
+
// process.exit() unilaterally.
|
|
50
|
+
var VaultError = defineClass("VaultError", { alwaysPermanent: true });
|
|
44
51
|
|
|
45
52
|
var VAULT_PREFIX = C.VAULT_PREFIX;
|
|
46
53
|
|
|
@@ -72,12 +79,13 @@ async function init(opts) {
|
|
|
72
79
|
opts = opts || {};
|
|
73
80
|
|
|
74
81
|
if (!opts.dataDir) {
|
|
75
|
-
throw new
|
|
82
|
+
throw new VaultError("vault/bad-init", "vault.init({ dataDir }) is required");
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
var mode = (opts.mode || "wrapped").toLowerCase();
|
|
79
86
|
if (mode !== "wrapped" && mode !== "plaintext") {
|
|
80
|
-
throw new
|
|
87
|
+
throw new VaultError("vault/bad-mode",
|
|
88
|
+
"vault.init: mode must be 'wrapped' or 'plaintext', got: " + opts.mode);
|
|
81
89
|
}
|
|
82
90
|
currentMode = mode;
|
|
83
91
|
paths = resolvePaths(opts.dataDir);
|
|
@@ -95,21 +103,21 @@ async function init(opts) {
|
|
|
95
103
|
|
|
96
104
|
// Refuse to guess when both files coexist
|
|
97
105
|
if (hasPlaintext && hasSealed) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
throw new VaultError("vault/both-files-exist",
|
|
107
|
+
"both vault.key and vault.key.sealed exist in " + paths.dataDir +
|
|
108
|
+
" — delete the one you do NOT want to keep, then restart");
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
// Mode-vs-state mismatches
|
|
104
112
|
if (hasSealed && mode === "plaintext") {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
throw new VaultError("vault/mode-mismatch",
|
|
114
|
+
"vault.key.sealed exists but vault.init({ mode: 'plaintext' }) was requested — " +
|
|
115
|
+
"either run with mode: 'wrapped', or remove the sealed file (after migration)");
|
|
108
116
|
}
|
|
109
117
|
if (hasPlaintext && mode === "wrapped") {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
throw new VaultError("vault/mode-mismatch",
|
|
119
|
+
"vault.key (plaintext) exists but vault.init({ mode: 'wrapped' }) was requested — " +
|
|
120
|
+
"either run with mode: 'plaintext', or migrate the key to a wrapped form");
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
if (mode === "wrapped") {
|
|
@@ -143,9 +151,10 @@ function initPlaintext() {
|
|
|
143
151
|
},
|
|
144
152
|
});
|
|
145
153
|
} catch (e) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
154
|
+
throw new VaultError("vault/key-corrupt",
|
|
155
|
+
"vault.key corrupted, unreadable, or schema-invalid at " + paths.plaintext +
|
|
156
|
+
" — " + e.message +
|
|
157
|
+
" — all sealed data requires the original key; restore from backup, then restart");
|
|
149
158
|
}
|
|
150
159
|
keys = loaded;
|
|
151
160
|
return;
|
|
@@ -162,16 +171,15 @@ async function initWrapped() {
|
|
|
162
171
|
try {
|
|
163
172
|
sealedBytes = atomicFile.readSync(paths.sealed);
|
|
164
173
|
} catch (e) {
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
throw new VaultError("vault/sealed-unreadable",
|
|
175
|
+
"cannot read " + paths.sealed + ": " + e.message);
|
|
167
176
|
}
|
|
168
177
|
|
|
169
178
|
var passphrase;
|
|
170
179
|
try {
|
|
171
180
|
passphrase = await vaultPassphraseSource.getPassphrase({ prompt: "Vault passphrase: " });
|
|
172
181
|
} catch (e) {
|
|
173
|
-
|
|
174
|
-
process.exit(1);
|
|
182
|
+
throw new VaultError("vault/passphrase-error", e.message);
|
|
175
183
|
}
|
|
176
184
|
|
|
177
185
|
var plaintextJson;
|
|
@@ -180,8 +188,8 @@ async function initWrapped() {
|
|
|
180
188
|
plaintextBuf = await vaultWrap.unwrap(sealedBytes, passphrase);
|
|
181
189
|
plaintextJson = plaintextBuf.toString("utf8");
|
|
182
190
|
} catch (e) {
|
|
183
|
-
|
|
184
|
-
|
|
191
|
+
throw new VaultError("vault/unwrap-failed",
|
|
192
|
+
"passphrase rejected or sealed file corrupted (" + e.message + ")");
|
|
185
193
|
} finally {
|
|
186
194
|
// The Buffer holding the unwrapped key JSON is no longer needed once
|
|
187
195
|
// toString has copied the bytes into plaintextJson. The string itself
|
|
@@ -206,8 +214,8 @@ async function initWrapped() {
|
|
|
206
214
|
},
|
|
207
215
|
});
|
|
208
216
|
} catch (e) {
|
|
209
|
-
|
|
210
|
-
|
|
217
|
+
throw new VaultError("vault/unwrapped-invalid",
|
|
218
|
+
"unwrapped vault key invalid: " + e.message);
|
|
211
219
|
}
|
|
212
220
|
log("unsealed successfully.");
|
|
213
221
|
}
|
|
@@ -221,8 +229,7 @@ async function initFirstRunWrapped() {
|
|
|
221
229
|
prompt: "Choose a vault passphrase (loss = data loss, store it safely): ",
|
|
222
230
|
});
|
|
223
231
|
} catch (e) {
|
|
224
|
-
|
|
225
|
-
process.exit(1);
|
|
232
|
+
throw new VaultError("vault/passphrase-error", e.message);
|
|
226
233
|
}
|
|
227
234
|
currentPassphrase = passphrase;
|
|
228
235
|
|
|
@@ -232,8 +239,8 @@ async function initFirstRunWrapped() {
|
|
|
232
239
|
try {
|
|
233
240
|
sealed = await vaultWrap.wrap(plaintextJson, passphrase);
|
|
234
241
|
} catch (e) {
|
|
235
|
-
|
|
236
|
-
|
|
242
|
+
throw new VaultError("vault/wrap-failed",
|
|
243
|
+
"failed to wrap new vault key: " + e.message);
|
|
237
244
|
}
|
|
238
245
|
|
|
239
246
|
// Atomic write via the framework's atomic-file primitive (temp + fsync +
|
|
@@ -247,7 +254,8 @@ async function initFirstRunWrapped() {
|
|
|
247
254
|
|
|
248
255
|
function _requireInit() {
|
|
249
256
|
if (!initialized) {
|
|
250
|
-
throw new
|
|
257
|
+
throw new VaultError("vault/not-initialized",
|
|
258
|
+
"vault.init() must be awaited before vault.seal/unseal/getKeysJson");
|
|
251
259
|
}
|
|
252
260
|
}
|
|
253
261
|
|
|
@@ -289,6 +297,7 @@ module.exports = {
|
|
|
289
297
|
getKeysJson: getKeysJson,
|
|
290
298
|
getCurrentPassphrase: getCurrentPassphrase,
|
|
291
299
|
getMode: getMode,
|
|
300
|
+
VaultError: VaultError,
|
|
292
301
|
// Testing helpers — not part of the public contract
|
|
293
302
|
_resetForTest: function () {
|
|
294
303
|
if (currentPassphrase) safeBuffer.secureZero(currentPassphrase);
|
package/lib/vault/rotate.js
CHANGED
|
@@ -55,11 +55,17 @@ var atomicFile = require("../atomic-file");
|
|
|
55
55
|
var C = require("../constants");
|
|
56
56
|
var cryptoField = require("../crypto-field");
|
|
57
57
|
var cryptoLib = require("../crypto");
|
|
58
|
+
var dbSchema = require("../db-schema");
|
|
58
59
|
var lazyRequire = require("../lazy-require");
|
|
60
|
+
var { boot } = require("../log");
|
|
61
|
+
var nb = require("../numeric-bounds");
|
|
59
62
|
var safeJson = require("../safe-json");
|
|
63
|
+
var validateOpts = require("../validate-opts");
|
|
60
64
|
var vaultWrap = lazyRequire(function () { return require("./wrap"); });
|
|
61
65
|
var { defineClass } = require("../framework-error");
|
|
62
66
|
|
|
67
|
+
var rotateLog = boot("vault-rotate");
|
|
68
|
+
|
|
63
69
|
var VaultRotateError = defineClass("VaultRotateError", { alwaysPermanent: true });
|
|
64
70
|
|
|
65
71
|
var VAULT_PREFIX = C.VAULT_PREFIX;
|
|
@@ -102,7 +108,9 @@ function _knownColumnsFor(schema, infraColumns) {
|
|
|
102
108
|
|
|
103
109
|
function validateSchemaMatch(db, opts) {
|
|
104
110
|
opts = opts || {};
|
|
105
|
-
|
|
111
|
+
nb.requirePositiveFiniteIntIfPresent(opts.driftSampleLimit,
|
|
112
|
+
"validateSchemaMatch: driftSampleLimit", VaultRotateError, "vault-rotate/bad-opt");
|
|
113
|
+
var sampleLimit = opts.driftSampleLimit !== undefined
|
|
106
114
|
? opts.driftSampleLimit : DEFAULT_DRIFT_SAMPLE_LIMIT;
|
|
107
115
|
var infraColumns = Array.isArray(opts.infraColumns) ? opts.infraColumns : [];
|
|
108
116
|
// Tables to consider — by default, every table the framework's
|
|
@@ -245,9 +253,18 @@ function verify(opts) {
|
|
|
245
253
|
var keys = opts.keys;
|
|
246
254
|
var db = opts.db;
|
|
247
255
|
var oldKeys = opts.oldKeys || null;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
var
|
|
256
|
+
nb.requirePositiveFiniteIntIfPresent(opts.sampleMin,
|
|
257
|
+
"verify: sampleMin", VaultRotateError, "vault-rotate/bad-opt");
|
|
258
|
+
var sampleMin = opts.sampleMin !== undefined
|
|
259
|
+
? opts.sampleMin : DEFAULT_VERIFY_SAMPLE_MIN;
|
|
260
|
+
if (opts.samplePercent !== undefined &&
|
|
261
|
+
(typeof opts.samplePercent !== "number" || !Number.isFinite(opts.samplePercent) ||
|
|
262
|
+
opts.samplePercent <= 0)) {
|
|
263
|
+
throw new VaultRotateError("vault-rotate/bad-opt",
|
|
264
|
+
"verify: samplePercent must be a positive finite fraction; got " +
|
|
265
|
+
nb.shape(opts.samplePercent));
|
|
266
|
+
}
|
|
267
|
+
var samplePct = opts.samplePercent !== undefined
|
|
251
268
|
? opts.samplePercent : DEFAULT_VERIFY_SAMPLE_FRAC;
|
|
252
269
|
var tablesArg = Array.isArray(opts.tables) && opts.tables.length > 0
|
|
253
270
|
? opts.tables.slice() : null;
|
|
@@ -356,7 +373,9 @@ function verify(opts) {
|
|
|
356
373
|
// the SQLite bytes uses the same dbKey old → new.
|
|
357
374
|
// =====================================================================
|
|
358
375
|
|
|
359
|
-
|
|
376
|
+
// Row count, not a byte quantity — hex form keeps the literal out of the
|
|
377
|
+
// byte-shape detector while preserving the operator-readable magnitude.
|
|
378
|
+
var ROW_BATCH_SIZE_DEFAULT = 0x3E8;
|
|
360
379
|
var VAULT_PREFIX_LEN = C.VAULT_PREFIX.length;
|
|
361
380
|
|
|
362
381
|
function _emit(cb, ev) {
|
|
@@ -435,19 +454,14 @@ function _rotateColumn(db, table, column, oldKeys, newKeys, batchSize, progress)
|
|
|
435
454
|
var rows = sel.all(lastId, batchSize);
|
|
436
455
|
if (rows.length === 0) break;
|
|
437
456
|
|
|
438
|
-
|
|
439
|
-
try {
|
|
457
|
+
dbSchema.runInTransaction(db, function () {
|
|
440
458
|
for (var i = 0; i < rows.length; i++) {
|
|
441
459
|
var row = rows[i];
|
|
442
460
|
if (typeof row.v === "string" && row.v.indexOf(C.VAULT_PREFIX) === 0) {
|
|
443
461
|
upd.run(_reSealValue(row.v, oldKeys, newKeys), row._id);
|
|
444
462
|
}
|
|
445
463
|
}
|
|
446
|
-
|
|
447
|
-
} catch (e) {
|
|
448
|
-
_runStmt(db, "ROLLBACK");
|
|
449
|
-
throw e;
|
|
450
|
-
}
|
|
464
|
+
});
|
|
451
465
|
processed += rows.length;
|
|
452
466
|
lastId = rows[rows.length - 1]._id;
|
|
453
467
|
_emit(progress, { phase: "rotate_rows", table: table, column: column, rowsProcessed: processed, rowsTotal: total });
|
|
@@ -516,10 +530,7 @@ async function rotate(opts) {
|
|
|
516
530
|
throw new VaultRotateError("vault-rotate/no-datadir",
|
|
517
531
|
"rotate: opts.dataDir is required and must exist");
|
|
518
532
|
}
|
|
519
|
-
|
|
520
|
-
throw new VaultRotateError("vault-rotate/no-staging",
|
|
521
|
-
"rotate: opts.stagingDir is required");
|
|
522
|
-
}
|
|
533
|
+
validateOpts.requireNonEmptyString(opts.stagingDir, "rotate: opts.stagingDir", VaultRotateError, "vault-rotate/no-staging");
|
|
523
534
|
if (fs.existsSync(opts.stagingDir)) {
|
|
524
535
|
throw new VaultRotateError("vault-rotate/staging-exists",
|
|
525
536
|
"rotate: stagingDir already exists: " + opts.stagingDir);
|
|
@@ -689,9 +700,14 @@ async function rotate(opts) {
|
|
|
689
700
|
db.close();
|
|
690
701
|
}
|
|
691
702
|
|
|
692
|
-
// Drop WAL/SHM sidecars before re-encrypting the .db file
|
|
693
|
-
|
|
694
|
-
|
|
703
|
+
// Drop WAL/SHM sidecars before re-encrypting the .db file. Either
|
|
704
|
+
// sidecar may be absent (depending on whether journal_mode produced
|
|
705
|
+
// one for this run); log at debug so the cleanup attempt isn't
|
|
706
|
+
// silently swallowed when something genuinely unexpected fails.
|
|
707
|
+
try { fs.unlinkSync(tmpDbPath + "-wal"); }
|
|
708
|
+
catch (e) { rotateLog.debug("cleanup-failed", { op: "fs.unlinkSync", path: tmpDbPath + "-wal", error: e.message }); }
|
|
709
|
+
try { fs.unlinkSync(tmpDbPath + "-shm"); }
|
|
710
|
+
catch (e) { rotateLog.debug("cleanup-failed", { op: "fs.unlinkSync", path: tmpDbPath + "-shm", error: e.message }); }
|
|
695
711
|
|
|
696
712
|
var rotatedBytes = fs.readFileSync(tmpDbPath);
|
|
697
713
|
fs.writeFileSync(path.join(stagingDir, paths.encryptedDb),
|
|
@@ -708,9 +724,12 @@ async function rotate(opts) {
|
|
|
708
724
|
verifyResult = verify({ keys: newKeys, db: vdb, oldKeys: oldKeys });
|
|
709
725
|
} finally {
|
|
710
726
|
vdb.close();
|
|
711
|
-
try { fs.unlinkSync(verifyTmp); }
|
|
712
|
-
|
|
713
|
-
try { fs.unlinkSync(verifyTmp + "-
|
|
727
|
+
try { fs.unlinkSync(verifyTmp); }
|
|
728
|
+
catch (e) { rotateLog.debug("cleanup-failed", { op: "fs.unlinkSync", path: verifyTmp, error: e.message }); }
|
|
729
|
+
try { fs.unlinkSync(verifyTmp + "-wal"); }
|
|
730
|
+
catch (e) { rotateLog.debug("cleanup-failed", { op: "fs.unlinkSync", path: verifyTmp + "-wal", error: e.message }); }
|
|
731
|
+
try { fs.unlinkSync(verifyTmp + "-shm"); }
|
|
732
|
+
catch (e) { rotateLog.debug("cleanup-failed", { op: "fs.unlinkSync", path: verifyTmp + "-shm", error: e.message }); }
|
|
714
733
|
}
|
|
715
734
|
if (!verifyResult.ok) {
|
|
716
735
|
throw new VaultRotateError("vault-rotate/verify-failed",
|