@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.
Files changed (164) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +11 -1
  3. package/index.js +2 -5
  4. package/lib/api-key.js +24 -55
  5. package/lib/api-snapshot.js +4 -1
  6. package/lib/app-shutdown.js +20 -16
  7. package/lib/app.js +6 -5
  8. package/lib/archive.js +58 -50
  9. package/lib/atomic-file.js +11 -5
  10. package/lib/audit-chain.js +12 -4
  11. package/lib/audit-sign.js +38 -13
  12. package/lib/audit-tools.js +4 -4
  13. package/lib/audit.js +8 -4
  14. package/lib/auth/jwt.js +3 -2
  15. package/lib/auth/lockout.js +3 -13
  16. package/lib/auth/oauth.js +27 -17
  17. package/lib/auth/passkey.js +1 -5
  18. package/lib/auth/password.js +23 -13
  19. package/lib/backup/bundle.js +2 -4
  20. package/lib/backup/crypto.js +8 -6
  21. package/lib/backup/index.js +18 -14
  22. package/lib/backup/manifest.js +6 -2
  23. package/lib/break-glass.js +12 -7
  24. package/lib/bundler.js +32 -21
  25. package/lib/cache-redis.js +8 -3
  26. package/lib/cache.js +39 -59
  27. package/lib/canonical-json.js +17 -2
  28. package/lib/chain-writer.js +2 -2
  29. package/lib/cli-helpers.js +9 -4
  30. package/lib/cli.js +27 -5
  31. package/lib/cluster-provider-db.js +2 -1
  32. package/lib/cluster-storage.js +71 -11
  33. package/lib/cluster.js +30 -25
  34. package/lib/config-drift.js +3 -4
  35. package/lib/constants.js +8 -3
  36. package/lib/cookies.js +16 -4
  37. package/lib/credential-hash.js +4 -7
  38. package/lib/crypto.js +9 -9
  39. package/lib/csv.js +11 -8
  40. package/lib/db-declare-row-policy.js +4 -9
  41. package/lib/db-declare-view.js +3 -6
  42. package/lib/db-query.js +9 -3
  43. package/lib/db-schema.js +75 -10
  44. package/lib/db.js +116 -70
  45. package/lib/deprecate.js +3 -8
  46. package/lib/dev.js +30 -19
  47. package/lib/dual-control.js +2 -4
  48. package/lib/error-page.js +32 -39
  49. package/lib/external-db-migrate.js +23 -26
  50. package/lib/external-db.js +11 -3
  51. package/lib/file-type.js +16 -8
  52. package/lib/forms.js +48 -12
  53. package/lib/framework-error.js +1 -2
  54. package/lib/{internal-sha1-hibp.js → framework-sha1-hibp.js} +1 -1
  55. package/lib/handlers.js +1 -1
  56. package/lib/http-client-cookie-jar.js +25 -18
  57. package/lib/http-client.js +46 -23
  58. package/lib/i18n.js +6 -16
  59. package/lib/jobs.js +5 -1
  60. package/lib/log-stream-cloudwatch.js +31 -30
  61. package/lib/log-stream-local.js +12 -3
  62. package/lib/log-stream-otlp-grpc.js +29 -23
  63. package/lib/log-stream-otlp.js +13 -19
  64. package/lib/log-stream-syslog.js +27 -22
  65. package/lib/log-stream-webhook.js +8 -17
  66. package/lib/log-stream.js +13 -2
  67. package/lib/log.js +83 -5
  68. package/lib/mail-bounce.js +22 -15
  69. package/lib/mail-dkim.js +3 -6
  70. package/lib/mail.js +62 -34
  71. package/lib/metrics.js +61 -25
  72. package/lib/middleware/api-encrypt.js +15 -13
  73. package/lib/middleware/body-parser.js +46 -39
  74. package/lib/middleware/bot-guard.js +25 -4
  75. package/lib/middleware/compression.js +26 -11
  76. package/lib/middleware/cors.js +6 -5
  77. package/lib/middleware/csp-nonce.js +4 -3
  78. package/lib/middleware/csrf-protect.js +1 -1
  79. package/lib/middleware/db-role-for.js +7 -30
  80. package/lib/middleware/health.js +42 -8
  81. package/lib/middleware/index.js +55 -36
  82. package/lib/middleware/rate-limit.js +11 -8
  83. package/lib/middleware/request-id.js +10 -2
  84. package/lib/middleware/request-log.js +3 -2
  85. package/lib/middleware/require-auth.js +5 -2
  86. package/lib/middleware/sse.js +5 -4
  87. package/lib/migration-files.js +37 -0
  88. package/lib/migrations.js +26 -15
  89. package/lib/mtls-ca.js +38 -17
  90. package/lib/mtls-engine-default.js +15 -7
  91. package/lib/network-dns.js +49 -20
  92. package/lib/network-heartbeat.js +7 -7
  93. package/lib/network-nts.js +70 -48
  94. package/lib/network-proxy.js +34 -15
  95. package/lib/network-tls.js +7 -5
  96. package/lib/network.js +3 -3
  97. package/lib/notify.js +14 -36
  98. package/lib/ntp-check.js +7 -6
  99. package/lib/numeric-bounds.js +35 -3
  100. package/lib/object-store/azure-blob-bucket-ops.js +73 -15
  101. package/lib/object-store/azure-blob.js +31 -24
  102. package/lib/object-store/gcs-bucket-ops.js +41 -17
  103. package/lib/object-store/gcs.js +41 -34
  104. package/lib/object-store/http-put.js +2 -11
  105. package/lib/object-store/http-request.js +38 -0
  106. package/lib/object-store/index.js +1 -1
  107. package/lib/object-store/sigv4-bucket-ops.js +33 -37
  108. package/lib/object-store/sigv4.js +50 -30
  109. package/lib/observability.js +14 -3
  110. package/lib/otel-export.js +16 -14
  111. package/lib/pagination.js +23 -8
  112. package/lib/parsers/index.js +11 -5
  113. package/lib/parsers/safe-env.js +43 -14
  114. package/lib/parsers/safe-ini.js +29 -10
  115. package/lib/parsers/safe-toml.js +55 -25
  116. package/lib/parsers/safe-xml.js +22 -11
  117. package/lib/parsers/safe-yaml.js +55 -17
  118. package/lib/permissions.js +24 -33
  119. package/lib/pqc-agent.js +4 -2
  120. package/lib/pqc-gate.js +28 -15
  121. package/lib/protobuf-encoder.js +9 -3
  122. package/lib/protocol-dispatcher.js +29 -12
  123. package/lib/pubsub-redis.js +25 -8
  124. package/lib/pubsub.js +84 -12
  125. package/lib/queue-local.js +13 -4
  126. package/lib/queue-redis.js +2 -2
  127. package/lib/queue-sqs.js +21 -16
  128. package/lib/queue.js +8 -4
  129. package/lib/redis-client.js +13 -10
  130. package/lib/request-helpers.js +33 -2
  131. package/lib/restore-bundle.js +2 -4
  132. package/lib/restore-rollback.js +4 -1
  133. package/lib/restore.js +12 -10
  134. package/lib/retry.js +58 -9
  135. package/lib/router.js +96 -27
  136. package/lib/safe-async.js +97 -4
  137. package/lib/safe-buffer.js +47 -0
  138. package/lib/safe-json.js +18 -9
  139. package/lib/safe-schema.js +54 -18
  140. package/lib/safe-url.js +6 -2
  141. package/lib/scheduler.js +25 -14
  142. package/lib/security-assert.js +31 -26
  143. package/lib/seeders.js +71 -60
  144. package/lib/session.js +13 -7
  145. package/lib/slug.js +35 -8
  146. package/lib/ssrf-guard.js +58 -43
  147. package/lib/static.js +11 -7
  148. package/lib/storage.js +2 -1
  149. package/lib/template.js +17 -7
  150. package/lib/testing.js +28 -8
  151. package/lib/time.js +26 -5
  152. package/lib/totp.js +18 -9
  153. package/lib/tracing.js +15 -5
  154. package/lib/uuid.js +47 -20
  155. package/lib/validate-opts.js +213 -0
  156. package/lib/vault/index.js +36 -27
  157. package/lib/vault/rotate.js +41 -22
  158. package/lib/vault/wrap.js +26 -19
  159. package/lib/webhook.js +37 -68
  160. package/lib/websocket.js +60 -46
  161. package/package.json +1 -1
  162. package/sbom.cyclonedx.json +6 -6
  163. package/lib/auth/totp.js +0 -17
  164. package/lib/object-store/retry.js +0 -15
@@ -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;
@@ -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 Error("vault.init({ dataDir }) is required");
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 Error("vault.init: mode must be 'wrapped' or 'plaintext', got: " + opts.mode);
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
- log.error("FATAL: both vault.key and vault.key.sealed exist in " + paths.dataDir);
99
- log.error("Delete the one you do NOT want to keep, then restart.");
100
- process.exit(1);
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
- log.error("FATAL: vault.key.sealed exists but vault.init({ mode: 'plaintext' }) was requested.");
106
- log.error("Either run with mode: 'wrapped', or remove the sealed file (after migration).");
107
- process.exit(1);
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
- log.error("FATAL: vault.key (plaintext) exists but vault.init({ mode: 'wrapped' }) was requested.");
111
- log.error("Either run with mode: 'plaintext', or migrate the key to a wrapped form.");
112
- process.exit(1);
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
- log.error("FATAL: vault.key corrupted, unreadable, or schema-invalid at " + paths.plaintext + " — " + e.message);
147
- log.error("All sealed data requires the original key. Restore from backup, then restart.");
148
- process.exit(1);
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
- log.error("FATAL: cannot read " + paths.sealed + ": " + e.message);
166
- process.exit(1);
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
- log.error("FATAL: " + e.message);
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
- log.error("FATAL: passphrase rejected or sealed file corrupted (" + e.message + ")");
184
- process.exit(1);
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
- log.error("FATAL: unwrapped vault key invalid: " + e.message);
210
- process.exit(1);
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
- log.error("FATAL: " + e.message);
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
- log.error("FATAL: failed to wrap new vault key: " + e.message);
236
- process.exit(1);
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 Error("vault.init() must be awaited before vault.seal/unseal/getKeysJson");
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);
@@ -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
- var sampleLimit = typeof opts.driftSampleLimit === "number" && opts.driftSampleLimit > 0
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
- var sampleMin = typeof opts.sampleMin === "number" && opts.sampleMin >= 1
249
- ? Math.floor(opts.sampleMin) : DEFAULT_VERIFY_SAMPLE_MIN;
250
- var samplePct = typeof opts.samplePercent === "number" && opts.samplePercent > 0
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
- var ROW_BATCH_SIZE_DEFAULT = 1000;
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
- _runStmt(db, "BEGIN");
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
- _runStmt(db, "COMMIT");
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
- if (typeof opts.stagingDir !== "string" || opts.stagingDir.length === 0) {
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
- try { fs.unlinkSync(tmpDbPath + "-wal"); } catch (_e) {}
694
- try { fs.unlinkSync(tmpDbPath + "-shm"); } catch (_e) {}
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); } catch (_e) {}
712
- try { fs.unlinkSync(verifyTmp + "-wal"); } catch (_e) {}
713
- try { fs.unlinkSync(verifyTmp + "-shm"); } catch (_e) {}
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: 65536,
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 = 4096;
55
- // argon2 memoryCost is in KiB, not bytes these are NOT C.BYTES.* values.
56
- var MIN_ARGON2_MEMORY = 1024; // 1 MiB-in-KiB
57
- var MAX_ARGON2_MEMORY = 4 * 1024 * 1024; // 4 GiB-in-KiB
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
- var MAX_ARGON2_PARALLELISM = 32;
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(16) +
102
- " != 0x" + MAGIC.toString(16) + ")");
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,