@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
@@ -343,6 +343,98 @@ function safeEvent(name, value, labels) {
343
343
  catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
344
344
  }
345
345
 
346
+ /**
347
+ * @primitive b.observability.safeEmit
348
+ * @signature b.observability.safeEmit(sink, name, value, labels)
349
+ * @since 0.15.13
350
+ * @related b.observability.safeEvent, b.observability.event
351
+ *
352
+ * The sink-aware sibling of `safeEvent`: emit a metric event to an
353
+ * explicitly-configured observability `sink` (a per-instance
354
+ * observability object) when one is supplied, otherwise fall back to
355
+ * the global registry — each path wrapped in a try/catch so a
356
+ * misconfigured counter never crashes the request that triggered it.
357
+ * Replaces the `_emitObs` + `_safeGlobalObs` helper pair that the auth
358
+ * brute-force modules (bot-challenge / lockout / session-device-binding)
359
+ * each duplicated to route a configured observability instance with a
360
+ * global fallback.
361
+ *
362
+ * @example
363
+ * b.observability.safeEmit(opts.observability, "auth.lockout.hit", 1,
364
+ * { namespace: ns });
365
+ */
366
+ function safeEmit(sink, name, value, labels) {
367
+ if (sink) {
368
+ try { sink.event(name, value, labels); }
369
+ catch (_e) { /* per-instance observability sink — drops silent */ }
370
+ return;
371
+ }
372
+ safeEvent(name, value, labels);
373
+ }
374
+
375
+ /**
376
+ * @primitive b.observability.makeCounterEmitter
377
+ * @signature b.observability.makeCounterEmitter(sink)
378
+ * @since 0.15.13
379
+ * @status stable
380
+ * @related b.observability.safeEmit, b.observability.namespaced
381
+ *
382
+ * Bind a per-instance counter emitter. Returns `(name, labels)` that
383
+ * increments metric `name` by 1 (with `labels`) on the supplied
384
+ * observability `sink`, drop-silent on a sink throw and falling back to
385
+ * the global tap when `sink` is null. The shorthand every primitive that
386
+ * accepts an `observability` instance wrapped in a private
387
+ * `_emitObs(name, labels)` closure around
388
+ * `safeEmit(obsInst, name, 1, labels)` — build it once with the instance
389
+ * and call the returned emitter.
390
+ *
391
+ * @example
392
+ * var b = require("blamejs");
393
+ * var emit = b.observability.makeCounterEmitter(myObsInstance);
394
+ * emit("auth.lockout.tripped", { actor: "alice" });
395
+ */
396
+ function makeCounterEmitter(sink) {
397
+ return function (name, labels) {
398
+ safeEmit(sink, name, 1, labels);
399
+ };
400
+ }
401
+
402
+ /**
403
+ * @primitive b.observability.namespaced
404
+ * @signature b.observability.namespaced(prefix, gateFlag?)
405
+ * @since 0.15.13
406
+ * @status stable
407
+ * @related b.observability.safeEvent, b.observability.event, b.audit.namespaced
408
+ *
409
+ * Build a drop-silent metric emitter bound to one name prefix — the shape every
410
+ * primitive hand-rolled as a private `_emitMetric(verb, n, labels)` closure
411
+ * (`try { observability().safeEvent("ns." + verb, n || 1, labels || {}); }
412
+ * catch {}`). The returned function prefixes `verb` with `prefix + "."`,
413
+ * defaults the value to `1` and labels to `{}`, and routes through `safeEvent`
414
+ * so a misconfigured counter / label name cannot crash the caller. The metric
415
+ * sibling of `b.audit.namespaced`. Metrics emit unconditionally by default;
416
+ * pass `gateFlag === false` to disable a primitive's own metrics in lockstep
417
+ * with its audit (the few primitives that gate both behind one `opts.audit`).
418
+ *
419
+ * @example
420
+ * var emitMetric = b.observability.namespaced("network.byte_quota");
421
+ * emitMetric("exceeded", 1, { key: k });
422
+ * // → safeEvent("network.byte_quota.exceeded", 1, { key: k })
423
+ * emitMetric("reset");
424
+ * // → safeEvent("network.byte_quota.reset", 1, {})
425
+ */
426
+ function namespaced(prefix, gateFlag) {
427
+ var on = gateFlag !== false;
428
+ return function (verb, n, labels) {
429
+ if (!on) return;
430
+ // module.exports.safeEvent (late-bound) so a test that stubs
431
+ // b.observability.safeEvent still observes the emit — the closures this
432
+ // replaces all called `observability().safeEvent(...)`.
433
+ try { module.exports.safeEvent(prefix + "." + verb, n || 1, labels || {}); }
434
+ catch (_e) { /* drop-silent — observability sink */ }
435
+ };
436
+ }
437
+
346
438
  // timed — convenience wrapper that measures wall-clock duration of a
347
439
  // sync or async operation and emits a counter event with
348
440
  // duration_ms in the labels. Returns the wrapped function's return
@@ -870,6 +962,9 @@ module.exports = {
870
962
  tap: tap,
871
963
  event: event,
872
964
  safeEvent: safeEvent,
965
+ safeEmit: safeEmit,
966
+ makeCounterEmitter: makeCounterEmitter,
967
+ namespaced: namespaced,
873
968
  timed: timed,
874
969
  setTap: setTap,
875
970
  setRedactor: setRedactor,
@@ -25,6 +25,7 @@
25
25
 
26
26
  var validateOpts = require("./validate-opts");
27
27
  var schemaWalk = require("./openapi-schema-walk");
28
+ var pick = require("./pick");
28
29
  var { defineClass } = require("./framework-error");
29
30
  var OpenApiError = defineClass("OpenApiError", { alwaysPermanent: true });
30
31
 
@@ -44,7 +45,10 @@ function _extractPathParams(pathTemplate) {
44
45
  }
45
46
 
46
47
  function PathsBuilder() {
47
- this._paths = {};
48
+ // null-proto so a urlPattern keyed into _paths (operator-supplied API path)
49
+ // can never reach Object.prototype — a "__proto__" / "constructor" path lands
50
+ // as an own property, not a prototype mutation (prototype-polluting-assignment).
51
+ this._paths = Object.create(null);
48
52
  }
49
53
 
50
54
  // _buildOperation — normalise a single Operation Object from operator
@@ -133,7 +137,7 @@ PathsBuilder.prototype.add = function (method, urlPattern, opts) {
133
137
  }
134
138
  }
135
139
 
136
- if (!this._paths[urlPattern]) this._paths[urlPattern] = {};
140
+ if (!this._paths[urlPattern]) this._paths[urlPattern] = Object.create(null);
137
141
  if (this._paths[urlPattern][method.toLowerCase()]) {
138
142
  throw new OpenApiError("openapi/duplicate-operation",
139
143
  "paths.add: duplicate operation " + method.toUpperCase() + " " + urlPattern);
@@ -279,7 +283,7 @@ WebhooksBuilder.prototype.add = function (name, method, opts) {
279
283
  opts = opts || {};
280
284
  validateOpts.requireNonEmptyString(name, "webhook.add: name",
281
285
  OpenApiError, "openapi/bad-webhook");
282
- if (name === "__proto__" || name === "constructor" || name === "prototype") {
286
+ if (pick.isPoisonedKey(name)) {
283
287
  throw new OpenApiError("openapi/bad-webhook",
284
288
  "webhook.add: name must not be a reserved object key (" + JSON.stringify(name) + ")");
285
289
  }
@@ -40,6 +40,7 @@
40
40
  * hand-rolled wire format with no compelling benefit at this scope.
41
41
  */
42
42
  var C = require("./constants");
43
+ var boundedMap = require("./bounded-map");
43
44
  var canonicalJson = require("./canonical-json");
44
45
  var httpClient = require("./http-client");
45
46
  var observability = require("./observability");
@@ -144,11 +145,9 @@ function create(opts) {
144
145
  if (typeof name !== "string" || name.length === 0) return;
145
146
  var v = typeof value === "number" && isFinite(value) ? value : 1;
146
147
  var key = _bucketKey(name, attrs);
147
- var b = counters.get(key);
148
- if (!b) {
149
- b = { name: name, attrs: attrs || {}, value: 0, startUnixNano: startUnixNano };
150
- counters.set(key, b);
151
- }
148
+ var b = boundedMap.getOrInsert(counters, key, function () {
149
+ return { name: name, attrs: attrs || {}, value: 0, startUnixNano: startUnixNano };
150
+ });
152
151
  b.value += v;
153
152
  }
154
153
 
@@ -157,11 +156,9 @@ function create(opts) {
157
156
  if (typeof name !== "string" || name.length === 0) return;
158
157
  if (typeof value !== "number" || !isFinite(value)) return;
159
158
  var key = _bucketKey(name, attrs);
160
- var b = observations.get(key);
161
- if (!b) {
162
- b = { name: name, attrs: attrs || {}, sum: 0, count: 0, min: value, max: value, startUnixNano: startUnixNano };
163
- observations.set(key, b);
164
- }
159
+ var b = boundedMap.getOrInsert(observations, key, function () {
160
+ return { name: name, attrs: attrs || {}, sum: 0, count: 0, min: value, max: value, startUnixNano: startUnixNano };
161
+ });
165
162
  b.sum += value;
166
163
  b.count += 1;
167
164
  if (value < b.min) b.min = value;
package/lib/outbox.js CHANGED
@@ -208,29 +208,47 @@ function create(opts) {
208
208
  "envelope", "connectorName", "connectorVersion", "dbName",
209
209
  ], "outbox.create");
210
210
 
211
- if (!opts.externalDb || typeof opts.externalDb.transaction !== "function") {
212
- throw new OutboxError("outbox/bad-externaldb",
213
- "outbox.create: externalDb must be the b.externalDb namespace (with transaction/query)");
214
- }
215
- validateOpts.requireNonEmptyString(opts.table,
216
- "outbox.create: table", OutboxError, "outbox/bad-table");
217
- // Validate the table identifier at create-time so a bad name throws at
218
- // boot, not at first query. b.sql re-quotes the name by construction on
219
- // every emitted statement (the builder owns identifier quoting now).
220
- _validateTableName(opts.table);
221
-
222
- if (typeof opts.publisher !== "function") {
223
- throw new OutboxError("outbox/bad-publisher",
224
- "outbox.create: publisher must be an async function (event) → void");
225
- }
226
- validateOpts.optionalPositiveFinite(opts.pollIntervalMs,
227
- "outbox.create: pollIntervalMs", OutboxError, "outbox/bad-opts");
228
- validateOpts.optionalPositiveFinite(opts.batchSize,
229
- "outbox.create: batchSize", OutboxError, "outbox/bad-opts");
230
- validateOpts.optionalPositiveFinite(opts.maxAttempts,
231
- "outbox.create: maxAttempts", OutboxError, "outbox/bad-opts");
232
- validateOpts.optionalPositiveFinite(opts.claimReclaimMs,
233
- "outbox.create: claimReclaimMs", OutboxError, "outbox/bad-opts");
211
+ validateOpts.shape(opts, {
212
+ externalDb: function (v) {
213
+ if (!v || typeof v.transaction !== "function") {
214
+ throw new OutboxError("outbox/bad-externaldb",
215
+ "outbox.create: externalDb must be the b.externalDb namespace (with transaction/query)");
216
+ }
217
+ },
218
+ table: function (v) {
219
+ validateOpts.requireNonEmptyString(v,
220
+ "outbox.create: table", OutboxError, "outbox/bad-table");
221
+ // Validate the table identifier at create-time so a bad name throws
222
+ // at boot, not at first query. b.sql re-quotes the name by
223
+ // construction on every emitted statement (the builder owns
224
+ // identifier quoting now).
225
+ _validateTableName(v);
226
+ },
227
+ publisher: function (v) {
228
+ if (typeof v !== "function") {
229
+ throw new OutboxError("outbox/bad-publisher",
230
+ "outbox.create: publisher must be an async function (event) → void");
231
+ }
232
+ },
233
+ pollIntervalMs: "optional-positive-finite",
234
+ batchSize: "optional-positive-finite",
235
+ maxAttempts: "optional-positive-finite",
236
+ claimReclaimMs: "optional-positive-finite",
237
+ retryBackoff: {
238
+ optional: true,
239
+ shape: {
240
+ initialMs: "optional-positive-finite",
241
+ maxMs: "optional-positive-finite",
242
+ factor: "optional-positive-finite",
243
+ },
244
+ },
245
+ audit: "optional-boolean",
246
+ name: "optional-string",
247
+ envelope: "optional-string",
248
+ connectorName: "optional-string",
249
+ connectorVersion: "optional-string",
250
+ dbName: "optional-string",
251
+ }, "outbox.create", OutboxError, "outbox/bad-opts");
234
252
 
235
253
  var pollIntervalMs = opts.pollIntervalMs || DEFAULT_POLL_MS;
236
254
  var batchSize = opts.batchSize || DEFAULT_BATCH_SIZE;
@@ -268,20 +286,8 @@ function create(opts) {
268
286
  return Math.floor(ms);
269
287
  }
270
288
 
271
- function _emitMetric(verb, n) {
272
- try { observability().safeEvent("outbox." + verb, n || 1, {}); }
273
- catch (_e) { /* drop-silent */ }
274
- }
275
- function _emitAudit(action, outcome, metadata) {
276
- if (!auditOn) return;
277
- try {
278
- audit().safeEmit({
279
- action: action,
280
- outcome: outcome,
281
- metadata: metadata || {},
282
- });
283
- } catch (_e) { /* drop-silent */ }
284
- }
289
+ var _emitMetric = observability().namespaced("outbox");
290
+ var _emitAudit = audit().namespaced(null, { audit: auditOn });
285
291
 
286
292
  async function enqueue(event, txn) {
287
293
  if (!txn || typeof txn.query !== "function") {
package/lib/pagination.js CHANGED
@@ -50,6 +50,7 @@ var nodeCrypto = require("node:crypto");
50
50
  var C = require("./constants");
51
51
  var canonicalJson = require("./canonical-json");
52
52
  var bCrypto = require("./crypto");
53
+ var validateOpts = require("./validate-opts");
53
54
  var numericBounds = require("./numeric-bounds");
54
55
  var safeJson = require("./safe-json");
55
56
  var safeSql = require("./safe-sql");
@@ -85,11 +86,12 @@ function _toBuf(secret) {
85
86
 
86
87
  function _b64urlEncode(buf) { return bCrypto.toBase64Url(buf); }
87
88
 
88
- function _b64urlDecode(s) {
89
- if (typeof s !== "string") throw new PaginationError("pagination/bad-cursor", "cursor must be a string");
90
- try { return bCrypto.fromBase64Url(s); }
91
- catch (_e) { throw new PaginationError("pagination/bad-cursor", "cursor is not valid base64url"); }
92
- }
89
+ var _b64urlDecode = bCrypto.makeBase64UrlDecoder({
90
+ errorClass: PaginationError,
91
+ code: "pagination/bad-cursor",
92
+ typeMessage: "cursor must be a string",
93
+ badMessage: "cursor is not valid base64url",
94
+ });
93
95
 
94
96
  function _tag(secretBuf, stateJson) {
95
97
  var h = nodeCrypto.createHash("sha3-512");
@@ -345,11 +347,8 @@ function _buildKeysetWhere(orderEntries, cursorVals, forward) {
345
347
  * });
346
348
  */
347
349
  async function cursor(query, opts) {
348
- if (!query || typeof query.where !== "function" || typeof query.orderBy !== "function" ||
349
- typeof query.limit !== "function" || typeof query.all !== "function") {
350
- throw new PaginationError("pagination/bad-query",
351
- "cursor: first arg must be a db Query (must support where, orderBy, limit, all)");
352
- }
350
+ validateOpts.requireMethods(query, ["where", "orderBy", "limit", "all"],
351
+ "cursor: first arg (db Query)", PaginationError, "pagination/bad-query");
353
352
  opts = opts || {};
354
353
  if (opts.secret == null) {
355
354
  throw new PaginationError("pagination/no-secret",
@@ -484,11 +483,8 @@ async function cursor(query, opts) {
484
483
  * page.hasMore; // → true when page < totalPages
485
484
  */
486
485
  async function offset(query, opts) {
487
- if (!query || typeof query.limit !== "function" || typeof query.offset !== "function" ||
488
- typeof query.all !== "function" || typeof query.count !== "function") {
489
- throw new PaginationError("pagination/bad-query",
490
- "offset: first arg must be a db Query (must support limit, offset, all, count)");
491
- }
486
+ validateOpts.requireMethods(query, ["limit", "offset", "all", "count"],
487
+ "offset: first arg (db Query)", PaginationError, "pagination/bad-query");
492
488
  opts = opts || {};
493
489
  var perPage = _resolveLimit({ limit: opts.perPage, max: opts.max, default: opts.default });
494
490
  var page = parseInt(opts.page, 10);
@@ -55,6 +55,8 @@
55
55
  */
56
56
 
57
57
  var C = require("../constants");
58
+ var pick = require("../pick");
59
+ var boundedMap = require("../bounded-map");
58
60
  var atomicFile = require("../atomic-file");
59
61
  var lazyRequire = require("../lazy-require");
60
62
  var numericBounds = require("../numeric-bounds");
@@ -96,7 +98,6 @@ var DEFAULTS = {
96
98
  audit: true,
97
99
  };
98
100
 
99
- var POISONED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
100
101
 
101
102
  // ---- parse ----
102
103
 
@@ -153,7 +154,7 @@ function parse(input, opts) {
153
154
  if (key.length === 0) {
154
155
  throw new SafeEnvError("empty key", "env/empty-key", lineNumber);
155
156
  }
156
- if (POISONED_KEYS.has(key)) {
157
+ if (pick.isPoisonedKey(key)) {
157
158
  throw new SafeEnvError("forbidden key '" + key + "'", "env/poisoned-key", lineNumber);
158
159
  }
159
160
  if (!keyShape.test(key)) {
@@ -162,9 +163,9 @@ function parse(input, opts) {
162
163
  "env/bad-key-shape", lineNumber
163
164
  );
164
165
  }
165
- if (seen.has(key)) {
166
+ boundedMap.requireAbsentMember(seen, key, function () {
166
167
  throw new SafeEnvError("duplicate key '" + key + "'", "env/duplicate-key", lineNumber);
167
- }
168
+ });
168
169
 
169
170
  // Tab at start of value is almost always a paste accident
170
171
  if (rest.charAt(0) === "\t") {
@@ -36,6 +36,7 @@
36
36
  */
37
37
 
38
38
  var C = require("../constants");
39
+ var pick = require("../pick");
39
40
  var numericBounds = require("../numeric-bounds");
40
41
  var safeBuffer = require("../safe-buffer");
41
42
  var { defineClass } = require("../framework-error");
@@ -51,7 +52,6 @@ var DEFAULT_MAX_SECTIONS = 500;
51
52
  var DEFAULT_MAX_KEYS_SECTION = 1_000;
52
53
  var DEFAULT_MAX_VALUE_BYTES = C.BYTES.kib(64);
53
54
 
54
- var FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
55
55
 
56
56
  var TRUE_VALUES = new Set(["true", "yes", "on"]);
57
57
  var FALSE_VALUES = new Set(["false", "no", "off"]);
@@ -136,7 +136,7 @@ function _coerceValue(raw) {
136
136
  }
137
137
 
138
138
  function _validateKey(name) {
139
- if (FORBIDDEN_KEYS.has(name)) {
139
+ if (pick.isPoisonedKey(name)) {
140
140
  throw _err("ini/forbidden-key", "key '" + name + "' is reserved (prototype pollution defense)");
141
141
  }
142
142
  }
@@ -155,7 +155,7 @@ function _ensureSection(root, sectionPath) {
155
155
  }
156
156
  node = existing;
157
157
  } else {
158
- var child = {};
158
+ var child = Object.create(null); // null-proto: see root (pollution-safe)
159
159
  node[seg] = child;
160
160
  node = child;
161
161
  }
@@ -218,7 +218,13 @@ function parse(input, opts) {
218
218
  "ini.parse: onDuplicate must be 'throw' | 'first' | 'last', got " + JSON.stringify(onDuplicate));
219
219
  }
220
220
 
221
- var root = {};
221
+ // Defense-in-depth against prototype pollution: every parsed node is a
222
+ // null-prototype object, so a key like __proto__ / constructor / prototype
223
+ // (already REFUSED by _validateKey → pick.isPoisonedKey before any write)
224
+ // could only ever land as an own property here, never reach Object.prototype.
225
+ // safe-ini's own reads use Object.prototype.hasOwnProperty.call(node, ...),
226
+ // so a null-proto tree is fully compatible.
227
+ var root = Object.create(null);
222
228
  var currentSectionPath = [];
223
229
  var currentSection = root;
224
230
  var sectionCount = 0;
@@ -45,6 +45,9 @@
45
45
  */
46
46
 
47
47
  var C = require("../constants");
48
+ var pick = require("../pick");
49
+ var boundedMap = require("../bounded-map");
50
+ var codepointClass = require("../codepoint-class");
48
51
  var numericBounds = require("../numeric-bounds");
49
52
  var safeBuffer = require("../safe-buffer");
50
53
  var { FrameworkError } = require("../framework-error");
@@ -76,7 +79,6 @@ var DEFAULTS = {
76
79
  maxKeys: 50_000,
77
80
  };
78
81
 
79
- var POISONED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
80
82
 
81
83
  function parse(input, opts) {
82
84
  opts = opts || {};
@@ -222,7 +224,7 @@ function parse(input, opts) {
222
224
 
223
225
  function _parseDottedKey() {
224
226
  var segments = [_parseSingleKeySegment()];
225
- if (POISONED_KEYS.has(segments[0])) {
227
+ if (pick.isPoisonedKey(segments[0])) {
226
228
  throw _err("forbidden key '" + segments[0] + "'", "toml/poisoned-key");
227
229
  }
228
230
  while (true) {
@@ -231,7 +233,7 @@ function parse(input, opts) {
231
233
  _advance();
232
234
  _skipSpacesAndTabs();
233
235
  var seg = _parseSingleKeySegment();
234
- if (POISONED_KEYS.has(seg)) {
236
+ if (pick.isPoisonedKey(seg)) {
235
237
  throw _err("forbidden key '" + seg + "'", "toml/poisoned-key");
236
238
  }
237
239
  segments.push(seg);
@@ -305,7 +307,7 @@ function parse(input, opts) {
305
307
  continue;
306
308
  }
307
309
  var cc = _peekCode();
308
- if ((cc < 0x20 && cc !== 0x09) || cc === 0x7F) {
310
+ if (codepointClass.isForbiddenControlChar(cc)) { // C0 (except TAB) + DEL refusal
309
311
  throw _err("unescaped control char in string", "toml/bad-string");
310
312
  }
311
313
  _advance();
@@ -622,7 +624,7 @@ function parse(input, opts) {
622
624
  var t = table;
623
625
  for (var i = 0; i < segments.length - 1; i++) {
624
626
  var seg = segments[i];
625
- if (POISONED_KEYS.has(seg)) throw _err("forbidden key segment", "toml/poisoned-key");
627
+ if (pick.isPoisonedKey(seg)) throw _err("forbidden key segment", "toml/poisoned-key");
626
628
  if (Object.prototype.hasOwnProperty.call(t, seg)) {
627
629
  var sub = t[seg];
628
630
  if (sub == null || typeof sub !== "object" || Array.isArray(sub)) {
@@ -639,7 +641,7 @@ function parse(input, opts) {
639
641
  }
640
642
  }
641
643
  var last = segments[segments.length - 1];
642
- if (POISONED_KEYS.has(last)) throw _err("forbidden key segment", "toml/poisoned-key");
644
+ if (pick.isPoisonedKey(last)) throw _err("forbidden key segment", "toml/poisoned-key");
643
645
  if (Object.prototype.hasOwnProperty.call(t, last)) {
644
646
  throw _err("duplicate key '" + last + "'", "toml/duplicate-key");
645
647
  }
@@ -665,7 +667,7 @@ function parse(input, opts) {
665
667
  var t = root;
666
668
  for (var i = 0; i < segments.length; i++) {
667
669
  var seg = segments[i];
668
- if (POISONED_KEYS.has(seg)) throw _err("forbidden key segment", "toml/poisoned-key");
670
+ if (pick.isPoisonedKey(seg)) throw _err("forbidden key segment", "toml/poisoned-key");
669
671
  var isLast = (i === segments.length - 1);
670
672
  if (Object.prototype.hasOwnProperty.call(t, seg)) {
671
673
  var sub = t[seg];
@@ -699,9 +701,9 @@ function parse(input, opts) {
699
701
  }
700
702
  if (isLast && !isAoT) {
701
703
  var fullPath = segments.slice(0, i + 1).join(".");
702
- if (definedTables.has(fullPath)) {
704
+ boundedMap.requireAbsentMember(definedTables, fullPath, function () {
703
705
  throw _err("table '" + fullPath + "' defined twice", "toml/redefine");
704
- }
706
+ });
705
707
  definedTables.add(fullPath);
706
708
  current = sub;
707
709
  return;
@@ -48,6 +48,7 @@
48
48
  */
49
49
 
50
50
  var C = require("../constants");
51
+ var pick = require("../pick");
51
52
  var numericBounds = require("../numeric-bounds");
52
53
  var safeBuffer = require("../safe-buffer");
53
54
  var { FrameworkError } = require("../framework-error");
@@ -93,7 +94,6 @@ var BUILT_IN_ENTITIES = { lt: "<", gt: ">", amp: "&", quot: "\"", apos: "'" };
93
94
  // the result is always a clean key→value map. Mirrors the
94
95
  // __proto__/constructor/prototype rejection the toml / yaml / ini
95
96
  // parsers in this family already apply.
96
- var FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
97
97
 
98
98
  function _validateAndCap(name, value, defaultValue, ceiling) {
99
99
  if (value === undefined) return defaultValue;
@@ -199,7 +199,7 @@ function parse(input, opts) {
199
199
  }
200
200
  if (pos === start) throw _err("expected name", "xml/bad-name");
201
201
  var parsed = input.substring(start, pos);
202
- if (FORBIDDEN_KEYS.has(parsed)) {
202
+ if (pick.isPoisonedKey(parsed)) {
203
203
  throw _err("element/attribute name '" + parsed +
204
204
  "' is reserved (prototype-pollution defense)", "xml/forbidden-name");
205
205
  }
@@ -53,6 +53,8 @@
53
53
  */
54
54
 
55
55
  var C = require("../constants");
56
+ var pick = require("../pick");
57
+ var boundedMap = require("../bounded-map");
56
58
  var numericBounds = require("../numeric-bounds");
57
59
  var safeBuffer = require("../safe-buffer");
58
60
  var { FrameworkError } = require("../framework-error");
@@ -85,7 +87,6 @@ var DEFAULTS = {
85
87
  maxKeys: 50_000,
86
88
  };
87
89
 
88
- var POISONED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
89
90
 
90
91
  // YAML 1.2 core-schema scalar resolution. Order matters: null first
91
92
  // (covers ~ and empty), then bool, then int (with base prefixes), then
@@ -333,7 +334,7 @@ function parse(input, opts) {
333
334
  return _decodeSingleQuoted(raw, lineNumber, col);
334
335
  }
335
336
  var trimmed = safeBuffer.stripTrailingHspace(raw);
336
- if (POISONED_KEYS.has(trimmed)) {
337
+ if (pick.isPoisonedKey(trimmed)) {
337
338
  throw new SafeYamlError("forbidden key '" + trimmed + "'",
338
339
  "yaml/poisoned-key", lineNumber, col);
339
340
  }
@@ -368,7 +369,7 @@ function parse(input, opts) {
368
369
  throw new SafeYamlError("non-string mapping key not supported",
369
370
  "yaml/bad-key", ln.lineNumber, ln.indent + 1);
370
371
  }
371
- if (POISONED_KEYS.has(key)) {
372
+ if (pick.isPoisonedKey(key)) {
372
373
  throw new SafeYamlError("forbidden key '" + key + "'",
373
374
  "yaml/poisoned-key", ln.lineNumber, ln.indent + 1);
374
375
  }
@@ -376,10 +377,10 @@ function parse(input, opts) {
376
377
  throw new SafeYamlError("merge key '<<' not supported (anchor-using feature)",
377
378
  "yaml/merge-key-banned", ln.lineNumber, ln.indent + 1);
378
379
  }
379
- if (seen.has(key)) {
380
+ boundedMap.requireAbsentMember(seen, key, function () {
380
381
  throw new SafeYamlError("duplicate mapping key '" + key + "'",
381
382
  "yaml/duplicate-key", ln.lineNumber, ln.indent + 1);
382
- }
383
+ });
383
384
  seen.add(key);
384
385
  _bumpKeys(ln.lineNumber);
385
386
 
@@ -563,7 +564,7 @@ function parse(input, opts) {
563
564
  throw new SafeYamlError("non-string flow-mapping key",
564
565
  "yaml/bad-key", lineNumber, col + p);
565
566
  }
566
- if (POISONED_KEYS.has(key)) {
567
+ if (pick.isPoisonedKey(key)) {
567
568
  throw new SafeYamlError("forbidden key '" + key + "'",
568
569
  "yaml/poisoned-key", lineNumber, col + p);
569
570
  }