@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/storage.js CHANGED
@@ -38,10 +38,12 @@
38
38
  * Filesystem-and-cloud-backed object storage with sealed per-file encryption keys, classification routing, and residency enforcement.
39
39
  */
40
40
  var C = require("./constants");
41
+ var safeBuffer = require("./safe-buffer");
41
42
  var { generateBytes, encryptPacked, decryptPacked } = require("./crypto");
42
43
  var objectStore = require("./object-store");
43
44
  var lazyRequire = require("./lazy-require");
44
45
  var numericBounds = require("./numeric-bounds");
46
+ var codepointClass = require("./codepoint-class");
45
47
  var canonicalJson = require("./canonical-json");
46
48
  var { StorageError } = require("./framework-error");
47
49
 
@@ -950,7 +952,7 @@ function _validateAssemblyId(id) {
950
952
  for (var i = 0; i < id.length; i += 1) {
951
953
  var c = id.charCodeAt(i);
952
954
  // Refuse: C0 (0x00-0x1F), DEL (0x7F), slash, backslash, dot-prefix
953
- if (c < 0x20 || c === 0x2F || c === 0x5C || c === 0x7F) {
955
+ if (codepointClass.isForbiddenControlChar(c, { forbidTab: true }) || c === 0x2F || c === 0x5C) {
954
956
  throw _err("INVALID_ARGUMENT",
955
957
  "chunkScratch: assemblyId carries forbidden character at byte " + i, true);
956
958
  }
@@ -1083,7 +1085,7 @@ function chunkScratch(opts) {
1083
1085
  if (!Buffer.isBuffer(args.data)) {
1084
1086
  throw _err("INVALID_ARGUMENT", "chunkScratch.saveChunk: data must be a Buffer", true);
1085
1087
  }
1086
- if (args.data.length > maxChunkBytes) {
1088
+ if (safeBuffer.byteLengthOf(args.data) > maxChunkBytes) {
1087
1089
  throw _err("INVALID_ARGUMENT",
1088
1090
  "chunkScratch.saveChunk: chunk exceeds maxChunkBytes (" + args.data.length + " > " + maxChunkBytes + ")", true);
1089
1091
  }
@@ -70,6 +70,7 @@
70
70
  // node:util is a builtin (no lib require cycle) — used for strict UTF-8
71
71
  // validation of RFC 9651 Display Strings.
72
72
  var TextDecoder = require("node:util").TextDecoder;
73
+ var codepointClass = require("./codepoint-class");
73
74
 
74
75
  function splitTopLevel(s, sep) {
75
76
  if (typeof s !== "string") return [];
@@ -103,6 +104,261 @@ function splitTopLevel(s, sep) {
103
104
  return out;
104
105
  }
105
106
 
107
+ /**
108
+ * @primitive b.structuredFields.splitUnquoted
109
+ * @signature b.structuredFields.splitUnquoted(s, sep)
110
+ * @since 0.15.13
111
+ * @status stable
112
+ * @related b.structuredFields.splitTopLevel
113
+ *
114
+ * Split `s` on every `sep` that falls OUTSIDE a `"..."` quoted run — the
115
+ * iCalendar / vCard variant of the quote-aware splitter. A `"` toggles the
116
+ * quoted state and there is NO backslash escaping of the quote (RFC 5545
117
+ * 3.1.1 / RFC 6350 3.3 QSAFE-CHAR excludes DQUOTE, so a `"` always opens or
118
+ * closes a run). This is deliberately simpler than `splitTopLevel`, which
119
+ * honours HTTP structured-field backslash-quote escapes; the two are NOT
120
+ * interchangeable. Accepts any single-character separator.
121
+ *
122
+ * @example
123
+ * b.structuredFields.splitUnquoted('a;b="x;y";c', ";");
124
+ * // returns ['a', 'b="x;y"', 'c']
125
+ */
126
+ function splitUnquoted(s, sep) {
127
+ var out = [];
128
+ var inQ = false;
129
+ var start = 0;
130
+ for (var i = 0; i < s.length; i++) {
131
+ var c = s.charAt(i);
132
+ if (c === '"') { inQ = !inQ; continue; }
133
+ if (c === sep && !inQ) {
134
+ out.push(s.slice(start, i));
135
+ start = i + 1;
136
+ }
137
+ }
138
+ out.push(s.slice(start));
139
+ return out;
140
+ }
141
+
142
+ /**
143
+ * @primitive b.structuredFields.stripDoubleQuotes
144
+ * @signature b.structuredFields.stripDoubleQuotes(s)
145
+ * @since 0.15.13
146
+ * @status stable
147
+ * @related b.structuredFields.splitUnquoted, b.structuredFields.unquoteSfString
148
+ *
149
+ * Strip ONE layer of surrounding `"` from `s` when both ends are a double
150
+ * quote (length ≥ 2), otherwise return `s` unchanged. The plain DQUOTE
151
+ * unwrap used by the iCal / vCard parsers for a quoted parameter value —
152
+ * no backslash-escape processing (RFC 5545 3.1 / RFC 6350 5: a quoted
153
+ * param value is QSAFE-CHAR, which excludes DQUOTE, so there is nothing to
154
+ * unescape). Distinct from `unquoteSfString`, which decodes HTTP
155
+ * structured-field `\"` / `\\` escapes.
156
+ *
157
+ * @example
158
+ * b.structuredFields.stripDoubleQuotes('"a;b"');
159
+ * // returns 'a;b'
160
+ *
161
+ * b.structuredFields.stripDoubleQuotes('plain');
162
+ * // returns 'plain'
163
+ */
164
+ function stripDoubleQuotes(s) {
165
+ if (s.length >= 2 && s.charAt(0) === '"' && s.charAt(s.length - 1) === '"') {
166
+ return s.slice(1, -1);
167
+ }
168
+ return s;
169
+ }
170
+
171
+ /**
172
+ * @primitive b.structuredFields.unfoldHeaderContinuations
173
+ * @signature b.structuredFields.unfoldHeaderContinuations(value)
174
+ * @since 0.15.13
175
+ * @status stable
176
+ * @related b.structuredFields.parseTagList
177
+ *
178
+ * Collapse RFC 5322 folding whitespace — a CRLF (or bare LF) followed by one
179
+ * or more spaces/tabs — back to a single space, reversing the line folding a
180
+ * header value may carry in transit. Used before parsing DKIM / ARC /
181
+ * Authentication-Results tag lists, where a folded `b=` / `bh=` value must be
182
+ * rejoined before its base64 is read.
183
+ *
184
+ * @example
185
+ * b.structuredFields.unfoldHeaderContinuations("v=DKIM1;\r\n k=rsa");
186
+ * // returns "v=DKIM1; k=rsa"
187
+ */
188
+ function unfoldHeaderContinuations(value) {
189
+ return String(value).replace(/\r?\n[ \t]+/g, " ");
190
+ }
191
+
192
+ /**
193
+ * @primitive b.structuredFields.parseKeyValuePiece
194
+ * @signature b.structuredFields.parseKeyValuePiece(piece, kvSep?, lowerKey?)
195
+ * @since 0.15.13
196
+ * @status stable
197
+ * @related b.structuredFields.parseTagList, b.structuredFields.splitTopLevel
198
+ *
199
+ * Parse ONE already-split list piece into a `{ key, value }` pair: `key` is the
200
+ * text before the first `kvSep` (default "="), trimmed and — unless `lowerKey`
201
+ * is `false` — lower-cased; `value` is the raw remainder (the caller trims /
202
+ * unquotes / sf-string-parses it per its own grammar). A piece with NO `kvSep`
203
+ * is a "bare" item — `value` is null — which the caller handles per its grammar
204
+ * (skip it, or treat as a flag-style directive).
205
+ *
206
+ * The shared per-pair step behind every `key=value` list parser: the naive
207
+ * `parseTagList` loop AND the quote-aware parsers that first `splitTopLevel`
208
+ * then parse each piece (Cache-Control directives, Client-Hints brand params,
209
+ * Content-Type parameters). `lowerKey:false` serves the rare grammar whose key
210
+ * is case-sensitive (a verbatim tag-list passthrough).
211
+ *
212
+ * @example
213
+ * b.structuredFields.parseKeyValuePiece("Max-Age=60"); // → { key: "max-age", value: "60" }
214
+ * b.structuredFields.parseKeyValuePiece("no-store"); // → { key: "no-store", value: null }
215
+ * b.structuredFields.parseKeyValuePiece("Key=a=b", "=", false); // → { key: "Key", value: "a=b" }
216
+ */
217
+ function parseKeyValuePiece(piece, kvSep, lowerKey) {
218
+ var sep = kvSep || "=";
219
+ var at = piece.indexOf(sep);
220
+ var rawKey = (at === -1 ? piece : piece.slice(0, at)).trim();
221
+ var key = lowerKey === false ? rawKey : rawKey.toLowerCase();
222
+ return { key: key, value: at === -1 ? null : piece.slice(at + sep.length) };
223
+ }
224
+
225
+ /**
226
+ * @primitive b.structuredFields.parseTagList
227
+ * @signature b.structuredFields.parseTagList(input, opts?)
228
+ * @since 0.15.13
229
+ * @status stable
230
+ * @related b.structuredFields.splitTopLevel
231
+ *
232
+ * Parse a NAIVE delimited `key<kvSep>value` tag list into an ordered
233
+ * array of `[key, value]` pairs. This is the non-quote-aware sibling
234
+ * of `splitTopLevel`: it is for grammars whose RFC forbids the DQUOTE
235
+ * structured-string form, so a bare `split` is correct and a
236
+ * quote-aware walk would be wrong — DKIM (RFC 6376 §3.2), DMARC
237
+ * (RFC 7489 §6.4), ARC (RFC 8617 §4), BIMI (RFC 9091 §4), and the
238
+ * MTA-STS policy grammar (RFC 8461, line/colon delimited).
239
+ *
240
+ * Pairs are returned (not a map) so a caller whose grammar permits a
241
+ * repeated key — MTA-STS `mx:` lines list one host each — keeps every
242
+ * occurrence; a caller wanting last-wins map semantics folds the
243
+ * pairs into a plain object itself. Order is preserved, so a caller
244
+ * that throws on a malformed value throws at the same point it would
245
+ * have mid-loop.
246
+ *
247
+ * Entries that are empty after trimming, or carry no `kvSep`, are
248
+ * skipped (matching every shipped parser's prior behavior).
249
+ *
250
+ * @opts
251
+ * sep: string|RegExp, // entry separator. default: ";" (MTA-STS uses /\r?\n/)
252
+ * kvSep: string, // key/value separator. default: "=" (MTA-STS uses ":")
253
+ * unfold: boolean, // collapse CRLF+WSP folds to a space first (DKIM FWS). default: false
254
+ * stripValueWs: boolean, // strip all whitespace inside each value (DKIM/ARC FWS). default: false
255
+ * lowerKey: boolean, // lower-case each key. default: true (set false to preserve case)
256
+ *
257
+ * @example
258
+ * b.structuredFields.parseTagList("v=DKIM1; k=rsa; p=MIGf");
259
+ * // → [ ["v", "DKIM1"], ["k", "rsa"], ["p", "MIGf"] ]
260
+ *
261
+ * b.structuredFields.parseTagList("version:STSv1\nmx:a.example\nmx:b.example",
262
+ * { sep: /\r?\n/, kvSep: ":" });
263
+ * // → [ ["version","STSv1"], ["mx","a.example"], ["mx","b.example"] ]
264
+ */
265
+ function parseTagList(input, opts) {
266
+ opts = opts || {};
267
+ var sep = opts.sep === undefined ? ";" : opts.sep;
268
+ var kvSep = opts.kvSep === undefined ? "=" : opts.kvSep;
269
+ var s = String(input);
270
+ if (opts.unfold) s = unfoldHeaderContinuations(s);
271
+ var kvps = parseKeyValuePieces(s.split(sep), 0, kvSep, opts.lowerKey);
272
+ var out = [];
273
+ forEachKeyValue(kvps, function (key, val) {
274
+ if (opts.stripValueWs) val = val.replace(/\s+/g, "");
275
+ out.push([key, val]);
276
+ });
277
+ return out;
278
+ }
279
+
280
+ /**
281
+ * @primitive b.structuredFields.parseKeyValuePieces
282
+ * @signature b.structuredFields.parseKeyValuePieces(pieces, startIndex?, kvSep?, lowerKey?)
283
+ * @since 0.15.13
284
+ * @status stable
285
+ * @related b.structuredFields.parseKeyValuePiece, b.structuredFields.parseTagList
286
+ *
287
+ * Iterate an already-split list of structured-field pieces, trimming each,
288
+ * dropping empties, and parsing the survivors into `{ key, value }` records
289
+ * via `parseKeyValuePiece`. `startIndex` skips a leading non-pair token (the
290
+ * media type in a `Content-Type`, the brand in a `Sec-CH-UA` member); `kvSep`
291
+ * overrides the default `"="`; `lowerKey:false` preserves key case for a
292
+ * case-sensitive grammar.
293
+ *
294
+ * The split is left to the caller because the boundary discipline differs by
295
+ * grammar — `splitTopLevel` honours quotes/parens for RFC 8941 lists, while a
296
+ * plain `String(value).split(";")` is right where inner separators cannot be
297
+ * quoted. Per-piece dispatch (unquote, numeric coercion, poisoned-key drops)
298
+ * stays with the caller; this owns only the uniform iterate-trim-skip-parse
299
+ * spine the parsers shared verbatim.
300
+ *
301
+ * @opts
302
+ * startIndex: number, // default: 0 — first piece treated as a key/value pair
303
+ * kvSep: string, // default: "=" — key/value separator within a piece
304
+ * lowerKey: boolean, // default: true — lower-case each parsed key
305
+ *
306
+ * @example
307
+ * var kvps = b.structuredFields.parseKeyValuePieces(
308
+ * b.structuredFields.splitTopLevel("max-age=600, immutable", ","));
309
+ * // → [ { key: "max-age", value: "600" }, { key: "immutable", value: null } ]
310
+ */
311
+ function parseKeyValuePieces(pieces, startIndex, kvSep, lowerKey) {
312
+ var out = [];
313
+ for (var i = startIndex || 0; i < pieces.length; i += 1) {
314
+ var p = pieces[i].trim();
315
+ if (p.length === 0) continue;
316
+ out.push(parseKeyValuePiece(p, kvSep, lowerKey));
317
+ }
318
+ return out;
319
+ }
320
+
321
+ /**
322
+ * @primitive b.structuredFields.forEachKeyValue
323
+ * @signature b.structuredFields.forEachKeyValue(kvps, handler)
324
+ * @since 0.15.13
325
+ * @status stable
326
+ * @related b.structuredFields.parseKeyValuePieces, b.structuredFields.parseTagList
327
+ *
328
+ * Consume the `{ key, value }` records from `parseKeyValuePieces`: skip
329
+ * the bare entries (those with a `null` value — a key that carried no
330
+ * separator), trim each surviving value, and invoke
331
+ * `handler(key, trimmedValue, index)`. The mirror of
332
+ * `parseKeyValuePieces` on the consuming side — that primitive owns the
333
+ * parse spine, this owns the iterate-skip-bare-trim spine every header
334
+ * parser repeated verbatim before dispatching.
335
+ *
336
+ * Per-key dispatch (sf-string unquoting, numeric coercion, poisoned-key
337
+ * drops, building a typed result) stays in the handler — a handler that
338
+ * `return`s skips the current entry, exactly like a `continue`. Parsers
339
+ * that instead treat a bare key as meaningful (a value-less directive)
340
+ * iterate the records directly rather than calling this.
341
+ *
342
+ * @example
343
+ * var b = require("blamejs");
344
+ *
345
+ * var out = {};
346
+ * var kvps = b.structuredFields.parseKeyValuePieces("a=1; b=2".split(";"));
347
+ * b.structuredFields.forEachKeyValue(kvps, function (key, value) {
348
+ * out[key] = value;
349
+ * });
350
+ * // out → { a: "1", b: "2" }
351
+ */
352
+ function forEachKeyValue(kvps, handler) {
353
+ if (typeof handler !== "function") {
354
+ throw new TypeError("structuredFields.forEachKeyValue: handler must be a function");
355
+ }
356
+ for (var i = 0; i < kvps.length; i += 1) {
357
+ if (kvps[i].value === null) continue;
358
+ handler(kvps[i].key, kvps[i].value.trim(), i);
359
+ }
360
+ }
361
+
106
362
  /**
107
363
  * @primitive b.structuredFields.refuseControlBytes
108
364
  * @signature b.structuredFields.refuseControlBytes(value, opts)
@@ -155,22 +411,18 @@ function refuseControlBytes(value, opts) {
155
411
  if (!opts.label || typeof opts.label !== "string") {
156
412
  throw new TypeError("refuseControlBytes: opts.label (non-empty string) is required");
157
413
  }
158
- var allowHt = opts.allowHt !== false;
159
- for (var i = 0; i < value.length; i += 1) {
160
- var cc = value.charCodeAt(i);
161
- if (allowHt && cc === 9) continue; // ASCII HT (folding whitespace)
162
- if (cc < 32 || cc === 127) { // C0 + DEL codepoint range
163
- var msg = opts.label + ": value contains control characters (C0 / DEL)";
164
- // opts.useNativeError === true → call the ErrorClass with a
165
- // single-arg `message` (matches native Error / TypeError /
166
- // RangeError signatures used by defensive request-shape
167
- // readers). Default false → call with (code, message) which
168
- // matches every framework-error class generated by `defineClass`.
169
- if (opts.useNativeError === true) {
170
- throw new opts.ErrorClass(msg);
171
- }
172
- throw new opts.ErrorClass(opts.code, msg);
414
+ var allowHt = opts.allowHt !== false; // ASCII HT (folding whitespace) permitted unless allowHt === false
415
+ if (codepointClass.firstControlCharOffset(value, { forbidTab: !allowHt }) !== -1) { // C0 + DEL codepoint range
416
+ var msg = opts.label + ": value contains control characters (C0 / DEL)";
417
+ // opts.useNativeError === true call the ErrorClass with a
418
+ // single-arg `message` (matches native Error / TypeError /
419
+ // RangeError signatures used by defensive request-shape
420
+ // readers). Default false → call with (code, message) which
421
+ // matches every framework-error class generated by `defineClass`.
422
+ if (opts.useNativeError === true) {
423
+ throw new opts.ErrorClass(msg);
173
424
  }
425
+ throw new opts.ErrorClass(opts.code, msg);
174
426
  }
175
427
  }
176
428
 
@@ -204,7 +456,43 @@ function unquoteSfString(s) {
204
456
  if (t.length === 0) return "";
205
457
  if (t.charAt(0) !== "\"") return t;
206
458
  if (t.length < 2 || t.charAt(t.length - 1) !== "\"") return null;
207
- return t.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
459
+ return unescapeSfStringBody(t.slice(1, -1));
460
+ }
461
+
462
+ /**
463
+ * @primitive b.structuredFields.unescapeSfStringBody
464
+ * @signature b.structuredFields.unescapeSfStringBody(body)
465
+ * @since 0.15.12
466
+ * @related b.structuredFields.unquoteSfString
467
+ *
468
+ * Undo the RFC 8941 §3.3.3 quoted-string backslash-escapes from the BODY of
469
+ * an sf-string (the bytes BETWEEN the surrounding double quotes). Only `\\\\`
470
+ * and `\\"` are legal escapes; every other backslash is literal.
471
+ *
472
+ * This is a single left-to-right scan, NOT two chained `.replace()` passes.
473
+ * The two-pass form (`.replace(/\\\\/g,"\\").replace(/\\"/g,'"')`, in either
474
+ * order) is not equivalent to a single decode: whichever pass runs first can
475
+ * rewrite a backslash the other escape sequence legitimately owns, so a lone
476
+ * escaped backslash (`\\\\`) decodes to two backslashes instead of one. The
477
+ * single pass consumes each escape exactly once. Non-string input passes
478
+ * through unchanged.
479
+ *
480
+ * @example
481
+ * b.structuredFields.unescapeSfStringBody('a\\"b\\\\c');
482
+ * // → 'a"b\c'
483
+ */
484
+ function unescapeSfStringBody(body) {
485
+ if (typeof body !== "string") return body;
486
+ var out = "";
487
+ for (var i = 0; i < body.length; i++) {
488
+ var c = body.charAt(i);
489
+ if (c === "\\" && i + 1 < body.length) {
490
+ var n = body.charAt(i + 1);
491
+ if (n === "\\" || n === "\"") { out += n; i += 1; continue; }
492
+ }
493
+ out += c;
494
+ }
495
+ return out;
208
496
  }
209
497
 
210
498
  /**
@@ -235,7 +523,7 @@ function containsControlBytes(value, opts) {
235
523
  for (var i = 0; i < value.length; i += 1) {
236
524
  var cc = value.charCodeAt(i);
237
525
  if (allowHt && cc === 9) continue; // ASCII HT (folding whitespace)
238
- if (cc < 32 || cc === 127) return true; // C0 + DEL codepoint range
526
+ if (codepointClass.isForbiddenControlChar(cc, { forbidTab: true })) return true; // C0 + DEL codepoint range
239
527
  }
240
528
  return false;
241
529
  }
@@ -671,9 +959,17 @@ function serialize(value, type, opts) {
671
959
 
672
960
  module.exports = {
673
961
  splitTopLevel: splitTopLevel,
962
+ splitUnquoted: splitUnquoted,
963
+ stripDoubleQuotes: stripDoubleQuotes,
964
+ parseTagList: parseTagList,
965
+ unfoldHeaderContinuations: unfoldHeaderContinuations,
966
+ parseKeyValuePiece: parseKeyValuePiece,
967
+ parseKeyValuePieces: parseKeyValuePieces,
968
+ forEachKeyValue: forEachKeyValue,
674
969
  refuseControlBytes: refuseControlBytes,
675
970
  containsControlBytes: containsControlBytes,
676
971
  unquoteSfString: unquoteSfString,
972
+ unescapeSfStringBody: unescapeSfStringBody,
677
973
  parse: parse,
678
974
  serialize: serialize,
679
975
  Token: SfToken,
@@ -183,16 +183,7 @@ function create(opts) {
183
183
  return defaultBytesCap;
184
184
  }
185
185
 
186
- function _emitAudit(action, outcome, metadata) {
187
- if (!auditOn) return;
188
- try {
189
- audit().safeEmit({
190
- action: action,
191
- outcome: outcome,
192
- metadata: metadata || {},
193
- });
194
- } catch (_e) { /* audit best-effort */ }
195
- }
186
+ var _emitAudit = audit().namespaced(null, { audit: auditOn });
196
187
 
197
188
  function _emitMetric(name, n) {
198
189
  try { observability().safeEvent(name, n || 1, {}); }
@@ -375,16 +366,7 @@ function budget(opts) {
375
366
  return c;
376
367
  }
377
368
 
378
- function _emitAudit(action, outcome, metadata) {
379
- if (!auditOn) return;
380
- try {
381
- audit().safeEmit({
382
- action: action,
383
- outcome: outcome,
384
- metadata: metadata || {},
385
- });
386
- } catch (_e) { /* audit best-effort */ }
387
- }
369
+ var _emitAudit = audit().namespaced(null, { audit: auditOn });
388
370
 
389
371
  function _emitMetric(name, n) {
390
372
  try { observability().safeEvent(name, n || 1, {}); }
package/lib/tsa.js CHANGED
@@ -53,6 +53,7 @@
53
53
 
54
54
  var nodeCrypto = require("node:crypto");
55
55
  var bCrypto = require("./crypto");
56
+ var safeBuffer = require("./safe-buffer");
56
57
  var asn1 = require("./asn1-der");
57
58
  var cms = require("./cms-codec");
58
59
  var validateOpts = require("./validate-opts");
@@ -104,12 +105,12 @@ var DIGEST_OID_TO_NODE = {
104
105
  "2.16.840.1.101.3.4.2.10": "sha3-512",
105
106
  };
106
107
 
107
- function _bytes(x, what) {
108
- if (Buffer.isBuffer(x)) return x;
109
- if (x instanceof Uint8Array) return Buffer.from(x);
110
- if (typeof x === "string") return Buffer.from(x, "utf8");
111
- throw new TsaError("tsa/bad-bytes", "tsa: " + what + " must be Buffer / Uint8Array / string");
112
- }
108
+ var _bytes = safeBuffer.makeByteCoercer({
109
+ errorClass: TsaError,
110
+ typeCode: "tsa/bad-bytes",
111
+ messagePrefix: "tsa: ",
112
+ messageSuffix: " must be Buffer / Uint8Array / string",
113
+ });
113
114
 
114
115
  function _normHex(h) {
115
116
  return String(h).replace(/[^0-9a-fA-F]/g, "").replace(/^0+(?=.)/, "").toUpperCase();
@@ -658,15 +659,10 @@ function verifyToken(token, opts) {
658
659
  throw new TsaError("tsa/bad-trust-anchors",
659
660
  "tsa.verifyToken: trustAnchorsPem must be a non-empty PEM string or array of PEM strings");
660
661
  }
661
- // A supplied opts.at must be a valid Date — an Invalid Date would
662
- // make every validity-window comparison NaN (silently disabling it).
663
- var at = tst.genTime;
664
- if (opts.at !== undefined && opts.at !== null) {
665
- if (!(opts.at instanceof Date) || !isFinite(opts.at.getTime())) {
666
- throw new TsaError("tsa/bad-at", "tsa.verifyToken: opts.at must be a valid Date");
667
- }
668
- at = opts.at;
669
- }
662
+ // A supplied opts.at must be a valid Date — an Invalid Date would make
663
+ // every validity-window comparison NaN (silently disabling it).
664
+ validateOpts.optionalDate(opts.at, "tsa.verifyToken: opts.at", TsaError, "tsa/bad-at");
665
+ var at = (opts.at !== undefined && opts.at !== null) ? opts.at : tst.genTime;
670
666
  _verifyChain(signerCertDer, sd.certificates, anchors, at);
671
667
  }
672
668