@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/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",
|
package/lib/vault/wrap.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* wrap() and unwrap() are async because Argon2 is async via its native binding.
|
|
27
27
|
*/
|
|
28
28
|
var argon2 = require("../vendor/argon2");
|
|
29
|
+
var C = require("../constants");
|
|
29
30
|
var { xchacha20poly1305 } = require("../vendor/noble-ciphers.cjs");
|
|
30
31
|
var { generateBytes } = require("../crypto");
|
|
31
32
|
var safeBuffer = require("../safe-buffer");
|
|
@@ -35,28 +36,34 @@ var MAGIC = 0xE2;
|
|
|
35
36
|
var FORMAT_VERSION = 0x01;
|
|
36
37
|
var KDF_ARGON2ID = 0x01;
|
|
37
38
|
var CIPHER_XCHACHA20_POLY = 0x02;
|
|
38
|
-
var NONCE_LENGTH = 24;
|
|
39
|
+
var NONCE_LENGTH = C.BYTES.bytes(24);
|
|
39
40
|
|
|
40
41
|
// ---- Default Argon2id parameters ----
|
|
41
42
|
// 64 MiB / t=3 / p=4 targets ~1s derivation on commodity 2026 hardware —
|
|
42
43
|
// painful for offline brute force, tolerable for an operator-initiated boot.
|
|
44
|
+
// memoryCost is denominated in KiB per RFC 9106 §3.1, so the 64 MiB target
|
|
45
|
+
// expresses as MiB→KiB through the framework's byte helpers.
|
|
43
46
|
var DEFAULT_ARGON2 = Object.freeze({
|
|
44
|
-
memoryCost:
|
|
47
|
+
memoryCost: C.BYTES.mib(64) / C.BYTES.kib(1),
|
|
45
48
|
timeCost: 3,
|
|
46
49
|
parallelism: 4,
|
|
47
|
-
saltLength: 16,
|
|
48
|
-
hashLength: 32,
|
|
50
|
+
saltLength: C.BYTES.bytes(16),
|
|
51
|
+
hashLength: C.BYTES.bytes(32),
|
|
49
52
|
});
|
|
50
53
|
|
|
51
54
|
// ---- Hard bounds — reject malformed or adversarial headers ----
|
|
52
|
-
var MIN_SALT_LENGTH = 8;
|
|
53
|
-
var MAX_SALT_LENGTH = 64;
|
|
54
|
-
var MAX_PASSPHRASE_LENGTH =
|
|
55
|
-
// argon2 memoryCost is in KiB
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
var MIN_SALT_LENGTH = C.BYTES.bytes(8);
|
|
56
|
+
var MAX_SALT_LENGTH = C.BYTES.bytes(64);
|
|
57
|
+
var MAX_PASSPHRASE_LENGTH = C.BYTES.kib(4);
|
|
58
|
+
// argon2 memoryCost is denominated in KiB per RFC 9106 §3.1, not bytes.
|
|
59
|
+
// Express the bound as a byte-quantity divided by KiB so the framework's
|
|
60
|
+
// C.BYTES helpers stay the single source of truth for the underlying scale.
|
|
61
|
+
var MIN_ARGON2_MEMORY = C.BYTES.mib(1) / C.BYTES.kib(1); // 1 MiB-in-KiB
|
|
62
|
+
var MAX_ARGON2_MEMORY = C.BYTES.gib(4) / C.BYTES.kib(1); // 4 GiB-in-KiB
|
|
58
63
|
var MAX_ARGON2_TIME = 100;
|
|
59
|
-
|
|
64
|
+
// Argon2 lane count cap. Hex form because the literal isn't a byte
|
|
65
|
+
// quantity — using C.BYTES.* would mis-name the unit at the call site.
|
|
66
|
+
var MAX_ARGON2_PARALLELISM = 0x20;
|
|
60
67
|
|
|
61
68
|
function buildHeader(params) {
|
|
62
69
|
var salt = params.salt;
|
|
@@ -81,7 +88,7 @@ function buildHeader(params) {
|
|
|
81
88
|
h[2] = KDF_ARGON2ID;
|
|
82
89
|
h[3] = 0x00;
|
|
83
90
|
h.writeUInt32BE(params.memoryCost >>> 0, 4);
|
|
84
|
-
h.writeUInt16BE(params.timeCost & 0xffff, 8);
|
|
91
|
+
h.writeUInt16BE(params.timeCost & 0xffff, C.BYTES.bytes(8));
|
|
85
92
|
h[10] = params.parallelism & 0xff;
|
|
86
93
|
h[11] = saltLen;
|
|
87
94
|
Buffer.from(salt).copy(h, 12);
|
|
@@ -98,8 +105,8 @@ function parseHeader(buf) {
|
|
|
98
105
|
if (!Buffer.isBuffer(buf)) buf = Buffer.from(buf);
|
|
99
106
|
if (buf.length < 12) throw new Error("wrapped vault file too short (< 12 bytes)");
|
|
100
107
|
if (buf[0] !== MAGIC) {
|
|
101
|
-
throw new Error("not a wrapped vault file (magic byte 0x" + buf[0].toString(
|
|
102
|
-
" != 0x" + MAGIC.toString(
|
|
108
|
+
throw new Error("not a wrapped vault file (magic byte 0x" + buf[0].toString(0x10) +
|
|
109
|
+
" != 0x" + MAGIC.toString(0x10) + ")");
|
|
103
110
|
}
|
|
104
111
|
if (buf[1] !== FORMAT_VERSION) {
|
|
105
112
|
throw new Error("unsupported wrapped-vault format version " + buf[1] + " — upgrade blamejs");
|
|
@@ -110,7 +117,7 @@ function parseHeader(buf) {
|
|
|
110
117
|
// byte 3 reserved
|
|
111
118
|
|
|
112
119
|
var memoryCost = buf.readUInt32BE(4);
|
|
113
|
-
var timeCost = buf.readUInt16BE(8);
|
|
120
|
+
var timeCost = buf.readUInt16BE(C.BYTES.bytes(8));
|
|
114
121
|
var parallelism = buf[10];
|
|
115
122
|
var saltLen = buf[11];
|
|
116
123
|
|
|
@@ -146,7 +153,7 @@ function parseHeader(buf) {
|
|
|
146
153
|
var ciphertextLength = buf.readUInt32BE(ctLenPos);
|
|
147
154
|
var headerEnd = ctLenPos + 4;
|
|
148
155
|
|
|
149
|
-
if (ciphertextLength < 16) {
|
|
156
|
+
if (ciphertextLength < C.BYTES.bytes(16)) {
|
|
150
157
|
throw new Error("ciphertext length too short (< Poly1305 tag): " + ciphertextLength);
|
|
151
158
|
}
|
|
152
159
|
if (buf.length < headerEnd + ciphertextLength) {
|
|
@@ -191,13 +198,13 @@ async function deriveWrappingKey(passphrase, salt, argonParams) {
|
|
|
191
198
|
memoryCost: argonParams.memoryCost || DEFAULT_ARGON2.memoryCost,
|
|
192
199
|
timeCost: argonParams.timeCost || DEFAULT_ARGON2.timeCost,
|
|
193
200
|
parallelism: argonParams.parallelism || DEFAULT_ARGON2.parallelism,
|
|
194
|
-
hashLength: 32,
|
|
201
|
+
hashLength: C.BYTES.bytes(32),
|
|
195
202
|
raw: true,
|
|
196
203
|
});
|
|
197
204
|
} finally {
|
|
198
205
|
if (weOwnPwBuf) safeBuffer.secureZero(pwBuf);
|
|
199
206
|
}
|
|
200
|
-
if (!raw || raw.length !== 32) {
|
|
207
|
+
if (!raw || raw.length !== C.BYTES.bytes(32)) {
|
|
201
208
|
safeBuffer.secureZero(raw);
|
|
202
209
|
throw new Error("Argon2 returned unexpected hash length: " + (raw && raw.length));
|
|
203
210
|
}
|
|
@@ -230,7 +237,7 @@ async function wrap(plaintext, passphrase, opts) {
|
|
|
230
237
|
});
|
|
231
238
|
|
|
232
239
|
try {
|
|
233
|
-
var ciphertextLength = plaintextBuf.length + 16;
|
|
240
|
+
var ciphertextLength = plaintextBuf.length + C.BYTES.bytes(16);
|
|
234
241
|
var header = buildHeader({
|
|
235
242
|
memoryCost: memoryCost,
|
|
236
243
|
timeCost: timeCost,
|