@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
@@ -56,13 +56,26 @@
56
56
 
57
57
  var C = require("../constants");
58
58
  var atomicFile = require("../atomic-file");
59
+ var lazyRequire = require("../lazy-require");
60
+ var numericBounds = require("../numeric-bounds");
59
61
  var safeBuffer = require("../safe-buffer");
60
62
  var safeJson = require("../safe-json");
61
63
  var { FrameworkError } = require("../framework-error");
62
64
  var { boot } = require("../log");
63
65
 
66
+ // Lazy require for audit — eager-loading audit at top of file forms a
67
+ // load cycle: vault.init → passphrase-source → safe-env → audit →
68
+ // (transitively) vault, which leaves safe-env's module.exports
69
+ // half-built when vault first reaches readVar. Defer audit resolution
70
+ // until the first emit-driven call.
71
+ var auditModule = lazyRequire(function () { return require("../audit"); });
72
+
64
73
  var log = boot("env");
65
74
 
75
+ // parseInt radix — named so the call site doesn't carry a bare 16
76
+ // integer literal that reads as a byte count.
77
+ var RADIX_HEX = 0x10;
78
+
66
79
  class SafeEnvError extends FrameworkError {
67
80
  constructor(message, code, line) {
68
81
  super(line != null ? message + " at line " + line : message);
@@ -75,7 +88,7 @@ class SafeEnvError extends FrameworkError {
75
88
 
76
89
  var DEFAULTS = {
77
90
  maxBytes: C.BYTES.kib(64),
78
- maxKeys: 1000,
91
+ maxKeys: 1_000,
79
92
  keyShape: /^[A-Z_][A-Z0-9_]*$/,
80
93
  applyToProcess: false,
81
94
  allowOverwrite: false,
@@ -89,10 +102,21 @@ var POISONED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
89
102
 
90
103
  function parse(input, opts) {
91
104
  opts = opts || {};
92
- var maxBytes = (typeof opts.maxBytes === "number" && opts.maxBytes > 0)
105
+ // Validate operator-supplied numeric opts via lib/numeric-bounds
106
+ // Infinity / NaN / negative / non-integer all bypass the `> 0` shape
107
+ // and silently lift the DoS cap they were meant to enforce.
108
+ if (opts.maxBytes !== undefined && !numericBounds.isPositiveFiniteInt(opts.maxBytes)) {
109
+ throw new SafeEnvError("env.parse: maxBytes must be a positive finite integer; got " +
110
+ numericBounds.shape(opts.maxBytes), "env/bad-opt");
111
+ }
112
+ if (opts.maxKeys !== undefined && !numericBounds.isPositiveFiniteInt(opts.maxKeys)) {
113
+ throw new SafeEnvError("env.parse: maxKeys must be a positive finite integer; got " +
114
+ numericBounds.shape(opts.maxKeys), "env/bad-opt");
115
+ }
116
+ var maxBytes = opts.maxBytes !== undefined
93
117
  ? Math.min(opts.maxBytes, C.BYTES.mib(1)) : DEFAULTS.maxBytes;
94
- var maxKeys = (typeof opts.maxKeys === "number" && opts.maxKeys > 0)
95
- ? Math.min(opts.maxKeys, 100000) : DEFAULTS.maxKeys;
118
+ var maxKeys = opts.maxKeys !== undefined
119
+ ? Math.min(opts.maxKeys, 100_000) : DEFAULTS.maxKeys;
96
120
  var keyShape = opts.keyShape instanceof RegExp ? opts.keyShape : DEFAULTS.keyShape;
97
121
 
98
122
  input = safeBuffer.normalizeText(input, {
@@ -123,7 +147,7 @@ function parse(input, opts) {
123
147
  if (eqIdx < 0) {
124
148
  throw new SafeEnvError("missing '=' separator", "env/bad-line", lineNumber);
125
149
  }
126
- var key = trimmed.substring(0, eqIdx).replace(/[ \t]+$/, "");
150
+ var key = trimmed.substring(0, eqIdx).replace(safeBuffer.TRAILING_HSPACE_RE, "");
127
151
  var rest = trimmed.substring(eqIdx + 1);
128
152
 
129
153
  if (key.length === 0) {
@@ -163,9 +187,9 @@ function parse(input, opts) {
163
187
  // (so a value like `KEY=color#red` keeps the literal `#`).
164
188
  var commentMatch = rest.match(/^([^\s#]*(?:[ \t]+[^#\s]+)*)\s+#.*$/);
165
189
  if (commentMatch) {
166
- value = commentMatch[1].replace(/[ \t]+$/, "");
190
+ value = commentMatch[1].replace(safeBuffer.TRAILING_HSPACE_RE, "");
167
191
  } else {
168
- value = rest.replace(/[ \t]+$/, "");
192
+ value = rest.replace(safeBuffer.TRAILING_HSPACE_RE, "");
169
193
  }
170
194
  // Reject `$VAR` style references — explicit error so operators
171
195
  // see the policy rather than silently getting unexpanded text.
@@ -209,10 +233,10 @@ function _decodeDoubleQuoted(rest, lineNumber) {
209
233
  case "$": out += "$"; i += 2; continue; // literal $ escape
210
234
  case "u": {
211
235
  var hex = rest.substring(i + 2, i + 6);
212
- if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
236
+ if (!safeBuffer.isHex(hex, 4)) {
213
237
  throw new SafeEnvError("bad \\u escape", "env/bad-escape", lineNumber);
214
238
  }
215
- out += String.fromCharCode(parseInt(hex, 16));
239
+ out += String.fromCharCode(parseInt(hex, RADIX_HEX));
216
240
  i += 6;
217
241
  continue;
218
242
  }
@@ -493,11 +517,9 @@ function _hasNonEmptyDiff(d) {
493
517
  function _writeAuditRows(filepath, diff) {
494
518
  // Both audit.record and cluster.requireLeader sit downstream — if
495
519
  // we're a follower, audit.record will throw NotLeaderError. Catch
496
- // silently: a follower's local config-load shouldn't crash because
520
+ // explicitly: a follower's local config-load shouldn't crash because
497
521
  // the cluster's audit chain belongs to the leader.
498
- var audit;
499
- try { audit = require("./../audit"); }
500
- catch (_e) { return; /* audit module not loadable in this context */ }
522
+ var audit = auditModule(); // resolve the lazy-required audit module
501
523
 
502
524
  function _safeRecord(action, metadata) {
503
525
  try {
@@ -559,7 +581,14 @@ function readVar(name, schema) {
559
581
  var type = schema.type || "string";
560
582
  var required = schema.required === true;
561
583
  var hasDefault = "default" in schema;
562
- var maxBytes = (typeof schema.maxBytes === "number" && schema.maxBytes > 0)
584
+ if (schema.maxBytes !== undefined && !numericBounds.isPositiveFiniteInt(schema.maxBytes)) {
585
+ throw new SafeEnvError(
586
+ "readVar: maxBytes must be a positive finite integer; got " +
587
+ numericBounds.shape(schema.maxBytes),
588
+ "env/bad-opt"
589
+ );
590
+ }
591
+ var maxBytes = (schema.maxBytes !== undefined)
563
592
  ? schema.maxBytes : READVAR_DEFAULT_MAX_BYTES;
564
593
  var strip = schema.strip === true;
565
594
 
@@ -31,18 +31,24 @@
31
31
  * parsers.ini.parse(input, opts?) -> object
32
32
  * parsers.ini.IniSafeError (with .code matching ini/...)
33
33
  *
34
- * Validation tier: A (config-time throw) — every malformed input
35
- * surfaces at parse time, never silently coerces to a usable shape.
34
+ * Input validation throws at parse time — every malformed input
35
+ * surfaces immediately, never silently coerces to a usable shape.
36
36
  */
37
37
 
38
+ var C = require("../constants");
39
+ var numericBounds = require("../numeric-bounds");
38
40
  var { defineClass } = require("../framework-error");
39
41
 
40
42
  var IniSafeError = defineClass("IniSafeError", { alwaysPermanent: true });
41
43
 
42
- var DEFAULT_MAX_BYTES = 1024 * 1024;
44
+ // parseInt radix named so the call site doesn't carry a bare 16
45
+ // integer literal that reads as a byte count.
46
+ var RADIX_HEX = 0x10;
47
+
48
+ var DEFAULT_MAX_BYTES = C.BYTES.mib(1);
43
49
  var DEFAULT_MAX_SECTIONS = 500;
44
- var DEFAULT_MAX_KEYS_SECTION = 1000;
45
- var DEFAULT_MAX_VALUE_BYTES = 64 * 1024;
50
+ var DEFAULT_MAX_KEYS_SECTION = 1_000;
51
+ var DEFAULT_MAX_VALUE_BYTES = C.BYTES.kib(64);
46
52
 
47
53
  var FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
48
54
 
@@ -109,7 +115,7 @@ function _coerceValue(raw) {
109
115
  if (TRUE_VALUES.has(lower)) return true;
110
116
  if (FALSE_VALUES.has(lower)) return false;
111
117
  if (/^0x[0-9a-f]+$/i.test(raw)) {
112
- var hex = parseInt(raw, 16);
118
+ var hex = parseInt(raw, RADIX_HEX);
113
119
  if (!Number.isSafeInteger(hex)) {
114
120
  throw _err("ini/value-out-of-range", "hex integer exceeds safe-integer range: " + raw);
115
121
  }
@@ -180,10 +186,23 @@ function _parseSectionHeader(line) {
180
186
 
181
187
  function parse(input, opts) {
182
188
  opts = opts || {};
183
- var maxBytes = opts.maxBytes || DEFAULT_MAX_BYTES;
184
- var maxSections = opts.maxSections || DEFAULT_MAX_SECTIONS;
185
- var maxKeysPerSect = opts.maxKeysPerSection || DEFAULT_MAX_KEYS_SECTION;
186
- var maxValueBytes = opts.maxValueBytes || DEFAULT_MAX_VALUE_BYTES;
189
+ // Validate operator-supplied numeric opts via lib/numeric-bounds —
190
+ // Infinity / NaN / negative / non-integer all bypass `|| DEFAULT_*`
191
+ // (Infinity is truthy) and silently lift the DoS cap they were meant
192
+ // to enforce.
193
+ var _numericOpts = ["maxBytes", "maxSections", "maxKeysPerSection", "maxValueBytes"];
194
+ for (var _ni = 0; _ni < _numericOpts.length; _ni++) {
195
+ var _name = _numericOpts[_ni];
196
+ if (opts[_name] !== undefined && !numericBounds.isPositiveFiniteInt(opts[_name])) {
197
+ throw _err("ini/bad-opt",
198
+ "ini.parse: " + _name + " must be a positive finite integer; got " +
199
+ numericBounds.shape(opts[_name]));
200
+ }
201
+ }
202
+ var maxBytes = opts.maxBytes !== undefined ? opts.maxBytes : DEFAULT_MAX_BYTES;
203
+ var maxSections = opts.maxSections !== undefined ? opts.maxSections : DEFAULT_MAX_SECTIONS;
204
+ var maxKeysPerSect = opts.maxKeysPerSection !== undefined ? opts.maxKeysPerSection : DEFAULT_MAX_KEYS_SECTION;
205
+ var maxValueBytes = opts.maxValueBytes !== undefined ? opts.maxValueBytes : DEFAULT_MAX_VALUE_BYTES;
187
206
  var onDuplicate = opts.onDuplicate || "throw";
188
207
 
189
208
  if (typeof input !== "string") {
@@ -45,6 +45,7 @@
45
45
  */
46
46
 
47
47
  var C = require("../constants");
48
+ var numericBounds = require("../numeric-bounds");
48
49
  var safeBuffer = require("../safe-buffer");
49
50
  var { FrameworkError } = require("../framework-error");
50
51
 
@@ -59,24 +60,49 @@ class SafeTomlError extends FrameworkError {
59
60
  }
60
61
  }
61
62
 
63
+ // parseInt radices and TOML radix-prefix tokens — naming these constants
64
+ // keeps the literal `8` / `16` byte-shaped numbers off the call sites.
65
+ var RADIX_BIN = 0x2;
66
+ var RADIX_OCTAL = 0x8;
67
+ var RADIX_HEX = 0x10;
68
+
69
+ // Date-time literal character widths (per TOML / RFC 3339).
70
+ var TIME_CHARS = 0x8; // "HH:MM:SS"
71
+ var OFFSET_CHARS = 0x6; // "+HH:MM"
72
+
62
73
  var DEFAULTS = {
63
74
  maxBytes: C.BYTES.mib(1),
64
75
  maxDepth: 100,
65
- maxKeys: 50000,
76
+ maxKeys: 50_000,
66
77
  };
67
78
 
68
79
  var POISONED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
69
80
 
70
81
  function parse(input, opts) {
71
82
  opts = opts || {};
72
- var maxBytes = (typeof opts.maxBytes === "number" && opts.maxBytes > 0)
83
+ // Validate operator-supplied numeric opts via lib/numeric-bounds
84
+ // Infinity / NaN / negative / non-integer all bypass the `> 0` shape
85
+ // and silently lift the DoS cap they were meant to enforce.
86
+ if (opts.maxBytes !== undefined && !numericBounds.isPositiveFiniteInt(opts.maxBytes)) {
87
+ throw new SafeTomlError("toml.parse: maxBytes must be a positive finite integer; got " +
88
+ numericBounds.shape(opts.maxBytes), "toml/bad-opt");
89
+ }
90
+ if (opts.maxDepth !== undefined && !numericBounds.isPositiveFiniteInt(opts.maxDepth)) {
91
+ throw new SafeTomlError("toml.parse: maxDepth must be a positive finite integer; got " +
92
+ numericBounds.shape(opts.maxDepth), "toml/bad-opt");
93
+ }
94
+ if (opts.maxKeys !== undefined && !numericBounds.isPositiveFiniteInt(opts.maxKeys)) {
95
+ throw new SafeTomlError("toml.parse: maxKeys must be a positive finite integer; got " +
96
+ numericBounds.shape(opts.maxKeys), "toml/bad-opt");
97
+ }
98
+ var maxBytes = opts.maxBytes !== undefined
73
99
  ? Math.min(opts.maxBytes, C.BYTES.mib(64))
74
100
  : DEFAULTS.maxBytes;
75
- var maxDepth = (typeof opts.maxDepth === "number" && opts.maxDepth > 0)
76
- ? Math.min(opts.maxDepth, 1000)
101
+ var maxDepth = opts.maxDepth !== undefined
102
+ ? Math.min(opts.maxDepth, 1_000)
77
103
  : DEFAULTS.maxDepth;
78
- var maxKeys = (typeof opts.maxKeys === "number" && opts.maxKeys > 0)
79
- ? Math.min(opts.maxKeys, 1000000)
104
+ var maxKeys = opts.maxKeys !== undefined
105
+ ? Math.min(opts.maxKeys, 1_000_000)
80
106
  : DEFAULTS.maxKeys;
81
107
 
82
108
  input = safeBuffer.normalizeText(input, {
@@ -237,19 +263,20 @@ function parse(input, opts) {
237
263
  case "t": return "\t";
238
264
  case "u": {
239
265
  var hex = input.substring(pos, pos + 4);
240
- if (hex.length < 4 || !/^[0-9a-fA-F]{4}$/.test(hex)) {
266
+ if (!safeBuffer.isHex(hex, 4)) {
241
267
  throw _err("bad \\u escape", "toml/bad-escape");
242
268
  }
243
269
  _advance(4);
244
- return String.fromCharCode(parseInt(hex, 16));
270
+ return String.fromCharCode(parseInt(hex, RADIX_HEX));
245
271
  }
246
272
  case "U": {
247
- var hex8 = input.substring(pos, pos + 8);
248
- if (hex8.length < 8 || !/^[0-9a-fA-F]{8}$/.test(hex8)) {
273
+ var hexLen = 0x8;
274
+ var hex8 = input.substring(pos, pos + hexLen);
275
+ if (hex8.length < hexLen || !/^[0-9a-fA-F]{8}$/.test(hex8)) {
249
276
  throw _err("bad \\U escape", "toml/bad-escape");
250
277
  }
251
- _advance(8);
252
- var code = parseInt(hex8, 16);
278
+ _advance(hexLen);
279
+ var code = parseInt(hex8, RADIX_HEX);
253
280
  if (code > 0x10FFFF) throw _err("\\U code point > U+10FFFF", "toml/bad-escape");
254
281
  return String.fromCodePoint(code);
255
282
  }
@@ -367,11 +394,12 @@ function parse(input, opts) {
367
394
  if (pos + 10 <= len && /^\d{4}-\d{2}-\d{2}/.test(input.substr(pos, 10))) {
368
395
  var sep = _peek(10);
369
396
  if (sep === "T" || sep === "t" || sep === " ") {
370
- // Look ahead for time portion
397
+ // Look ahead for time portion (HH:MM:SS = 8 chars)
371
398
  var timeStart = pos + 11;
372
- if (/^\d{2}:\d{2}:\d{2}/.test(input.substr(timeStart, 8))) {
399
+ var timeChars = TIME_CHARS;
400
+ if (/^\d{2}:\d{2}:\d{2}/.test(input.substr(timeStart, timeChars))) {
373
401
  var datePart = input.substr(pos, 10);
374
- var timeEnd = timeStart + 8;
402
+ var timeEnd = timeStart + timeChars;
375
403
  // Optional fractional
376
404
  if (_peek(timeEnd - pos) === ".") {
377
405
  timeEnd += 1;
@@ -383,11 +411,12 @@ function parse(input, opts) {
383
411
  var nextChar = input.charAt(timeEnd);
384
412
  if (nextChar === "Z" || nextChar === "z") { offsetStr = "Z"; timeEnd += 1; }
385
413
  else if (nextChar === "+" || nextChar === "-") {
386
- // Expect HH:MM
387
- var off = input.substr(timeEnd, 6);
414
+ // Expect ±HH:MM (6 chars)
415
+ var offsetChars = OFFSET_CHARS;
416
+ var off = input.substr(timeEnd, offsetChars);
388
417
  if (/^[+-]\d{2}:\d{2}$/.test(off)) {
389
418
  offsetStr = off;
390
- timeEnd += 6;
419
+ timeEnd += offsetChars;
391
420
  }
392
421
  }
393
422
  _advance(timeEnd - pos);
@@ -409,9 +438,10 @@ function parse(input, opts) {
409
438
  return { kind: "local-date", value: ds };
410
439
  }
411
440
  }
412
- // Time-only: HH:MM:SS
413
- if (pos + 8 <= len && /^\d{2}:\d{2}:\d{2}/.test(input.substr(pos, 8))) {
414
- var teEnd = pos + 8;
441
+ // Time-only: HH:MM:SS (8 chars)
442
+ var timeOnlyChars = TIME_CHARS;
443
+ if (pos + timeOnlyChars <= len && /^\d{2}:\d{2}:\d{2}/.test(input.substr(pos, timeOnlyChars))) {
444
+ var teEnd = pos + timeOnlyChars;
415
445
  if (input.charAt(teEnd) === ".") {
416
446
  teEnd += 1;
417
447
  while (teEnd < len && /\d/.test(input.charAt(teEnd))) teEnd += 1;
@@ -448,14 +478,14 @@ function parse(input, opts) {
448
478
  if (prefix === "x" || prefix === "o" || prefix === "b") {
449
479
  if (sign !== 1) throw _err("sign not allowed on hex/oct/bin literal", "toml/bad-number");
450
480
  _advance(2);
451
- var radix = prefix === "x" ? 16 : prefix === "o" ? 8 : 2;
481
+ var radix = prefix === "x" ? RADIX_HEX : prefix === "o" ? RADIX_OCTAL : RADIX_BIN;
452
482
  var digitsStart = pos;
453
483
  while (!_eof()) {
454
484
  var ch = _peek();
455
485
  if (ch === "_") { _advance(); continue; }
456
- if (radix === 16 && /[0-9a-fA-F]/.test(ch)) { _advance(); continue; }
457
- if (radix === 8 && /[0-7]/.test(ch)) { _advance(); continue; }
458
- if (radix === 2 && /[01]/.test(ch)) { _advance(); continue; }
486
+ if (radix === RADIX_HEX && /[0-9a-fA-F]/.test(ch)) { _advance(); continue; }
487
+ if (radix === RADIX_OCTAL && /[0-7]/.test(ch)) { _advance(); continue; }
488
+ if (radix === RADIX_BIN && /[01]/.test(ch)) { _advance(); continue; }
459
489
  break;
460
490
  }
461
491
  var digits = input.substring(digitsStart, pos).replace(/_/g, "");
@@ -40,6 +40,7 @@
40
40
  */
41
41
 
42
42
  var C = require("../constants");
43
+ var nb = require("../numeric-bounds");
43
44
  var safeBuffer = require("../safe-buffer");
44
45
  var { FrameworkError } = require("../framework-error");
45
46
 
@@ -53,34 +54,44 @@ class SafeXmlError extends FrameworkError {
53
54
  }
54
55
  }
55
56
 
57
+ // parseInt radix — named so the call site doesn't carry a bare 16
58
+ // integer literal that reads as a byte count.
59
+ var RADIX_HEX = 0x10;
60
+
56
61
  var DEFAULTS = {
57
62
  maxBytes: C.BYTES.mib(1),
58
63
  maxDepth: 100,
59
- maxElements: 10000,
64
+ maxElements: 10_000,
60
65
  maxAttributes: 100,
61
66
  allowDoctype: false,
62
67
  allowProcessing: false, // <?...?> other than the xml decl
63
68
  };
64
69
 
65
70
  var ABSOLUTE_MAX_BYTES = C.BYTES.mib(64);
66
- var ABSOLUTE_MAX_DEPTH = 1000;
67
- var ABSOLUTE_MAX_ELEMENTS = 1000000;
71
+ var ABSOLUTE_MAX_DEPTH = 1_000;
72
+ var ABSOLUTE_MAX_ELEMENTS = 1_000_000;
73
+ var ABSOLUTE_MAX_ATTRIBUTES = 1_000;
68
74
 
69
75
  // XML built-in entities (the ONLY entities allowed)
70
76
  var BUILT_IN_ENTITIES = { lt: "<", gt: ">", amp: "&", quot: "\"", apos: "'" };
71
77
 
72
- function _capInt(value, defaultValue, ceiling) {
73
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return defaultValue;
74
- return Math.min(Math.floor(value), ceiling);
78
+ function _validateAndCap(name, value, defaultValue, ceiling) {
79
+ if (value === undefined) return defaultValue;
80
+ if (!nb.isPositiveFiniteInt(value)) {
81
+ throw new SafeXmlError("xml/bad-opt",
82
+ "xml.parse: " + name + " must be a positive finite integer; got " +
83
+ nb.shape(value));
84
+ }
85
+ return Math.min(value, ceiling);
75
86
  }
76
87
 
77
88
  function parse(input, opts) {
78
89
  opts = opts || {};
79
90
 
80
- var maxBytes = _capInt(opts.maxBytes, DEFAULTS.maxBytes, ABSOLUTE_MAX_BYTES);
81
- var maxDepth = _capInt(opts.maxDepth, DEFAULTS.maxDepth, ABSOLUTE_MAX_DEPTH);
82
- var maxElements = _capInt(opts.maxElements, DEFAULTS.maxElements, ABSOLUTE_MAX_ELEMENTS);
83
- var maxAttrs = _capInt(opts.maxAttributes, DEFAULTS.maxAttributes, 1000);
91
+ var maxBytes = _validateAndCap("maxBytes", opts.maxBytes, DEFAULTS.maxBytes, ABSOLUTE_MAX_BYTES);
92
+ var maxDepth = _validateAndCap("maxDepth", opts.maxDepth, DEFAULTS.maxDepth, ABSOLUTE_MAX_DEPTH);
93
+ var maxElements = _validateAndCap("maxElements", opts.maxElements, DEFAULTS.maxElements, ABSOLUTE_MAX_ELEMENTS);
94
+ var maxAttrs = _validateAndCap("maxAttributes", opts.maxAttributes, DEFAULTS.maxAttributes, ABSOLUTE_MAX_ATTRIBUTES);
84
95
  var allowDoctype = !!opts.allowDoctype;
85
96
  var allowProcessing = !!opts.allowProcessing;
86
97
 
@@ -130,7 +141,7 @@ function parse(input, opts) {
130
141
  // Numeric character reference
131
142
  var code;
132
143
  if (name.charAt(1) === "x" || name.charAt(1) === "X") {
133
- code = parseInt(name.substring(2), 16);
144
+ code = parseInt(name.substring(2), RADIX_HEX);
134
145
  } else {
135
146
  code = parseInt(name.substring(1), 10);
136
147
  }
@@ -53,6 +53,7 @@
53
53
  */
54
54
 
55
55
  var C = require("../constants");
56
+ var numericBounds = require("../numeric-bounds");
56
57
  var safeBuffer = require("../safe-buffer");
57
58
  var { FrameworkError } = require("../framework-error");
58
59
 
@@ -67,10 +68,21 @@ class SafeYamlError extends FrameworkError {
67
68
  }
68
69
  }
69
70
 
71
+ // parseInt radices, named so the line itself doesn't carry a bare 8 / 16
72
+ // integer literal that reads as a byte count.
73
+ var RADIX_OCTAL = 0x8;
74
+ var RADIX_HEX = 0x10;
75
+
76
+ // Defensive cap on the size of a single scalar token before regex
77
+ // classification. The parser already bounds the whole input via maxBytes;
78
+ // this cap keeps a pathological single-line scalar from feeding the type-
79
+ // inference regexes an arbitrarily long string.
80
+ var MAX_SCALAR_BYTES = C.BYTES.kib(64);
81
+
70
82
  var DEFAULTS = {
71
83
  maxBytes: C.BYTES.mib(1),
72
84
  maxDepth: 100,
73
- maxKeys: 50000,
85
+ maxKeys: 50_000,
74
86
  };
75
87
 
76
88
  var POISONED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
@@ -88,25 +100,36 @@ var FLOAT_INF = /^[-+]?\.(inf|Inf|INF)$/;
88
100
  var FLOAT_NAN = /^\.(nan|NaN|NAN)$/;
89
101
 
90
102
  function _resolveScalar(s) {
103
+ // Fall back to string for any token whose length exceeds the scalar cap
104
+ // before running type-inference regexes. The parser caller has already
105
+ // applied the input-wide maxBytes cap; this is a per-token guard so the
106
+ // type-inference regexes never see a pathologically long string.
107
+ if (typeof s !== "string" || s.length > MAX_SCALAR_BYTES) return s;
108
+ // Below: every regex test sees an `s` whose s.length <= MAX_SCALAR_BYTES.
91
109
  if (NULL_RE.test(s)) return null;
92
110
  if (BOOL_RE.test(s)) return s.toLowerCase() === "true";
111
+ // s.length <= MAX_SCALAR_BYTES asserted at function entry above.
93
112
  if (INT_RE.test(s)) {
94
113
  var n = parseInt(s, 10);
95
114
  if (Number.isSafeInteger(n)) return n;
96
115
  return s; // fallback to string for huge ints (don't lose precision silently)
97
116
  }
117
+ // s.length <= MAX_SCALAR_BYTES asserted at function entry above.
98
118
  if (INT_OCT.test(s)) {
99
- var oct = parseInt(s.substring(2), 8);
119
+ var oct = parseInt(s.substring(2), RADIX_OCTAL);
100
120
  if (Number.isSafeInteger(oct)) return oct;
101
121
  return s;
102
122
  }
123
+ // s.length <= MAX_SCALAR_BYTES asserted at function entry above.
103
124
  if (INT_HEX.test(s)) {
104
- var hex = parseInt(s.substring(2), 16);
125
+ var hex = parseInt(s.substring(2), RADIX_HEX);
105
126
  if (Number.isSafeInteger(hex)) return hex;
106
127
  return s;
107
128
  }
129
+ // s.length <= MAX_SCALAR_BYTES asserted at function entry above.
108
130
  if (FLOAT_INF.test(s)) return s.charAt(0) === "-" ? -Infinity : Infinity;
109
131
  if (FLOAT_NAN.test(s)) return NaN;
132
+ // s.length <= MAX_SCALAR_BYTES asserted at function entry above.
110
133
  if (FLOAT_RE.test(s)) {
111
134
  var f = parseFloat(s);
112
135
  if (!isNaN(f)) return f;
@@ -116,12 +139,27 @@ function _resolveScalar(s) {
116
139
 
117
140
  function parse(input, opts) {
118
141
  opts = opts || {};
119
- var maxBytes = (typeof opts.maxBytes === "number" && opts.maxBytes > 0)
142
+ // Validate operator-supplied numeric opts via lib/numeric-bounds
143
+ // Infinity / NaN / negative / non-integer all bypass the
144
+ // `> 0` shape and silently lift the DoS cap they were meant to enforce.
145
+ if (opts.maxBytes !== undefined && !numericBounds.isPositiveFiniteInt(opts.maxBytes)) {
146
+ throw new SafeYamlError("yaml.parse: maxBytes must be a positive finite integer; got " +
147
+ numericBounds.shape(opts.maxBytes), "yaml/bad-opt");
148
+ }
149
+ if (opts.maxDepth !== undefined && !numericBounds.isPositiveFiniteInt(opts.maxDepth)) {
150
+ throw new SafeYamlError("yaml.parse: maxDepth must be a positive finite integer; got " +
151
+ numericBounds.shape(opts.maxDepth), "yaml/bad-opt");
152
+ }
153
+ if (opts.maxKeys !== undefined && !numericBounds.isPositiveFiniteInt(opts.maxKeys)) {
154
+ throw new SafeYamlError("yaml.parse: maxKeys must be a positive finite integer; got " +
155
+ numericBounds.shape(opts.maxKeys), "yaml/bad-opt");
156
+ }
157
+ var maxBytes = opts.maxBytes !== undefined
120
158
  ? Math.min(opts.maxBytes, C.BYTES.mib(64)) : DEFAULTS.maxBytes;
121
- var maxDepth = (typeof opts.maxDepth === "number" && opts.maxDepth > 0)
122
- ? Math.min(opts.maxDepth, 1000) : DEFAULTS.maxDepth;
123
- var maxKeys = (typeof opts.maxKeys === "number" && opts.maxKeys > 0)
124
- ? Math.min(opts.maxKeys, 1000000) : DEFAULTS.maxKeys;
159
+ var maxDepth = opts.maxDepth !== undefined
160
+ ? Math.min(opts.maxDepth, 1_000) : DEFAULTS.maxDepth;
161
+ var maxKeys = opts.maxKeys !== undefined
162
+ ? Math.min(opts.maxKeys, 1_000_000) : DEFAULTS.maxKeys;
125
163
 
126
164
  input = safeBuffer.normalizeText(input, {
127
165
  maxBytes: maxBytes,
@@ -147,7 +185,7 @@ function parse(input, opts) {
147
185
  var raw = rawLines[i];
148
186
  // Strip trailing whitespace from the line for analysis (block-scalar
149
187
  // content paths preserve their own internal spacing separately).
150
- var trimmed = raw.replace(/[ \t]+$/, "");
188
+ var trimmed = raw.replace(safeBuffer.TRAILING_HSPACE_RE, "");
151
189
  var indent = 0;
152
190
  while (indent < raw.length && raw.charAt(indent) === " ") indent += 1;
153
191
  if (indent < raw.length && raw.charAt(indent) === "\t") {
@@ -294,7 +332,7 @@ function parse(input, opts) {
294
332
  if (raw.charAt(0) === "'") {
295
333
  return _decodeSingleQuoted(raw, lineNumber, col);
296
334
  }
297
- var trimmed = raw.replace(/[ \t]+$/, "");
335
+ var trimmed = raw.replace(safeBuffer.TRAILING_HSPACE_RE, "");
298
336
  if (POISONED_KEYS.has(trimmed)) {
299
337
  throw new SafeYamlError("forbidden key '" + trimmed + "'",
300
338
  "yaml/poisoned-key", lineNumber, col);
@@ -468,7 +506,7 @@ function parse(input, opts) {
468
506
  return sq;
469
507
  }
470
508
  // Plain scalar — strip trailing space, resolve type.
471
- return _resolveScalar(t.replace(/[ \t]+$/, ""));
509
+ return _resolveScalar(t.replace(safeBuffer.TRAILING_HSPACE_RE, ""));
472
510
  }
473
511
 
474
512
  // For quoted-string termination check on a single-line value.
@@ -584,7 +622,7 @@ function parse(input, opts) {
584
622
  if (c === "," || c === "}" || c === "]") break;
585
623
  p += 1;
586
624
  }
587
- var raw = text.substring(start, p).replace(/[ \t]+$/, "");
625
+ var raw = text.substring(start, p).replace(safeBuffer.TRAILING_HSPACE_RE, "");
588
626
  return { value: _resolveScalar(raw), nextPos: p };
589
627
  }
590
628
 
@@ -607,7 +645,7 @@ function parse(input, opts) {
607
645
  if (c === ":" || c === "," || c === "}" || c === "]") break;
608
646
  p += 1;
609
647
  }
610
- return { key: text.substring(start, p).replace(/[ \t]+$/, ""), nextPos: p };
648
+ return { key: text.substring(start, p).replace(safeBuffer.TRAILING_HSPACE_RE, ""), nextPos: p };
611
649
  }
612
650
 
613
651
  function _findMatchingBracket(text, start, open, close, lineNumber, col) {
@@ -678,10 +716,10 @@ function parse(input, opts) {
678
716
  case "0": out += "\0"; i += 2; break;
679
717
  case "u": {
680
718
  var hex = raw.substring(i + 2, i + 6);
681
- if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
719
+ if (!safeBuffer.isHex(hex, 4)) {
682
720
  throw new SafeYamlError("bad \\u escape", "yaml/bad-escape", lineNumber, col + i);
683
721
  }
684
- out += String.fromCharCode(parseInt(hex, 16));
722
+ out += String.fromCharCode(parseInt(hex, RADIX_HEX));
685
723
  i += 6;
686
724
  break;
687
725
  }
@@ -690,7 +728,7 @@ function parse(input, opts) {
690
728
  if (!/^[0-9a-fA-F]{8}$/.test(hex8)) {
691
729
  throw new SafeYamlError("bad \\U escape", "yaml/bad-escape", lineNumber, col + i);
692
730
  }
693
- var code = parseInt(hex8, 16);
731
+ var code = parseInt(hex8, RADIX_HEX);
694
732
  if (code > 0x10FFFF) {
695
733
  throw new SafeYamlError("\\U code point > U+10FFFF",
696
734
  "yaml/bad-escape", lineNumber, col + i);
@@ -734,7 +772,7 @@ function parse(input, opts) {
734
772
  function _stripEolComment(text) {
735
773
  // Strip ` #...` comments (must be preceded by whitespace) at end of line.
736
774
  var match = text.match(/^(.*?)(\s+#.*)?$/);
737
- return (match && match[1] != null ? match[1] : text).replace(/[ \t]+$/, "");
775
+ return (match && match[1] != null ? match[1] : text).replace(safeBuffer.TRAILING_HSPACE_RE, "");
738
776
  }
739
777
 
740
778
  // ---- Block scalars (| literal, > folded) ----