@blamejs/core 0.15.12 → 0.15.14

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 (365) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +45 -29
  4. package/lib/acme.js +6 -5
  5. package/lib/agent-event-bus.js +18 -4
  6. package/lib/agent-idempotency.js +7 -6
  7. package/lib/agent-orchestrator.js +2 -5
  8. package/lib/agent-saga.js +3 -5
  9. package/lib/agent-snapshot.js +32 -2
  10. package/lib/agent-tenant.js +2 -5
  11. package/lib/ai-adverse-decision.js +2 -15
  12. package/lib/ai-aedt-bias-audit.js +2 -1
  13. package/lib/ai-capability.js +1 -6
  14. package/lib/ai-content-detect.js +1 -3
  15. package/lib/ai-dp.js +1 -5
  16. package/lib/ai-frontier-protocol.js +1 -1
  17. package/lib/ai-input.js +2 -2
  18. package/lib/ai-model-manifest.js +1 -1
  19. package/lib/ai-output.js +16 -7
  20. package/lib/ai-pref.js +3 -8
  21. package/lib/ai-quota.js +3 -14
  22. package/lib/api-key.js +37 -28
  23. package/lib/api-snapshot.js +4 -1
  24. package/lib/app-shutdown.js +7 -1
  25. package/lib/archive-adapters.js +2 -4
  26. package/lib/archive-entry-policy.js +32 -0
  27. package/lib/archive-gz.js +9 -0
  28. package/lib/archive-read.js +14 -24
  29. package/lib/archive-tar-read.js +56 -24
  30. package/lib/archive.js +6 -12
  31. package/lib/arg-parser.js +7 -6
  32. package/lib/asn1-der.js +70 -22
  33. package/lib/asyncapi-traits.js +2 -6
  34. package/lib/atomic-file.js +312 -33
  35. package/lib/audit-chain.js +183 -54
  36. package/lib/audit-daily-review.js +36 -16
  37. package/lib/audit-emit.js +82 -0
  38. package/lib/audit-sign.js +258 -0
  39. package/lib/audit-tools.js +108 -23
  40. package/lib/audit.js +91 -2
  41. package/lib/auth/access-lock.js +7 -29
  42. package/lib/auth/bot-challenge.js +1 -1
  43. package/lib/auth/ciba.js +43 -4
  44. package/lib/auth/dpop.js +29 -36
  45. package/lib/auth/fido-mds3.js +14 -16
  46. package/lib/auth/jwt-external.js +26 -8
  47. package/lib/auth/jwt.js +15 -17
  48. package/lib/auth/lockout.js +24 -27
  49. package/lib/auth/oauth.js +67 -45
  50. package/lib/auth/oid4vci.js +55 -32
  51. package/lib/auth/oid4vp.js +3 -2
  52. package/lib/auth/openid-federation.js +6 -6
  53. package/lib/auth/passkey.js +4 -4
  54. package/lib/auth/password.js +8 -2
  55. package/lib/auth/saml.js +70 -40
  56. package/lib/auth/sd-jwt-vc-holder.js +2 -14
  57. package/lib/auth/sd-jwt-vc-issuer.js +2 -14
  58. package/lib/auth/sd-jwt-vc.js +25 -5
  59. package/lib/auth/status-list.js +21 -9
  60. package/lib/auth/step-up.js +15 -13
  61. package/lib/auth-bot-challenge.js +27 -39
  62. package/lib/backup/bundle.js +18 -20
  63. package/lib/backup/crypto.js +23 -8
  64. package/lib/backup/index.js +55 -67
  65. package/lib/backup/manifest.js +7 -1
  66. package/lib/bounded-map.js +112 -1
  67. package/lib/breach-deadline.js +1 -11
  68. package/lib/break-glass.js +41 -22
  69. package/lib/cache.js +7 -18
  70. package/lib/cbor.js +36 -12
  71. package/lib/cdn-cache-control.js +15 -12
  72. package/lib/cert.js +8 -13
  73. package/lib/chain-writer.js +162 -47
  74. package/lib/cli.js +10 -2
  75. package/lib/client-hints.js +7 -9
  76. package/lib/cloud-events.js +43 -33
  77. package/lib/cluster-storage.js +9 -41
  78. package/lib/cms-codec.js +2 -1
  79. package/lib/codepoint-class.js +84 -1
  80. package/lib/compliance-ai-act-logging.js +1 -6
  81. package/lib/compliance-ai-act-transparency.js +2 -4
  82. package/lib/compliance-eaa.js +2 -12
  83. package/lib/compliance-sanctions-fetcher.js +21 -28
  84. package/lib/compliance-sanctions.js +11 -21
  85. package/lib/compliance.js +3 -10
  86. package/lib/config-drift.js +24 -20
  87. package/lib/content-credentials.js +11 -8
  88. package/lib/content-digest.js +21 -12
  89. package/lib/cookies.js +15 -13
  90. package/lib/cose.js +39 -11
  91. package/lib/cra-report.js +1 -11
  92. package/lib/crdt.js +2 -1
  93. package/lib/crypto-field.js +34 -33
  94. package/lib/crypto.js +138 -7
  95. package/lib/csp.js +239 -3
  96. package/lib/daemon.js +46 -42
  97. package/lib/data-act.js +46 -13
  98. package/lib/db-file-lifecycle.js +14 -6
  99. package/lib/db-query.js +75 -49
  100. package/lib/db.js +66 -27
  101. package/lib/dbsc.js +5 -6
  102. package/lib/ddl-change-control.js +18 -14
  103. package/lib/deprecate.js +4 -5
  104. package/lib/did.js +6 -2
  105. package/lib/dora.js +43 -13
  106. package/lib/dr-runbook.js +1 -1
  107. package/lib/dsa.js +2 -1
  108. package/lib/dsr.js +30 -20
  109. package/lib/early-hints.js +19 -0
  110. package/lib/eat.js +5 -1
  111. package/lib/external-db-migrate.js +21 -22
  112. package/lib/external-db.js +74 -30
  113. package/lib/fda-21cfr11.js +42 -13
  114. package/lib/fdx.js +22 -18
  115. package/lib/file-upload.js +80 -66
  116. package/lib/flag-evaluation-context.js +5 -8
  117. package/lib/flag-providers.js +6 -2
  118. package/lib/forms.js +1 -1
  119. package/lib/framework-error.js +12 -0
  120. package/lib/framework-schema.js +19 -0
  121. package/lib/fsm.js +7 -12
  122. package/lib/gate-contract.js +911 -39
  123. package/lib/gdpr-ropa.js +22 -22
  124. package/lib/graphql-federation.js +18 -5
  125. package/lib/guard-agent-registry.js +2 -1
  126. package/lib/guard-all.js +3 -2
  127. package/lib/guard-archive.js +9 -30
  128. package/lib/guard-auth.js +23 -80
  129. package/lib/guard-cidr.js +20 -96
  130. package/lib/guard-csv.js +135 -196
  131. package/lib/guard-domain.js +23 -106
  132. package/lib/guard-dsn.js +17 -14
  133. package/lib/guard-email.js +46 -53
  134. package/lib/guard-envelope.js +2 -2
  135. package/lib/guard-event-bus-topic.js +2 -1
  136. package/lib/guard-filename.js +12 -60
  137. package/lib/guard-graphql.js +28 -75
  138. package/lib/guard-html-wcag.js +15 -2
  139. package/lib/guard-html.js +74 -128
  140. package/lib/guard-idempotency-key.js +2 -1
  141. package/lib/guard-image.js +280 -77
  142. package/lib/guard-imap-command.js +9 -10
  143. package/lib/guard-jmap.js +1 -1
  144. package/lib/guard-json.js +101 -109
  145. package/lib/guard-jsonpath.js +20 -88
  146. package/lib/guard-jwt.js +32 -114
  147. package/lib/guard-list-id.js +2 -7
  148. package/lib/guard-list-unsubscribe.js +2 -7
  149. package/lib/guard-mail-compose.js +5 -6
  150. package/lib/guard-mail-move.js +3 -2
  151. package/lib/guard-mail-query.js +5 -3
  152. package/lib/guard-mail-sieve.js +6 -7
  153. package/lib/guard-managesieve-command.js +6 -5
  154. package/lib/guard-markdown.js +76 -110
  155. package/lib/guard-message-id.js +5 -6
  156. package/lib/guard-mime.js +20 -99
  157. package/lib/guard-oauth.js +19 -73
  158. package/lib/guard-pdf.js +65 -72
  159. package/lib/guard-pop3-command.js +13 -14
  160. package/lib/guard-posture-chain.js +2 -1
  161. package/lib/guard-regex.js +24 -99
  162. package/lib/guard-saga-config.js +2 -1
  163. package/lib/guard-shell.js +22 -87
  164. package/lib/guard-smtp-command.js +9 -12
  165. package/lib/guard-sql.js +15 -13
  166. package/lib/guard-stream-args.js +2 -1
  167. package/lib/guard-svg.js +103 -149
  168. package/lib/guard-template.js +23 -80
  169. package/lib/guard-tenant-id.js +2 -1
  170. package/lib/guard-text.js +592 -0
  171. package/lib/guard-time.js +27 -95
  172. package/lib/guard-uuid.js +21 -93
  173. package/lib/guard-xml.js +76 -106
  174. package/lib/guard-yaml.js +24 -60
  175. package/lib/html-balance.js +7 -3
  176. package/lib/http-client-cache.js +5 -12
  177. package/lib/http-client-cookie-jar.js +35 -16
  178. package/lib/http-client.js +233 -74
  179. package/lib/http-message-signature.js +3 -2
  180. package/lib/i18n-messageformat.js +5 -7
  181. package/lib/i18n.js +83 -26
  182. package/lib/iab-tcf.js +3 -2
  183. package/lib/importmap-integrity.js +41 -1
  184. package/lib/inbox.js +8 -8
  185. package/lib/incident-report.js +12 -27
  186. package/lib/ip-utils.js +49 -6
  187. package/lib/jobs.js +3 -2
  188. package/lib/json-patch.js +1 -1
  189. package/lib/json-path.js +24 -3
  190. package/lib/jtd.js +2 -2
  191. package/lib/keychain.js +6 -18
  192. package/lib/legal-hold.js +30 -23
  193. package/lib/log-stream-cloudwatch.js +44 -112
  194. package/lib/log-stream-otlp-grpc.js +17 -14
  195. package/lib/log-stream-otlp.js +16 -80
  196. package/lib/log-stream-webhook.js +20 -92
  197. package/lib/log.js +2 -2
  198. package/lib/mail-agent.js +2 -2
  199. package/lib/mail-arc-sign.js +8 -3
  200. package/lib/mail-arf.js +4 -3
  201. package/lib/mail-auth.js +43 -69
  202. package/lib/mail-bimi.js +35 -55
  203. package/lib/mail-bounce.js +3 -3
  204. package/lib/mail-crypto-pgp.js +3 -2
  205. package/lib/mail-crypto-smime.js +71 -6
  206. package/lib/mail-dav.js +8 -35
  207. package/lib/mail-deploy.js +15 -14
  208. package/lib/mail-dkim.js +19 -38
  209. package/lib/mail-greylist.js +17 -30
  210. package/lib/mail-helo.js +4 -7
  211. package/lib/mail-journal.js +13 -9
  212. package/lib/mail-mdn.js +12 -7
  213. package/lib/mail-rbl.js +7 -12
  214. package/lib/mail-scan.js +5 -7
  215. package/lib/mail-send-deliver.js +33 -20
  216. package/lib/mail-server-imap.js +32 -87
  217. package/lib/mail-server-jmap.js +39 -52
  218. package/lib/mail-server-managesieve.js +29 -83
  219. package/lib/mail-server-mx.js +33 -74
  220. package/lib/mail-server-net.js +177 -0
  221. package/lib/mail-server-pop3.js +30 -83
  222. package/lib/mail-server-rate-limit.js +30 -6
  223. package/lib/mail-server-registry.js +1 -1
  224. package/lib/mail-server-submission.js +34 -73
  225. package/lib/mail-server-tls.js +98 -2
  226. package/lib/mail-spam-score.js +9 -12
  227. package/lib/mail-store-fts.js +1 -1
  228. package/lib/mail-store.js +3 -2
  229. package/lib/mail.js +28 -13
  230. package/lib/markup-escape.js +31 -0
  231. package/lib/markup-tokenizer.js +78 -0
  232. package/lib/mcp-tool-registry.js +26 -23
  233. package/lib/mcp.js +7 -5
  234. package/lib/mdoc.js +36 -14
  235. package/lib/metrics.js +46 -33
  236. package/lib/middleware/age-gate.js +3 -23
  237. package/lib/middleware/api-encrypt.js +13 -24
  238. package/lib/middleware/assetlinks.js +2 -7
  239. package/lib/middleware/bearer-auth.js +2 -1
  240. package/lib/middleware/body-parser.js +41 -52
  241. package/lib/middleware/bot-disclose.js +7 -10
  242. package/lib/middleware/bot-guard.js +28 -28
  243. package/lib/middleware/clear-site-data.js +5 -1
  244. package/lib/middleware/compression.js +9 -0
  245. package/lib/middleware/cors.js +32 -23
  246. package/lib/middleware/csrf-protect.js +73 -23
  247. package/lib/middleware/daily-byte-quota.js +12 -30
  248. package/lib/middleware/deny-response.js +19 -1
  249. package/lib/middleware/fetch-metadata.js +35 -4
  250. package/lib/middleware/idempotency-key.js +4 -20
  251. package/lib/middleware/network-allowlist.js +61 -30
  252. package/lib/middleware/rate-limit.js +45 -44
  253. package/lib/middleware/scim-server.js +5 -3
  254. package/lib/middleware/security-headers.js +33 -7
  255. package/lib/middleware/security-txt.js +2 -6
  256. package/lib/middleware/speculation-rules.js +6 -3
  257. package/lib/middleware/tus-upload.js +14 -29
  258. package/lib/middleware/web-app-manifest.js +2 -7
  259. package/lib/migrations.js +4 -13
  260. package/lib/mime-parse.js +34 -17
  261. package/lib/module-loader.js +44 -0
  262. package/lib/money.js +105 -0
  263. package/lib/mtls-ca.js +125 -41
  264. package/lib/network-byte-quota.js +4 -18
  265. package/lib/network-dane.js +8 -6
  266. package/lib/network-dns-resolver.js +98 -7
  267. package/lib/network-dns.js +9 -2
  268. package/lib/network-dnssec.js +18 -17
  269. package/lib/network-heartbeat.js +3 -2
  270. package/lib/network-smtp-policy.js +52 -46
  271. package/lib/network-tls.js +67 -47
  272. package/lib/network-tsig.js +2 -2
  273. package/lib/nis2-report.js +2 -12
  274. package/lib/nist-crosswalk.js +1 -1
  275. package/lib/nonce-store.js +81 -3
  276. package/lib/ntp-check.js +28 -0
  277. package/lib/numeric-bounds.js +31 -8
  278. package/lib/object-store/azure-blob-bucket-ops.js +2 -6
  279. package/lib/object-store/azure-blob.js +6 -47
  280. package/lib/object-store/gcs-bucket-ops.js +4 -2
  281. package/lib/object-store/gcs.js +14 -75
  282. package/lib/object-store/http-put.js +2 -7
  283. package/lib/object-store/http-request.js +157 -0
  284. package/lib/object-store/local.js +37 -17
  285. package/lib/object-store/sigv4-bucket-ops.js +2 -1
  286. package/lib/object-store/sigv4.js +10 -79
  287. package/lib/observability-otlp-exporter.js +29 -29
  288. package/lib/observability.js +95 -0
  289. package/lib/openapi-paths-builder.js +7 -3
  290. package/lib/otel-export.js +7 -10
  291. package/lib/outbox.js +54 -41
  292. package/lib/pagination.js +11 -15
  293. package/lib/parsers/safe-env.js +5 -4
  294. package/lib/parsers/safe-ini.js +10 -4
  295. package/lib/parsers/safe-toml.js +11 -9
  296. package/lib/parsers/safe-xml.js +3 -3
  297. package/lib/parsers/safe-yaml.js +28 -9
  298. package/lib/pick.js +63 -5
  299. package/lib/pipl-cn.js +69 -59
  300. package/lib/privacy-pass.js +8 -6
  301. package/lib/problem-details.js +2 -5
  302. package/lib/pubsub.js +4 -8
  303. package/lib/queue-local.js +10 -3
  304. package/lib/redact.js +9 -4
  305. package/lib/render.js +4 -2
  306. package/lib/request-helpers.js +334 -29
  307. package/lib/resource-access-lock.js +3 -3
  308. package/lib/restore-bundle.js +46 -18
  309. package/lib/restore-rollback.js +10 -4
  310. package/lib/restore.js +24 -22
  311. package/lib/retention.js +23 -9
  312. package/lib/router.js +17 -4
  313. package/lib/safe-async.js +457 -0
  314. package/lib/safe-buffer.js +137 -6
  315. package/lib/safe-decompress.js +2 -1
  316. package/lib/safe-dns.js +2 -1
  317. package/lib/safe-ical.js +14 -28
  318. package/lib/safe-icap.js +1 -1
  319. package/lib/safe-json.js +51 -8
  320. package/lib/safe-jsonpath.js +6 -5
  321. package/lib/safe-mime.js +25 -29
  322. package/lib/safe-redirect.js +2 -5
  323. package/lib/safe-schema.js +7 -6
  324. package/lib/safe-sieve.js +1 -1
  325. package/lib/safe-sql.js +126 -0
  326. package/lib/safe-vcard.js +13 -27
  327. package/lib/sandbox-worker.js +15 -2
  328. package/lib/sandbox.js +4 -3
  329. package/lib/scheduler.js +22 -13
  330. package/lib/sec-cyber.js +82 -37
  331. package/lib/seeders.js +4 -11
  332. package/lib/self-update-standalone-verifier.js +16 -0
  333. package/lib/self-update.js +92 -69
  334. package/lib/session-device-binding.js +112 -66
  335. package/lib/session.js +219 -7
  336. package/lib/sql.js +228 -81
  337. package/lib/sse.js +6 -10
  338. package/lib/static.js +201 -111
  339. package/lib/storage.js +4 -2
  340. package/lib/structured-fields.js +275 -16
  341. package/lib/template.js +7 -5
  342. package/lib/tenant-quota.js +53 -38
  343. package/lib/tsa.js +16 -17
  344. package/lib/validate-opts.js +154 -8
  345. package/lib/vault/index.js +5 -0
  346. package/lib/vault/passphrase-ops.js +22 -26
  347. package/lib/vault/passphrase-source.js +8 -3
  348. package/lib/vault/rotate.js +13 -18
  349. package/lib/vault/seal-pem-file.js +34 -49
  350. package/lib/vault-aad.js +3 -2
  351. package/lib/vc.js +3 -8
  352. package/lib/vendor/MANIFEST.json +10 -10
  353. package/lib/vendor/public-suffix-list.dat +23 -10
  354. package/lib/vendor/public-suffix-list.data.js +5498 -5494
  355. package/lib/vex.js +35 -13
  356. package/lib/web-push-vapid.js +23 -20
  357. package/lib/webhook-dispatcher.js +706 -0
  358. package/lib/webhook.js +59 -19
  359. package/lib/websocket-channels.js +9 -7
  360. package/lib/websocket.js +8 -4
  361. package/lib/worm.js +3 -4
  362. package/lib/ws-client.js +65 -56
  363. package/lib/x509-chain.js +44 -0
  364. package/package.json +1 -1
  365. package/sbom.cdx.json +6 -6
package/lib/dsr.js CHANGED
@@ -114,6 +114,7 @@ var bCrypto = require("./crypto");
114
114
  var lazyRequire = require("./lazy-require");
115
115
  var validateOpts = require("./validate-opts");
116
116
  var safeSql = require("./safe-sql");
117
+ var boundedMap = require("./bounded-map");
117
118
  var { defineClass } = require("./framework-error");
118
119
 
119
120
  var DsrError = defineClass("DsrError", { alwaysPermanent: true });
@@ -209,7 +210,7 @@ var POSTURE_DEADLINE_MS = Object.freeze({
209
210
  // + extension). Read-only at module scope.
210
211
  function _deadlineForPosture(posture) {
211
212
  if (typeof posture !== "string") return POSTURE_DEADLINE_MS["default"];
212
- return POSTURE_DEADLINE_MS[posture] || POSTURE_DEADLINE_MS["default"];
213
+ return Object.prototype.hasOwnProperty.call(POSTURE_DEADLINE_MS, posture) ? POSTURE_DEADLINE_MS[posture] : POSTURE_DEADLINE_MS["default"];
213
214
  }
214
215
 
215
216
  function _now() { return Date.now(); }
@@ -372,10 +373,7 @@ function create(opts) {
372
373
  } catch (_e) { /* drop-silent — audit sink */ }
373
374
  }
374
375
 
375
- function _emitMetric(verb, n, labels) {
376
- try { observability().safeEvent("dsr." + verb, n || 1, labels || {}); }
377
- catch (_e) { /* drop-silent */ }
378
- }
376
+ var _emitMetric = observability().namespaced("dsr");
379
377
 
380
378
  function _newTicketId() {
381
379
  var ts = String(Date.now()).slice(-7); // last 7 chars of unix-ms timestamp; collision-resistant when paired with the random suffix
@@ -894,10 +892,10 @@ function memoryTicketStore() {
894
892
  var byId = new Map();
895
893
  return {
896
894
  insert: async function (ticket) {
897
- if (byId.has(ticket.id)) {
895
+ boundedMap.requireAbsent(byId, ticket.id, function () {
898
896
  throw new DsrError("dsr/duplicate-ticket-id",
899
897
  "memoryTicketStore: duplicate ticket id " + ticket.id);
900
- }
898
+ });
901
899
  byId.set(ticket.id, Object.assign({}, ticket));
902
900
  },
903
901
  get: async function (id) {
@@ -919,10 +917,10 @@ function memoryTicketStore() {
919
917
  return out;
920
918
  },
921
919
  update: async function (id, ticket) {
922
- if (!byId.has(id)) {
920
+ boundedMap.requirePresent(byId, id, function () {
923
921
  throw new DsrError("dsr/ticket-not-found",
924
922
  "memoryTicketStore: ticket " + id + " not found for update");
925
- }
923
+ });
926
924
  byId.set(id, Object.assign({}, ticket));
927
925
  },
928
926
  delete: async function (id) {
@@ -1023,10 +1021,8 @@ function _ensureDsrSealTable() {
1023
1021
  function dbTicketStore(opts) {
1024
1022
  opts = opts || {};
1025
1023
  var db = opts.db;
1026
- if (!db || typeof db.runSql !== "function" || typeof db.prepare !== "function") {
1027
- throw new DsrError("dsr/bad-db",
1028
- "dbTicketStore: opts.db must be a b.db-shaped handle (with runSql + prepare)");
1029
- }
1024
+ validateOpts.requireMethods(db, ["runSql", "prepare"],
1025
+ "dbTicketStore: opts.db (b.db-shaped handle)", DsrError, "dsr/bad-db");
1030
1026
  var tableRaw = opts.table || "dsr_tickets";
1031
1027
  var qTable, qEmailIdx, qStatusIdx;
1032
1028
  try {
@@ -1202,12 +1198,26 @@ function dbTicketStore(opts) {
1202
1198
  SUBJECT_FILTER_SPEC.forEach(function (spec) {
1203
1199
  var supplied = filter.subject[spec.key];
1204
1200
  if (!supplied) return;
1205
- var column = vaulted ? spec.hashCol : spec.plainCol;
1206
- var match = vaulted
1207
- ? (function () { var d = cryptoField().computeDerived(DSR_SEAL_TABLE, spec.sealField, supplied); return d ? d.value : null; })()
1208
- : supplied;
1209
- conds.push(column + " = " + spec.param);
1210
- params[spec.param] = match;
1201
+ if (!vaulted) {
1202
+ conds.push(spec.plainCol + " = " + spec.param);
1203
+ params[spec.param] = supplied;
1204
+ return;
1205
+ }
1206
+ // Vaulted: match BOTH the active keyed-MAC digest AND the legacy
1207
+ // salted-sha3 digest via the dual-read, so a ticket written before the
1208
+ // keyed-MAC default flip is still FOUND. A single-value equality on the
1209
+ // keyed-MAC digest silently skips those pre-flip rows, so an Art.17
1210
+ // erasure purge (list-by-subject → delete) would leave the subject's
1211
+ // legacy-hashed PII-bearing tickets behind. Mirrors subject.eraseHard.
1212
+ var cand = cryptoField().lookupHashCandidates(DSR_SEAL_TABLE, spec.sealField, supplied);
1213
+ var values = cand && cand.values ? cand.values : [];
1214
+ if (values.length === 0) return;
1215
+ var placeholders = values.map(function (v, i) {
1216
+ var p = spec.param + "_" + i;
1217
+ params[p] = v;
1218
+ return p;
1219
+ });
1220
+ conds.push(spec.hashCol + " IN (" + placeholders.join(", ") + ")");
1211
1221
  });
1212
1222
  }
1213
1223
 
@@ -1459,7 +1469,7 @@ var STATE_RULES = Object.freeze({
1459
1469
  function stateRules(state) {
1460
1470
  if (typeof state !== "string" || state.length === 0) return null;
1461
1471
  // Direct posture-name lookup first
1462
- if (STATE_RULES[state]) return Object.assign({}, STATE_RULES[state]);
1472
+ if (Object.prototype.hasOwnProperty.call(STATE_RULES, state)) return Object.assign({}, STATE_RULES[state]);
1463
1473
  // 2-letter state abbreviation lookup (case-insensitive)
1464
1474
  var u = state.toUpperCase();
1465
1475
  var keys = Object.keys(STATE_RULES);
@@ -170,6 +170,13 @@ function send(res, opts) {
170
170
  throw new EarlyHintsError("early-hints/bad-header-value",
171
171
  "earlyHints.send: header '" + name + "' must be a string or string[]", true);
172
172
  }
173
+ var vals = Array.isArray(canonical[name]) ? canonical[name] : [canonical[name]];
174
+ for (var vi = 0; vi < vals.length; vi += 1) {
175
+ if (typeof vals[vi] === "string" && _hasHeaderInjection(vals[vi])) {
176
+ throw new EarlyHintsError("early-hints/bad-header-value",
177
+ "earlyHints.send: header '" + name + "' value contains a CR/LF/NUL header-injection character", true);
178
+ }
179
+ }
173
180
  headers[name] = canonical[name];
174
181
  }
175
182
 
@@ -182,9 +189,21 @@ function send(res, opts) {
182
189
  }
183
190
  }
184
191
 
192
+ // A header value reaching res.writeEarlyHints must carry no CR/LF/NUL — those
193
+ // split the interim response into attacker-chosen headers. Node rejects them
194
+ // too, but screen defensively (matching the cookies / security-headers
195
+ // boundary) so the framework surfaces a typed error, not a raw Node throw.
196
+ function _hasHeaderInjection(s) {
197
+ return s.indexOf("\r") !== -1 || s.indexOf("\n") !== -1 || s.indexOf("\0") !== -1;
198
+ }
199
+
185
200
  function _validateLink(linkValue, idx) {
186
201
  validateOpts.requireNonEmptyString(linkValue, "earlyHints.send.link[" + idx + "]",
187
202
  EarlyHintsError, "early-hints/bad-link");
203
+ if (_hasHeaderInjection(linkValue)) {
204
+ throw new EarlyHintsError("early-hints/bad-link",
205
+ "link[" + idx + "] contains a CR/LF/NUL header-injection character");
206
+ }
188
207
  if (linkValue.length > LINK_MAX_BYTES) {
189
208
  throw new EarlyHintsError("early-hints/bad-link",
190
209
  "link[" + idx + "] exceeds " + LINK_MAX_BYTES + " bytes");
package/lib/eat.js CHANGED
@@ -205,7 +205,11 @@ async function verify(eat, opts) {
205
205
  // Debug status — refuse a token that can't prove debug is disabled.
206
206
  if (opts.requireDebugDisabled === true) {
207
207
  var ds = raw.get(EAT.dbgstat);
208
- if (typeof ds !== "number" || ds < DBGSTAT.disabled) {
208
+ // Accept ONLY a defined disabled enum value (1..4). `ds < disabled` alone
209
+ // let any out-of-range value >= 1 (5, 99, non-integers) pass as "disabled"
210
+ // — a fail-open on an undefined dbgstat (RFC 9711 §3.3 defines 0..4 only).
211
+ if (typeof ds !== "number" || !Number.isInteger(ds) ||
212
+ ds < DBGSTAT.disabled || ds > DBGSTAT["disabled-fully-and-permanently"]) {
209
213
  throw new EatError("eat/debug-not-disabled",
210
214
  "eat.verify: requireDebugDisabled — dbgstat is " +
211
215
  (ds === undefined ? "absent" : (DBGSTAT_BY_VALUE[ds] || ds)) + ", not a disabled state");
@@ -49,6 +49,7 @@
49
49
  * - externaldb.migrate.lock.released { holder }
50
50
  */
51
51
  var nodePath = require("node:path");
52
+ var moduleLoader = require("./module-loader");
52
53
  var atomicFile = require("./atomic-file");
53
54
  var canonicalJson = require("./canonical-json");
54
55
  var { sha3Hash } = require("./crypto");
@@ -379,19 +380,10 @@ function _list(dir) {
379
380
  }
380
381
 
381
382
  function _loadMigration(file, dir) {
382
- var fullPath = nodePath.join(dir, file);
383
- // Drop the require cache so a test/dev that edits a file picks up the
384
- // new content. Matches lib/migrations.js semantics.
385
- try { delete require.cache[require.resolve(fullPath)]; } catch (_e) { /* not yet cached */ }
386
- var mod;
387
- // Operator-supplied migration file — dynamic by design, won't survive
388
- // SEA / pkg bundling. External DB migration tooling is host-CLI scope,
389
- // not framework-internal scope.
390
- try { mod = require(fullPath); } // allow:dynamic-require — operator-supplied migration
391
- catch (e) {
392
- throw _err("externaldb-migrate/load-failed",
383
+ var mod = moduleLoader.requireFresh(nodePath.join(dir, file), function (e) {
384
+ return _err("externaldb-migrate/load-failed",
393
385
  "migration '" + file + "' failed to load: " + ((e && e.message) || String(e)));
394
- }
386
+ });
395
387
  if (!mod || typeof mod.up !== "function") {
396
388
  throw _err("externaldb-migrate/missing-up",
397
389
  "migration '" + file + "' must export an `up(xdb, ctx)` function");
@@ -437,16 +429,23 @@ function _resolveBackendName(opts) {
437
429
 
438
430
  function create(opts) {
439
431
  opts = opts || {};
440
- validateOpts(opts, [
441
- "dir", "backend", "audit", "staleAfterMs",
442
- "schemaIntrospect", "ranBy", "signHistory",
443
- ], "b.externalDb.migrate");
444
- validateOpts.requireNonEmptyString(opts.dir, "externalDb.migrate.create: opts.dir (path to migrations directory)", ExternalDbMigrateError, "externaldb-migrate/no-dir");
445
- validateOpts.optionalFiniteNonNegative(opts.staleAfterMs, "externalDb.migrate: staleAfterMs", ExternalDbMigrateError, "externaldb-migrate/bad-stale");
446
- validateOpts.auditShape(opts.audit, "externalDb.migrate", ExternalDbMigrateError, "externaldb-migrate/bad-audit");
447
- validateOpts.optionalFunction(opts.schemaIntrospect,
448
- "externalDb.migrate: schemaIntrospect", ExternalDbMigrateError,
449
- "externaldb-migrate/bad-introspect");
432
+ validateOpts.shape(opts, {
433
+ dir: { rule: "required-string", code: "externaldb-migrate/no-dir",
434
+ label: "externalDb.migrate.create: opts.dir (path to migrations directory)" },
435
+ staleAfterMs: { rule: "optional-non-negative", code: "externaldb-migrate/bad-stale",
436
+ label: "externalDb.migrate: staleAfterMs" },
437
+ audit: function (value) {
438
+ validateOpts.auditShape(value, "externalDb.migrate", ExternalDbMigrateError, "externaldb-migrate/bad-audit");
439
+ },
440
+ schemaIntrospect: { rule: "optional-function", code: "externaldb-migrate/bad-introspect",
441
+ label: "externalDb.migrate: schemaIntrospect" },
442
+ backend: { rule: "optional-string", code: "externaldb-migrate/bad-backend",
443
+ label: "externalDb.migrate: backend (externalDb backend name)" },
444
+ ranBy: { rule: "optional-string", code: "externaldb-migrate/bad-ran-by",
445
+ label: "externalDb.migrate: ranBy (schema-history actor)" },
446
+ signHistory: { rule: "optional-boolean", code: "externaldb-migrate/bad-sign-history",
447
+ label: "externalDb.migrate: signHistory" },
448
+ }, "externalDb.migrate", ExternalDbMigrateError, "externaldb-migrate/bad-opt");
450
449
  var dir = opts.dir;
451
450
  var audit = opts.audit || null;
452
451
  var schemaIntrospect = typeof opts.schemaIntrospect === "function"
@@ -36,7 +36,9 @@
36
36
  * External-database integration for app data — Postgres / MySQL / SQLite / MongoDB connection pooling, retry, circuit breaker, classification routing, residency enforcement, and audit hooks.
37
37
  */
38
38
  var retryHelper = require("./retry");
39
+ var safeBuffer = require("./safe-buffer");
39
40
  var bCrypto = require("./crypto");
41
+ var numericBounds = require("./numeric-bounds");
40
42
  var C = require("./constants");
41
43
  var dbRoleContext = require("./db-role-context");
42
44
  var externalDbMigrate = require("./external-db-migrate");
@@ -45,6 +47,7 @@ var { boot } = require("./log");
45
47
  var safeAsync = require("./safe-async");
46
48
  var safeSql = require("./safe-sql");
47
49
  var validateOpts = require("./validate-opts");
50
+ var codepointClass = require("./codepoint-class");
48
51
  var { ExternalDbError } = require("./framework-error");
49
52
 
50
53
  var log = boot("external-db");
@@ -336,6 +339,50 @@ function _classifyStatement(sql) {
336
339
  return _STATEMENT_CLASS_MAP[kw] || "OTHER";
337
340
  }
338
341
 
342
+ // True when a top-level (paren-depth 0, outside any string / comment / quoted-
343
+ // identifier / dollar-quoted span) RETURNING keyword is present — a DML
344
+ // statement with RETURNING yields rows to the caller. Quote-aware so the literal
345
+ // word inside a value ('see RETURNING docs') is not mistaken for a clause, and
346
+ // depth-aware so a RETURNING buried in a CTE / subquery paren does not count for
347
+ // the OUTER statement. Reuses the shared opaque-span scanner; single linear pass.
348
+ function _hasTopLevelReturning(sql) {
349
+ var n = sql.length, i = 0, depth = 0;
350
+ while (i < n) {
351
+ var skipped = _skipOpaqueSpan(sql, i);
352
+ if (skipped === -1) return false; // unterminated span — nothing top-level beyond
353
+ if (skipped !== i) { i = skipped; continue; }
354
+ var ch = sql.charAt(i);
355
+ if (ch === "(") { depth += 1; i += 1; continue; }
356
+ if (ch === ")") { depth -= 1; i += 1; continue; }
357
+ if (_isIdentStart(ch)) {
358
+ var we = i + 1;
359
+ while (we < n && _isIdentChar(sql.charAt(we))) we += 1;
360
+ if (depth === 0 && sql.slice(i, we).toUpperCase() === "RETURNING") return true;
361
+ i = we;
362
+ continue;
363
+ }
364
+ i += 1;
365
+ }
366
+ return false;
367
+ }
368
+
369
+ // Does this statement return a row set to the caller? Distinct from the
370
+ // residency gate's read/write split: an `EXPLAIN ANALYZE INSERT` is a WRITE for
371
+ // the gate yet returns plan rows; a DML `... RETURNING` is a write that yields
372
+ // rows. Consumers (e.g. cluster-storage's local exec choosing .all() vs .run())
373
+ // must use .all() for any row-returning statement, including a WITH (CTE) read
374
+ // that the leading keyword alone would mis-route to .run() and SILENTLY DROP.
375
+ // Statement classes that produce a row set for the caller (SELECT and the
376
+ // READ_INFO family — SHOW / DESCRIBE / PRAGMA). Keyed by class name, mirroring
377
+ // _STATEMENT_CLASS_MAP / _RESIDENCY_READ_CLASS.
378
+ var _ROW_RETURNING_CLASS = Object.freeze({ SELECT: true, READ_INFO: true });
379
+
380
+ function statementReturnsRows(sql) {
381
+ if (typeof sql !== "string" || sql.length === 0) return false;
382
+ if (_ROW_RETURNING_CLASS[_classifyStatement(sql)] === true) return true;
383
+ return _hasTopLevelReturning(sql);
384
+ }
385
+
339
386
  // Statement classes that place no rows on the backend, so the cross-
340
387
  // border residency write gate lets them pass without a row tag. Every
341
388
  // other class — DML, ROUTINE (CALL / EXECUTE / DO), a COPY ... FROM
@@ -417,11 +464,7 @@ function _otelDbAttributes(b, sql, includeStatement) {
417
464
  // legitimately-quoted relation name still surfaces in the audit row.
418
465
  var _RELATION_RE = /\b(?:FROM|INTO|UPDATE|JOIN|TABLE|COPY)\s+((?:"[^"]+"|`[^`]+`|[A-Za-z_][\w$]*)(?:\.(?:"[^"]+"|`[^`]+`|[A-Za-z_][\w$]*))?)/ig;
419
466
  function _hasControlChar(s) {
420
- for (var i = 0; i < s.length; i += 1) {
421
- var c = s.charCodeAt(i);
422
- if (c < 0x20 || c === 0x7f) return true;
423
- }
424
- return false;
467
+ return codepointClass.firstControlCharOffset(s, { forbidTab: true }) !== -1;
425
468
  }
426
469
  function _extractTargetRelation(sql) {
427
470
  if (typeof sql !== "string" || sql.length === 0) return null;
@@ -747,7 +790,7 @@ function init(opts) {
747
790
  throw _err("INVALID_CONFIG",
748
791
  "backend '" + name + "': applicationName must not contain CR, LF, or NUL characters", true);
749
792
  }
750
- if (applicationName.length > C.BYTES.bytes(63)) {
793
+ if (safeBuffer.byteLengthOf(applicationName) > C.BYTES.bytes(63)) {
751
794
  throw _err("INVALID_CONFIG",
752
795
  "backend '" + name + "': applicationName exceeds Postgres 63-byte limit (got " +
753
796
  applicationName.length + ")", true);
@@ -1440,7 +1483,7 @@ function _buildSessionGucsStatements(sessionGucs) {
1440
1483
  // that bloats query logs and consumes max_stack_depth. The cap
1441
1484
  // is generous for legitimate tenant identifiers but rejects
1442
1485
  // amplification.
1443
- if (value.length > C.BYTES.kib(4)) {
1486
+ if (safeBuffer.byteLengthOf(value) > C.BYTES.kib(4)) {
1444
1487
  throw _err("INVALID_SESSION_GUCS",
1445
1488
  "sessionGucs['" + name + "']: value exceeds 4 KiB cap (got " +
1446
1489
  value.length + " chars)", true);
@@ -1725,11 +1768,9 @@ function _buildReplicas(backendName, cfg) {
1725
1768
  "backend '" + backendName + "': replicas[" + i + "].query must be a function", true);
1726
1769
  }
1727
1770
  var weight = r.weight !== undefined ? r.weight : 1;
1728
- if (typeof weight !== "number" || !isFinite(weight) || weight <= 0 ||
1729
- Math.floor(weight) !== weight) {
1730
- throw _err("INVALID_CONFIG",
1731
- "backend '" + backendName + "': replicas[" + i + "].weight must be a positive integer", true);
1732
- }
1771
+ numericBounds.requirePositiveFiniteInt(weight,
1772
+ "backend '" + backendName + "': replicas[" + i + "].weight", ExternalDbError,
1773
+ "INVALID_CONFIG", null, { permanent: true });
1733
1774
  var replicaTag = r.residencyTag || "unrestricted";
1734
1775
  var allowCrossBorder = r.allowCrossBorder === true;
1735
1776
  if (!_residencyCompatible(primaryTag, replicaTag) && !allowCrossBorder) {
@@ -1813,12 +1854,19 @@ async function _readQuery(sql, params, opts) {
1813
1854
  // was explicitly configured allowCrossBorder (which is audited).
1814
1855
  var _readPosture = _activePosture();
1815
1856
  var _tagPresent = opts.rowResidencyTag && typeof opts.rowResidencyTag === "string";
1857
+ // A replica only carries a real residency CONSTRAINT when its tag names a
1858
+ // specific region. "unrestricted" / "global" mean "no constraint" (the tag
1859
+ // defaults to "unrestricted" when none is configured), so such a replica may
1860
+ // serve any region's rows and must NOT require opts.rowResidencyTag — gating
1861
+ // it would over-reject every regulated read to an untagged replica.
1862
+ var _replicaConstrained = replica.residencyTag &&
1863
+ replica.residencyTag !== "unrestricted" && replica.residencyTag !== "global";
1816
1864
  // Fail CLOSED when the row's region is not identified: a regulated read to a
1817
- // residency-tagged replica without opts.rowResidencyTag would otherwise route
1818
- // residency-restricted rows to an arbitrary-region replica with no check at
1819
- // all (symmetric with the write gate's RESIDENCY_GATE_REQUIRED).
1865
+ // residency-CONSTRAINED replica without opts.rowResidencyTag would otherwise
1866
+ // route residency-restricted rows to an arbitrary-region replica with no check
1867
+ // at all (symmetric with the write gate's RESIDENCY_GATE_REQUIRED).
1820
1868
  if (!_tagPresent && _crossBorderRegulated(_readPosture) &&
1821
- replica.residencyTag && !replica.allowCrossBorder) {
1869
+ _replicaConstrained && !replica.allowCrossBorder) {
1822
1870
  _emit("db.residency.replica.tag_required", "denied", {
1823
1871
  backend: b.name, replicaIdx: replica.index,
1824
1872
  replicaTag: replica.residencyTag, posture: _readPosture,
@@ -2066,15 +2114,9 @@ function configurePool(backendName, opts) {
2066
2114
  "configurePool: unknown option '" + k + "'. Allowed: " + allowed.join(", "), true);
2067
2115
  }
2068
2116
  }
2069
- function _requirePosInt(name, value) {
2070
- if (typeof value !== "number" || !isFinite(value) || value <= 0 || Math.floor(value) !== value) {
2071
- throw _err("INVALID_CONFIG",
2072
- "configurePool: " + name + " must be a positive integer, got " + JSON.stringify(value), true);
2073
- }
2074
- }
2075
- if (opts.min !== undefined) _requirePosInt("min", opts.min);
2076
- if (opts.max !== undefined) _requirePosInt("max", opts.max);
2077
- if (opts.idleTimeoutMs !== undefined) _requirePosInt("idleTimeoutMs", opts.idleTimeoutMs);
2117
+ numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
2118
+ ["min", "max", "idleTimeoutMs"], "configurePool", ExternalDbError,
2119
+ "INVALID_CONFIG", { permanent: true });
2078
2120
  if (opts.min !== undefined && opts.max !== undefined && opts.min > opts.max) {
2079
2121
  throw _err("INVALID_CONFIG", "configurePool: min must be <= max", true);
2080
2122
  }
@@ -2146,11 +2188,8 @@ function _connectAs(rawConnect, query, opts) {
2146
2188
  throw _err("INVALID_CONFIG", "connectAs: applicationName must be a string", true);
2147
2189
  }
2148
2190
  if (opts.statementTimeoutMs !== undefined) {
2149
- if (typeof opts.statementTimeoutMs !== "number" || !isFinite(opts.statementTimeoutMs) ||
2150
- opts.statementTimeoutMs <= 0 || Math.floor(opts.statementTimeoutMs) !== opts.statementTimeoutMs) {
2151
- throw _err("INVALID_CONFIG",
2152
- "connectAs: statementTimeoutMs must be a positive integer", true);
2153
- }
2191
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.statementTimeoutMs,
2192
+ "connectAs: statementTimeoutMs", ExternalDbError, "INVALID_CONFIG", { permanent: true });
2154
2193
  }
2155
2194
  if (opts.gucs !== undefined && (typeof opts.gucs !== "object" || opts.gucs === null)) {
2156
2195
  throw _err("INVALID_CONFIG", "connectAs: gucs must be an object", true);
@@ -2586,6 +2625,11 @@ module.exports = {
2586
2625
  // lock tables live on the externalDb side. See lib/external-db-migrate.js.
2587
2626
  migrate: externalDbMigrate,
2588
2627
  Pool: Pool,
2628
+ // Shared SQL statement classifier (CTE / EXPLAIN aware): does the statement
2629
+ // return a row set? Internal — consumed by cluster-storage's local exec to
2630
+ // choose .all() vs .run() so a WITH (CTE) read is not mis-routed and its rows
2631
+ // lost. Underscore-prefixed: framework-internal, not an operator API surface.
2632
+ _statementReturnsRows: statementReturnsRows,
2589
2633
  _resetForTest: _resetForTest,
2590
2634
  _extractTargetRelation: _extractTargetRelation,
2591
2635
  };
@@ -91,6 +91,18 @@ var DEFAULT_SIGNATURE_MEANINGS = Object.freeze([
91
91
  // must carry §11.10(e) shape. Operators add more via opts.gxpNamespaces.
92
92
  var DEFAULT_GXP_NAMESPACES = Object.freeze(["subject", "consent", "db", "breakglass"]);
93
93
 
94
+ // Non-mutating verb segments. A GxP-namespace event whose final action segment
95
+ // is NOT one of these is treated as a record MODIFICATION and must carry the
96
+ // §11.10(e) before/after/reason shape (fail-closed — an unknown verb is assumed
97
+ // to mutate). hasOwnProperty-guarded lookup (untrusted action key).
98
+ var READ_VERBS = Object.freeze({
99
+ read: 1, viewed: 1, view: 1, get: 1, got: 1, list: 1, listed: 1, query: 1,
100
+ queried: 1, access: 1, accessed: 1, export: 1, exported: 1, history: 1,
101
+ isgranted: 1, check: 1, checked: 1, verify: 1, verified: 1, lookup: 1,
102
+ search: 1, searched: 1, fetch: 1, fetched: 1, render: 1, rendered: 1,
103
+ download: 1, downloaded: 1,
104
+ });
105
+
94
106
  // §11.10(e) — every modification audit must carry timestamp, actor,
95
107
  // before/after pair, and a reason. The framework's audit row shape
96
108
  // already carries `recordedAt` (timestamp) + `actorUserId` + `reason`;
@@ -114,11 +126,15 @@ function _hasRequiredAuditShape(row) {
114
126
  if (!row.action || typeof row.action !== "string") {
115
127
  return { ok: false, reason: "row missing action verb (§11.10(e))" };
116
128
  }
117
- // Modification-shaped events (verbs containing "update" / "modif" /
118
- // "delete" / "rectif" / "erase" / "set") must carry before/after.
129
+ // §11.10(e) requires before/after on every record MODIFICATION. Inferring
130
+ // "is this a modification" from a denylist of mutating verbs silently let
131
+ // off-list verbs (anonymize / revoke / overwrite / replace / merge /
132
+ // withdraw / restrict / objection …) bypass the requirement. Fail CLOSED
133
+ // instead: a GxP-namespace event is treated as a modification UNLESS its
134
+ // final verb segment is on an explicit non-mutating READ allowlist.
119
135
  var verb = row.action.toLowerCase();
120
- var modShape = /\.(update|updated|modif|modified|delete|deleted|rectif|rectified|erase|erased|set|setrole|put|patched)\b/.test(verb) ||
121
- /\.(update|delete|modif|set|put|patch|rectif|erase)/.test(verb);
136
+ var lastSeg = verb.indexOf(".") === -1 ? verb : verb.slice(verb.lastIndexOf(".") + 1);
137
+ var modShape = !Object.prototype.hasOwnProperty.call(READ_VERBS, lastSeg);
122
138
  if (modShape) {
123
139
  var meta = row.metadata;
124
140
  // Audit chain stores metadata as a JSON string when read back —
@@ -181,14 +197,18 @@ function posture(opts) {
181
197
  "audit", "signWith", "verifyWith", "meanings", "gxpNamespaces",
182
198
  "interceptAudit", "now",
183
199
  ], "fda21cfr11.posture");
184
- validateOpts.auditShape(opts.audit, "fda21cfr11.posture",
185
- Fda21Cfr11Error, "fda21cfr11/bad-audit");
186
- validateOpts.optionalFunction(opts.signWith,
187
- "fda21cfr11.posture: signWith", Fda21Cfr11Error, "fda21cfr11/bad-signer");
188
- validateOpts.optionalFunction(opts.verifyWith,
189
- "fda21cfr11.posture: verifyWith", Fda21Cfr11Error, "fda21cfr11/bad-verifier");
190
- validateOpts.optionalFunction(opts.now,
191
- "fda21cfr11.posture: now", Fda21Cfr11Error, "fda21cfr11/bad-now");
200
+ validateOpts.shape(opts, {
201
+ audit: function (value) {
202
+ validateOpts.auditShape(value, "fda21cfr11.posture",
203
+ Fda21Cfr11Error, "fda21cfr11/bad-audit");
204
+ },
205
+ signWith: { rule: "optional-function", code: "fda21cfr11/bad-signer" },
206
+ verifyWith: { rule: "optional-function", code: "fda21cfr11/bad-verifier" },
207
+ now: { rule: "optional-function", code: "fda21cfr11/bad-now" },
208
+ meanings: { rule: "optional-string-array", code: "fda21cfr11/bad-meanings" },
209
+ gxpNamespaces: { rule: "optional-string-array", code: "fda21cfr11/bad-gxp-namespaces" },
210
+ interceptAudit: { rule: "optional-boolean", code: "fda21cfr11/bad-intercept-audit" },
211
+ }, "fda21cfr11.posture", Fda21Cfr11Error, "fda21cfr11/bad-opts");
192
212
 
193
213
  var auditMod = opts.audit && typeof opts.audit.safeEmit === "function" ? opts.audit : null;
194
214
  var signWith = typeof opts.signWith === "function" ? opts.signWith : null;
@@ -259,7 +279,16 @@ function posture(opts) {
259
279
  }, "denied");
260
280
  return { ok: false, reason: "record-hash-mismatch" };
261
281
  }
262
- if (verifyWith && signed.signature) {
282
+ if (verifyWith) {
283
+ // A configured verifier means a signature is REQUIRED — a null/empty
284
+ // signature must fail closed (recordHash alone is self-consistency, not
285
+ // authentication; accepting it is alg:none-style signature stripping).
286
+ if (!signed.signature) {
287
+ _emit("fda21cfr11.signature.verified", {
288
+ printedName: signed.printedName, ok: false, reason: "signature-required",
289
+ }, "denied");
290
+ return { ok: false, reason: "signature-required" };
291
+ }
263
292
  var sigBuf = Buffer.from(signed.signature, "base64");
264
293
  var payload = JSON.stringify({
265
294
  printedName: signed.printedName,
package/lib/fdx.js CHANGED
@@ -168,10 +168,13 @@ function bind(opts) {
168
168
  throw FdxError.factory("fdx/bad-auth-server",
169
169
  "fdx.bind: authServer object required");
170
170
  }
171
- validateOpts.requireNonEmptyString(opts.authServer.issuer,
172
- "fdx.bind: authServer.issuer", FdxError, "BAD_ISSUER");
173
- validateOpts.requireNonEmptyString(opts.authServer.jwksUri,
174
- "fdx.bind: authServer.jwksUri", FdxError, "BAD_JWKS_URI");
171
+ validateOpts.shape(opts.authServer, {
172
+ issuer: { rule: "required-string", code: "BAD_ISSUER", label: "fdx.bind: authServer.issuer" },
173
+ jwksUri: { rule: "required-string", code: "BAD_JWKS_URI", label: "fdx.bind: authServer.jwksUri" },
174
+ }, "fdx.bind: authServer", FdxError, "fdx/bad-auth-server",
175
+ // fapi2 is forwarded verbatim to fapi2.assertOAuthConfig, which owns
176
+ // its shape ({ pkce, dpop?, mtls?, par }) — validated there, not here.
177
+ { allow: ["fapi2"] });
175
178
 
176
179
  if (!Array.isArray(opts.resources) || opts.resources.length === 0) {
177
180
  throw FdxError.factory("fdx/bad-resources",
@@ -315,20 +318,21 @@ function consentReceipt(opts) {
315
318
  if (!opts || typeof opts !== "object") {
316
319
  throw FdxError.factory("fdx/bad-opts", "fdx.consentReceipt: opts required");
317
320
  }
318
- validateOpts.requireNonEmptyString(opts.dataProvider,
319
- "fdx.consentReceipt: dataProvider", FdxError, "BAD_DATA_PROVIDER");
320
- validateOpts.requireNonEmptyString(opts.consumerRef,
321
- "fdx.consentReceipt: consumerRef", FdxError, "BAD_CONSUMER_REF");
322
- validateOpts.requireNonEmptyString(opts.thirdParty,
323
- "fdx.consentReceipt: thirdParty", FdxError, "BAD_THIRD_PARTY");
324
- validateOpts.requireNonEmptyString(opts.revocationUrl,
325
- "fdx.consentReceipt: revocationUrl", FdxError, "BAD_REVOCATION_URL");
326
- if (!Array.isArray(opts.scopes) || opts.scopes.length === 0) {
327
- throw FdxError.factory("fdx/bad-scopes",
328
- "fdx.consentReceipt: scopes must be a non-empty array");
329
- }
330
- numericBounds.requirePositiveFiniteIntIfPresent(opts.durationMs,
331
- "fdx.consentReceipt: durationMs", FdxError, "BAD_DURATION");
321
+ validateOpts.shape(opts, {
322
+ dataProvider: { rule: "required-string", code: "BAD_DATA_PROVIDER" },
323
+ consumerRef: { rule: "required-string", code: "BAD_CONSUMER_REF" },
324
+ thirdParty: { rule: "required-string", code: "BAD_THIRD_PARTY" },
325
+ revocationUrl: { rule: "required-string", code: "BAD_REVOCATION_URL" },
326
+ scopes: function (value) {
327
+ if (!Array.isArray(value) || value.length === 0) {
328
+ throw FdxError.factory("fdx/bad-scopes",
329
+ "fdx.consentReceipt: scopes must be a non-empty array");
330
+ }
331
+ },
332
+ durationMs: function (value, label) {
333
+ numericBounds.requirePositiveFiniteIntIfPresent(value, label, FdxError, "BAD_DURATION");
334
+ },
335
+ }, "fdx.consentReceipt", FdxError, "fdx/bad-opts");
332
336
 
333
337
  var issuedAt = Date.now();
334
338
  var expiresAt = issuedAt + (opts.durationMs || C.TIME.weeks(52));