@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
@@ -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,11 +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");
47
+ var numericBounds = require("./numeric-bounds");
44
48
  var dbModule = lazyRequire(function () { return require("./db"); });
45
49
  var validateOpts = require("./validate-opts");
46
50
  var { FrameworkError } = require("./framework-error");
47
51
 
52
+ var log = boot("migrations");
53
+
48
54
  class MigrationError extends FrameworkError {
49
55
  constructor(code, message, permanent) {
50
56
  super(message, code);
@@ -63,12 +69,22 @@ var Q_MIGRATIONS_TABLE = '"' + MIGRATIONS_TABLE + '"';
63
69
  // non-empty body, then '.js'. Numeric prefix orders execution. Letters
64
70
  // in the body include hyphens, underscores, and alphanumerics; anything
65
71
  // else is rejected so an operator can't sneak path tricks into require.
66
- 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
+ }
67
83
 
68
84
  // Bracket-notation wrapper for the SQLite handle's exec method. The
69
85
  // rest of the framework uses db["exec"] to avoid hook false-positives
70
86
  // matching the literal '.exec(' token; mirror that here.
71
- function _runSql(db, sql) { return db["exec"](sql); }
87
+ var _runSql = dbSchema.runSqlOnHandle;
72
88
 
73
89
  function _ensureTable(db) {
74
90
  _runSql(db,
@@ -108,8 +124,21 @@ function _acquireLock(db, opts) {
108
124
  _ensureLockTable(db);
109
125
  var holder = _lockHolderId();
110
126
  var nowMs = Date.now();
111
- var staleAfterMs = (opts && typeof opts.staleAfterMs === "number" && opts.staleAfterMs > 0)
112
- ? opts.staleAfterMs : 0;
127
+ // staleAfterMs gates whether an existing lock is considered stale.
128
+ // 0 (the default) means "never replace — wait for the operator".
129
+ // Pre-fix Infinity was accepted but degenerate `(now - lockedAt) >
130
+ // Infinity` to always-false, identical to 0 but with an obscured
131
+ // typo. Reject Infinity / NaN / non-integer / negative — operators
132
+ // wanting "never" pass 0 explicitly.
133
+ var staleAfterMs;
134
+ if (!opts || opts.staleAfterMs === undefined) {
135
+ staleAfterMs = 0;
136
+ } else if (!numericBounds.isNonNegativeFiniteInt(opts.staleAfterMs)) {
137
+ throw new Error("migrations.acquireLock: staleAfterMs must be a " +
138
+ "non-negative finite integer; got " + numericBounds.shape(opts.staleAfterMs));
139
+ } else {
140
+ staleAfterMs = opts.staleAfterMs;
141
+ }
113
142
  // Try to insert; if there's a stale lock, optionally force-replace it.
114
143
  try {
115
144
  db.prepare(
@@ -148,7 +177,13 @@ function _acquireLock(db, opts) {
148
177
  _runSql(db, "COMMIT");
149
178
  return holder;
150
179
  } catch (forceErr) {
151
- 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
+ }
152
187
  throw new MigrationError("migrations/lock-stale-replace-failed",
153
188
  "could not replace stale lock: " + ((forceErr && forceErr.message) || String(forceErr)),
154
189
  true);
@@ -181,7 +216,7 @@ function _withLock(db, opts, fn) {
181
216
 
182
217
  function _list(dir) {
183
218
  return atomicFile.listDir(dir, {
184
- filter: function (f) { return FILE_RE.test(f); },
219
+ filter: _isMigrationFile,
185
220
  }).map(function (e) { return e.name; }).sort();
186
221
  }
187
222
 
@@ -219,17 +254,7 @@ function _loadMigration(file, dir) {
219
254
  return mod;
220
255
  }
221
256
 
222
- function _txn(db, fn) {
223
- _runSql(db, "BEGIN");
224
- try {
225
- var v = fn();
226
- _runSql(db, "COMMIT");
227
- return v;
228
- } catch (e) {
229
- try { _runSql(db, "ROLLBACK"); } catch (_e) { /* already in error path */ }
230
- throw e;
231
- }
232
- }
257
+ var _txn = dbSchema.runInTransaction;
233
258
 
234
259
  function create(opts) {
235
260
  opts = opts || {};