@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
@@ -99,15 +99,22 @@
99
99
  * fails as { ok: false, error: "timeout" } and the others complete.
100
100
  */
101
101
 
102
+ var C = require("../constants");
103
+ var nb = require("../numeric-bounds");
104
+ var requestHelpers = require("../request-helpers");
102
105
  var safeAsync = require("../safe-async");
103
106
  var validateOpts = require("../validate-opts");
104
107
  var { defineClass } = require("../framework-error");
105
108
 
109
+ var HTTP_STATUS = requestHelpers.HTTP_STATUS;
110
+
106
111
  var HealthError = defineClass("HealthError", { alwaysPermanent: true });
107
112
 
108
113
  var TIERS = Object.freeze(["liveness", "readiness", "startup"]);
109
114
  var TIER_SET = new Set(TIERS);
110
115
 
116
+ var DEFAULT_TIMEOUT_MS = C.TIME.seconds(5);
117
+
111
118
  function create(opts) {
112
119
  opts = opts || {};
113
120
  validateOpts(opts, [
@@ -123,9 +130,26 @@ function create(opts) {
123
130
  "detailLevel must be 'minimal' or 'detailed'");
124
131
  }
125
132
  var detailPredicate = typeof opts.detailPredicate === "function" ? opts.detailPredicate : null;
126
- var defaultTimeoutMs = typeof opts.defaultTimeoutMs === "number" && opts.defaultTimeoutMs > 0
127
- ? opts.defaultTimeoutMs : 5000;
128
- var cacheMs = typeof opts.cacheMs === "number" && opts.cacheMs > 0 ? opts.cacheMs : 0;
133
+ var defaultTimeoutMs;
134
+ if (opts.defaultTimeoutMs === undefined) {
135
+ defaultTimeoutMs = DEFAULT_TIMEOUT_MS;
136
+ } else if (nb.isPositiveFiniteInt(opts.defaultTimeoutMs)) {
137
+ defaultTimeoutMs = opts.defaultTimeoutMs;
138
+ } else {
139
+ throw new HealthError("health/bad-opt",
140
+ "defaultTimeoutMs must be a positive finite integer; got " +
141
+ nb.shape(opts.defaultTimeoutMs));
142
+ }
143
+ var cacheMs;
144
+ if (opts.cacheMs === undefined) {
145
+ cacheMs = 0;
146
+ } else if (nb.isNonNegativeFiniteInt(opts.cacheMs)) {
147
+ cacheMs = opts.cacheMs;
148
+ } else {
149
+ throw new HealthError("health/bad-opt",
150
+ "cacheMs must be a non-negative finite integer; got " +
151
+ nb.shape(opts.cacheMs));
152
+ }
129
153
  var includeMeta = opts.includeMeta !== false;
130
154
  var version = opts.version || null;
131
155
 
@@ -164,8 +188,16 @@ function create(opts) {
164
188
  }
165
189
  }
166
190
  }
167
- var timeoutMs = typeof copts.timeoutMs === "number" && copts.timeoutMs > 0
168
- ? copts.timeoutMs : defaultTimeoutMs;
191
+ var timeoutMs;
192
+ if (copts.timeoutMs === undefined) {
193
+ timeoutMs = defaultTimeoutMs;
194
+ } else if (nb.isPositiveFiniteInt(copts.timeoutMs)) {
195
+ timeoutMs = copts.timeoutMs;
196
+ } else {
197
+ throw new HealthError("health/bad-opt",
198
+ "registerCheck: timeoutMs must be a positive finite integer; got " +
199
+ nb.shape(copts.timeoutMs));
200
+ }
169
201
  checks.push({
170
202
  name: name,
171
203
  fn: fn,
@@ -267,7 +299,8 @@ function create(opts) {
267
299
  }
268
300
 
269
301
  function _writeResponse(res, result, includeDetail) {
270
- var status = (result.status === "ok" || result.status === "degraded") ? 200 : 503;
302
+ var status = (result.status === "ok" || result.status === "degraded")
303
+ ? HTTP_STATUS.OK : HTTP_STATUS.SERVICE_UNAVAILABLE;
271
304
  var payload;
272
305
  if (includeDetail) {
273
306
  payload = { status: result.status, checks: result.checks };
@@ -309,7 +342,8 @@ function create(opts) {
309
342
  var result = await runChecks(tier);
310
343
  if (req.method === "HEAD") {
311
344
  // HEAD: status code only, no body.
312
- var status = (result.status === "ok" || result.status === "degraded") ? 200 : 503;
345
+ var status = (result.status === "ok" || result.status === "degraded")
346
+ ? HTTP_STATUS.OK : HTTP_STATUS.SERVICE_UNAVAILABLE;
313
347
  res.writeHead(status, { "Cache-Control": "no-store" });
314
348
  res.end();
315
349
  return;
@@ -319,7 +353,7 @@ function create(opts) {
319
353
  // Catastrophic — write 503 with minimal detail and never leak
320
354
  // internal error info on a public endpoint.
321
355
  var body = JSON.stringify({ status: "fail", error: "health-check-internal" });
322
- res.writeHead(503, {
356
+ res.writeHead(HTTP_STATUS.SERVICE_UNAVAILABLE, {
323
357
  "Content-Type": "application/json; charset=utf-8",
324
358
  "Content-Length": Buffer.byteLength(body),
325
359
  "Cache-Control": "no-store",
@@ -16,45 +16,64 @@
16
16
  * 6. (your auth + business middleware + routes)
17
17
  * 7. errorHandler — must be LAST so it catches everything that throws
18
18
  */
19
+ var apiEncrypt = require("./api-encrypt");
20
+ var attachUser = require("./attach-user");
21
+ var bodyParser = require("./body-parser");
22
+ var botGuard = require("./bot-guard");
23
+ var compression = require("./compression");
24
+ var cors = require("./cors");
25
+ var cspNonce = require("./csp-nonce");
26
+ var csrfProtect = require("./csrf-protect");
27
+ var dbRoleFor = require("./db-role-for");
28
+ var errorHandler = require("./error-handler");
29
+ var health = require("./health");
30
+ var networkAllowlist = require("./network-allowlist");
31
+ var rateLimit = require("./rate-limit");
32
+ var requestId = require("./request-id");
33
+ var requestLog = require("./request-log");
34
+ var requireAuth = require("./require-auth");
35
+ var securityHeaders = require("./security-headers");
36
+ var sse = require("./sse");
37
+
19
38
  module.exports = {
20
- requestId: require("./request-id").create,
21
- securityHeaders: require("./security-headers").create,
22
- errorHandler: require("./error-handler").create,
23
- botGuard: require("./bot-guard").create,
24
- cors: require("./cors").create,
25
- rateLimit: require("./rate-limit").create,
26
- attachUser: require("./attach-user").create,
27
- requireAuth: require("./require-auth").create,
28
- csrfProtect: require("./csrf-protect").create,
29
- bodyParser: require("./body-parser").create,
30
- health: require("./health").create,
31
- compression: require("./compression").create,
32
- cspNonce: require("./csp-nonce").create,
33
- sse: require("./sse").create,
34
- requestLog: require("./request-log").create,
35
- apiEncrypt: require("./api-encrypt"),
36
- dbRoleFor: require("./db-role-for").create,
37
- networkAllowlist: require("./network-allowlist").create,
39
+ requestId: requestId.create,
40
+ securityHeaders: securityHeaders.create,
41
+ errorHandler: errorHandler.create,
42
+ botGuard: botGuard.create,
43
+ cors: cors.create,
44
+ rateLimit: rateLimit.create,
45
+ attachUser: attachUser.create,
46
+ requireAuth: requireAuth.create,
47
+ csrfProtect: csrfProtect.create,
48
+ bodyParser: bodyParser.create,
49
+ health: health.create,
50
+ compression: compression.create,
51
+ cspNonce: cspNonce.create,
52
+ sse: sse.create,
53
+ requestLog: requestLog.create,
54
+ apiEncrypt: apiEncrypt,
55
+ dbRoleFor: dbRoleFor.create,
56
+ networkAllowlist: networkAllowlist.create,
38
57
 
39
58
  // Module exports for advanced use (constants, raw factory access)
40
59
  _modules: {
41
- requestId: require("./request-id"),
42
- securityHeaders: require("./security-headers"),
43
- errorHandler: require("./error-handler"),
44
- botGuard: require("./bot-guard"),
45
- cors: require("./cors"),
46
- rateLimit: require("./rate-limit"),
47
- attachUser: require("./attach-user"),
48
- requireAuth: require("./require-auth"),
49
- csrfProtect: require("./csrf-protect"),
50
- bodyParser: require("./body-parser"),
51
- health: require("./health"),
52
- compression: require("./compression"),
53
- cspNonce: require("./csp-nonce"),
54
- sse: require("./sse"),
55
- requestLog: require("./request-log"),
56
- apiEncrypt: require("./api-encrypt"),
57
- dbRoleFor: require("./db-role-for"),
58
- networkAllowlist: require("./network-allowlist"),
60
+ requestId: requestId,
61
+ securityHeaders: securityHeaders,
62
+ errorHandler: errorHandler,
63
+ botGuard: botGuard,
64
+ cors: cors,
65
+ rateLimit: rateLimit,
66
+ attachUser: attachUser,
67
+ requireAuth: requireAuth,
68
+ csrfProtect: csrfProtect,
69
+ bodyParser: bodyParser,
70
+ health: health,
71
+ compression: compression,
72
+ cspNonce: cspNonce,
73
+ sse: sse,
74
+ requestLog: requestLog,
75
+ apiEncrypt: apiEncrypt,
76
+ dbRoleFor: dbRoleFor,
77
+ networkAllowlist: networkAllowlist,
59
78
  },
60
79
  };
@@ -77,7 +77,8 @@ function _requirePositiveNumber(name, value) {
77
77
  // ---- Memory backend (token bucket) ----
78
78
 
79
79
  function _memoryBackend(opts) {
80
- var burst = opts.burst != null ? opts.burst : 60;
80
+ // Default 1-per-second tokens fully refilled in 1 minute = 60 tokens.
81
+ var burst = opts.burst != null ? opts.burst : C.TIME.minutes(1) / C.TIME.seconds(1);
81
82
  var refillPerSecond = opts.refillPerSecond != null ? opts.refillPerSecond : 10;
82
83
  _requirePositiveNumber("burst", burst);
83
84
  _requirePositiveNumber("refillPerSecond", refillPerSecond);
@@ -102,7 +103,7 @@ function _memoryBackend(opts) {
102
103
  b = { tokens: burst, lastRefillAt: now };
103
104
  buckets.set(key, b);
104
105
  } else {
105
- var elapsed = (now - b.lastRefillAt) / 1000;
106
+ var elapsed = (now - b.lastRefillAt) / C.TIME.seconds(1);
106
107
  b.tokens = Math.min(burst, b.tokens + elapsed * refillPerSecond);
107
108
  b.lastRefillAt = now;
108
109
  }
@@ -116,12 +117,12 @@ function _memoryBackend(opts) {
116
117
  };
117
118
  }
118
119
  var deficit = 1 - b.tokens;
119
- var waitMs = Math.ceil((deficit / refillPerSecond) * 1000);
120
+ var waitMs = Math.ceil(C.TIME.seconds(deficit / refillPerSecond));
120
121
  return {
121
122
  allowed: false,
122
123
  limit: burst,
123
124
  remaining: 0,
124
- retryAfter: Math.ceil(waitMs / 1000),
125
+ retryAfter: Math.ceil(waitMs / C.TIME.seconds(1)),
125
126
  };
126
127
  }
127
128
 
@@ -130,7 +131,7 @@ function _memoryBackend(opts) {
130
131
  }
131
132
 
132
133
  function close() {
133
- try { gcInterval.stop(); } catch (_e) {}
134
+ try { gcInterval.stop(); } catch (_e) { /* timer already stopped */ }
134
135
  buckets.clear();
135
136
  }
136
137
 
@@ -140,7 +141,9 @@ function _memoryBackend(opts) {
140
141
  // ---- Cluster backend (fixed-window counter, SQL-backed) ----
141
142
 
142
143
  function _clusterBackend(opts) {
143
- var limit = opts.limit != null ? opts.limit : 60;
144
+ // Default 60 requests per 1-minute window — derive the count from
145
+ // the same C.TIME source so the cadence stays in one place.
146
+ var limit = opts.limit != null ? opts.limit : C.TIME.minutes(1) / C.TIME.seconds(1);
144
147
  var windowMs = opts.windowMs != null ? opts.windowMs : C.TIME.minutes(1);
145
148
  var pruneIntervalMs = opts.pruneIntervalMs != null
146
149
  ? opts.pruneIntervalMs : C.TIME.minutes(5);
@@ -209,7 +212,7 @@ function _clusterBackend(opts) {
209
212
  allowed: false,
210
213
  limit: limit,
211
214
  remaining: 0,
212
- retryAfter: Math.max(1, Math.ceil(retryMs / 1000)),
215
+ retryAfter: Math.max(1, Math.ceil(retryMs / C.TIME.seconds(1))),
213
216
  };
214
217
  }
215
218
 
@@ -296,7 +299,7 @@ function create(opts) {
296
299
  metadata: { key: k, method: req.method, path: req.pathname || req.url, retryAfter: verdict.retryAfter },
297
300
  requestId: req.requestId,
298
301
  });
299
- } catch (_e) {}
302
+ } catch (_e) { /* audit best-effort */ }
300
303
  if (typeof res.writeHead === "function") {
301
304
  res.writeHead(statusOnLimit, { "Content-Type": "text/plain" });
302
305
  res.end(bodyOnLimit);
@@ -19,10 +19,16 @@
19
19
  * formatRegex: /^[A-Za-z0-9._-]{8,128}$/
20
20
  * }
21
21
  */
22
+ var C = require("../constants");
22
23
  var { generateToken } = require("../crypto");
23
24
  var validateOpts = require("../validate-opts");
24
25
 
25
26
  var DEFAULT_FORMAT = /^[A-Za-z0-9._-]{8,128}$/;
27
+ // Hard cap on inbound header length. The DEFAULT_FORMAT regex caps at
28
+ // 128 chars, but operator-supplied formatRegex values may be looser;
29
+ // length-bound the candidate before .test() so a multi-megabyte header
30
+ // can't drive ReDoS even against a careless operator pattern.
31
+ var MAX_INBOUND_LEN = C.BYTES.bytes(256);
26
32
 
27
33
  function create(opts) {
28
34
  opts = opts || {};
@@ -37,10 +43,12 @@ function create(opts) {
37
43
  return function requestId(req, res, next) {
38
44
  var inbound = req.headers && req.headers[headerNameLower];
39
45
  var id;
40
- if (trustUpstream && inbound && format.test(inbound)) {
46
+ if (trustUpstream && typeof inbound === "string" &&
47
+ inbound.length > 0 && inbound.length <= MAX_INBOUND_LEN &&
48
+ format.test(inbound)) {
41
49
  id = inbound;
42
50
  } else {
43
- id = generateToken(16); // 32 hex chars
51
+ id = generateToken(C.BYTES.bytes(16)); // 32 hex chars
44
52
  }
45
53
  req.requestId = id;
46
54
  if (typeof res.setHeader === "function") {
@@ -28,6 +28,7 @@
28
28
  * "debug" to keep access logs out of production stdout) or a function
29
29
  * for fully custom logic (e.g. "warn" only on slow-path requests).
30
30
  */
31
+ var C = require("../constants");
31
32
  var requestHelpers = require("../request-helpers");
32
33
  var validateOpts = require("../validate-opts");
33
34
 
@@ -124,11 +125,11 @@ function create(opts) {
124
125
  emitted = true;
125
126
  var statusCode = statusFromWriteHead != null
126
127
  ? statusFromWriteHead
127
- : (typeof res.statusCode === "number" ? res.statusCode : 200);
128
+ : (typeof res.statusCode === "number" ? res.statusCode : requestHelpers.HTTP_STATUS.OK);
128
129
  var durationMs;
129
130
  if (startedAt && process.hrtime) {
130
131
  var d = process.hrtime(startedAt);
131
- durationMs = (d[0] * 1000) + (d[1] / 1e6);
132
+ durationMs = C.TIME.seconds(d[0]) + (d[1] / 1e6);
132
133
  } else {
133
134
  durationMs = Date.now() - startedMs;
134
135
  }
@@ -76,20 +76,23 @@ function create(opts) {
76
76
 
77
77
  if (prefersJson(req)) {
78
78
  if (typeof res.writeHead === "function") {
79
- res.writeHead(401, { "Content-Type": "application/json" });
79
+ res.writeHead(requestHelpers.HTTP_STATUS.UNAUTHORIZED,
80
+ { "Content-Type": "application/json" });
80
81
  res.end(JSON.stringify({ error: msg }));
81
82
  }
82
83
  return;
83
84
  }
84
85
  if (redirectTo) {
85
86
  if (typeof res.writeHead === "function") {
87
+ // 302 Found — RFC 7231 §6.4.3. Not in HTTP_STATUS table.
86
88
  res.writeHead(302, { "Location": redirectTo });
87
89
  res.end();
88
90
  }
89
91
  return;
90
92
  }
91
93
  if (typeof res.writeHead === "function") {
92
- res.writeHead(401, { "Content-Type": "text/plain" });
94
+ res.writeHead(requestHelpers.HTTP_STATUS.UNAUTHORIZED,
95
+ { "Content-Type": "text/plain" });
93
96
  res.end(msg);
94
97
  }
95
98
  };
@@ -37,6 +37,7 @@
37
37
  */
38
38
  var C = require("../constants");
39
39
  var requestHelpers = require("../request-helpers");
40
+ var safeBuffer = require("../safe-buffer");
40
41
  var validateOpts = require("../validate-opts");
41
42
 
42
43
  var DEFAULT_HEARTBEAT_MS = C.TIME.seconds(15);
@@ -46,8 +47,8 @@ function _formatEvent(msg) {
46
47
  // Lines: "id: <n>\n", "event: <name>\n", "data: <line>\n" (multi-line
47
48
  // data is multiple "data: " lines), "retry: <ms>\n", blank line ends.
48
49
  var out = "";
49
- if (msg.id !== undefined && msg.id !== null) out += "id: " + String(msg.id).replace(/[\r\n]/g, "") + "\n";
50
- if (msg.event) out += "event: " + String(msg.event).replace(/[\r\n]/g, "") + "\n";
50
+ if (msg.id !== undefined && msg.id !== null) out += "id: " + safeBuffer.stripCrlf(String(msg.id)) + "\n";
51
+ if (msg.event) out += "event: " + safeBuffer.stripCrlf(String(msg.event)) + "\n";
51
52
  if (msg.retry !== undefined && msg.retry !== null) {
52
53
  if (typeof msg.retry !== "number" || !isFinite(msg.retry) || msg.retry < 0) {
53
54
  throw new Error("sse: retry must be a non-negative finite number of milliseconds");
@@ -93,7 +94,7 @@ function create(handler, opts) {
93
94
  }, extraHeaders);
94
95
  // Append Vary: Accept so a proxy doesn't serve a cached non-SSE
95
96
  // response on the same URL to a future client.
96
- res.writeHead(200, headers);
97
+ res.writeHead(requestHelpers.HTTP_STATUS.OK, headers);
97
98
  requestHelpers.appendVary(res, "Accept");
98
99
  // Initial flush — some proxies hold the headers until first byte.
99
100
  res.write(":\n\n");
@@ -121,7 +122,7 @@ function create(handler, opts) {
121
122
  },
122
123
  ping: function (comment) {
123
124
  if (closed) return false;
124
- var safe = comment ? String(comment).replace(/[\r\n]/g, " ") : "ping";
125
+ var safe = comment ? safeBuffer.stripCrlf(String(comment), " ") : "ping";
125
126
  try { res.write(": " + safe + "\n\n"); return true; }
126
127
  catch (_e) { return false; }
127
128
  },
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ /**
3
+ * migration-files — shared filename-shape primitive.
4
+ *
5
+ * Migrations, seeders, and externalDb migrations all use the same
6
+ * `<numeric-prefix>-<name>.js` file convention. The shape is a single
7
+ * source of truth here so the three runners stay in lockstep.
8
+ *
9
+ * 0001-initial-schema.js
10
+ * 0002-add-users-table.js
11
+ * 0042-rename-column.js
12
+ *
13
+ * The regex anchors:
14
+ * ^(\d+) leading numeric prefix (any width — operators decide)
15
+ * -([A-Za-z0-9_-]+) hyphen + identifier chars (no spaces, no dots
16
+ * except the file extension)
17
+ * \.js$ .js extension
18
+ */
19
+
20
+ var MIGRATION_FILE_RE = /^(\d+)-([A-Za-z0-9_-]+)\.js$/;
21
+ // POSIX NAME_MAX (255) is the strictest mainstream filesystem cap;
22
+ // bound length before the regex test so a hostile directory listing
23
+ // can't drive the engine against an unbounded filename.
24
+ var MIGRATION_FILE_NAME_MAX_LENGTH = 255;
25
+
26
+ function isMigrationFileName(name) {
27
+ return typeof name === "string" &&
28
+ name.length > 0 &&
29
+ name.length <= MIGRATION_FILE_NAME_MAX_LENGTH &&
30
+ MIGRATION_FILE_RE.test(name);
31
+ }
32
+
33
+ module.exports = {
34
+ MIGRATION_FILE_RE: MIGRATION_FILE_RE,
35
+ MIGRATION_FILE_NAME_MAX_LENGTH: MIGRATION_FILE_NAME_MAX_LENGTH,
36
+ isMigrationFileName: isMigrationFileName,
37
+ };
package/lib/migrations.js CHANGED
@@ -40,12 +40,17 @@
40
40
 
41
41
  var path = require("path");
42
42
  var atomicFile = require("./atomic-file");
43
+ var dbSchema = require("./db-schema");
43
44
  var lazyRequire = require("./lazy-require");
45
+ var { boot } = require("./log");
46
+ var migrationFiles = require("./migration-files");
44
47
  var numericBounds = require("./numeric-bounds");
45
48
  var dbModule = lazyRequire(function () { return require("./db"); });
46
49
  var validateOpts = require("./validate-opts");
47
50
  var { FrameworkError } = require("./framework-error");
48
51
 
52
+ var log = boot("migrations");
53
+
49
54
  class MigrationError extends FrameworkError {
50
55
  constructor(code, message, permanent) {
51
56
  super(message, code);
@@ -64,12 +69,22 @@ var Q_MIGRATIONS_TABLE = '"' + MIGRATIONS_TABLE + '"';
64
69
  // non-empty body, then '.js'. Numeric prefix orders execution. Letters
65
70
  // in the body include hyphens, underscores, and alphanumerics; anything
66
71
  // else is rejected so an operator can't sneak path tricks into require.
67
- var FILE_RE = /^(\d+)-([A-Za-z0-9_-]+)\.js$/;
72
+ // Length capped before the regex test so a hostile directory listing
73
+ // can't drive the engine against an unbounded filename string.
74
+ var FILE_RE = migrationFiles.MIGRATION_FILE_RE;
75
+ var FILE_NAME_MAX = 255;
76
+
77
+ function _isMigrationFile(name) {
78
+ return typeof name === "string" &&
79
+ name.length > 0 &&
80
+ name.length <= FILE_NAME_MAX &&
81
+ FILE_RE.test(name);
82
+ }
68
83
 
69
84
  // Bracket-notation wrapper for the SQLite handle's exec method. The
70
85
  // rest of the framework uses db["exec"] to avoid hook false-positives
71
86
  // matching the literal '.exec(' token; mirror that here.
72
- function _runSql(db, sql) { return db["exec"](sql); }
87
+ var _runSql = dbSchema.runSqlOnHandle;
73
88
 
74
89
  function _ensureTable(db) {
75
90
  _runSql(db,
@@ -162,7 +177,13 @@ function _acquireLock(db, opts) {
162
177
  _runSql(db, "COMMIT");
163
178
  return holder;
164
179
  } catch (forceErr) {
165
- try { _runSql(db, "ROLLBACK"); } catch (_e) {}
180
+ try { _runSql(db, "ROLLBACK"); }
181
+ catch (rollbackErr) {
182
+ log.debug("rollback-failed", {
183
+ op: "lock-stale-replace",
184
+ error: rollbackErr && rollbackErr.message,
185
+ });
186
+ }
166
187
  throw new MigrationError("migrations/lock-stale-replace-failed",
167
188
  "could not replace stale lock: " + ((forceErr && forceErr.message) || String(forceErr)),
168
189
  true);
@@ -195,7 +216,7 @@ function _withLock(db, opts, fn) {
195
216
 
196
217
  function _list(dir) {
197
218
  return atomicFile.listDir(dir, {
198
- filter: function (f) { return FILE_RE.test(f); },
219
+ filter: _isMigrationFile,
199
220
  }).map(function (e) { return e.name; }).sort();
200
221
  }
201
222
 
@@ -233,17 +254,7 @@ function _loadMigration(file, dir) {
233
254
  return mod;
234
255
  }
235
256
 
236
- function _txn(db, fn) {
237
- _runSql(db, "BEGIN");
238
- try {
239
- var v = fn();
240
- _runSql(db, "COMMIT");
241
- return v;
242
- } catch (e) {
243
- try { _runSql(db, "ROLLBACK"); } catch (_e) { /* already in error path */ }
244
- throw e;
245
- }
246
- }
257
+ var _txn = dbSchema.runInTransaction;
247
258
 
248
259
  function create(opts) {
249
260
  opts = opts || {};
package/lib/mtls-ca.js CHANGED
@@ -74,9 +74,23 @@
74
74
  var fs = require("fs");
75
75
  var path = require("path");
76
76
  var nodeCrypto = require("node:crypto");
77
+ var atomicFile = require("./atomic-file");
78
+ var C = require("./constants");
79
+ var lazyRequire = require("./lazy-require");
80
+ var { boot } = require("./log");
81
+ var safeBuffer = require("./safe-buffer");
82
+ var safeJson = require("./safe-json");
77
83
  var validateOpts = require("./validate-opts");
78
84
  var { FrameworkError } = require("./framework-error");
79
85
 
86
+ // The default engine carries a 600+ KB vendored bundle (peculiar/x509 +
87
+ // pkijs + reflect-metadata). Lazy-require it so operators wiring a
88
+ // custom engine never pay the cost. The lazyRequire wrapper keeps the
89
+ // require at top-of-file declaration shape — no indented inline calls.
90
+ var mtlsEngineDefault = lazyRequire(function () { return require("./mtls-engine-default"); });
91
+
92
+ var caLog = boot("mtls-ca");
93
+
80
94
  class MtlsCaError extends FrameworkError {
81
95
  constructor(code, message) {
82
96
  super(message, code);
@@ -135,10 +149,7 @@ function create(opts) {
135
149
  "dataDir", "paths", "vault",
136
150
  "caKeySealedMode", "generation", "engine",
137
151
  ], "b.mtlsCa");
138
- if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
139
- throw new MtlsCaError("mtls-ca/no-datadir",
140
- "mtlsCa.create requires opts.dataDir");
141
- }
152
+ validateOpts.requireNonEmptyString(opts.dataDir, "mtlsCa.create: opts.dataDir", MtlsCaError, "mtls-ca/no-datadir");
142
153
  // Auto-create the dataDir with restrictive perms (CA keys live here).
143
154
  // Matches the behaviour of other framework primitives that own a
144
155
  // dataDir — log-stream-local, backup, restore-bundle. Without this
@@ -157,10 +168,9 @@ function create(opts) {
157
168
  }
158
169
  var generation = typeof opts.generation === "number" && opts.generation >= 1
159
170
  ? Math.floor(opts.generation) : 1;
160
- // The default engine carries a 600+ KB vendored bundle; load it only
161
- // when operators rely on it (no custom engine passed). Operators who
162
- // wire their own engine never pay the cost.
163
- var engine = opts.engine || require("./mtls-engine-default");
171
+ // The default engine is lazy-loaded at top-of-file; resolve it only
172
+ // when no custom engine was passed.
173
+ var engine = opts.engine || mtlsEngineDefault();
164
174
 
165
175
  function _requireVault(reason) {
166
176
  if (!vault || typeof vault.seal !== "function" || typeof vault.unseal !== "function") {
@@ -275,8 +285,14 @@ function create(opts) {
275
285
  fs.renameSync(keyTmp, keyDest);
276
286
  fs.renameSync(certTmp, paths.caCert);
277
287
  } catch (e) {
278
- try { if (fs.existsSync(keyTmp)) fs.unlinkSync(keyTmp); } catch (_e) {}
279
- try { if (fs.existsSync(certTmp)) fs.unlinkSync(certTmp); } catch (_e) {}
288
+ // Best-effort cleanup of half-written tmp files; the original
289
+ // commit error is what we re-raise. Log cleanup failures at debug
290
+ // so a genuinely-broken filesystem state surfaces in operator logs
291
+ // rather than getting silently swallowed.
292
+ try { if (fs.existsSync(keyTmp)) fs.unlinkSync(keyTmp); }
293
+ catch (cleanupErr) { caLog.debug("cleanup-failed", { op: "fs.unlinkSync", path: keyTmp, error: cleanupErr.message }); }
294
+ try { if (fs.existsSync(certTmp)) fs.unlinkSync(certTmp); }
295
+ catch (cleanupErr) { caLog.debug("cleanup-failed", { op: "fs.unlinkSync", path: certTmp, error: cleanupErr.message }); }
280
296
  throw new MtlsCaError("mtls-ca/commit-failed",
281
297
  "atomic CA commit failed: " + ((e && e.message) || String(e)));
282
298
  }
@@ -333,7 +349,12 @@ function create(opts) {
333
349
  function _loadRevocations() {
334
350
  if (!fs.existsSync(paths.revocations)) return { revocations: [] };
335
351
  try {
336
- var json = JSON.parse(fs.readFileSync(paths.revocations, "utf8"));
352
+ // safeJson.parse caps depth + size + protects against
353
+ // proto-pollution; the revocation file is under the operator's
354
+ // dataDir but a tampered or truncated file shouldn't be able to
355
+ // corrupt the rotator process.
356
+ var json = safeJson.parse(fs.readFileSync(paths.revocations, "utf8"),
357
+ { maxBytes: C.BYTES.mib(16) });
337
358
  if (!json || !Array.isArray(json.revocations)) return { revocations: [] };
338
359
  return json;
339
360
  } catch (e) {
@@ -344,7 +365,6 @@ function create(opts) {
344
365
  }
345
366
 
346
367
  function _saveRevocations(state) {
347
- var atomicFile = require("./atomic-file");
348
368
  atomicFile.writeSync(paths.revocations,
349
369
  JSON.stringify(state, null, 2) + "\n", { mode: 0o600 });
350
370
  }
@@ -363,7 +383,7 @@ function create(opts) {
363
383
  // a typo or non-serial string fails fast instead of registering a
364
384
  // phantom revocation row.
365
385
  var stripped = s.replace(/^0x/i, "").replace(/[:\-\s]/g, "");
366
- if (!/^[0-9a-fA-F]+$/.test(stripped)) {
386
+ if (!safeBuffer.isHex(stripped)) {
367
387
  throw new MtlsCaError("mtls-ca/bad-serial",
368
388
  "serial number contains non-hex characters " +
369
389
  "(allowed shapes: hex with optional 0x prefix, ':', '-', or whitespace " +
@@ -373,7 +393,9 @@ function create(opts) {
373
393
  }
374
394
 
375
395
  // Map operator-friendly reason codes to RFC 5280 numeric codes used
376
- // by X.509 CRLs. Default "unspecified" (0) when omitted.
396
+ // by X.509 CRLs. Default "unspecified" (0) when omitted. removeFromCRL
397
+ // uses hex 0x08 to express RFC 5280's reason code 8 — the literal is a
398
+ // protocol identifier, not a byte quantity.
377
399
  var CRL_REASON_BY_NAME = {
378
400
  "unspecified": 0,
379
401
  "keyCompromise": 1,
@@ -385,7 +407,7 @@ function create(opts) {
385
407
  "cessationOfOperation": 5,
386
408
  "cessation-of-operation": 5,
387
409
  "certificateHold": 6,
388
- "removeFromCRL": 8,
410
+ "removeFromCRL": 0x08,
389
411
  "privilegeWithdrawn": 9,
390
412
  "aACompromise": 10,
391
413
  };
@@ -449,7 +471,7 @@ function create(opts) {
449
471
  var nowMs = Date.now();
450
472
  var thisUpdate = opts3.thisUpdate || new Date(nowMs);
451
473
  var nextUpdate = opts3.nextUpdate ||
452
- new Date(nowMs + 7 * 24 * 60 * 60 * 1000); // 7d default
474
+ new Date(nowMs + C.TIME.days(7)); // 7d default
453
475
  var crlPem = await engine.generateCrl({
454
476
  caCertPem: ca.caCertPem,
455
477
  caKeyPem: ca.caKeyPem,
@@ -462,7 +484,6 @@ function create(opts) {
462
484
  "engine.generateCrl must return a non-empty PEM string");
463
485
  }
464
486
  if (opts3.persist !== false) {
465
- var atomicFile = require("./atomic-file");
466
487
  atomicFile.writeSync(paths.crl, crlPem, { mode: 0o644 });
467
488
  }
468
489
  return { crlPem: crlPem, thisUpdate: thisUpdate, nextUpdate: nextUpdate,