@blamejs/core 0.15.11 → 0.15.13

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 (305) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +7 -23
  4. package/lib/acme.js +13 -16
  5. package/lib/agent-event-bus.js +5 -4
  6. package/lib/agent-idempotency.js +2 -5
  7. package/lib/agent-orchestrator.js +2 -5
  8. package/lib/agent-saga.js +3 -5
  9. package/lib/agent-tenant.js +2 -5
  10. package/lib/ai-adverse-decision.js +2 -15
  11. package/lib/ai-capability.js +1 -6
  12. package/lib/ai-dp.js +1 -5
  13. package/lib/ai-input.js +2 -2
  14. package/lib/ai-pref.js +3 -8
  15. package/lib/ai-quota.js +3 -14
  16. package/lib/api-key.js +37 -28
  17. package/lib/archive-adapters.js +2 -4
  18. package/lib/archive-entry-policy.js +32 -0
  19. package/lib/archive-read.js +5 -17
  20. package/lib/archive-tar-read.js +5 -16
  21. package/lib/archive.js +2 -10
  22. package/lib/arg-parser.js +7 -6
  23. package/lib/asyncapi-traits.js +2 -6
  24. package/lib/atomic-file.js +108 -31
  25. package/lib/audit-chain.js +133 -53
  26. package/lib/audit-daily-review.js +24 -14
  27. package/lib/audit-emit.js +82 -0
  28. package/lib/audit-sign.js +257 -0
  29. package/lib/audit.js +84 -0
  30. package/lib/auth/access-lock.js +5 -27
  31. package/lib/auth/dpop.js +23 -35
  32. package/lib/auth/fido-mds3.js +9 -15
  33. package/lib/auth/jwt-external.js +26 -8
  34. package/lib/auth/jwt.js +13 -15
  35. package/lib/auth/lockout.js +6 -25
  36. package/lib/auth/oauth.js +67 -45
  37. package/lib/auth/oid4vci.js +55 -32
  38. package/lib/auth/oid4vp.js +3 -2
  39. package/lib/auth/openid-federation.js +6 -6
  40. package/lib/auth/passkey.js +3 -3
  41. package/lib/auth/password.js +7 -1
  42. package/lib/auth/saml.js +37 -27
  43. package/lib/auth/sd-jwt-vc-holder.js +2 -14
  44. package/lib/auth/sd-jwt-vc-issuer.js +2 -14
  45. package/lib/auth/sd-jwt-vc.js +1 -1
  46. package/lib/auth/status-list.js +7 -7
  47. package/lib/auth/step-up.js +6 -12
  48. package/lib/auth-bot-challenge.js +6 -37
  49. package/lib/backup/bundle.js +11 -18
  50. package/lib/backup/index.js +14 -47
  51. package/lib/bounded-map.js +112 -1
  52. package/lib/breach-deadline.js +1 -11
  53. package/lib/cache.js +7 -18
  54. package/lib/cbor.js +2 -1
  55. package/lib/cdn-cache-control.js +8 -9
  56. package/lib/cert.js +3 -10
  57. package/lib/chain-writer.js +162 -47
  58. package/lib/cli.js +5 -1
  59. package/lib/client-hints.js +10 -10
  60. package/lib/cloud-events.js +40 -31
  61. package/lib/cluster-storage.js +2 -38
  62. package/lib/cluster.js +4 -2
  63. package/lib/cms-codec.js +2 -1
  64. package/lib/codepoint-class.js +67 -1
  65. package/lib/compliance-ai-act-logging.js +1 -6
  66. package/lib/compliance-ai-act-transparency.js +2 -4
  67. package/lib/compliance-eaa.js +1 -11
  68. package/lib/compliance-sanctions-fetcher.js +21 -28
  69. package/lib/compliance-sanctions.js +2 -14
  70. package/lib/compliance.js +2 -9
  71. package/lib/config-drift.js +2 -12
  72. package/lib/content-digest.js +10 -8
  73. package/lib/cookies.js +5 -11
  74. package/lib/cose.js +19 -11
  75. package/lib/cra-report.js +1 -11
  76. package/lib/crypto-field.js +5 -10
  77. package/lib/crypto.js +120 -3
  78. package/lib/csp.js +235 -3
  79. package/lib/daemon.js +42 -41
  80. package/lib/data-act.js +19 -9
  81. package/lib/db-query.js +6 -40
  82. package/lib/db.js +34 -12
  83. package/lib/dbsc.js +5 -6
  84. package/lib/ddl-change-control.js +18 -14
  85. package/lib/deprecate.js +4 -5
  86. package/lib/did.js +6 -2
  87. package/lib/dsr.js +8 -12
  88. package/lib/external-db-migrate.js +21 -22
  89. package/lib/external-db.js +14 -26
  90. package/lib/fda-21cfr11.js +12 -8
  91. package/lib/fdx.js +22 -18
  92. package/lib/file-upload.js +80 -66
  93. package/lib/flag-evaluation-context.js +5 -8
  94. package/lib/framework-error.js +12 -0
  95. package/lib/framework-schema.js +19 -0
  96. package/lib/fsm.js +7 -12
  97. package/lib/gate-contract.js +869 -38
  98. package/lib/gdpr-ropa.js +4 -13
  99. package/lib/graphql-federation.js +1 -1
  100. package/lib/guard-agent-registry.js +2 -1
  101. package/lib/guard-all.js +1 -0
  102. package/lib/guard-archive.js +9 -30
  103. package/lib/guard-auth.js +23 -80
  104. package/lib/guard-cidr.js +20 -96
  105. package/lib/guard-csv.js +135 -196
  106. package/lib/guard-domain.js +23 -106
  107. package/lib/guard-dsn.js +16 -13
  108. package/lib/guard-email.js +46 -53
  109. package/lib/guard-envelope.js +1 -1
  110. package/lib/guard-event-bus-topic.js +2 -1
  111. package/lib/guard-filename.js +18 -62
  112. package/lib/guard-graphql.js +28 -75
  113. package/lib/guard-html-wcag.js +15 -2
  114. package/lib/guard-html.js +65 -117
  115. package/lib/guard-idempotency-key.js +2 -1
  116. package/lib/guard-image.js +280 -77
  117. package/lib/guard-imap-command.js +8 -9
  118. package/lib/guard-json.js +87 -103
  119. package/lib/guard-jsonpath.js +20 -88
  120. package/lib/guard-jwt.js +32 -114
  121. package/lib/guard-list-id.js +2 -7
  122. package/lib/guard-list-unsubscribe.js +2 -7
  123. package/lib/guard-mail-compose.js +5 -6
  124. package/lib/guard-mail-move.js +2 -1
  125. package/lib/guard-mail-query.js +5 -3
  126. package/lib/guard-mail-sieve.js +6 -7
  127. package/lib/guard-managesieve-command.js +5 -4
  128. package/lib/guard-markdown.js +76 -110
  129. package/lib/guard-message-id.js +5 -6
  130. package/lib/guard-mime.js +20 -99
  131. package/lib/guard-oauth.js +19 -73
  132. package/lib/guard-pdf.js +65 -72
  133. package/lib/guard-pop3-command.js +12 -13
  134. package/lib/guard-posture-chain.js +2 -1
  135. package/lib/guard-regex.js +24 -99
  136. package/lib/guard-saga-config.js +2 -1
  137. package/lib/guard-shell.js +22 -87
  138. package/lib/guard-smtp-command.js +8 -11
  139. package/lib/guard-sql.js +15 -13
  140. package/lib/guard-stream-args.js +2 -1
  141. package/lib/guard-svg.js +95 -140
  142. package/lib/guard-template.js +23 -80
  143. package/lib/guard-tenant-id.js +2 -1
  144. package/lib/guard-text.js +592 -0
  145. package/lib/guard-time.js +27 -95
  146. package/lib/guard-uuid.js +21 -93
  147. package/lib/guard-xml.js +76 -106
  148. package/lib/guard-yaml.js +24 -60
  149. package/lib/http-client-cache.js +8 -13
  150. package/lib/http-client-cookie-jar.js +2 -4
  151. package/lib/http-client.js +8 -21
  152. package/lib/http-message-signature.js +26 -8
  153. package/lib/i18n-messageformat.js +5 -7
  154. package/lib/i18n.js +83 -26
  155. package/lib/inbox.js +8 -8
  156. package/lib/incident-report.js +3 -21
  157. package/lib/ip-utils.js +49 -6
  158. package/lib/jobs.js +3 -2
  159. package/lib/keychain.js +6 -18
  160. package/lib/legal-hold.js +6 -15
  161. package/lib/log-stream-cloudwatch.js +44 -112
  162. package/lib/log-stream-otlp-grpc.js +28 -14
  163. package/lib/log-stream-otlp.js +16 -80
  164. package/lib/log-stream-syslog.js +6 -0
  165. package/lib/log-stream-webhook.js +20 -92
  166. package/lib/log.js +24 -2
  167. package/lib/mail-arc-sign.js +8 -3
  168. package/lib/mail-arf.js +3 -2
  169. package/lib/mail-auth.js +40 -66
  170. package/lib/mail-bimi.js +19 -39
  171. package/lib/mail-crypto-pgp.js +3 -2
  172. package/lib/mail-dav.js +8 -35
  173. package/lib/mail-deploy.js +6 -9
  174. package/lib/mail-dkim.js +19 -38
  175. package/lib/mail-greylist.js +15 -26
  176. package/lib/mail-helo.js +2 -3
  177. package/lib/mail-journal.js +2 -1
  178. package/lib/mail-mdn.js +4 -3
  179. package/lib/mail-rbl.js +5 -8
  180. package/lib/mail-scan.js +2 -2
  181. package/lib/mail-send-deliver.js +33 -20
  182. package/lib/mail-server-imap.js +32 -87
  183. package/lib/mail-server-jmap.js +35 -51
  184. package/lib/mail-server-managesieve.js +29 -83
  185. package/lib/mail-server-mx.js +33 -74
  186. package/lib/mail-server-net.js +177 -0
  187. package/lib/mail-server-pop3.js +30 -83
  188. package/lib/mail-server-rate-limit.js +30 -6
  189. package/lib/mail-server-submission.js +34 -73
  190. package/lib/mail-server-tls.js +89 -0
  191. package/lib/mail-spam-score.js +7 -8
  192. package/lib/mail-store.js +3 -2
  193. package/lib/mail.js +11 -11
  194. package/lib/markup-escape.js +31 -0
  195. package/lib/markup-tokenizer.js +54 -0
  196. package/lib/mcp-tool-registry.js +26 -23
  197. package/lib/mcp.js +1 -1
  198. package/lib/mdoc.js +11 -12
  199. package/lib/metrics.js +32 -30
  200. package/lib/middleware/age-gate.js +3 -23
  201. package/lib/middleware/api-encrypt.js +11 -22
  202. package/lib/middleware/assetlinks.js +2 -7
  203. package/lib/middleware/bearer-auth.js +2 -1
  204. package/lib/middleware/body-parser.js +79 -54
  205. package/lib/middleware/bot-disclose.js +7 -10
  206. package/lib/middleware/bot-guard.js +2 -10
  207. package/lib/middleware/csrf-protect.js +13 -2
  208. package/lib/middleware/daily-byte-quota.js +6 -26
  209. package/lib/middleware/deny-response.js +19 -1
  210. package/lib/middleware/fetch-metadata.js +7 -0
  211. package/lib/middleware/idempotency-key.js +4 -20
  212. package/lib/middleware/rate-limit.js +20 -28
  213. package/lib/middleware/scim-server.js +3 -2
  214. package/lib/middleware/security-headers.js +9 -1
  215. package/lib/middleware/security-txt.js +2 -6
  216. package/lib/middleware/tus-upload.js +12 -27
  217. package/lib/middleware/web-app-manifest.js +2 -7
  218. package/lib/migrations.js +4 -13
  219. package/lib/mime-parse.js +34 -17
  220. package/lib/module-loader.js +44 -0
  221. package/lib/money.js +105 -0
  222. package/lib/mtls-ca.js +116 -36
  223. package/lib/network-byte-quota.js +4 -18
  224. package/lib/network-dane.js +8 -6
  225. package/lib/network-dns-resolver.js +97 -6
  226. package/lib/network-dns.js +22 -26
  227. package/lib/network-dnssec.js +16 -16
  228. package/lib/network-heartbeat.js +6 -5
  229. package/lib/network-proxy.js +3 -7
  230. package/lib/network-smtp-policy.js +29 -41
  231. package/lib/network-tls.js +74 -55
  232. package/lib/network.js +2 -6
  233. package/lib/nis2-report.js +1 -11
  234. package/lib/nonce-store.js +81 -3
  235. package/lib/notify.js +7 -12
  236. package/lib/numeric-bounds.js +22 -8
  237. package/lib/object-store/azure-blob-bucket-ops.js +2 -6
  238. package/lib/object-store/azure-blob.js +5 -45
  239. package/lib/object-store/gcs.js +8 -71
  240. package/lib/object-store/http-put.js +1 -5
  241. package/lib/object-store/http-request.js +128 -0
  242. package/lib/object-store/sigv4-bucket-ops.js +2 -1
  243. package/lib/object-store/sigv4.js +9 -77
  244. package/lib/observability-otlp-exporter.js +9 -25
  245. package/lib/observability.js +95 -0
  246. package/lib/openapi-paths-builder.js +7 -3
  247. package/lib/otel-export.js +7 -10
  248. package/lib/outbox.js +43 -37
  249. package/lib/pagination.js +11 -15
  250. package/lib/parsers/safe-env.js +5 -4
  251. package/lib/parsers/safe-ini.js +10 -4
  252. package/lib/parsers/safe-toml.js +11 -9
  253. package/lib/parsers/safe-xml.js +2 -2
  254. package/lib/parsers/safe-yaml.js +7 -6
  255. package/lib/pick.js +63 -5
  256. package/lib/pipl-cn.js +69 -59
  257. package/lib/privacy-pass.js +8 -6
  258. package/lib/problem-details.js +2 -5
  259. package/lib/pubsub.js +4 -8
  260. package/lib/redact.js +2 -1
  261. package/lib/render.js +4 -2
  262. package/lib/request-helpers.js +133 -6
  263. package/lib/restore.js +5 -22
  264. package/lib/retention.js +3 -5
  265. package/lib/safe-async.js +457 -0
  266. package/lib/safe-buffer.js +137 -6
  267. package/lib/safe-decompress.js +2 -1
  268. package/lib/safe-dns.js +2 -1
  269. package/lib/safe-ical.js +12 -26
  270. package/lib/safe-json.js +7 -8
  271. package/lib/safe-jsonpath.js +6 -5
  272. package/lib/safe-mime.js +25 -29
  273. package/lib/safe-redirect.js +2 -5
  274. package/lib/safe-schema.js +7 -6
  275. package/lib/safe-sql.js +126 -0
  276. package/lib/safe-vcard.js +12 -26
  277. package/lib/sandbox-worker.js +9 -2
  278. package/lib/sandbox.js +3 -2
  279. package/lib/scheduler.js +5 -12
  280. package/lib/sec-cyber.js +82 -37
  281. package/lib/seeders.js +9 -21
  282. package/lib/self-update.js +92 -69
  283. package/lib/session-device-binding.js +112 -66
  284. package/lib/session.js +194 -6
  285. package/lib/sql.js +225 -78
  286. package/lib/sse.js +6 -10
  287. package/lib/static.js +136 -98
  288. package/lib/storage.js +4 -2
  289. package/lib/structured-fields.js +313 -17
  290. package/lib/tenant-quota.js +2 -20
  291. package/lib/tsa.js +11 -15
  292. package/lib/validate-opts.js +154 -8
  293. package/lib/vault/seal-pem-file.js +30 -48
  294. package/lib/vault-aad.js +3 -2
  295. package/lib/vc.js +2 -7
  296. package/lib/vex.js +35 -13
  297. package/lib/web-push-vapid.js +23 -20
  298. package/lib/webhook-dispatcher.js +706 -0
  299. package/lib/webhook.js +43 -18
  300. package/lib/websocket-channels.js +9 -7
  301. package/lib/websocket.js +7 -3
  302. package/lib/worm.js +2 -3
  303. package/lib/ws-client.js +8 -10
  304. package/package.json +1 -1
  305. package/sbom.cdx.json +6 -6
package/lib/guard-yaml.js CHANGED
@@ -69,7 +69,6 @@ var codepointClass = require("./codepoint-class");
69
69
  var lazyRequire = require("./lazy-require");
70
70
  var gateContract = require("./gate-contract");
71
71
  var C = require("./constants");
72
- var numericBounds = require("./numeric-bounds");
73
72
  var safeYamlLazy = lazyRequire(function () { return require("./parsers/safe-yaml"); });
74
73
  var { GuardYamlError } = require("./framework-error");
75
74
 
@@ -120,10 +119,7 @@ var PROFILES = Object.freeze({
120
119
  leadingZeroPolicy: "reject",
121
120
  duplicateKeyPolicy: "reject",
122
121
  mergeKeyPolicy: "reject",
123
- bidiPolicy: "reject",
124
- controlPolicy: "reject",
125
- nullBytePolicy: "reject",
126
- zeroWidthPolicy: "reject",
122
+ ...gateContract.CHAR_THREATS_REJECT_ALL,
127
123
  safeCoreTagsAllowed: false,
128
124
  maxBytes: C.BYTES.mib(2),
129
125
  maxDepth: 8, // recursion depth, not byte size
@@ -177,37 +173,13 @@ var PROFILES = Object.freeze({
177
173
  },
178
174
  });
179
175
 
180
- var DEFAULTS = Object.freeze(Object.assign({}, PROFILES["strict"], {
181
- mode: "enforce",
182
- maxRuntimeMs: C.TIME.seconds(10),
183
- }));
184
-
185
- var COMPLIANCE_POSTURES = Object.freeze({
186
- "hipaa": Object.assign({}, PROFILES["strict"], {
187
- forensicSnippetBytes: C.BYTES.bytes(256),
188
- }),
189
- "pci-dss": Object.assign({}, PROFILES["strict"], {
190
- forensicSnippetBytes: C.BYTES.bytes(256),
191
- }),
192
- "gdpr": Object.assign({}, PROFILES["balanced"], {
193
- forensicSnippetBytes: C.BYTES.bytes(128),
194
- }),
195
- "soc2": Object.assign({}, PROFILES["strict"], {
196
- forensicSnippetBytes: C.BYTES.bytes(512),
197
- }),
176
+ var DEFAULTS = gateContract.strictDefaults(PROFILES, {
177
+ maxRuntimeMs: C.TIME.seconds(10),
198
178
  });
199
179
 
200
- // ---- Helpers ----
180
+ var COMPLIANCE_POSTURES = gateContract.compliancePostures(PROFILES, { base: 256 });
201
181
 
202
- function _resolveOpts(opts) {
203
- return gateContract.resolveProfileAndPosture(opts, {
204
- profiles: PROFILES,
205
- compliancePostures: COMPLIANCE_POSTURES,
206
- defaults: DEFAULTS,
207
- errorClass: GuardYamlError,
208
- errCodePrefix: "yaml",
209
- });
210
- }
182
+ // ---- Helpers ----
211
183
 
212
184
  function _isDangerousTag(tag) {
213
185
  for (var i = 0; i < DANGEROUS_TAG_PREFIXES.length; i += 1) {
@@ -237,16 +209,9 @@ function _scanTags(text) {
237
209
  }
238
210
 
239
211
  function _detectIssues(input, opts) {
240
- var issues = [];
241
- if (typeof input !== "string") {
242
- return [{ kind: "bad-input", severity: "high",
243
- snippet: "input is not a string" }];
244
- }
245
- if (input.length > opts.maxBytes) {
246
- return [{ kind: "too-large", severity: "high", ruleId: "yaml.too-large",
247
- snippet: "input " + input.length +
248
- " bytes exceeds maxBytes " + opts.maxBytes }];
249
- }
212
+ var pre = gateContract.detectStringInput(input, opts, { name: "yaml", noun: "input", emptyMode: "skip", scanCodepoints: false, cap: { bytes: opts.maxBytes, kind: "too-large", snippet: function (byteLen, max) { return "input " + byteLen + " bytes exceeds maxBytes " + max; } } });
213
+ if (pre.done) return pre.issues;
214
+ var issues = pre.issues;
250
215
 
251
216
  // 1. Tag-injection scan.
252
217
  var tagHits = _scanTags(input);
@@ -496,21 +461,12 @@ function _detectDuplicateKeysYaml(text) {
496
461
  * rv.ok; // → false
497
462
  * rv.issues.some(function (i) { return i.kind === "dangerous-tag"; }); // → true
498
463
  */
499
- function validate(input, opts) {
500
- opts = _resolveOpts(opts);
501
- numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
502
- ["maxBytes", "maxDepth", "maxAnchors", "maxAliasDepth",
503
- "maxDocuments", "maxNodes", "maxScalarLength"],
504
- "guardYaml.validate", GuardYamlError, "yaml.bad-opt");
505
- if (typeof input !== "string") {
506
- return {
507
- ok: false,
508
- issues: [{ kind: "bad-input", severity: "high",
509
- snippet: "input is not a string" }],
510
- };
511
- }
512
- return gateContract.aggregateIssues(_detectIssues(input, opts));
513
- }
464
+ // validate is assembled by gateContract.defineGuard from `detect`
465
+ // (_detectIssues) below — `validate(input, opts) = aggregateIssues(detect(
466
+ // input, resolveOpts(opts)))`, with the positive-finite-int opt caps declared
467
+ // via `intOpts`. A non-string input refuses through `_detectIssues`'s own
468
+ // bad-input branch, so the result shape is unchanged. The @primitive block
469
+ // above documents the resulting public ABI.
514
470
 
515
471
  /**
516
472
  * @primitive b.guardYaml.parse
@@ -548,7 +504,13 @@ function validate(input, opts) {
548
504
  * safe.age; // → 30
549
505
  */
550
506
  function parse(input, opts) {
551
- opts = _resolveOpts(opts);
507
+ opts = gateContract.resolveProfileAndPosture(opts, {
508
+ profiles: PROFILES,
509
+ compliancePostures: COMPLIANCE_POSTURES,
510
+ defaults: DEFAULTS,
511
+ errorClass: GuardYamlError,
512
+ errCodePrefix: "yaml",
513
+ });
552
514
  if (typeof input !== "string") {
553
515
  throw _err("yaml.bad-input", "parse requires string input");
554
516
  }
@@ -605,7 +567,9 @@ module.exports = gateContract.defineGuard({
605
567
  mimeTypes: ["application/yaml", "application/x-yaml", "text/yaml", "text/x-yaml"],
606
568
  extensions: [".yml", ".yaml"],
607
569
  integrationFixtures: INTEGRATION_FIXTURES,
608
- validate: validate,
570
+ detect: _detectIssues,
571
+ intOpts: ["maxBytes", "maxDepth", "maxAnchors", "maxAliasDepth",
572
+ "maxDocuments", "maxNodes", "maxScalarLength"],
609
573
  extra: {
610
574
  parse: parse,
611
575
  DANGEROUS_TAG_PREFIXES: DANGEROUS_TAG_PREFIXES,
@@ -47,6 +47,7 @@ var canonicalJson = require("./canonical-json");
47
47
  var safeUrl = require("./safe-url");
48
48
  var structuredFields = require("./structured-fields");
49
49
  var validateOpts = require("./validate-opts");
50
+ var numericBounds = require("./numeric-bounds");
50
51
  var { HttpClientError } = require("./framework-error");
51
52
 
52
53
  // ---- Tunables ----------------------------------------------------------
@@ -107,7 +108,9 @@ function _parseCacheControl(value) {
107
108
  else { k = p.slice(0, eq).trim(); v = p.slice(eq + 1).trim(); }
108
109
  // Strip surrounding quotes from value.
109
110
  if (v.length >= 2 && v.charAt(0) === '"' && v.charAt(v.length - 1) === '"') {
110
- v = v.slice(1, v.length - 1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
111
+ // Single-pass RFC 8941 unescape (chained .replace() mis-decodes
112
+ // an escaped backslash adjacent to another escape).
113
+ v = structuredFields.unescapeSfStringBody(v.slice(1, v.length - 1));
111
114
  }
112
115
  out[k.toLowerCase()] = v;
113
116
  }
@@ -391,18 +394,10 @@ function memoryStore(opts) {
391
394
  if (typeof opts !== "object") {
392
395
  throw _hcErr("httpclient/cache-bad-opts", "memoryStore: opts must be an object");
393
396
  }
394
- if (opts.maxBytes !== undefined &&
395
- (typeof opts.maxBytes !== "number" || !isFinite(opts.maxBytes) ||
396
- opts.maxBytes <= 0 || Math.floor(opts.maxBytes) !== opts.maxBytes)) {
397
- throw _hcErr("httpclient/cache-bad-opts",
398
- "memoryStore: maxBytes must be a positive integer");
399
- }
400
- if (opts.maxEntries !== undefined &&
401
- (typeof opts.maxEntries !== "number" || !isFinite(opts.maxEntries) ||
402
- opts.maxEntries <= 0 || Math.floor(opts.maxEntries) !== opts.maxEntries)) {
403
- throw _hcErr("httpclient/cache-bad-opts",
404
- "memoryStore: maxEntries must be a positive integer");
405
- }
397
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.maxBytes,
398
+ "memoryStore: maxBytes", HttpClientError, "httpclient/cache-bad-opts", { permanent: true });
399
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.maxEntries,
400
+ "memoryStore: maxEntries", HttpClientError, "httpclient/cache-bad-opts", { permanent: true });
406
401
  if (opts.evictionPolicy !== undefined && opts.evictionPolicy !== "lru") {
407
402
  throw _hcErr("httpclient/cache-bad-opts",
408
403
  "memoryStore: evictionPolicy must be 'lru' (got " + JSON.stringify(opts.evictionPolicy) + ")");
@@ -170,10 +170,8 @@ function create(opts) {
170
170
  }
171
171
  var vault = opts.vault || null;
172
172
  if (persist === "vault") {
173
- if (!vault || typeof vault.seal !== "function" || typeof vault.unseal !== "function") {
174
- throw _err("BAD_OPT",
175
- "cookieJar.create: persist: 'vault' requires opts.vault with seal/unseal (pass b.vault)");
176
- }
173
+ validateOpts.requireMethods(vault, ["seal", "unseal"],
174
+ "cookieJar.create: persist: 'vault' opts.vault (pass b.vault)", CookieJarError, "BAD_OPT");
177
175
  }
178
176
  var filePath = null;
179
177
  if (persist === "file") {
@@ -53,6 +53,7 @@ var safeBuffer = require("./safe-buffer");
53
53
  var safeUrl = require("./safe-url");
54
54
  var ssrfGuard = require("./ssrf-guard");
55
55
  var networkProxy = require("./network-proxy");
56
+ var numericBounds = require("./numeric-bounds");
56
57
  var validateOpts = require("./validate-opts");
57
58
  var { FrameworkError, HttpClientError } = require("./framework-error");
58
59
 
@@ -165,15 +166,9 @@ function configurePool(opts) {
165
166
  "'. Allowed: " + allowed.join(", "));
166
167
  }
167
168
  }
168
- function _requirePositiveInt(name, value) {
169
- if (typeof value !== "number" || !isFinite(value) || value <= 0 || Math.floor(value) !== value) {
170
- throw new Error("httpClient.configurePool: " + name +
171
- " must be a positive integer, got " + JSON.stringify(value));
172
- }
173
- }
174
- if (opts.maxSockets !== undefined) _requirePositiveInt("maxSockets", opts.maxSockets);
175
- if (opts.maxFreeSockets !== undefined) _requirePositiveInt("maxFreeSockets", opts.maxFreeSockets);
176
- if (opts.keepAliveMsecs !== undefined) _requirePositiveInt("keepAliveMsecs", opts.keepAliveMsecs);
169
+ numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
170
+ ["maxSockets", "maxFreeSockets", "keepAliveMsecs"], "httpClient.configurePool",
171
+ HttpClientError, "httpclient/bad-opts", { permanent: true });
177
172
  if (opts.keepAlive !== undefined && typeof opts.keepAlive !== "boolean") {
178
173
  throw new Error("httpClient.configurePool: keepAlive must be a boolean");
179
174
  }
@@ -1825,12 +1820,8 @@ function _validateDownloadOpts(opts) {
1825
1820
  }
1826
1821
  validateOpts.optionalPositiveFinite(opts.timeoutMs, "downloadStream: timeoutMs",
1827
1822
  HttpClientError, "httpclient/bad-opts");
1828
- if (opts.maxBytes !== undefined &&
1829
- (typeof opts.maxBytes !== "number" || !isFinite(opts.maxBytes) || opts.maxBytes <= 0 ||
1830
- Math.floor(opts.maxBytes) !== opts.maxBytes)) {
1831
- throw _hcErr("httpclient/bad-opts",
1832
- "downloadStream: maxBytes must be a positive finite integer");
1833
- }
1823
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.maxBytes,
1824
+ "downloadStream: maxBytes", HttpClientError, "httpclient/bad-opts", { permanent: true });
1834
1825
  }
1835
1826
 
1836
1827
  function _emitAudit(opts, action, outcome, metadata) {
@@ -2050,12 +2041,8 @@ function _validateUploadOpts(opts) {
2050
2041
  }
2051
2042
  validateOpts.optionalPositiveFinite(opts.timeoutMs, "uploadMultipartStream: timeoutMs",
2052
2043
  HttpClientError, "httpclient/bad-opts");
2053
- if (opts.maxBytes !== undefined &&
2054
- (typeof opts.maxBytes !== "number" || !isFinite(opts.maxBytes) || opts.maxBytes <= 0 ||
2055
- Math.floor(opts.maxBytes) !== opts.maxBytes)) {
2056
- throw _hcErr("httpclient/bad-opts",
2057
- "uploadMultipartStream: maxBytes must be a positive finite integer");
2058
- }
2044
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.maxBytes,
2045
+ "uploadMultipartStream: maxBytes", HttpClientError, "httpclient/bad-opts", { permanent: true });
2059
2046
  }
2060
2047
 
2061
2048
  /**
@@ -60,6 +60,7 @@
60
60
  */
61
61
 
62
62
  var nodeCrypto = require("node:crypto");
63
+ var bCrypto = require("./crypto");
63
64
  var safeUrl = require("./safe-url");
64
65
  var safeBuffer = require("./safe-buffer");
65
66
  var C = require("./constants");
@@ -406,7 +407,9 @@ function _parseSignatureInput(headerValue) {
406
407
  throw _err("BAD_HEADER",
407
408
  "httpSig: Signature-Input: unterminated quoted token");
408
409
  }
409
- var bareName = coveredRaw.slice(qStart, qEnd).replace(/\\\\/g, "\\").replace(/\\"/g, "\"");
410
+ // Single-pass RFC 8941 §3.3.3 unescape — NOT two chained .replace() passes,
411
+ // which mis-decode an escaped backslash adjacent to another escape.
412
+ var bareName = structuredFields.unescapeSfStringBody(coveredRaw.slice(qStart, qEnd));
410
413
  i2 = qEnd + 1;
411
414
  // Optional ;param=value;param=... suffix immediately following.
412
415
  var suffixStart = i2;
@@ -525,13 +528,28 @@ function verify(msg, opts) {
525
528
  if (!presented) {
526
529
  return { valid: false, reason: "content-digest-header-missing" };
527
530
  }
528
- var actual = contentDigest(m.body);
529
- // RFC 9530 allows multiple algorithms in one header (sha-512=...,
530
- // sha-256=...). For SHA3-512 specifically exact substring match
531
- // against the presented header. For peer-supplied SHA-512 / SHA-256
532
- // identifiers the operator is responsible for re-validating; this
533
- // primitive only auto-checks SHA3-512.
534
- if (presented.indexOf(actual.replace(/^sha3-512=/, "sha3-512=")) === -1) {
531
+ // contentDigest() returns the canonical structured-field form
532
+ // `sha3-512=:<base64>:`. RFC 9530 permits a multi-member header
533
+ // (e.g. `sha-256=:...:, sha3-512=:...:`); split on top-level commas and
534
+ // match the sha3-512 member EXACTLY, in constant time, rather than by an
535
+ // unanchored substring scan that could spuriously match the digest text
536
+ // buried inside another member's value or parameters. Peer-supplied
537
+ // sha-512 / sha-256 identifiers stay the operator's responsibility.
538
+ var expectedDigest = contentDigest(m.body); // "sha3-512=:<b64>:"
539
+ var matchedDigest = false;
540
+ var digestMembers = structuredFields.splitTopLevel(presented, ",");
541
+ for (var di = 0; di < digestMembers.length; di++) {
542
+ var member = digestMembers[di].trim();
543
+ var deq = member.indexOf("=");
544
+ if (deq < 1) continue;
545
+ var dkv = structuredFields.parseKeyValuePiece(member);
546
+ if (dkv.key !== "sha3-512") continue;
547
+ var memberCanonical = "sha3-512=" + dkv.value.trim();
548
+ // crypto.timingSafeEqual is the length-tolerant constant-time wrapper
549
+ // (returns false for unequal lengths without leaking via a length branch).
550
+ if (bCrypto.timingSafeEqual(memberCanonical, expectedDigest)) { matchedDigest = true; break; }
551
+ }
552
+ if (!matchedDigest) {
535
553
  return { valid: false, reason: "content-digest-mismatch" };
536
554
  }
537
555
  }
@@ -45,6 +45,7 @@
45
45
  * shaped JSON entries continue to work unchanged.
46
46
  */
47
47
  var lazyRequire = require("./lazy-require");
48
+ var boundedMap = require("./bounded-map");
48
49
  var { defineClass } = require("./framework-error");
49
50
 
50
51
  var I18nMessageFormatError = defineClass("I18nMessageFormatError",
@@ -314,12 +315,9 @@ function _skipWs(state) {
314
315
  var _pluralRulesCache = new Map();
315
316
  function _pluralRules(locale, type) {
316
317
  var key = locale + "\x1f" + type;
317
- var pr = _pluralRulesCache.get(key);
318
- if (!pr) {
319
- pr = new Intl.PluralRules(locale, { type: type });
320
- _pluralRulesCache.set(key, pr);
321
- }
322
- return pr;
318
+ return boundedMap.getOrInsert(_pluralRulesCache, key, function () {
319
+ return new Intl.PluralRules(locale, { type: type });
320
+ });
323
321
  }
324
322
 
325
323
  function format(template, vars, locale) {
@@ -382,7 +380,7 @@ function looksLikeMessageFormat(template) {
382
380
  if (typeof template !== "string") return false;
383
381
  // Cheap structural check — full-syntax detection comes from parse()
384
382
  // throwing if it isn't valid MessageFormat.
385
- return /\{[^}]+,\s*(plural|select|selectordinal)\b/.test(template);
383
+ return /\{[^{}]+,\s*(plural|select|selectordinal)\b/.test(template);
386
384
  }
387
385
 
388
386
  module.exports = {
package/lib/i18n.js CHANGED
@@ -64,7 +64,7 @@ var requestHelpers = require("./request-helpers");
64
64
  var safeJson = require("./safe-json");
65
65
  var validateOpts = require("./validate-opts");
66
66
  var { I18nError } = require("./framework-error");
67
- var { boundedMap } = require("./bounded-map");
67
+ var { boundedMap, getOrInsert } = require("./bounded-map");
68
68
 
69
69
  // Per-instance formatter caches are keyed on (locale, JSON.stringify
70
70
  // formatOpts). A fixed `locales` set bounds the locale axis, but operators
@@ -133,6 +133,80 @@ function _validateLocaleArray(name, value) {
133
133
  }
134
134
  }
135
135
 
136
+ // Pure resolution-chain builder — the subtag-strip walk shared by the
137
+ // instance lookup and the public localeChain primitive. Start at the requested
138
+ // locale and strip subtag suffixes right-to-left ("pt-BR" -> "pt"); subtag
139
+ // stripping always applies (same language, less specific). Cross-locale
140
+ // fallback (to fallbackLocale, then defaultLocale) fires only when
141
+ // fallbackLocale is non-null — fallbackLocale: null is strict "this locale or
142
+ // miss". No duplicate baseline.
143
+ function _buildLocaleChain(locale, fallbackLocale, defaultLocale) {
144
+ var chain = [];
145
+ var current = locale;
146
+ while (current && chain.indexOf(current) === -1) {
147
+ chain.push(current);
148
+ var dash = current.lastIndexOf("-");
149
+ if (dash === -1) break;
150
+ current = current.slice(0, dash);
151
+ }
152
+ if (fallbackLocale === null) return chain;
153
+ if (chain.indexOf(fallbackLocale) === -1) chain.push(fallbackLocale);
154
+ if (chain.indexOf(defaultLocale) === -1) chain.push(defaultLocale);
155
+ return chain;
156
+ }
157
+
158
+ /**
159
+ * @primitive b.i18n.localeChain
160
+ * @signature b.i18n.localeChain(locale, opts)
161
+ * @since 0.15.13
162
+ * @status stable
163
+ * @related b.i18n.create
164
+ *
165
+ * Resolve a requested BCP 47 locale to its ordered fallback chain — the same
166
+ * subtag-strip logic the file-backed `t()` lookup uses, surfaced as a pure,
167
+ * instance-independent function so a data-backed consumer (translations in a
168
+ * database row, a CMS page, a CDN-cached asset) can drive its own per-`(resource,
169
+ * locale, field)` lookups on the framework's BCP 47 fallback instead of
170
+ * re-deriving subtag stripping by hand.
171
+ *
172
+ * Pure: no file loading, no message lookup. The chain is the requested locale,
173
+ * then each subtag-stripped parent ("fr-CA" -> "fr"), then `fallbackLocale`,
174
+ * then `defaultLocale`, de-duplicated. `fallbackLocale: null` gives strict
175
+ * "this locale or its parents only" (no cross-locale jump).
176
+ *
177
+ * @opts
178
+ * defaultLocale: string, // BCP 47; required; the final baseline
179
+ * fallbackLocale: string | null, // null = strict; omitted = defaultLocale
180
+ * locales: string[], // optional configured set; when given, defaultLocale + fallbackLocale must be members
181
+ *
182
+ * @example
183
+ * b.i18n.localeChain("fr-CA", { defaultLocale: "en", fallbackLocale: "en" });
184
+ * // → ["fr-CA", "fr", "en"]
185
+ * b.i18n.localeChain("zh-Hant-TW", { defaultLocale: "en", fallbackLocale: null });
186
+ * // → ["zh-Hant-TW", "zh-Hant", "zh"] (strict — no jump to en)
187
+ */
188
+ function localeChain(locale, opts) {
189
+ opts = opts || {};
190
+ _validateLocale("i18n.localeChain: locale", locale);
191
+ _validateLocale("i18n.localeChain: defaultLocale", opts.defaultLocale);
192
+ var defaultLocale = opts.defaultLocale;
193
+ var fallbackLocale = (opts.fallbackLocale === null) ? null
194
+ : ((opts.fallbackLocale === undefined) ? defaultLocale : opts.fallbackLocale);
195
+ if (fallbackLocale !== null) _validateLocale("i18n.localeChain: fallbackLocale", fallbackLocale);
196
+ if (opts.locales !== undefined) {
197
+ _validateLocaleArray("i18n.localeChain: locales", opts.locales);
198
+ if (opts.locales.indexOf(defaultLocale) === -1) {
199
+ throw _err("BAD_OPT", "i18n.localeChain: defaultLocale '" + defaultLocale +
200
+ "' must be one of the configured locales");
201
+ }
202
+ if (fallbackLocale !== null && opts.locales.indexOf(fallbackLocale) === -1) {
203
+ throw _err("BAD_OPT", "i18n.localeChain: fallbackLocale '" + fallbackLocale +
204
+ "' must be one of the configured locales");
205
+ }
206
+ }
207
+ return _buildLocaleChain(locale, fallbackLocale, defaultLocale);
208
+ }
209
+
136
210
  function _validateInterpolation(value) {
137
211
  if (value === undefined) return DEFAULTS.interpolation;
138
212
  if (typeof value !== "object" || value === null) {
@@ -366,13 +440,11 @@ function _makeFormatterCache(make, kind, emitObs) {
366
440
  return function getFormatter(locale, formatOpts) {
367
441
  var optsKey = formatOpts ? JSON.stringify(formatOpts) : "";
368
442
  var cacheKey = locale + "\x1f" + optsKey;
369
- var f = cache.get(cacheKey);
370
- if (!f) {
371
- f = make(locale, formatOpts);
372
- cache.set(cacheKey, f);
443
+ return getOrInsert(cache, cacheKey, function () {
444
+ var f = make(locale, formatOpts);
373
445
  emitObs("i18n.format.created", { kind: kind, locale: locale });
374
- }
375
- return f;
446
+ return f;
447
+ });
376
448
  };
377
449
  }
378
450
 
@@ -572,25 +644,9 @@ function create(opts) {
572
644
  }
573
645
 
574
646
  function _localeChain(locale) {
575
- // Build the resolution chain. Start with the requested locale and
576
- // strip subtag suffixes (`pt-BR` `pt`); subtag stripping always
577
- // applies because it's "same language, less specific" rather than a
578
- // cross-locale jump. Cross-locale fallback (to fallbackLocale, then
579
- // defaultLocale) only fires when fallbackLocale is non-null —
580
- // operators who set fallbackLocale: null get strict "this locale or
581
- // miss" semantics.
582
- var chain = [];
583
- var current = locale;
584
- while (current && chain.indexOf(current) === -1) {
585
- chain.push(current);
586
- var dash = current.lastIndexOf("-");
587
- if (dash === -1) break;
588
- current = current.slice(0, dash);
589
- }
590
- if (fallbackLocale === null) return chain;
591
- if (chain.indexOf(fallbackLocale) === -1) chain.push(fallbackLocale);
592
- if (chain.indexOf(defaultLocale) === -1) chain.push(defaultLocale);
593
- return chain;
647
+ // Delegates to the module-level pure builder; fallbackLocale /
648
+ // defaultLocale are this instance's resolved config.
649
+ return _buildLocaleChain(locale, fallbackLocale, defaultLocale);
594
650
  }
595
651
 
596
652
  function _lookupRaw(key, locale) {
@@ -932,6 +988,7 @@ var messageFormat = require("./i18n-messageformat");
932
988
 
933
989
  module.exports = {
934
990
  create: create,
991
+ localeChain: localeChain,
935
992
  messageFormat: messageFormat,
936
993
  I18nError: I18nError,
937
994
  DEFAULTS: DEFAULTS,
package/lib/inbox.js CHANGED
@@ -43,9 +43,11 @@
43
43
  */
44
44
 
45
45
  var C = require("./constants");
46
+ var codepointClass = require("./codepoint-class");
46
47
  var lazyRequire = require("./lazy-require");
47
48
  var safeJson = require("./safe-json");
48
49
  var safeSql = require("./safe-sql");
50
+ var safeBuffer = require("./safe-buffer");
49
51
  var sql = require("./sql");
50
52
  var validateOpts = require("./validate-opts");
51
53
  var { defineClass } = require("./framework-error");
@@ -204,13 +206,11 @@ function create(opts) {
204
206
  }
205
207
 
206
208
  function _rejectControlChars(value, label, field) {
207
- for (var i = 0; i < value.length; i += 1) {
208
- var code = value.charCodeAt(i);
209
- if (code === 0 || (code < 32 && code !== 9) || code === 127) { // ASCII control codepoints (NUL + C0 + DEL); allow tab
210
- throw new InboxError("inbox/bad-receive",
211
- label + ": " + field + " contains control character at index " + i +
212
- " (codepoint " + code + ")");
213
- }
209
+ var _off = codepointClass.firstControlCharOffset(value); // allow tab (RFC 5322 folding WS)
210
+ if (_off !== -1) {
211
+ throw new InboxError("inbox/bad-receive",
212
+ label + ": " + field + " contains control character at index " + _off +
213
+ " (codepoint " + value.charCodeAt(_off) + ")");
214
214
  }
215
215
  }
216
216
 
@@ -223,7 +223,7 @@ function create(opts) {
223
223
  var metaJson = null;
224
224
  if (receiveOpts.metadata != null) {
225
225
  var serialized = safeJson.stringify(receiveOpts.metadata);
226
- if (serialized.length > maxPayloadBytes) {
226
+ if (safeBuffer.byteLengthOf(serialized) > maxPayloadBytes) {
227
227
  throw new InboxError("inbox/bad-receive",
228
228
  "recordReceive: metadata serialized exceeds maxPayloadBytes (" +
229
229
  maxPayloadBytes + " bytes)");
@@ -138,7 +138,6 @@ function create(opts) {
138
138
  "audit", "persist", "onStage", "deadlines", "now",
139
139
  ], "incident.report");
140
140
 
141
- var auditOn = opts.audit !== false;
142
141
  var persist = typeof opts.persist === "function" ? opts.persist : null;
143
142
  var onStage = typeof opts.onStage === "function" ? opts.onStage : null;
144
143
  var deadlinesOverride = opts.deadlines || null;
@@ -150,20 +149,8 @@ function create(opts) {
150
149
  var incidents = new Map();
151
150
  var seq = 0;
152
151
 
153
- function _emitAudit(action, outcome, metadata) {
154
- if (!auditOn) return;
155
- try {
156
- audit().safeEmit({
157
- action: "incident.report." + action,
158
- outcome: outcome,
159
- metadata: metadata || {},
160
- });
161
- } catch (_e) { /* drop-silent */ }
162
- }
163
- function _emitMetric(verb, n, labels) {
164
- try { observability().safeEvent("incident.report." + verb, n || 1, labels || {}); }
165
- catch (_e) { /* drop-silent */ }
166
- }
152
+ var _emitAudit = audit().namespaced("incident.report", opts.audit);
153
+ var _emitMetric = observability().namespaced("incident.report");
167
154
 
168
155
  function _genIncidentId(regime, detectedAt) {
169
156
  seq += 1;
@@ -339,12 +326,7 @@ function createDeadlineClock(opts) {
339
326
  var tracked = new Map(); // incidentId -> { detectedAt, dueBy, regime, acked, fired }
340
327
  var timer = null;
341
328
 
342
- function _emit(action, outcome, metadata) {
343
- if (!auditOn) return;
344
- try {
345
- audit().safeEmit({ action: "incident.report.clock." + action, outcome: outcome, metadata: metadata || {} });
346
- } catch (_e) { /* drop-silent — by design */ }
347
- }
329
+ var _emit = audit().namespaced("incident.report.clock", auditOn);
348
330
  function _notify(payload) {
349
331
  if (!notify) return;
350
332
  try {
package/lib/ip-utils.js CHANGED
@@ -82,9 +82,9 @@ function expandIpv6Groups(ip) {
82
82
  // Loose IPv4 textual-form check — for primitives that need to
83
83
  // classify a string as "looks like dotted-quad IPv4" but don't need
84
84
  // the strict per-octet bound check (callers that DO need octet
85
- // bounds use the strict form in `mail-rbl.js` etc.). Shared so
86
- // `lib/mail.js`, `lib/mail-helo.js`, and `lib/redis-client.js`
87
- // don't drift on the same shape.
85
+ // bounds use `isIPv4` below). Shared so `lib/mail.js`,
86
+ // `lib/mail-helo.js`, and `lib/redis-client.js` don't drift on the
87
+ // same shape.
88
88
  //
89
89
  // isIPv4Shape("1.2.3.4") → true
90
90
  // isIPv4Shape("999.0.0.0") → true (shape only; octets unbounded)
@@ -95,8 +95,51 @@ function isIPv4Shape(s) {
95
95
  return typeof s === "string" && IPV4_SHAPE_RE.test(s);
96
96
  }
97
97
 
98
+ // Strict RFC 791 dotted-quad IPv4: four 0-255 octets separated by `.`.
99
+ // The single source for textual IPv4 validation across the framework
100
+ // — `b.mail.greylist` / `b.mail.rbl` CIDR fingerprinting, `b.mail.helo`
101
+ // RFC 5321 §4.1.3 address-literals, `b.guardDomain` IPv4-as-domain
102
+ // detection, and `b.safeSchema` `.ip()`. Hand-rolling the per-octet
103
+ // alternation had drifted into several spellings; this is canonical.
104
+ //
105
+ // isIPv4("203.0.113.42") → true
106
+ // isIPv4("256.0.0.1") → false (octet > 255)
107
+ // isIPv4("1.2.3") → false (three octets)
108
+ var IPV4_RE = /^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/; // allow:regex-no-length-cap — anchored + per-octet repeat-cap
109
+ function isIPv4(s) {
110
+ return typeof s === "string" && IPV4_RE.test(s);
111
+ }
112
+
113
+ // RFC 5321 §4.1.3 IPv4 address-literal `[1.2.3.4]`. Capture group 1 is
114
+ // the inner dotted-quad; `b.mail.helo` matches a claimed literal
115
+ // against the connection IP. Same per-octet bound as `IPV4_RE`.
116
+ var IPV4_ADDR_LITERAL_RE = /^\[((?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})\]$/; // allow:regex-no-length-cap — anchored + per-octet repeat-cap
117
+
118
+ // Loose IPv6 textual pre-filter: hex digits + colons only, bounded to
119
+ // the RFC 4291 §2.2 max no-IPv4-tail textual length (8 groups × 4 hex
120
+ // + 7 colons = 39). A cheap fail-fast before the full `expandIpv6Hex`
121
+ // parse — NOT a complete validator; callers follow with
122
+ // `expandIpv6Hex`. A hex-colon string longer than 39 chars is never a
123
+ // valid IPv6 textual form, so the bound only rejects garbage.
124
+ //
125
+ // looksLikeIPv6Hex("2001:db8::1") → true
126
+ // looksLikeIPv6Hex("::1") → true
127
+ // looksLikeIPv6Hex("::ffff:1.2.3.4")→ false (dotted tail — use expandIpv6Hex)
128
+ // looksLikeIPv6Hex("nothex") → false
129
+ var IPV6_TEXT_MAX_LEN = 39; // RFC 4291 §2.2 max no-IPv4-tail textual length
130
+ var IPV6_HEX_RE = /^[0-9a-fA-F:]+$/; // allow:regex-no-length-cap — length-bounded by IPV6_TEXT_MAX_LEN in looksLikeIPv6Hex
131
+ function looksLikeIPv6Hex(s) {
132
+ return typeof s === "string" && s.length <= IPV6_TEXT_MAX_LEN && IPV6_HEX_RE.test(s);
133
+ }
134
+
98
135
  module.exports = {
99
- expandIpv6Hex: expandIpv6Hex,
100
- expandIpv6Groups: expandIpv6Groups,
101
- isIPv4Shape: isIPv4Shape,
136
+ expandIpv6Hex: expandIpv6Hex,
137
+ expandIpv6Groups: expandIpv6Groups,
138
+ isIPv4Shape: isIPv4Shape,
139
+ isIPv4: isIPv4,
140
+ IPV4_RE: IPV4_RE,
141
+ IPV4_ADDR_LITERAL_RE: IPV4_ADDR_LITERAL_RE,
142
+ looksLikeIPv6Hex: looksLikeIPv6Hex,
143
+ IPV6_HEX_RE: IPV6_HEX_RE,
144
+ IPV6_TEXT_MAX_LEN: IPV6_TEXT_MAX_LEN,
102
145
  };