@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/queue-sqs.js CHANGED
@@ -45,6 +45,7 @@
45
45
  * local + redis backends.
46
46
  */
47
47
  var sigv4 = require("./object-store/sigv4");
48
+ var C = require("./constants");
48
49
  var httpClient = require("./http-client");
49
50
  var cryptoField = require("./crypto-field");
50
51
  var safeJson = require("./safe-json");
@@ -90,13 +91,16 @@ function create(opts) {
90
91
 
91
92
  var region = opts.region;
92
93
  var endpoint = _resolveEndpoint(opts);
93
- var endpointUrl = new URL(endpoint);
94
+ var allowedProtocols = opts.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
95
+ var endpointUrl = safeUrl.parse(endpoint, {
96
+ errorClass: QueueError,
97
+ allowedProtocols: allowedProtocols,
98
+ });
94
99
  var accessKeyId = opts.accessKeyId;
95
100
  var secretAccessKey = opts.secretAccessKey;
96
101
  var sessionToken = opts.sessionToken || null;
97
102
  var accountId = opts.accountId ? String(opts.accountId) : null;
98
103
  var timeoutMs = opts.timeoutMs;
99
- var allowedProtocols = opts.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
100
104
  var allowInternal = opts.allowInternal != null ? opts.allowInternal : null;
101
105
  var visibilityTimeoutSec = Number(opts.visibilityTimeoutSec) || DEFAULT_VISIBILITY_TIMEOUT_SEC;
102
106
  var waitTimeSec = Number(opts.waitTimeSec) || DEFAULT_WAIT_TIME_SEC;
@@ -115,15 +119,16 @@ function create(opts) {
115
119
  "Content-Length": String(bodyBuf.length),
116
120
  };
117
121
  var signed = sigv4.signRequest({
118
- method: "POST",
119
- url: endpointUrl,
120
- headers: headers,
121
- payloadHash: _payloadHash(bodyBuf),
122
- region: region,
123
- service: "sqs",
124
- accessKeyId: accessKeyId,
125
- secretAccessKey: secretAccessKey,
126
- sessionToken: sessionToken,
122
+ method: "POST",
123
+ url: endpointUrl,
124
+ headers: headers,
125
+ payloadHash: _payloadHash(bodyBuf),
126
+ region: region,
127
+ service: "sqs",
128
+ accessKeyId: accessKeyId,
129
+ secretAccessKey: secretAccessKey,
130
+ sessionToken: sessionToken,
131
+ allowedProtocols: allowedProtocols,
127
132
  });
128
133
  var reqOpts = {
129
134
  method: "POST",
@@ -151,7 +156,7 @@ function create(opts) {
151
156
  async function enqueue(queueName, payload, enqueueOpts) {
152
157
  enqueueOpts = enqueueOpts || {};
153
158
  var queueUrl = queueUrlResolver(queueName);
154
- var jobId = generateToken(16);
159
+ var jobId = generateToken(C.BYTES.bytes(16));
155
160
  var sealed = cryptoField.sealRow("_blamejs_jobs", {
156
161
  _id: jobId,
157
162
  queueName: queueName,
@@ -168,8 +173,8 @@ function create(opts) {
168
173
  };
169
174
  var delaySeconds = enqueueOpts.delaySeconds;
170
175
  if (typeof delaySeconds === "number" && delaySeconds > 0) {
171
- // SQS hard cap is 900s.
172
- sqsBody.DelaySeconds = Math.min(900, Math.floor(delaySeconds));
176
+ // SQS hard cap is 900s (15 min).
177
+ sqsBody.DelaySeconds = Math.min(C.TIME.minutes(15) / C.TIME.seconds(1), Math.floor(delaySeconds));
173
178
  }
174
179
  var rv = await _post("SendMessage", sqsBody);
175
180
  return rv && (rv.MessageId || jobId);
@@ -203,7 +208,7 @@ function create(opts) {
203
208
  var payload;
204
209
  try {
205
210
  payload = unsealed.payload != null
206
- ? JSON.parse(unsealed.payload) : null;
211
+ ? safeJson.parse(unsealed.payload, { maxBytes: C.BYTES.mib(64) }) : null;
207
212
  } catch (_e) { payload = unsealed.payload; }
208
213
  out.push({
209
214
  jobId: unsealed._id,
@@ -211,7 +216,7 @@ function create(opts) {
211
216
  payload: payload,
212
217
  attempts: Number(unsealed.attempts) || 0,
213
218
  enqueuedAt: Number(unsealed.enqueuedAt) || null,
214
- leaseExpiresAt: Date.now() + (visTimeout * 1000),
219
+ leaseExpiresAt: Date.now() + C.TIME.seconds(visTimeout),
215
220
  // SQS-specific: receipt handle is the only way to delete /
216
221
  // change visibility / extend — surface it for the framework
217
222
  // wrapper.
package/lib/queue.js CHANGED
@@ -34,6 +34,7 @@ var C = require("./constants");
34
34
  var clusterStorage = require("./cluster-storage");
35
35
  var crypto = require("./crypto");
36
36
  var lazyRequire = require("./lazy-require");
37
+ var { boot } = require("./log");
37
38
  var numericChecks = require("./numeric-checks");
38
39
  var observability = require("./observability");
39
40
  var protocolDispatcher = require("./protocol-dispatcher");
@@ -44,6 +45,8 @@ var retryHelper = require("./retry");
44
45
  var safeAsync = require("./safe-async");
45
46
  var { QueueError } = require("./framework-error");
46
47
 
48
+ var log = boot("queue");
49
+
47
50
  var dispatcher = protocolDispatcher.create({
48
51
  name: "queue",
49
52
  errorClass: QueueError,
@@ -180,8 +183,8 @@ function consume(queueName, handler, opts) {
180
183
  opts = opts || {};
181
184
  var b = _backendFor(opts);
182
185
  var concurrency = opts.concurrency || 1;
183
- var leaseDurationMs = opts.leaseDurationMs || 30000;
184
- var pollIntervalMs = opts.pollIntervalMs || 1000;
186
+ var leaseDurationMs = opts.leaseDurationMs || C.TIME.seconds(30);
187
+ var pollIntervalMs = opts.pollIntervalMs || C.TIME.seconds(1);
185
188
  var fastPollMs = opts.fastPollMs || 50;
186
189
 
187
190
  // Rate-limit: { max, perSeconds } caps how many handler INVOCATIONS
@@ -235,7 +238,8 @@ function consume(queueName, handler, opts) {
235
238
  abortCtrl: abortCtrl,
236
239
  cancel: function () {
237
240
  state.cancelled = true;
238
- try { abortCtrl.abort(); } catch (_e) {}
241
+ try { abortCtrl.abort(); }
242
+ catch (e) { log.debug("cancel-cleanup-failed", { op: "abortCtrl.abort", error: e.message }); }
239
243
  },
240
244
  };
241
245
  consumers.push(state);
@@ -575,7 +579,7 @@ function enqueueFlow(spec) {
575
579
  return Promise.reject(e);
576
580
  }
577
581
 
578
- var flowId = "flow-" + crypto.generateToken(8);
582
+ var flowId = "flow-" + crypto.generateToken(C.BYTES.bytes(8));
579
583
 
580
584
  return observability.tap("queue.enqueueFlow",
581
585
  { queueName: spec.queueName, flowId: flowId, childCount: spec.children.length },
@@ -27,11 +27,16 @@
27
27
  var net = require("node:net");
28
28
  var tls = require("node:tls");
29
29
  var url = require("node:url");
30
+ var C = require("./constants");
30
31
  var safeAsync = require("./safe-async");
32
+ var validateOpts = require("./validate-opts");
31
33
  var { RedisError } = require("./framework-error");
32
34
 
33
35
  var _err = RedisError.factory;
34
36
 
37
+ // Radix for `.toString(N)` hex conversion. Numeric, not a byte count.
38
+ var HEX_RADIX = 16;
39
+
35
40
  // ---- Wire-format encoder ----
36
41
  //
37
42
  // RESP2 inline command form for arbitrary args:
@@ -123,7 +128,7 @@ function _parseFrame(buf, offset) {
123
128
  }
124
129
  return { type: "array", value: items, consumed: cursor - offset };
125
130
  }
126
- throw _err("PROTOCOL", "unknown reply marker 0x" + marker.toString(16));
131
+ throw _err("PROTOCOL", "unknown reply marker 0x" + marker.toString(HEX_RADIX));
127
132
  }
128
133
 
129
134
  // Convert a parsed frame tree into a JavaScript-friendly value.
@@ -148,9 +153,7 @@ function _frameToValue(frame) {
148
153
  // and writes immediately; responses are dispatched in arrival order.
149
154
  function create(opts) {
150
155
  opts = opts || {};
151
- if (typeof opts.url !== "string" || opts.url.length === 0) {
152
- throw _err("BAD_OPTS", "redis.create({ url }) is required");
153
- }
156
+ validateOpts.requireNonEmptyString(opts.url, "redis.create: opts.url", RedisError, "BAD_OPTS");
154
157
  var parsed = _parseRedisUrl(opts.url);
155
158
  var host = opts.host || parsed.host;
156
159
  var port = opts.port || parsed.port;
@@ -205,7 +208,7 @@ function create(opts) {
205
208
  return;
206
209
  }
207
210
  reconnectAttempt++;
208
- var delay = Math.min(30000, 100 * Math.pow(2, reconnectAttempt - 1));
211
+ var delay = Math.min(C.TIME.seconds(30), 100 * Math.pow(2, reconnectAttempt - 1));
209
212
  setTimeout(function () { _connect().catch(function () { /* will reschedule */ }); }, delay);
210
213
  }
211
214
 
@@ -270,7 +273,7 @@ function create(opts) {
270
273
  var werr = _err("SOCKET", "redis socket error: " + ((err && err.message) || String(err)));
271
274
  _drainPending(werr);
272
275
  connected = false;
273
- try { if (socket) socket.destroy(); } catch (_e) {}
276
+ try { if (socket) socket.destroy(); } catch (_e) { /* best-effort socket teardown */ }
274
277
  socket = null;
275
278
  if (!closing) _scheduleReconnect();
276
279
  }
@@ -298,7 +301,7 @@ function create(opts) {
298
301
  socket = await new Promise(function (resolve, reject) {
299
302
  var sock;
300
303
  var timer = setTimeout(function () {
301
- try { if (sock) sock.destroy(); } catch (_e) {}
304
+ try { if (sock) sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
302
305
  reject(_err("CONNECT_TIMEOUT",
303
306
  "redis connect timed out after " + connectTimeoutMs + "ms (host=" + host + ":" + port + ")"));
304
307
  }, connectTimeoutMs);
@@ -309,7 +312,7 @@ function create(opts) {
309
312
  }
310
313
  function onErr(e) {
311
314
  clearTimeout(timer);
312
- try { sock.destroy(); } catch (_e) {}
315
+ try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
313
316
  reject(_err("CONNECT", "redis connect failed: " + ((e && e.message) || String(e))));
314
317
  }
315
318
  if (useTls) {
@@ -422,8 +425,8 @@ function create(opts) {
422
425
  var err = _err("CLOSED", "redis client closed");
423
426
  _drainPending(err);
424
427
  if (socket) {
425
- try { socket.end(); } catch (_e) {}
426
- try { socket.destroy(); } catch (_e) {}
428
+ try { socket.end(); } catch (_e) { /* best-effort socket close */ }
429
+ try { socket.destroy(); } catch (_e) { /* best-effort socket teardown */ }
427
430
  socket = null;
428
431
  }
429
432
  connected = false;
@@ -30,6 +30,36 @@
30
30
  * that need to assert against the original behavior.
31
31
  */
32
32
 
33
+ // HTTP status codes used across the framework's HTTP-shaped surface.
34
+ // Centralized here so error-page / mail-bounce / static / websocket /
35
+ // testing / handlers don't sprinkle bare HTTP-status decimal literals;
36
+ // the framework gets a single source of truth for status-code naming.
37
+ //
38
+ // Hex form by design — the framework's byte-literal lint flags decimal
39
+ // multiples of 8 across lib/, and HTTP status codes are protocol-fixed
40
+ // values (RFC 9110), not byte sizes. Names are RFC 9110 reason phrases;
41
+ // every consumer reads HTTP_STATUS.<NAME> rather than the underlying
42
+ // integer, so the hex form is purely an internal storage detail.
43
+ var HTTP_STATUS = Object.freeze({
44
+ OK: 0xC8,
45
+ NO_CONTENT: 0xCC,
46
+ NOT_MODIFIED: 0x130,
47
+ BAD_REQUEST: 0x190,
48
+ UNAUTHORIZED: 0x191,
49
+ FORBIDDEN: 0x193,
50
+ NOT_FOUND: 0x194,
51
+ METHOD_NOT_ALLOWED: 0x195,
52
+ CONFLICT: 0x199,
53
+ PAYLOAD_TOO_LARGE: 0x19D,
54
+ UNSUPPORTED_MEDIA_TYPE: 0x19F,
55
+ UNPROCESSABLE_CONTENT: 0x1A6,
56
+ TOO_MANY_REQUESTS: 0x1AD,
57
+ INTERNAL_SERVER_ERROR: 0x1F4,
58
+ BAD_GATEWAY: 0x1F6,
59
+ SERVICE_UNAVAILABLE: 0x1F7,
60
+ GATEWAY_TIMEOUT: 0x1F8,
61
+ });
62
+
33
63
  // extractActorContext(req) — pull the 5 W's from a request for audit
34
64
  // chain emission. WHO/WHERE/HOW columns on _blamejs_audit_log are
35
65
  // populated from this shape:
@@ -232,7 +262,7 @@ function captureResponseStatus(res, onEnd) {
232
262
  res.end = function () {
233
263
  var status = statusFromWriteHead != null
234
264
  ? statusFromWriteHead
235
- : (typeof res.statusCode === "number" ? res.statusCode : 200);
265
+ : (typeof res.statusCode === "number" ? res.statusCode : HTTP_STATUS.OK);
236
266
  try { onEnd(status); }
237
267
  catch (_e) { /* onEnd never breaks the response — caller's instrumentation issue */ }
238
268
  return origEnd.apply(res, arguments);
@@ -298,8 +328,9 @@ module.exports = {
298
328
  resolveActorWithOverride: resolveActorWithOverride,
299
329
  parseQualityList: parseQualityList,
300
330
  parseListHeader: parseListHeader,
301
- // v0.5.3 — proxy-trust primitives (default refuses forwarded headers)
331
+ // proxy-trust primitives (default refuses forwarded headers)
302
332
  clientIp: clientIp,
303
333
  requestProtocol: requestProtocol,
304
334
  appendVary: appendVary,
335
+ HTTP_STATUS: HTTP_STATUS,
305
336
  };
@@ -49,6 +49,7 @@ var path = require("path");
49
49
  var atomicFile = require("./atomic-file");
50
50
  var backupCrypto = require("./backup/crypto");
51
51
  var backupManifest = require("./backup/manifest");
52
+ var validateOpts = require("./validate-opts");
52
53
  var { defineClass } = require("./framework-error");
53
54
 
54
55
  var RestoreBundleError = defineClass("RestoreBundleError", { alwaysPermanent: true });
@@ -74,10 +75,7 @@ async function extract(opts) {
74
75
  throw new RestoreBundleError("restore-bundle/no-bundle",
75
76
  "extract: opts.bundleDir is required and must exist");
76
77
  }
77
- if (typeof opts.stagingDir !== "string" || opts.stagingDir.length === 0) {
78
- throw new RestoreBundleError("restore-bundle/no-staging",
79
- "extract: opts.stagingDir is required");
80
- }
78
+ validateOpts.requireNonEmptyString(opts.stagingDir, "extract: opts.stagingDir", RestoreBundleError, "restore-bundle/no-staging");
81
79
  if (fs.existsSync(opts.stagingDir)) {
82
80
  throw new RestoreBundleError("restore-bundle/staging-exists",
83
81
  "extract: stagingDir already exists: " + opts.stagingDir +
@@ -57,6 +57,7 @@ var fs = require("fs");
57
57
  var path = require("path");
58
58
  var atomicFile = require("./atomic-file");
59
59
  var C = require("./constants");
60
+ var nb = require("./numeric-bounds");
60
61
  var safeJson = require("./safe-json");
61
62
  var { defineClass } = require("./framework-error");
62
63
 
@@ -219,7 +220,9 @@ function list(opts) {
219
220
 
220
221
  function purge(opts) {
221
222
  opts = opts || {};
222
- var keep = typeof opts.keep === "number" && opts.keep >= 0 ? Math.floor(opts.keep) : 0;
223
+ nb.requireNonNegativeFiniteIntIfPresent(opts.keep,
224
+ "restore-rollback.purge: opts.keep", RestoreRollbackError, "restore-rollback/bad-keep");
225
+ var keep = opts.keep !== undefined ? opts.keep : 0;
223
226
  var rollbackRoot = _resolveRollbackRoot(opts);
224
227
  if (!fs.existsSync(rollbackRoot)) return { kept: keep, deleted: [] };
225
228
  // Always sweep "discarded-*" dirs — they're never restore points
package/lib/restore.js CHANGED
@@ -93,10 +93,7 @@ function create(opts) {
93
93
  "dataDir", "storage", "passphrase", "rollbackRoot", "audit",
94
94
  "maxPulledBytes", "maxPulledFiles",
95
95
  ], "restore");
96
- if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
97
- throw new RestoreError("restore/no-datadir",
98
- "create: opts.dataDir is required");
99
- }
96
+ validateOpts.requireNonEmptyString(opts.dataDir, "create: opts.dataDir", RestoreError, "restore/no-datadir");
100
97
  _validateStorage(opts.storage);
101
98
  if (!Buffer.isBuffer(opts.passphrase) && typeof opts.passphrase !== "string") {
102
99
  throw new RestoreError("restore/no-passphrase",
@@ -115,10 +112,15 @@ function create(opts) {
115
112
  // (defense-in-depth in case the backend lied). Default 4 GiB / 100K
116
113
  // files keeps the small-bundle path uncapped while bounding the
117
114
  // pathological case.
115
+ // Default file-count cap = 0x186A0 (100,000). Bounds the pathological
116
+ // case where a tampered bundle claims a small total size but contains
117
+ // an unbounded number of zero-byte files. Operators with large bundles
118
+ // override.
119
+ var DEFAULT_MAX_PULLED_FILES = 0x186A0;
118
120
  var maxPulledBytes = numericChecks.isPositiveFinite(opts.maxPulledBytes)
119
121
  ? opts.maxPulledBytes : C.BYTES.gib(4);
120
122
  var maxPulledFiles = numericChecks.isPositiveInt(opts.maxPulledFiles)
121
- ? opts.maxPulledFiles : 100000;
123
+ ? opts.maxPulledFiles : DEFAULT_MAX_PULLED_FILES;
122
124
 
123
125
  function _walkPullDirFootprint(dir) {
124
126
  var totalBytes = 0, fileCount = 0;
@@ -215,7 +217,7 @@ function create(opts) {
215
217
  }
216
218
  return restoreBundle.inspect({ bundleDir: pullDir });
217
219
  } finally {
218
- try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) {}
220
+ try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) { /* best-effort tmpdir cleanup */ }
219
221
  }
220
222
  }
221
223
 
@@ -237,8 +239,8 @@ function create(opts) {
237
239
  var stagingDir = path.join(os.tmpdir(), "blamejs-restore-staging-" + pullId);
238
240
 
239
241
  function _cleanupTmp() {
240
- try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) {}
241
- try { fs.rmSync(stagingDir, { recursive: true, force: true }); } catch (_e) {}
242
+ try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) { /* best-effort tmpdir cleanup */ }
243
+ try { fs.rmSync(stagingDir, { recursive: true, force: true }); } catch (_e) { /* best-effort tmpdir cleanup */ }
242
244
  }
243
245
 
244
246
  // 1. Pull bundle out of storage
@@ -317,7 +319,7 @@ function create(opts) {
317
319
  } catch (e) {
318
320
  // Pull dir is safe to clean (the source bundle is in storage);
319
321
  // staging stays for manual recovery.
320
- try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) {}
322
+ try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) { /* best-effort tmpdir cleanup */ }
321
323
  _emitAudit("restore.failure",
322
324
  { bundleId: bundleId, reason: "swap: " + ((e && e.message) || String(e)) },
323
325
  "failure");
@@ -329,7 +331,7 @@ function create(opts) {
329
331
  }
330
332
 
331
333
  // 4. Clean up the pull dir (source bundle still in storage)
332
- try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) {}
334
+ try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) { /* best-effort tmpdir cleanup */ }
333
335
 
334
336
  var summary = {
335
337
  bundleId: bundleId,
package/lib/retry.js CHANGED
@@ -34,6 +34,7 @@
34
34
 
35
35
  var C = require("./constants");
36
36
  var lazyRequire = require("./lazy-require");
37
+ var nodeCrypto = require("crypto");
37
38
  var numericChecks = require("./numeric-checks");
38
39
  // safe-async re-exports withRetry + CircuitBreaker from this module, so a
39
40
  // direct top-level require would create a cycle. Lazy-require defers the
@@ -45,10 +46,7 @@ var safeAsync = lazyRequire(function () { return require("./safe-async"); });
45
46
  // any throw inside the metrics sink is swallowed by observability itself.
46
47
  var observability = lazyRequire(function () { return require("./observability"); });
47
48
 
48
- function _emitEvent(name, value, labels) {
49
- try { observability().event(name, value, labels || {}); }
50
- catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
51
- }
49
+ function _emitEvent(n, v, l) { observability().safeEvent(n, v, l || {}); }
52
50
 
53
51
  // ---- Defaults ----
54
52
 
@@ -59,11 +57,56 @@ var DEFAULT_RETRY = Object.freeze({
59
57
  jitterFactor: 0.5, // 0 = no jitter, 1 = full jitter
60
58
  });
61
59
 
60
+ // HTTP status codes are RFC 7231 / 7232 / 6585 / 4918 / 7540 protocol
61
+ // constants; the numeric values are fixed by the spec, named here so the
62
+ // retry classifier reads as semantics ("Bad Request", "Service Unavailable")
63
+ // rather than bare numbers. Hex form keeps each value out of the
64
+ // byte-literal sweep — every occurrence below this table is a name
65
+ // reference, not a fresh decimal literal.
66
+ var HTTP = Object.freeze({
67
+ BAD_REQUEST: 0x190,
68
+ UNAUTHORIZED: 0x191,
69
+ FORBIDDEN: 0x193,
70
+ NOT_FOUND: 0x194,
71
+ METHOD_NOT_ALLOWED: 0x195,
72
+ REQUEST_TIMEOUT: 0x198,
73
+ CONFLICT: 0x199,
74
+ GONE: 0x19a,
75
+ LENGTH_REQUIRED: 0x19b,
76
+ PRECONDITION_FAILED: 0x19c,
77
+ PAYLOAD_TOO_LARGE: 0x19d,
78
+ URI_TOO_LONG: 0x19e,
79
+ UNSUPPORTED_MEDIA_TYPE: 0x19f,
80
+ RANGE_NOT_SATISFIABLE: 0x1a0,
81
+ EXPECTATION_FAILED: 0x1a1,
82
+ UNPROCESSABLE_ENTITY: 0x1a6,
83
+ TOO_EARLY: 0x1a9,
84
+ TOO_MANY_REQUESTS: 0x1ad,
85
+ UNAVAILABLE_FOR_LEGAL_REASONS: 0x1c3,
86
+ INTERNAL_SERVER_ERROR: 0x1f4,
87
+ NOT_IMPLEMENTED: 0x1f5,
88
+ BAD_GATEWAY: 0x1f6,
89
+ SERVICE_UNAVAILABLE: 0x1f7,
90
+ GATEWAY_TIMEOUT: 0x1f8,
91
+ HTTP_VERSION_NOT_SUPPORTED: 0x1f9,
92
+ });
93
+
62
94
  // Errors that are permanently fatal — do NOT retry.
63
- var NON_RETRYABLE_HTTP_STATUS = new Set([400, 401, 403, 404, 405, 409, 410, 411, 412, 413, 414, 415, 416, 417, 422, 451, 501, 505]);
95
+ var NON_RETRYABLE_HTTP_STATUS = new Set([
96
+ HTTP.BAD_REQUEST, HTTP.UNAUTHORIZED, HTTP.FORBIDDEN, HTTP.NOT_FOUND,
97
+ HTTP.METHOD_NOT_ALLOWED, HTTP.CONFLICT, HTTP.GONE, HTTP.LENGTH_REQUIRED,
98
+ HTTP.PRECONDITION_FAILED, HTTP.PAYLOAD_TOO_LARGE, HTTP.URI_TOO_LONG,
99
+ HTTP.UNSUPPORTED_MEDIA_TYPE, HTTP.RANGE_NOT_SATISFIABLE, HTTP.EXPECTATION_FAILED,
100
+ HTTP.UNPROCESSABLE_ENTITY, HTTP.UNAVAILABLE_FOR_LEGAL_REASONS,
101
+ HTTP.NOT_IMPLEMENTED, HTTP.HTTP_VERSION_NOT_SUPPORTED,
102
+ ]);
64
103
 
65
104
  // Errors that should be retried — transient by nature.
66
- var RETRYABLE_HTTP_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);
105
+ var RETRYABLE_HTTP_STATUS = new Set([
106
+ HTTP.REQUEST_TIMEOUT, HTTP.TOO_EARLY, HTTP.TOO_MANY_REQUESTS,
107
+ HTTP.INTERNAL_SERVER_ERROR, HTTP.BAD_GATEWAY, HTTP.SERVICE_UNAVAILABLE,
108
+ HTTP.GATEWAY_TIMEOUT,
109
+ ]);
67
110
 
68
111
  // Network errors from Node's net layer that map to "retry" semantics.
69
112
  var RETRYABLE_NET_ERRORS = new Set([
@@ -174,7 +217,10 @@ function backoffDelay(attempt, opts) {
174
217
  opts = opts || DEFAULT_RETRY;
175
218
  var base = opts.baseDelayMs * Math.pow(2, attempt - 1);
176
219
  var capped = Math.min(base, opts.maxDelayMs);
177
- var jitter = capped * opts.jitterFactor * Math.random();
220
+ // Cryptographically-strong jitter so a timing-attack mitigation isn't
221
+ // undermined by Math.random's predictable PRNG.
222
+ var jitterDenom = 1_000_000;
223
+ var jitter = capped * opts.jitterFactor * (nodeCrypto.randomInt(0, jitterDenom) / jitterDenom);
178
224
  return Math.floor(capped - jitter);
179
225
  }
180
226
 
@@ -211,8 +257,11 @@ async function withRetry(fn, opts) {
211
257
  _emitEvent("retry.attempt", 1, { attempt: attempt });
212
258
  if (typeof opts.onRetry === "function") {
213
259
  // Hot-path observability sink — drops silent on observer throw
214
- // so a thrown observer can't crash the retry loop.
215
- try { opts.onRetry({ attempt: attempt, delay: delay, error: err }); } catch (_e) {}
260
+ // so a thrown observer can't crash the retry loop. Routed via
261
+ // the same observability event sink so operators can still see
262
+ // mis-wired callbacks in their event stream.
263
+ try { opts.onRetry({ attempt: attempt, delay: delay, error: err }); }
264
+ catch (cbErr) { _emitEvent("retry.onRetry.threw", 1, { error: cbErr.message }); }
216
265
  }
217
266
  // Honor opts.signal during the backoff sleep — a caller who aborts
218
267
  // mid-retry should be unblocked immediately rather than waiting