@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.
Files changed (164) hide show
  1. package/CHANGELOG.md +6 -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 +27 -20
  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 +17 -4
  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 +42 -17
  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
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, 8) + "-" +
44
- hex.slice(8, 12) + "-" +
45
- hex.slice(12, 16) + "-" +
46
- hex.slice(16, 20) + "-" +
47
- hex.slice(20, 32);
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(16);
71
+ var b = generateBytes(UUID_BYTE_LEN);
52
72
  // version = 4 (0100): high nibble of byte 6
53
- b[6] = (b[6] & 0x0f) | 0x40;
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[8] = (b[8] & 0x3f) | 0x80;
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(16);
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 use Math + bit ops carefully
68
- var msHi = Math.floor(ms / 0x100000000); // top 16 bits live in low 16 of msHi
69
- var msLo = ms >>> 0; // bottom 32 bits unsigned
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 >>> 8) & 0xff;
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[6] = (b[6] & 0x0f) | 0x70;
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[8] = (b[8] & 0x3f) | 0x80;
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
- if (!UUID_RE.test(str)) return { ok: false, reason: "malformed" };
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[6] >> 4) & 0x0f;
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[8] >> 6) & 0x03;
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
- return typeof str === "string" && UUID_LOOSE_RE.test(str);
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 = {
@@ -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",