@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
@@ -5,6 +5,8 @@ var nodeFs = require("node:fs");
5
5
  var nodePath = require("node:path");
6
6
  var net = require("node:net");
7
7
  var nodeCrypto = require("node:crypto");
8
+ var numericBounds = require("./numeric-bounds");
9
+ var atomicFile = require("./atomic-file");
8
10
 
9
11
  var bCrypto = require("./crypto");
10
12
  var C = require("./constants");
@@ -23,6 +25,29 @@ var networkDns = lazyRequire(function () { return require("./network-dns"); });
23
25
  var httpClient = lazyRequire(function () { return require("./http-client"); });
24
26
  var asn1 = require("./asn1-der");
25
27
 
28
+ // Audit + observability emit for an outbound TLS connection that runs with
29
+ // peer-certificate validation DISABLED (an explicit operator opt-in —
30
+ // rejectUnauthorized:false / allowInsecure — never a framework default).
31
+ // Emitted at the point the disable is HONORED so the degraded posture is
32
+ // observable (compliance evidence + incident response), parallel to the
33
+ // tls.classical_downgrade audit. Drop-silent best-effort (§8 hot-path sink) —
34
+ // an audit-sink failure must never break the TLS connect itself.
35
+ function auditInsecureTls(meta) {
36
+ meta = meta || {};
37
+ try {
38
+ observability().safeEvent("tls.insecure_skip_verify", 1, {
39
+ host: meta.host || null, port: meta.port || null, source: meta.source || null,
40
+ });
41
+ } catch (_e) { /* drop-silent */ }
42
+ try {
43
+ audit().safeEmit({
44
+ action: "tls.insecure_skip_verify",
45
+ outcome: "success",
46
+ metadata: { host: meta.host || null, port: meta.port || null, source: meta.source || null },
47
+ });
48
+ } catch (_e) { /* drop-silent — audit best-effort, never break TLS */ }
49
+ }
50
+
26
51
  // STATE.tlsKeyShares is initialized to the default PQC group list at
27
52
  // module load — operator setKeyShares() overrides; resetKeyShares()
28
53
  // restores the default. Empty array means "fall back to Node's TLS
@@ -80,7 +105,7 @@ function _certMetadata(pem) {
80
105
 
81
106
  function _isPathLike(s) {
82
107
  if (s.indexOf("-----BEGIN") !== -1) return false;
83
- if (s.length > C.BYTES.kib(1)) return false;
108
+ if (safeBuffer.byteLengthOf(s) > C.BYTES.kib(1)) return false;
84
109
  if (safeBuffer.hasCrlf(s)) return false;
85
110
  return true;
86
111
  }
@@ -92,20 +117,13 @@ function _isPathLike(s) {
92
117
  // shape, where an attacker who could swap the file in-between could
93
118
  // short-circuit the PEM marker check downstream.
94
119
  function _readPathFile(p) {
95
- var fd = nodeFs.openSync(p, "r");
96
- try {
97
- var fstat = nodeFs.fstatSync(fd);
98
- var buf = Buffer.alloc(fstat.size);
99
- var read = 0;
100
- while (read < fstat.size) {
101
- var n = nodeFs.readSync(fd, buf, read, fstat.size - read, null);
102
- if (n === 0) break;
103
- read += n;
104
- }
105
- return buf.slice(0, read).toString("utf8");
106
- } finally {
107
- try { nodeFs.closeSync(fd); } catch (_c) { /* close best-effort */ }
108
- }
120
+ // TOCTOU-safe read via atomic-file: utf8 string, slice on a short read,
121
+ // raw ENOENT preserved (errorFor returns undefined → rethrow).
122
+ return atomicFile.fdSafeReadSync(p, {
123
+ encoding: "utf8",
124
+ allowShortRead: true,
125
+ errorFor: function () { return undefined; },
126
+ });
109
127
  }
110
128
 
111
129
  function _readPath(p) {
@@ -144,7 +162,7 @@ function addCa(pemOrPath, opts) {
144
162
  added.push(meta);
145
163
  }
146
164
  _emitAuditAdd(added, opts);
147
- _emitObs("network.tls.ca.added", { count: added.length });
165
+ observability().safeEvent("network.tls.ca.added", 1, { count: added.length });
148
166
  return added;
149
167
  }
150
168
 
@@ -154,7 +172,7 @@ function addCaBundle(p, opts) {
154
172
 
155
173
  function useSystemTrust(enable) {
156
174
  STATE.systemTrust = enable !== false;
157
- _emitObs("network.tls.system_trust.set", { enabled: STATE.systemTrust });
175
+ observability().safeEvent("network.tls.system_trust.set", 1, { enabled: STATE.systemTrust });
158
176
  }
159
177
 
160
178
  function isSystemTrustEnabled() { return !!STATE.systemTrust; }
@@ -175,6 +193,21 @@ function getTrustStore() {
175
193
  });
176
194
  }
177
195
 
196
+ // _certAuditMetadata(m) — the shared cert-identity fields stamped on every
197
+ // network.tls.ca.* audit event (the add / remove emitters overlay their
198
+ // own label / reason on top via Object.assign). Distinct from
199
+ // _certMetadata(pem), which PARSES a PEM into the meta object `m`.
200
+ function _certAuditMetadata(m) {
201
+ return {
202
+ subject: m.subject,
203
+ issuer: m.issuer,
204
+ fingerprint256: m.fingerprint256,
205
+ validFrom: m.validFrom,
206
+ validTo: m.validTo,
207
+ isSelfSigned: m.isSelfSigned,
208
+ };
209
+ }
210
+
178
211
  function _emitAuditRemove(metaList, reason) {
179
212
  var sink;
180
213
  try { sink = audit(); } catch (_e) { sink = null; }
@@ -185,16 +218,10 @@ function _emitAuditRemove(metaList, reason) {
185
218
  sink.safeEmit({
186
219
  action: "network.tls.ca.removed",
187
220
  outcome: "success",
188
- metadata: {
189
- subject: m.subject,
190
- issuer: m.issuer,
191
- fingerprint256: m.fingerprint256,
192
- validFrom: m.validFrom,
193
- validTo: m.validTo,
194
- isSelfSigned: m.isSelfSigned,
195
- label: m.label,
196
- reason: reason || "operator",
197
- },
221
+ metadata: Object.assign(_certAuditMetadata(m), {
222
+ label: m.label,
223
+ reason: reason || "operator",
224
+ }),
198
225
  });
199
226
  } catch (_e) { /* audit best-effort — never break the caller */ }
200
227
  }
@@ -216,7 +243,7 @@ function removeCa(fingerprint256, opts) {
216
243
  });
217
244
  if (removed.length === 0) return 0;
218
245
  if (!opts || opts.audit !== false) _emitAuditRemove(removed, "operator-remove");
219
- _emitObs("network.tls.ca.removed", { count: removed.length, reason: "operator" });
246
+ observability().safeEvent("network.tls.ca.removed", 1, { count: removed.length, reason: "operator" });
220
247
  return removed.length;
221
248
  }
222
249
 
@@ -234,7 +261,7 @@ function removeCaByLabel(label, opts) {
234
261
  });
235
262
  if (removed.length === 0) return 0;
236
263
  if (!opts || opts.audit !== false) _emitAuditRemove(removed, "operator-remove-by-label");
237
- _emitObs("network.tls.ca.removed", { count: removed.length, reason: "label" });
264
+ observability().safeEvent("network.tls.ca.removed", 1, { count: removed.length, reason: "label" });
238
265
  return removed.length;
239
266
  }
240
267
 
@@ -243,7 +270,7 @@ function clearAll(opts) {
243
270
  var removed = STATE.cas.map(function (e) { return Object.assign({ label: e.label }, e.meta); });
244
271
  STATE.cas = [];
245
272
  if (!opts || opts.audit !== false) _emitAuditRemove(removed, "operator-clear-all");
246
- _emitObs("network.tls.ca.cleared", { count: removed.length });
273
+ observability().safeEvent("network.tls.ca.cleared", 1, { count: removed.length });
247
274
  return removed.length;
248
275
  }
249
276
 
@@ -260,7 +287,7 @@ function purgeExpired(opts) {
260
287
  });
261
288
  if (removed.length === 0) return 0;
262
289
  if (!opts || opts.audit !== false) _emitAuditRemove(removed, "expired");
263
- _emitObs("network.tls.ca.purged_expired", { count: removed.length });
290
+ observability().safeEvent("network.tls.ca.purged_expired", 1, { count: removed.length });
264
291
  return removed.length;
265
292
  }
266
293
 
@@ -492,7 +519,7 @@ var DEFAULT_PQC_KEY_SHARES = Object.freeze([
492
519
  ]);
493
520
 
494
521
  function _validateKeyShare(name) {
495
- if (typeof name !== "string" || name.length === 0 || name.length > C.BYTES.bytes(64)) { // bound
522
+ if (typeof name !== "string" || name.length === 0 || safeBuffer.byteLengthOf(name) > C.BYTES.bytes(64)) { // bound
496
523
  throw new TlsTrustError("tls/bad-key-share",
497
524
  "tls.pqc.setKeyShares: each entry must be a non-empty string up to 64 chars");
498
525
  }
@@ -691,24 +718,12 @@ function _emitAuditAdd(metaList, opts) {
691
718
  sink.safeEmit({
692
719
  action: "network.tls.ca.added",
693
720
  outcome: "success",
694
- metadata: {
695
- subject: m.subject,
696
- issuer: m.issuer,
697
- fingerprint256: m.fingerprint256,
698
- validFrom: m.validFrom,
699
- validTo: m.validTo,
700
- isSelfSigned: m.isSelfSigned,
701
- label: opts.label || null,
702
- },
721
+ metadata: Object.assign(_certAuditMetadata(m), { label: opts.label || null }),
703
722
  });
704
723
  } catch (_e) { /* audit best-effort — never break the caller */ }
705
724
  }
706
725
  }
707
726
 
708
- function _emitObs(name, fields) {
709
- try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
710
- }
711
-
712
727
  function _resetForTest() {
713
728
  STATE.cas = [];
714
729
  STATE.systemTrust = false;
@@ -2682,11 +2697,8 @@ function connectWithEch(opts) {
2682
2697
  validateOpts.requireNonEmptyString(opts.host, "connectWithEch: host",
2683
2698
  NetworkTlsError, "tls/ech-bad-opts");
2684
2699
  var port = opts.port === undefined ? 443 : opts.port; // HTTPS default port
2685
- if (typeof port !== "number" || !isFinite(port) ||
2686
- port <= 0 || port > 65535 || Math.floor(port) !== port) { // TCP port range
2687
- throw new NetworkTlsError("tls/ech-bad-opts",
2688
- "connectWithEch: port must be an integer in 1..65535");
2689
- }
2700
+ numericBounds.requirePositiveFiniteInt(port,
2701
+ "connectWithEch: port", NetworkTlsError, "tls/ech-bad-opts", { max: 65535 });
2690
2702
  if (opts.alpn !== undefined && !Array.isArray(opts.alpn)) {
2691
2703
  throw new NetworkTlsError("tls/ech-bad-opts",
2692
2704
  "connectWithEch: alpn must be an array of strings");
@@ -2723,8 +2735,14 @@ function connectWithEch(opts) {
2723
2735
  if (typeof opts.checkServerIdentity === "function") {
2724
2736
  connectOpts.checkServerIdentity = opts.checkServerIdentity;
2725
2737
  }
2726
- if (opts.rejectUnauthorized === false) {
2727
- connectOpts.rejectUnauthorized = false;
2738
+ // rejectUnauthorized defaults to true (full validation). An operator may
2739
+ // explicitly opt out — audited, never a framework default. Derived from
2740
+ // the operator's own value (operator-governed shape, mirroring mail.js /
2741
+ // log-stream-syslog.js), not a hardcoded literal.
2742
+ var rejectUnauthorized = opts.rejectUnauthorized !== false;
2743
+ connectOpts.rejectUnauthorized = rejectUnauthorized;
2744
+ if (!rejectUnauthorized) {
2745
+ auditInsecureTls({ host: opts.host, port: port, source: "network.tls.connectWithEch" });
2728
2746
  }
2729
2747
  var echAttached = false;
2730
2748
  if (echConfigBuf && nodeSupportsEch) {
@@ -2735,7 +2753,7 @@ function connectWithEch(opts) {
2735
2753
  // gracefully with a one-shot warn so operators know they're
2736
2754
  // sending an outer-only ClientHello.
2737
2755
  try {
2738
- observability().emit("network.tls.ech.unsupported", {
2756
+ observability().safeEvent("network.tls.ech.unsupported", 1, {
2739
2757
  host: opts.host, source: sourceLabel,
2740
2758
  });
2741
2759
  } catch (_e) { /* drop-silent */ }
@@ -2772,7 +2790,7 @@ function connectWithEch(opts) {
2772
2790
  settled = true;
2773
2791
  if (to) clearTimeout(to);
2774
2792
  try {
2775
- observability().emit("network.tls.ech.connected", {
2793
+ observability().safeEvent("network.tls.ech.connected", 1, {
2776
2794
  host: opts.host, echAttached: echAttached, source: sourceLabel,
2777
2795
  });
2778
2796
  } catch (_e) { /* drop-silent */ }
@@ -2832,7 +2850,7 @@ function connectWithEch(opts) {
2832
2850
  // operator still gets a working TLS session. Emit obs so the
2833
2851
  // operator sees the degradation.
2834
2852
  try {
2835
- observability().emit("network.tls.ech.dns_failed", {
2853
+ observability().safeEvent("network.tls.ech.dns_failed", 1, {
2836
2854
  host: opts.host, error: (e && e.message) || String(e),
2837
2855
  });
2838
2856
  } catch (_e) { /* drop-silent */ }
@@ -3174,6 +3192,7 @@ function wrapSNICallback(operatorCb) {
3174
3192
  }
3175
3193
 
3176
3194
  module.exports = {
3195
+ auditInsecureTls: auditInsecureTls,
3177
3196
  addCa: addCa,
3178
3197
  addCaBundle: addCaBundle,
3179
3198
  removeCa: removeCa,
package/lib/network.js CHANGED
@@ -151,7 +151,7 @@ var ntpFacade = {
151
151
  throw new NetworkError("ntp/bad-servers", "ntp.setServers: expected non-empty array");
152
152
  }
153
153
  ntpFacade._defaultServers = list.slice();
154
- _emitObs("network.ntp.servers.set", { count: list.length });
154
+ observability().safeEvent("network.ntp.servers.set", 1, { count: list.length });
155
155
  },
156
156
  getServers: function () {
157
157
  return (ntpFacade._defaultServers || ntpCheck.DEFAULT_SERVERS).slice();
@@ -262,7 +262,7 @@ function bootFromEnv(opts) {
262
262
  } catch (_e) { /* audit best-effort — never break boot */ }
263
263
  }
264
264
  }
265
- _emitObs("network.boot.from_env", { source: "env" });
265
+ observability().safeEvent("network.boot.from_env", 1, { source: "env" });
266
266
  return applied;
267
267
  }
268
268
 
@@ -301,10 +301,6 @@ function snapshot() {
301
301
  };
302
302
  }
303
303
 
304
- function _emitObs(name, fields) {
305
- try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
306
- }
307
-
308
304
  function _resetForTest() {
309
305
  ntpFacade._defaultServers = null;
310
306
  ntpFacade._defaultTimeoutMs = null;
@@ -66,7 +66,6 @@ function create(opts) {
66
66
  var sectorAnnex = opts.sectorAnnex;
67
67
  var csirtEndpoint = opts.csirtEndpoint || null;
68
68
  var httpClient = opts.httpClient || null;
69
- var auditOn = opts.audit !== false;
70
69
 
71
70
  var ir = incidentReport().create({
72
71
  audit: opts.audit,
@@ -79,16 +78,7 @@ function create(opts) {
79
78
  },
80
79
  });
81
80
 
82
- function _emitAudit(action, outcome, metadata) {
83
- if (!auditOn) return;
84
- try {
85
- audit().safeEmit({
86
- action: "nis2.report." + action,
87
- outcome: outcome,
88
- metadata: metadata || {},
89
- });
90
- } catch (_e) { /* drop-silent */ }
91
- }
81
+ var _emitAudit = audit().namespaced("nis2.report", opts.audit);
92
82
 
93
83
  async function _submitToCsirt(payload) {
94
84
  if (!csirtEndpoint || !httpClient) {
@@ -13,6 +13,16 @@
13
13
  * inserting second would race on concurrent requests carrying
14
14
  * the same nonce.
15
15
  *
16
+ * await store.release(nonce) → boolean
17
+ * Un-claims a single nonce so a later checkAndInsert of the same value
18
+ * succeeds again — the rollback half of reserve -> commit -> rollback.
19
+ * A handler that claims an event id the moment a signature verifies (to
20
+ * win the race against a concurrent duplicate) MUST release it when
21
+ * downstream processing fails, otherwise the provider's at-least-once
22
+ * redelivery is reported as a replay and the event is dropped. Returns
23
+ * whether a live claim existed. Pair with checkAndInsert; without it an
24
+ * eager claim inverts the at-least-once contract on any failure.
25
+ *
16
26
  * await store.purgeExpired() → number
17
27
  * Removes entries past their expireAt. Called periodically by the
18
28
  * middleware that uses the store. Returns the count purged.
@@ -32,8 +42,11 @@
32
42
  * table _blamejs_api_encrypt_nonces. The PRIMARY KEY race is what
33
43
  * makes the check + insert atomic across nodes.
34
44
  *
35
- * { checkAndInsert, purgeExpired, close } — operator-supplied
36
- * custom backend (Redis SETNX, Memcached add, etc.). Use this
45
+ * { checkAndInsert, release?, purgeExpired?, close? } — operator-supplied
46
+ * custom backend (Redis SETNX, Memcached add, etc.). release() is
47
+ * optional but required to use reserve -> rollback; a backend omitting
48
+ * it throws NONCE_BACKEND_NO_RELEASE the first time release() is called
49
+ * (a loud gap, never a silent dropped rollback). Use this
37
50
  * for cross-process accuracy without a per-request SQL hop.
38
51
  *
39
52
  * Sweep cadence: memory backend sweeps every opts.sweepIntervalMs
@@ -132,6 +145,21 @@ function _memoryBackend(opts) {
132
145
  return Promise.resolve(true);
133
146
  }
134
147
 
148
+ // Un-claim a nonce so a future checkAndInsert of the same value succeeds
149
+ // again — the rollback half of reserve -> commit -> rollback. A handler
150
+ // that claims an event id the moment a signature verifies (to win the race
151
+ // against a concurrent duplicate) MUST release it when downstream processing
152
+ // fails, otherwise the provider's at-least-once redelivery is reported as a
153
+ // replay and the event is dropped. Returns whether a live claim existed.
154
+ function release(nonce) {
155
+ if (typeof nonce !== "string" || nonce.length === 0) {
156
+ return Promise.reject(_err("INVALID_NONCE", "nonce must be a non-empty string"));
157
+ }
158
+ var existed = seen.get(nonce) !== undefined;
159
+ if (existed) seen.delete(nonce);
160
+ return Promise.resolve(existed);
161
+ }
162
+
135
163
  function purgeExpired() {
136
164
  return Promise.resolve(_purgeExpiredSync());
137
165
  }
@@ -144,6 +172,7 @@ function _memoryBackend(opts) {
144
172
  return {
145
173
  name: "memory",
146
174
  checkAndInsert: checkAndInsert,
175
+ release: release,
147
176
  purgeExpired: purgeExpired,
148
177
  close: close,
149
178
  // Test hooks — underlying entry count + count of capacity fail-closed
@@ -178,6 +207,19 @@ function _clusterBackend(_opts) {
178
207
  return (result && result.rowCount > 0);
179
208
  }
180
209
 
210
+ async function release(nonce) {
211
+ if (typeof nonce !== "string" || nonce.length === 0) {
212
+ throw _err("INVALID_NONCE", "nonce must be a non-empty string");
213
+ }
214
+ // Un-claim a single nonce (the reserve -> rollback half). The middleware
215
+ // hashes the raw nonce before it reaches here, so we delete by hash.
216
+ var built = sql.delete(frameworkSchema.tableName(NONCE_TABLE), _nonceSqlOpts())
217
+ .where("nonceHash", "=", nonce)
218
+ .toSql();
219
+ var result = await clusterStorage.execute(built.sql, built.params);
220
+ return (result && result.rowCount > 0);
221
+ }
222
+
181
223
  async function purgeExpired() {
182
224
  var built = sql.delete(frameworkSchema.tableName(NONCE_TABLE), _nonceSqlOpts())
183
225
  .where("expireAt", "<=", Date.now())
@@ -191,6 +233,7 @@ function _clusterBackend(_opts) {
191
233
  return {
192
234
  name: "cluster",
193
235
  checkAndInsert: checkAndInsert,
236
+ release: release,
194
237
  purgeExpired: purgeExpired,
195
238
  close: close,
196
239
  };
@@ -208,6 +251,15 @@ function create(opts) {
208
251
  return Object.assign({
209
252
  name: "custom",
210
253
  purgeExpired: function () { return Promise.resolve(0); },
254
+ // A custom backend that omits release() cannot participate in
255
+ // reserve -> rollback. Fail LOUDLY (not a silent no-op) so the gap
256
+ // surfaces the first time a handler tries to un-claim, rather than
257
+ // silently dropping the rollback and inverting the at-least-once contract.
258
+ release: function () {
259
+ return Promise.reject(_err("BACKEND_NO_RELEASE",
260
+ "this custom nonce backend does not implement release(nonce); " +
261
+ "the reserve -> commit -> rollback pattern requires it"));
262
+ },
211
263
  close: function () {},
212
264
  }, backend);
213
265
  }
@@ -215,11 +267,37 @@ function create(opts) {
215
267
  if (!backend || backend === "memory") return _memoryBackend(opts);
216
268
  throw _err("UNKNOWN_BACKEND",
217
269
  "nonce-store: unknown backend '" + backend +
218
- "' (must be 'memory', 'cluster', or { checkAndInsert, purgeExpired, close })");
270
+ "' (must be 'memory', 'cluster', or { checkAndInsert, release?, purgeExpired?, close? })");
271
+ }
272
+
273
+ // enforceReplay(store, jti, expireAtMs, opts) — single-use enforcement
274
+ // against a replay store: `await store.checkAndInsert(jti, expireAtMs)`,
275
+ // raising opts.errorClass(opts.storeFailedCode, …) if the store itself
276
+ // fails (a store outage must NOT be mistaken for a clean token) and
277
+ // opts.errorClass(opts.replayCode, "<opts.tokenLabel> jti='<jti>' has
278
+ // been seen before — replay refused") if the jti was already seen. The
279
+ // fail-closed anti-replay control flow every JWT / DPoP verifier repeated
280
+ // identically — centralised here so the contract lives in one place.
281
+ // Verifiers with a divergent message or store contract (e.g. an atomic
282
+ // back-channel-logout-token store) keep their own inline check.
283
+ async function enforceReplay(store, jti, expireAtMs, opts) {
284
+ opts = opts || {};
285
+ var inserted;
286
+ try {
287
+ inserted = await store.checkAndInsert(jti, expireAtMs);
288
+ } catch (e) {
289
+ throw new opts.errorClass(opts.storeFailedCode,
290
+ "replayStore.checkAndInsert threw: " + ((e && e.message) || String(e)));
291
+ }
292
+ if (inserted === false) {
293
+ throw new opts.errorClass(opts.replayCode,
294
+ opts.tokenLabel + " jti='" + jti + "' has been seen before — replay refused");
295
+ }
219
296
  }
220
297
 
221
298
  module.exports = {
222
299
  create: create,
300
+ enforceReplay: enforceReplay,
223
301
  NonceStoreError: NonceStoreError,
224
302
  _memoryBackend: _memoryBackend,
225
303
  _clusterBackend: _clusterBackend,
package/lib/notify.js CHANGED
@@ -408,11 +408,6 @@ function create(opts) {
408
408
  channels[n] = registry;
409
409
  }
410
410
 
411
- function _emitObs(name, labels) {
412
- try { observability().event(name, 1, labels || {}); }
413
- catch (_e) { /* drop-silent — observability sink must not crash send() */ }
414
- }
415
-
416
411
  var _emitAudit = validateOpts.makeAuditEmitter(audit);
417
412
 
418
413
  function _actor(callerOpts) {
@@ -465,7 +460,7 @@ function create(opts) {
465
460
  // mechanism — no double-retry inside the breaker or transport).
466
461
  async function _oneAttempt(attemptIdx) {
467
462
  attemptCount = attemptIdx;
468
- _emitObs("notify.send.attempt", { channel: channel, attempt: attemptIdx });
463
+ observability().safeEvent("notify.send.attempt", 1, { channel: channel, attempt: attemptIdx });
469
464
  var sendPromise = entry.transport.send(message, input.sendOpts || null);
470
465
  // withTimeout from b.safeAsync — never re-implement timer races.
471
466
  var timed = (perCallTimeoutMs > 0)
@@ -478,7 +473,7 @@ function create(opts) {
478
473
  // classifies it correctly (operators can still opt OUT by setting
479
474
  // err.permanent in their transport).
480
475
  if (e && e.code === "async/timeout") {
481
- _emitObs("notify.send.timeout", { channel: channel });
476
+ observability().safeEvent("notify.send.timeout", 1, { channel: channel });
482
477
  var te = _err("TIMEOUT",
483
478
  "notify.send: '" + channel + "' transport timed out after " + perCallTimeoutMs + "ms");
484
479
  // Mark transient via a NETWORK-style code so b.retry.isRetryable
@@ -497,7 +492,7 @@ function create(opts) {
497
492
  return await entry.breaker.wrap(function () { return _oneAttempt(attemptIdx); });
498
493
  } catch (e) {
499
494
  if (e && e.code === "CIRCUIT_OPEN") {
500
- _emitObs("notify.send.breaker.open", { channel: channel });
495
+ observability().safeEvent("notify.send.breaker.open", 1, { channel: channel });
501
496
  }
502
497
  throw e;
503
498
  }
@@ -524,7 +519,7 @@ function create(opts) {
524
519
  }, perCallRetry);
525
520
 
526
521
  var durationMs = clock() - startedAt;
527
- _emitObs("notify.send.success", { channel: channel, durationMs: durationMs });
522
+ observability().safeEvent("notify.send.success", 1, { channel: channel, durationMs: durationMs });
528
523
  if (auditSuccess) {
529
524
  _emitAudit("notify.send.success", {
530
525
  actor: _actor(input),
@@ -543,7 +538,7 @@ function create(opts) {
543
538
  durationMs: durationMs,
544
539
  });
545
540
  } catch (e) {
546
- _emitObs("notify.send.failure", {
541
+ observability().safeEvent("notify.send.failure", 1, {
547
542
  channel: channel,
548
543
  reason: (e && e.code) || "unknown",
549
544
  });
@@ -594,7 +589,7 @@ function create(opts) {
594
589
  if (results[i] && results[i].isNotifyError) failed++;
595
590
  else ok++;
596
591
  }
597
- _emitObs("notify.batch", { size: inputs.length, ok: ok, failed: failed });
592
+ observability().safeEvent("notify.batch", 1, { size: inputs.length, ok: ok, failed: failed });
598
593
  return results;
599
594
  }
600
595
 
@@ -626,7 +621,7 @@ function create(opts) {
626
621
  } catch (_e) { /* operator may register their own handler */ }
627
622
  }
628
623
  var jobId = await q.enqueue(queueName, input);
629
- _emitObs("notify.queue.enqueued", { channel: input.channel, queueName: queueName });
624
+ observability().safeEvent("notify.queue.enqueued", 1, { channel: input.channel, queueName: queueName });
630
625
  return { jobId: jobId };
631
626
  }
632
627
 
@@ -42,6 +42,20 @@ function shape(value) {
42
42
  return (typeof value) + " " + String(value);
43
43
  }
44
44
 
45
+ // _throwInt — construct + throw a code-first defineClass error. When no
46
+ // errorOpts is supplied the call is the historical bare `new ErrorClass(
47
+ // code, message)` (byte-identical for every existing caller); pass
48
+ // errorOpts { permanent, statusCode } to forward the framework error's
49
+ // non-retryable flag / HTTP status (a config opt that's wrong on retry
50
+ // stays wrong; a request-shaped opt carries its 4xx). Message-FIRST
51
+ // classes (AtomicFileError / SafeBufferError / SafeUrlError) must NOT use
52
+ // the require* throwers — they use the isPositiveFiniteInt predicate + their
53
+ // own throw (see the module header) so code/message don't swap.
54
+ function _throwInt(errorClass, code, message, errorOpts) {
55
+ if (errorOpts) throw new errorClass(code, message, errorOpts.permanent, errorOpts.statusCode);
56
+ throw new errorClass(code, message);
57
+ }
58
+
45
59
  function isPositiveFiniteInt(value) {
46
60
  return typeof value === "number" && Number.isFinite(value) &&
47
61
  Number.isInteger(value) && value > 0;
@@ -64,11 +78,11 @@ function isNonNegativeFiniteInt(value) {
64
78
  // nb.requirePositiveFiniteIntIfPresent(opts.graceMs,
65
79
  // "app-shutdown.create: opts.graceMs", AppShutdownError,
66
80
  // "app-shutdown/bad-grace-ms");
67
- function requirePositiveFiniteIntIfPresent(value, label, errorClass, code) {
81
+ function requirePositiveFiniteIntIfPresent(value, label, errorClass, code, errorOpts) {
68
82
  if (value === undefined) return value;
69
83
  if (!isPositiveFiniteInt(value)) {
70
- throw new errorClass(code,
71
- (label || "value") + " must be a positive finite integer; got " + shape(value));
84
+ _throwInt(errorClass, code,
85
+ (label || "value") + " must be a positive finite integer; got " + shape(value), errorOpts);
72
86
  }
73
87
  return value;
74
88
  }
@@ -102,13 +116,13 @@ function _rangeSuffix(range) {
102
116
  if (range.min != null) return " >= " + range.min;
103
117
  return "";
104
118
  }
105
- function requirePositiveFiniteInt(value, label, errorClass, code, range) {
119
+ function requirePositiveFiniteInt(value, label, errorClass, code, range, errorOpts) {
106
120
  var inRange = !range ||
107
121
  ((range.min == null || value >= range.min) && (range.max == null || value <= range.max));
108
122
  if (!isPositiveFiniteInt(value) || !inRange) {
109
- throw new errorClass(code,
123
+ _throwInt(errorClass, code,
110
124
  (label || "value") + " must be a positive finite integer" +
111
- _rangeSuffix(range) + "; got " + shape(value));
125
+ _rangeSuffix(range) + "; got " + shape(value), errorOpts);
112
126
  }
113
127
  return value;
114
128
  }
@@ -123,12 +137,12 @@ function requirePositiveFiniteInt(value, label, errorClass, code, range) {
123
137
  // nb.requireAllPositiveFiniteIntIfPresent(opts,
124
138
  // ["maxBytes", "maxAttrValueBytes", "maxTagDepth", "maxAttrsPerTag"],
125
139
  // "guardHtml.validate", GuardHtmlError, "html.bad-opt");
126
- function requireAllPositiveFiniteIntIfPresent(opts, names, labelPrefix, errorClass, code) {
140
+ function requireAllPositiveFiniteIntIfPresent(opts, names, labelPrefix, errorClass, code, errorOpts) {
127
141
  if (!opts || !Array.isArray(names)) return;
128
142
  for (var i = 0; i < names.length; i += 1) {
129
143
  var n = names[i];
130
144
  requirePositiveFiniteIntIfPresent(opts[n],
131
- (labelPrefix || "") + ": " + n, errorClass, code);
145
+ (labelPrefix || "") + ": " + n, errorClass, code, errorOpts);
132
146
  }
133
147
  }
134
148
 
@@ -33,6 +33,7 @@ var C = require("../constants");
33
33
  var httpClient = require("../http-client");
34
34
  var requestHelpers = require("../request-helpers");
35
35
  var safeUrl = require("../safe-url");
36
+ var markupEscape = require("../markup-escape").markupEscape;
36
37
  var { ObjectStoreError } = require("../framework-error");
37
38
 
38
39
  var _err = ObjectStoreError.factory;
@@ -122,12 +123,7 @@ function _validateCorsRule(rule, idx) {
122
123
  }
123
124
 
124
125
  function _xmlEscape(s) {
125
- return String(s)
126
- .replace(/&/g, "&amp;")
127
- .replace(/</g, "&lt;")
128
- .replace(/>/g, "&gt;")
129
- .replace(/"/g, "&quot;")
130
- .replace(/'/g, "&apos;");
126
+ return markupEscape(s, { apos: "&apos;" });
131
127
  }
132
128
 
133
129
  function _buildCorsXml(rules) {