@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
package/lib/error-page.js CHANGED
@@ -39,61 +39,67 @@
39
39
  */
40
40
 
41
41
  var lazyRequire = require("./lazy-require");
42
+ var logModule = require("./log");
42
43
  var requestHelpers = require("./request-helpers");
43
44
  var safeEnv = require("./parsers/safe-env");
44
45
  var template = require("./template");
45
46
  var audit = lazyRequire(function () { return require("./audit"); });
46
47
 
48
+ var bootLog = logModule.boot("errors-page");
49
+
47
50
  var _esc = template.escapeHtml;
51
+ var H = requestHelpers.HTTP_STATUS;
48
52
 
49
53
  // Status code → default short reason. Used when the error doesn't carry
50
- // its own message (e.g. a generic Error thrown from a route).
51
- var STATUS_REASONS = {
52
- 400: "Bad Request",
53
- 401: "Unauthorized",
54
- 403: "Forbidden",
55
- 404: "Not Found",
56
- 405: "Method Not Allowed",
57
- 409: "Conflict",
58
- 413: "Payload Too Large",
59
- 415: "Unsupported Media Type",
60
- 422: "Unprocessable Content",
61
- 429: "Too Many Requests",
62
- 500: "Internal Server Error",
63
- 502: "Bad Gateway",
64
- 503: "Service Unavailable",
65
- 504: "Gateway Timeout",
66
- };
54
+ // its own message (e.g. a generic Error thrown from a route). Keyed by
55
+ // the framework's centralized HTTP_STATUS hex IDs (RFC 9110 reason
56
+ // phrases). JS coerces numeric keys to strings, so STATUS_REASONS[404]
57
+ // == STATUS_REASONS[H.NOT_FOUND].
58
+ var STATUS_REASONS = {};
59
+ STATUS_REASONS[H.BAD_REQUEST] = "Bad Request";
60
+ STATUS_REASONS[H.UNAUTHORIZED] = "Unauthorized";
61
+ STATUS_REASONS[H.FORBIDDEN] = "Forbidden";
62
+ STATUS_REASONS[H.NOT_FOUND] = "Not Found";
63
+ STATUS_REASONS[H.METHOD_NOT_ALLOWED] = "Method Not Allowed";
64
+ STATUS_REASONS[H.CONFLICT] = "Conflict";
65
+ STATUS_REASONS[H.PAYLOAD_TOO_LARGE] = "Payload Too Large";
66
+ STATUS_REASONS[H.UNSUPPORTED_MEDIA_TYPE] = "Unsupported Media Type";
67
+ STATUS_REASONS[H.UNPROCESSABLE_CONTENT] = "Unprocessable Content";
68
+ STATUS_REASONS[H.TOO_MANY_REQUESTS] = "Too Many Requests";
69
+ STATUS_REASONS[H.INTERNAL_SERVER_ERROR] = "Internal Server Error";
70
+ STATUS_REASONS[H.BAD_GATEWAY] = "Bad Gateway";
71
+ STATUS_REASONS[H.SERVICE_UNAVAILABLE] = "Service Unavailable";
72
+ STATUS_REASONS[H.GATEWAY_TIMEOUT] = "Gateway Timeout";
67
73
 
68
74
  // Map common framework error classes → HTTP status. Operators who
69
75
  // throw their own AppError-like classes set isAppError + statusCode
70
76
  // and bypass this map entirely.
71
77
  function _classify(err) {
72
78
  if (!err || typeof err !== "object") {
73
- return { status: 500, code: "INTERNAL_ERROR", message: "Internal Server Error", classified: false };
79
+ return { status: H.INTERNAL_SERVER_ERROR, code: "INTERNAL_ERROR", message: "Internal Server Error", classified: false };
74
80
  }
75
81
  // Apps that follow the standard convention: { isAppError, statusCode, code, message }
76
82
  if (err.isAppError) {
77
83
  return {
78
- status: err.statusCode || 500,
84
+ status: err.statusCode || H.INTERNAL_SERVER_ERROR,
79
85
  code: err.code || "APP_ERROR",
80
- message: err.message || STATUS_REASONS[err.statusCode || 500] || "Error",
86
+ message: err.message || STATUS_REASONS[err.statusCode || H.INTERNAL_SERVER_ERROR] || "Error",
81
87
  classified: true,
82
88
  };
83
89
  }
84
90
  // Framework error subclasses we know map to specific HTTP statuses.
85
91
  if (err.isAuthError) {
86
- return { status: 401, code: err.code || "AUTH_FAILED", message: err.message || "Unauthorized", classified: true };
92
+ return { status: H.UNAUTHORIZED, code: err.code || "AUTH_FAILED", message: err.message || "Unauthorized", classified: true };
87
93
  }
88
94
  if (err.code === "VALIDATION_ERROR" || err.name === "ValidationError") {
89
- return { status: 400, code: err.code || "VALIDATION_ERROR", message: err.message || "Bad Request", classified: true };
95
+ return { status: H.BAD_REQUEST, code: err.code || "VALIDATION_ERROR", message: err.message || "Bad Request", classified: true };
90
96
  }
91
97
  if (err.isSafeJsonError) {
92
- return { status: 400, code: err.code || "json/invalid", message: err.message || "Bad Request", path: err.path || null, classified: true };
98
+ return { status: H.BAD_REQUEST, code: err.code || "json/invalid", message: err.message || "Bad Request", path: err.path || null, classified: true };
93
99
  }
94
100
  // Permanent framework errors that map cleanly to 4xx
95
101
  if ((err.isStorageError || err.isQueueError || err.isExternalDbError) && err.permanent) {
96
- return { status: 400, code: err.code || "ERROR", message: err.message || "Bad Request", classified: true };
102
+ return { status: H.BAD_REQUEST, code: err.code || "ERROR", message: err.message || "Bad Request", classified: true };
97
103
  }
98
104
  if (typeof err.statusCode === "number") {
99
105
  return {
@@ -103,7 +109,7 @@ function _classify(err) {
103
109
  classified: true,
104
110
  };
105
111
  }
106
- return { status: 500, code: err.code || "INTERNAL_ERROR", message: err.message || "Internal Server Error", classified: false };
112
+ return { status: H.INTERNAL_SERVER_ERROR, code: err.code || "INTERNAL_ERROR", message: err.message || "Internal Server Error", classified: false };
107
113
  }
108
114
 
109
115
  function _wantsJson(req, defaultFormat) {
@@ -303,20 +309,7 @@ function create(opts) {
303
309
  var showEnvVars = mode === "dev" && opts.showEnvVars === true;
304
310
  var onErrorHook = typeof opts.onError === "function" ? opts.onError : null;
305
311
 
306
- function _log(level, message, fields) {
307
- if (log && typeof log[level] === "function") {
308
- try { log[level](message, fields); }
309
- catch (_e) { /* logger best-effort */ }
310
- return;
311
- }
312
- // Fallback: framework console with status-aware severity
313
- var line = "[blamejs:errors-page] " + message + " " + JSON.stringify(fields || {});
314
- if (level === "error" || level === "fatal" || level === "warn") {
315
- console.error(line);
316
- } else {
317
- console.log(line);
318
- }
319
- }
312
+ var _log = logModule.makeViaOrFallback(log, bootLog);
320
313
 
321
314
  function handler(err, req, res) {
322
315
  var info = _classify(err);
@@ -51,6 +51,7 @@
51
51
  var path = require("path");
52
52
  var atomicFile = require("./atomic-file");
53
53
  var lazyRequire = require("./lazy-require");
54
+ var migrationFiles = require("./migration-files");
54
55
  var numericBounds = require("./numeric-bounds");
55
56
  var validateOpts = require("./validate-opts");
56
57
  var { defineClass } = require("./framework-error");
@@ -68,9 +69,18 @@ var LOCK_TABLE = "_blamejs_externaldb_migrations_lock";
68
69
  var Q_TRACKING = '"' + TRACKING_TABLE + '"';
69
70
  var Q_LOCK = '"' + LOCK_TABLE + '"';
70
71
 
71
- // Filename grammar mirrors lib/migrations.js so operators with a
72
- // mixed local + externalDb workflow use the same numeric-prefix pattern.
73
- var FILE_RE = /^(\d+)-([A-Za-z0-9_-]+)\.js$/;
72
+ // Filename grammar lives in lib/migration-files (shared with the local
73
+ // migrations.js + seeders.js runners). Length capped before the regex
74
+ // test so a hostile directory listing can't drive the engine against
75
+ // an unbounded filename string.
76
+ var FILE_NAME_MAX = 255;
77
+
78
+ function _isMigrationFile(name) {
79
+ return typeof name === "string" &&
80
+ name.length > 0 &&
81
+ name.length <= FILE_NAME_MAX &&
82
+ migrationFiles.MIGRATION_FILE_RE.test(name);
83
+ }
74
84
 
75
85
  function _err(code, message) {
76
86
  return new ExternalDbMigrateError(code, message);
@@ -116,14 +126,12 @@ async function _acquireLock(xdb, opts) {
116
126
  var nowMs = Date.now();
117
127
  // See migrations.acquireLock for the same fix — Infinity was
118
128
  // silently identical to 0 (no staleness check) but obscured the typo.
119
- var staleAfterMs;
120
- if (!opts || opts.staleAfterMs === undefined) {
121
- staleAfterMs = 0;
122
- } else if (!numericBounds.isNonNegativeFiniteInt(opts.staleAfterMs)) {
123
- throw new Error("externalDb.migrate.acquireLock: staleAfterMs must " +
124
- "be a non-negative finite integer; got " + numericBounds.shape(opts.staleAfterMs));
125
- } else {
126
- staleAfterMs = opts.staleAfterMs;
129
+ var staleAfterMs = 0;
130
+ if (opts) {
131
+ numericBounds.requireNonNegativeFiniteIntIfPresent(opts.staleAfterMs,
132
+ "externalDb.migrate.acquireLock: staleAfterMs",
133
+ ExternalDbMigrateError, "externalDb-migrate/bad-opt");
134
+ if (opts.staleAfterMs !== undefined) staleAfterMs = opts.staleAfterMs;
127
135
  }
128
136
  try {
129
137
  await xdb.query(
@@ -185,7 +193,7 @@ async function _releaseLock(xdb, holder) {
185
193
 
186
194
  function _list(dir) {
187
195
  return atomicFile.listDir(dir, {
188
- filter: function (f) { return FILE_RE.test(f); },
196
+ filter: _isMigrationFile,
189
197
  }).map(function (e) { return e.name; }).sort();
190
198
  }
191
199
 
@@ -246,20 +254,9 @@ function _resolveBackendName(opts) {
246
254
  function create(opts) {
247
255
  opts = opts || {};
248
256
  validateOpts(opts, ["dir", "backend", "audit", "staleAfterMs"], "b.externalDb.migrate");
249
- if (typeof opts.dir !== "string" || opts.dir.length === 0) {
250
- throw _err("externaldb-migrate/no-dir",
251
- "externalDb.migrate.create requires opts.dir (path to migrations directory)");
252
- }
253
- if (opts.staleAfterMs !== undefined &&
254
- (typeof opts.staleAfterMs !== "number" || !isFinite(opts.staleAfterMs) || opts.staleAfterMs < 0)) {
255
- throw _err("externaldb-migrate/bad-stale",
256
- "staleAfterMs must be a non-negative finite number");
257
- }
258
- if (opts.audit !== undefined && opts.audit !== null &&
259
- (typeof opts.audit !== "object" || typeof opts.audit.safeEmit !== "function")) {
260
- throw _err("externaldb-migrate/bad-audit",
261
- "audit must be a b.audit-shaped object (safeEmit fn)");
262
- }
257
+ validateOpts.requireNonEmptyString(opts.dir, "externalDb.migrate.create: opts.dir (path to migrations directory)", ExternalDbMigrateError, "externaldb-migrate/no-dir");
258
+ validateOpts.optionalFiniteNonNegative(opts.staleAfterMs, "externalDb.migrate: staleAfterMs", ExternalDbMigrateError, "externaldb-migrate/bad-stale");
259
+ validateOpts.auditShape(opts.audit, "externalDb.migrate", ExternalDbMigrateError, "externaldb-migrate/bad-audit");
263
260
  var dir = opts.dir;
264
261
  var audit = opts.audit || null;
265
262
 
@@ -53,11 +53,15 @@
53
53
  var retryHelper = require("./retry");
54
54
  var C = require("./constants");
55
55
  var dbRoleContext = require("./db-role-context");
56
+ var externalDbMigrate = require("./external-db-migrate");
56
57
  var lazyRequire = require("./lazy-require");
58
+ var { boot } = require("./log");
57
59
  var safeAsync = require("./safe-async");
58
60
  var safeSql = require("./safe-sql");
59
61
  var { ExternalDbError } = require("./framework-error");
60
62
 
63
+ var log = boot("external-db");
64
+
61
65
  var audit = lazyRequire(function () { return require("./audit"); });
62
66
  var db = lazyRequire(function () { return require("./db"); });
63
67
  var observability = lazyRequire(function () { return require("./observability"); });
@@ -771,10 +775,14 @@ var write = {
771
775
 
772
776
  function _resetForTest() {
773
777
  Object.keys(backends).forEach(function (n) {
774
- try { backends[n].pool.drain(); } catch (_e) {}
778
+ try { backends[n].pool.drain(); }
779
+ catch (e) { log.debug("test-reset pool drain failed", { backend: n, error: e.message }); }
775
780
  var bk = backends[n];
776
781
  if (bk && bk.replicas) {
777
- bk.replicas.forEach(function (r) { try { r.pool.drain(); } catch (_e) {} });
782
+ bk.replicas.forEach(function (r) {
783
+ try { r.pool.drain(); }
784
+ catch (e2) { log.debug("test-reset replica drain failed", { backend: n, error: e2.message }); }
785
+ });
778
786
  }
779
787
  });
780
788
  backends = {};
@@ -1047,7 +1055,7 @@ module.exports = {
1047
1055
  // Migration runner targeting an externalDb backend. Mirrors b.migrations
1048
1056
  // (which targets local SQLite) but runs against externalDb. Tracking +
1049
1057
  // lock tables live on the externalDb side. See lib/external-db-migrate.js.
1050
- migrate: require("./external-db-migrate"),
1058
+ migrate: externalDbMigrate,
1051
1059
  Pool: Pool,
1052
1060
  _resetForTest: _resetForTest,
1053
1061
  };
package/lib/file-type.js CHANGED
@@ -42,11 +42,17 @@
42
42
  * extensions are operator-controlled metadata, magic bytes are
43
43
  * the wire truth.
44
44
  */
45
+ var C = require("./constants");
45
46
  var { defineClass } = require("./framework-error");
46
47
 
47
48
  var FileTypeError = defineClass("FileTypeError", { alwaysPermanent: true });
48
49
  var _err = FileTypeError.factory;
49
50
 
51
+ // Cap on bytes inspected for OOXML / WEBP shape-check probes. Set well
52
+ // above any legitimate Office central-directory prefix and any RIFF
53
+ // header — operators don't tune this, the magic-byte registry is fixed.
54
+ var SNIFF_HEAD_BYTES = C.BYTES.kib(4);
55
+
50
56
  // Signature registry. Each entry: { name, mime, extension, category,
51
57
  // offset, magic: Buffer | [Buffer, ...], extra?: function(buffer)→bool }.
52
58
  // Order matters — earlier entries win on ambiguous matches (e.g. ZIP
@@ -62,21 +68,21 @@ var SIGNATURES = [
62
68
  extension: "docx", category: "document",
63
69
  offset: 0, magic: Buffer.from([0x50, 0x4B, 0x03, 0x04]),
64
70
  extra: function (buf) {
65
- var head = buf.subarray(0, Math.min(buf.length, 4096)).toString("binary");
71
+ var head = buf.subarray(0, Math.min(buf.length, SNIFF_HEAD_BYTES)).toString("binary");
66
72
  return head.indexOf("word/") !== -1 || head.indexOf("[Content_Types].xml") !== -1 && head.indexOf("word") !== -1;
67
73
  } },
68
74
  { name: "xlsx", mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
69
75
  extension: "xlsx", category: "document",
70
76
  offset: 0, magic: Buffer.from([0x50, 0x4B, 0x03, 0x04]),
71
77
  extra: function (buf) {
72
- var head = buf.subarray(0, Math.min(buf.length, 4096)).toString("binary");
78
+ var head = buf.subarray(0, Math.min(buf.length, SNIFF_HEAD_BYTES)).toString("binary");
73
79
  return head.indexOf("xl/") !== -1;
74
80
  } },
75
81
  { name: "pptx", mime: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
76
82
  extension: "pptx", category: "document",
77
83
  offset: 0, magic: Buffer.from([0x50, 0x4B, 0x03, 0x04]),
78
84
  extra: function (buf) {
79
- var head = buf.subarray(0, Math.min(buf.length, 4096)).toString("binary");
85
+ var head = buf.subarray(0, Math.min(buf.length, SNIFF_HEAD_BYTES)).toString("binary");
80
86
  return head.indexOf("ppt/") !== -1;
81
87
  } },
82
88
  // ---- Plain ZIP (after OOXML so OOXML wins) ----
@@ -97,7 +103,9 @@ var SIGNATURES = [
97
103
  { name: "webp", mime: "image/webp", extension: "webp", category: "image",
98
104
  offset: 0, magic: Buffer.from("RIFF", "ascii"),
99
105
  extra: function (buf) {
100
- return buf.length >= 12 && buf.subarray(8, 12).toString("ascii") === "WEBP";
106
+ // RIFF header: "RIFF" (0..3), file size (4..7), format tag at offset 8..11.
107
+ return buf.length >= C.BYTES.bytes(12) &&
108
+ buf.subarray(C.BYTES.bytes(0x08), C.BYTES.bytes(12)).toString("ascii") === "WEBP";
101
109
  } },
102
110
  { name: "bmp", mime: "image/bmp", extension: "bmp", category: "image",
103
111
  offset: 0, magic: Buffer.from([0x42, 0x4D]) },
@@ -168,10 +176,10 @@ var SIGNATURES = [
168
176
  offset: 0, magic: Buffer.from([0x7F, 0x45, 0x4C, 0x46]) },
169
177
  { name: "macho", mime: "application/x-mach-binary", extension: "macho", category: "executable",
170
178
  offset: 0, magic: [
171
- Buffer.from([0xFE, 0xED, 0xFA, 0xCE]), // 32-bit BE
172
- Buffer.from([0xFE, 0xED, 0xFA, 0xCF]), // 64-bit BE
173
- Buffer.from([0xCE, 0xFA, 0xED, 0xFE]), // 32-bit LE
174
- Buffer.from([0xCF, 0xFA, 0xED, 0xFE]), // 64-bit LE
179
+ Buffer.from([0xFE, 0xED, 0xFA, 0xCE]), // big-endian, narrow word
180
+ Buffer.from([0xFE, 0xED, 0xFA, 0xCF]), // big-endian, wide word
181
+ Buffer.from([0xCE, 0xFA, 0xED, 0xFE]), // little-endian, narrow word
182
+ Buffer.from([0xCF, 0xFA, 0xED, 0xFE]), // little-endian, wide word
175
183
  Buffer.from([0xCA, 0xFE, 0xBA, 0xBE]), // universal binary (also Java .class)
176
184
  ] },
177
185
  ];
package/lib/forms.js CHANGED
@@ -35,7 +35,8 @@
35
35
  * forms.escapeAttribute(value) → string (double-quoted-attr context)
36
36
  * forms.escapeHtml = template.escapeHtml (re-export for convenience)
37
37
  */
38
- var crypto = require("./crypto");
38
+ var C = require("./constants");
39
+ var { generateToken, timingSafeEqual } = require("./crypto");
39
40
  var safeSchema = require("./safe-schema");
40
41
  var safeUrl = require("./safe-url");
41
42
  var template = require("./template");
@@ -44,16 +45,24 @@ var template = require("./template");
44
45
  // CSRF token primitives
45
46
  // ============================================================
46
47
 
47
- var CSRF_TOKEN_BYTES = 32;
48
+ var CSRF_TOKEN_BYTES = C.BYTES.bytes(32);
49
+
50
+ // RFC 5321 §4.5.3.1.3 caps an email address at 254 chars; bound the
51
+ // regex test on form submissions so a hostile caller can't stage a
52
+ // pathological-length input against the engine.
53
+ var MAX_EMAIL_LENGTH = 254;
54
+ // RFC 7230 §3.1.1 doesn't fix a URL length but the conventional cap
55
+ // (and what most servers / proxies enforce) is 8 KiB.
56
+ var MAX_URL_LENGTH = C.BYTES.kib(8);
48
57
 
49
58
  function generateCsrfToken() {
50
- return crypto.generateToken(CSRF_TOKEN_BYTES);
59
+ return generateToken(CSRF_TOKEN_BYTES);
51
60
  }
52
61
 
53
62
  function verifyCsrfToken(submitted, expected) {
54
63
  if (typeof submitted !== "string" || typeof expected !== "string") return false;
55
64
  if (submitted.length === 0 || submitted.length !== expected.length) return false;
56
- return crypto.timingSafeEqual(submitted, expected);
65
+ return timingSafeEqual(submitted, expected);
57
66
  }
58
67
 
59
68
  // ============================================================
@@ -129,7 +138,18 @@ function _renderInput(field) {
129
138
  if (field.disabled) attrs.push("disabled");
130
139
  if (field.checked) attrs.push("checked");
131
140
  if (field.autocomplete) attrs.push('autocomplete="' + escapeAttribute(field.autocomplete) + '"');
132
- if (field.pattern) attrs.push('pattern="' + escapeAttribute(field.pattern) + '"');
141
+ if (field.pattern) {
142
+ // Pattern is a RegExp at every boundary; render its source for the
143
+ // HTML attribute so client + server agree on the regex. String
144
+ // patterns are rejected at config time because validate() refuses
145
+ // them — symmetric handling avoids a silent server-validation gap.
146
+ if (!(field.pattern instanceof RegExp)) {
147
+ throw new Error("forms.render: field '" + field.name +
148
+ "'.pattern must be a pre-compiled RegExp; got " +
149
+ (typeof field.pattern) + ". Wrap the source string with `RegExp` at config time.");
150
+ }
151
+ attrs.push('pattern="' + escapeAttribute(field.pattern.source) + '"');
152
+ }
133
153
  if (field.min !== undefined) attrs.push('min="' + escapeAttribute(field.min) + '"');
134
154
  if (field.max !== undefined) attrs.push('max="' + escapeAttribute(field.max) + '"');
135
155
  if (field.step !== undefined) attrs.push('step="' + escapeAttribute(field.step) + '"');
@@ -317,18 +337,27 @@ function validate(spec, body) {
317
337
  }
318
338
  }
319
339
  if (f.type === "email" && typeof coerced === "string") {
340
+ // Length cap before the regex test (RFC 5321 §4.5.3.1.3 = 254
341
+ // chars). Any longer is rejected without engaging the regex.
320
342
  // Same pragmatic check the rest of the framework uses
321
343
  // (safeSchema.EMAIL_RE — shared so we don't carry parallel
322
344
  // copies of the same /^[^\s@]+@[^\s@]+\.[^\s@]+$/ regex).
323
- if (!safeSchema.EMAIL_RE.test(coerced)) {
345
+ if (coerced.length > MAX_EMAIL_LENGTH || !safeSchema.EMAIL_RE.test(coerced)) {
324
346
  errors[f.name] = (f.label || f.name) + " must be a valid email address";
325
347
  continue;
326
348
  }
327
349
  }
328
350
  if (f.type === "url" && typeof coerced === "string") {
329
351
  // Form `url` fields come from the request body — operator/external
330
- // input. Route through safeUrl so the scheme allowlist is honored
331
- // (https-only by default; operator opts in to http via field meta).
352
+ // input. Length cap (RFC 7230 §3.1.1 has no fixed limit; the
353
+ // conventional cap is 8 KiB) bounds the work before safeUrl.parse
354
+ // engages. Route through safeUrl so the scheme allowlist is
355
+ // honored (https-only by default; operator opts in to http via
356
+ // field meta).
357
+ if (coerced.length > MAX_URL_LENGTH) {
358
+ errors[f.name] = (f.label || f.name) + " must be a valid URL";
359
+ continue;
360
+ }
332
361
  try {
333
362
  safeUrl.parse(coerced, {
334
363
  allowedProtocols: f.allowHttp ? safeUrl.ALLOW_HTTP_ALL : safeUrl.ALLOW_HTTP_TLS,
@@ -348,10 +377,17 @@ function validate(spec, body) {
348
377
  continue;
349
378
  }
350
379
  if (f.pattern) {
351
- var re;
352
- try { re = new RegExp("^(?:" + f.pattern + ")$"); }
353
- catch (_e) { /* invalid pattern in spec — operator bug; skip */ continue; }
354
- if (!re.test(coerced)) {
380
+ // Pattern accepts a pre-compiled RegExp only — taking a string
381
+ // and compiling it here would be a ReDoS surface against the
382
+ // form-render engine. Operators construct the RegExp at config
383
+ // time so the framework never compiles operator-supplied source
384
+ // on the request path.
385
+ if (!(f.pattern instanceof RegExp)) {
386
+ throw new Error("forms.validate: field '" + f.name +
387
+ "'.pattern must be a pre-compiled RegExp; got " +
388
+ (typeof f.pattern) + ". Wrap the source string with `RegExp` at config time.");
389
+ }
390
+ if (!f.pattern.test(coerced)) {
355
391
  errors[f.name] = f.errorMessages && f.errorMessages.pattern
356
392
  ? f.errorMessages.pattern
357
393
  : (f.label || f.name) + " has an invalid format";
@@ -99,8 +99,7 @@ function defineClass(name, opts) {
99
99
  // call here even during framework-error's own load: observability's
100
100
  // dependencies on metrics + tracing are themselves lazy-required
101
101
  // and only resolve at first call (post-load).
102
- try { observability.event("error.construct", 1, { class: name }); }
103
- catch (_e) { /* defensive — no observability sink should ever break a constructor */ }
102
+ observability.safeEvent("error.construct", 1, { class: name });
104
103
  }
105
104
  };
106
105
  Object.defineProperty(GeneratedError, "name", { value: name, configurable: true });
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * internal-sha1-hibp — SHA-1 hex digest, RESTRICTED to the
3
+ * framework-sha1-hibp — SHA-1 hex digest, RESTRICTED to the
4
4
  * HaveIBeenPwned k-anonymity API caller (lib/auth/password.js policy).
5
5
  *
6
6
  * SHA-1 is broken for collision-resistance and trivially extendable;
package/lib/handlers.js CHANGED
@@ -91,7 +91,7 @@ var { boot } = require("./log");
91
91
  var DEFAULTS = {
92
92
  maxBatch: 100,
93
93
  maxAgeMs: C.TIME.seconds(1),
94
- maxBufferSize: 10000,
94
+ maxBufferSize: C.BYTES.bytes(10000),
95
95
  shutdownTimeoutMs: C.TIME.seconds(30),
96
96
  retry: {
97
97
  maxAttempts: 3,
@@ -63,7 +63,11 @@
63
63
  var fs = require("node:fs");
64
64
  var path = require("node:path");
65
65
  var C = require("./constants");
66
+ var numericBounds = require("./numeric-bounds");
67
+ var safeAsync = require("./safe-async");
68
+ var safeJson = require("./safe-json");
66
69
  var safeUrl = require("./safe-url");
70
+ var validateOpts = require("./validate-opts");
67
71
  var { defineClass } = require("./framework-error");
68
72
 
69
73
  var CookieJarError = defineClass("CookieJarError", { alwaysPermanent: true });
@@ -162,10 +166,7 @@ function create(opts) {
162
166
  }
163
167
  var filePath = null;
164
168
  if (persist === "file") {
165
- if (typeof opts.file !== "string" || opts.file.length === 0) {
166
- throw _err("BAD_OPT",
167
- "cookieJar.create: persist: 'file' requires opts.file (absolute path)");
168
- }
169
+ validateOpts.requireNonEmptyString(opts.file, "cookieJar.create: persist: 'file' opts.file (absolute path)", CookieJarError, "BAD_OPT");
169
170
  filePath = opts.file;
170
171
  // Refuse relative paths so a process running in a different cwd
171
172
  // doesn't accidentally serialize to a sibling directory.
@@ -174,8 +175,11 @@ function create(opts) {
174
175
  "cookieJar.create: opts.file must be an absolute path, got " + JSON.stringify(filePath));
175
176
  }
176
177
  }
177
- var flushDebounceMs = (typeof opts.flushDebounceMs === "number" && opts.flushDebounceMs >= 0)
178
- ? opts.flushDebounceMs : DEFAULTS.flushDebounceMs;
178
+ if (opts.flushDebounceMs !== undefined && !numericBounds.isNonNegativeFiniteInt(opts.flushDebounceMs)) {
179
+ throw _err("BAD_OPT", "cookieJar.create: flushDebounceMs must be a non-negative finite integer; got " +
180
+ numericBounds.shape(opts.flushDebounceMs));
181
+ }
182
+ var flushDebounceMs = opts.flushDebounceMs !== undefined ? opts.flushDebounceMs : DEFAULTS.flushDebounceMs;
179
183
  var clock = typeof opts.clock === "function" ? opts.clock : Date.now;
180
184
 
181
185
  // Storage map keyed by `<domain>|<path>|<name>` so a (domain, path)
@@ -194,7 +198,8 @@ function create(opts) {
194
198
 
195
199
  function _setOne(reqUrl, parsed) {
196
200
  var u;
197
- try { u = new URL(reqUrl); } catch (_e) { return; }
201
+ try { u = safeUrl.parse(reqUrl, { allowedProtocols: safeUrl.ALLOW_HTTP_ALL }); }
202
+ catch (_e) { return; }
198
203
  var host = u.hostname.toLowerCase();
199
204
  var attrs = parsed.attrs || {};
200
205
 
@@ -274,7 +279,8 @@ function create(opts) {
274
279
 
275
280
  function cookieHeaderFor(reqUrl) {
276
281
  var u;
277
- try { u = new URL(reqUrl); } catch (_e) { return null; }
282
+ try { u = safeUrl.parse(reqUrl, { allowedProtocols: safeUrl.ALLOW_HTTP_ALL }); }
283
+ catch (_e) { return null; }
278
284
  var host = u.hostname.toLowerCase();
279
285
  var path = u.pathname || "/";
280
286
  var isSecure = u.protocol === "https:";
@@ -419,7 +425,6 @@ function create(opts) {
419
425
  // is also passed, the file is sealed via vault.seal so the on-disk
420
426
  // bytes are encrypted at rest; otherwise plaintext (operator chose
421
427
  // the threat model by passing or omitting vault).
422
- var flushTimer = null;
423
428
  function _flushSync() {
424
429
  if (!filePath) return;
425
430
  var rows = getAll();
@@ -427,21 +432,20 @@ function create(opts) {
427
432
  var blob = vault ? vault.seal(serialized) : serialized;
428
433
  fs.writeFileSync(filePath, blob);
429
434
  }
435
+ var flushScheduler = safeAsync.makeScheduledFlush(flushDebounceMs, function () {
436
+ if (!filePath) return;
437
+ try { _flushSync(); } catch (_e) { /* operator can call flush() to retry */ }
438
+ });
430
439
  function _scheduleFlush() {
431
440
  if (!filePath) return;
432
- if (flushTimer) return;
433
- flushTimer = setTimeout(function () {
434
- flushTimer = null;
435
- try { _flushSync(); } catch (_e) { /* operator can call flush() to retry */ }
436
- }, flushDebounceMs);
437
- if (flushTimer.unref) flushTimer.unref();
441
+ flushScheduler.schedule();
438
442
  }
439
443
  function flush() {
440
- if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
444
+ flushScheduler.cancel();
441
445
  _flushSync();
442
446
  }
443
447
  function close() {
444
- if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
448
+ flushScheduler.cancel();
445
449
  if (filePath) try { _flushSync(); } catch (_e) { /* best-effort */ }
446
450
  }
447
451
 
@@ -459,12 +463,15 @@ function create(opts) {
459
463
  };
460
464
 
461
465
  // Initial load from file. Missing file is fine (first run).
466
+ // Persist file may be operator-tampered (or vault-sealed) — route through
467
+ // safeJson with an explicit byte cap so a maliciously-large file can't
468
+ // OOM the process before the parse fails.
462
469
  if (filePath && fs.existsSync(filePath)) {
463
470
  try {
464
471
  var raw = fs.readFileSync(filePath, "utf8");
465
472
  var serialized = vault ? vault.unseal(raw) : raw;
466
473
  if (serialized && serialized.length > 0) {
467
- var rows = JSON.parse(serialized);
474
+ var rows = safeJson.parse(serialized, { maxBytes: C.BYTES.mib(16) });
468
475
  setFromSerialized(rows);
469
476
  }
470
477
  } catch (e) {