@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
@@ -212,7 +212,7 @@ function issue(opts) {
212
212
  "issue: algorithm must be one of " + SUPPORTED_ALGS.join(", "));
213
213
  }
214
214
  var hashAlg = opts.hashAlg || DEFAULT_HASH_ALG;
215
- if (!SUPPORTED_HASH_ALGS[hashAlg]) {
215
+ if (!Object.prototype.hasOwnProperty.call(SUPPORTED_HASH_ALGS, hashAlg)) {
216
216
  throw new AuthError("auth-sd-jwt-vc/bad-hash",
217
217
  "issue: hashAlg must be one of " + Object.keys(SUPPORTED_HASH_ALGS).join(", "));
218
218
  }
@@ -413,8 +413,8 @@ async function verify(presentation, opts) {
413
413
  validateOpts.requireObject(opts, "auth.sdJwtVc.verify", AuthError);
414
414
  validateOpts(opts, [
415
415
  "issuerKeyResolver", "audience", "nonce",
416
- "now", "expectedVct", "maxClockSkewSec",
417
- "requireKeyBinding",
416
+ "now", "expectedVct", "expectedIssuer", "maxClockSkewSec",
417
+ "requireKeyBinding", "requireExp",
418
418
  "keyAttestationVerifier", "requireKeyAttestation",
419
419
  ], "auth.sdJwtVc.verify");
420
420
 
@@ -528,6 +528,15 @@ async function verify(presentation, opts) {
528
528
  throw new AuthError("auth-sd-jwt-vc/iat-future",
529
529
  "verify: iat is in the future (clock skew?)");
530
530
  }
531
+ // SD-JWT-VC makes `exp` OPTIONAL, so the default only checks it when present.
532
+ // Operators with a time-bounded trust expectation (esp. for FOREIGN issuers
533
+ // resolved via issuerKeyResolver) pass requireExp:true to fail closed on a
534
+ // missing / non-numeric exp — mirroring jwt-external's unconditional exp
535
+ // requirement (a credential that never expires is accepted otherwise).
536
+ if (opts.requireExp === true && typeof jwtParsed.payload.exp !== "number") {
537
+ throw new AuthError("auth-sd-jwt-vc/missing-exp",
538
+ "verify: requireExp is set but the credential has no numeric exp claim");
539
+ }
531
540
  if (typeof jwtParsed.payload.exp === "number" && jwtParsed.payload.exp < nowSec - skew) {
532
541
  throw new AuthError("auth-sd-jwt-vc/expired",
533
542
  "verify: token is expired");
@@ -556,7 +565,7 @@ async function verify(presentation, opts) {
556
565
  // to its own DEFAULT_HASH_ALG (`sha3-512`) which broke verification
557
566
  // against spec-conformant issuers when `_sd_alg` was omitted.
558
567
  var hashAlg = jwtParsed.payload._sd_alg || "sha-256";
559
- if (!SUPPORTED_HASH_ALGS[hashAlg]) {
568
+ if (!Object.prototype.hasOwnProperty.call(SUPPORTED_HASH_ALGS, hashAlg)) {
560
569
  throw new AuthError("auth-sd-jwt-vc/bad-hash",
561
570
  "verify: _sd_alg \"" + hashAlg + "\" not supported");
562
571
  }
@@ -636,8 +645,19 @@ async function verify(presentation, opts) {
636
645
  // refused with the precise alg-mismatch error rather than handed to
637
646
  // node:crypto.verify.
638
647
  jwtExternal._assertAlgKtyMatch(kbAlg, holderKey);
639
- var holderKeyObj = nodeCrypto.createPublicKey({ key: holderKey, format: "jwk" });
648
+ var holderKeyObj = bCrypto.importPublicJwk(holderKey);
640
649
  var kbParsed = _verifyJwt(maybeKbJwt, holderKeyObj, kbAlg);
650
+ // A KB-JWT proves holder possession of THIS presentation's bytes (sd_hash),
651
+ // but binds it to a verifier + a fresh challenge only via aud + nonce. If
652
+ // the caller processes a KB-JWT without supplying both, the aud/nonce
653
+ // compares below would silently skip — making the presentation replayable
654
+ // and acceptable at any verifier. Fail CLOSED: require both (oid4vp always
655
+ // supplies them). RFC: SD-JWT-VC §KB-JWT / OIDC4VP nonce binding.
656
+ if (typeof opts.audience !== "string" || opts.audience.length === 0 ||
657
+ typeof opts.nonce !== "string" || opts.nonce.length === 0) {
658
+ throw new AuthError("auth-sd-jwt-vc/missing-replay-binding",
659
+ "verify: a KB-JWT requires opts.audience + opts.nonce to bind the presentation to this verifier and a fresh challenge (replay / audience-redirection defense)");
660
+ }
641
661
  // Constant-time compares: the nonce is a verifier-issued replay-defense
642
662
  // value, so a short-circuiting !== leaks a matching-prefix timing oracle.
643
663
  // Matches the sd_hash check below (the framework's hash/token discipline).
@@ -69,16 +69,16 @@ var MAX_LIST_BYTES = C.BYTES.mib(1);
69
69
 
70
70
  function _b64url(buf) { return bCrypto.toBase64Url(buf); }
71
71
 
72
- function _fromB64url(s) {
73
- try { return bCrypto.fromBase64Url(s); }
74
- catch (_e) {
75
- throw new StatusListError("status-list/bad-base64",
76
- "status-list segment is not valid base64url");
77
- }
78
- }
72
+ // No typeMessage — a non-string input falls into the decode-failure path,
73
+ // matching this sink's original catch-everything behavior.
74
+ var _fromB64url = bCrypto.makeBase64UrlDecoder({
75
+ errorClass: StatusListError,
76
+ code: "status-list/bad-base64",
77
+ badMessage: "status-list segment is not valid base64url",
78
+ });
79
79
 
80
80
  function _validateBits(bits) {
81
- if (!SUPPORTED_BIT_SIZES[bits]) {
81
+ if (!Object.prototype.hasOwnProperty.call(SUPPORTED_BIT_SIZES, bits)) {
82
82
  throw new StatusListError("status-list/bad-bits",
83
83
  "statusList: bits must be 1, 2, 4, or 8 (draft §6.1.1) — got " + bits);
84
84
  }
@@ -249,7 +249,19 @@ async function fromJwt(token, opts) {
249
249
  list: {
250
250
  size: size,
251
251
  bits: bits,
252
- get: function (idx) { return _getAt(inflated, bits, idx); },
252
+ // Bounds-check the relying-party status read, mirroring create().get. An
253
+ // out-of-range index must FAIL CLOSED (throw): _getAt over-reads the buffer
254
+ // and returns 0 for an out-of-bounds index, and status 0 = VALID — so a
255
+ // credential whose status_list index points past the list would otherwise
256
+ // read as "not revoked", a revocation bypass.
257
+ get: function (idx) {
258
+ if (typeof idx !== "number" || idx < 0 || idx >= size || (idx >> 0) !== idx) {
259
+ throw new StatusListError("status-list/bad-index",
260
+ "statusList.fromJwt get: idx out of range — got " + idx + ", size=" + size +
261
+ " (an out-of-range status index fails closed, never reads as status 0/valid)");
262
+ }
263
+ return _getAt(inflated, bits, idx);
264
+ },
253
265
  snapshot: function () { return { size: size, bits: bits, bytes: Buffer.from(inflated) }; },
254
266
  },
255
267
  claims: claims,
@@ -64,6 +64,7 @@ var lazyRequire = require("../lazy-require");
64
64
  var validateOpts = require("../validate-opts");
65
65
  var safeJson = require("../safe-json");
66
66
  var structuredFields = require("../structured-fields");
67
+ var codepointClass = require("../codepoint-class");
67
68
  var C = require("../constants");
68
69
  var { AuthError } = require("../framework-error");
69
70
 
@@ -86,7 +87,7 @@ function _quote(value) {
86
87
  // Reject CTLs and quote-injecting characters.
87
88
  for (var i = 0; i < value.length; i += 1) {
88
89
  var code = value.charCodeAt(i);
89
- if (code < 32 || code === 127) { // ASCII control codepoints
90
+ if (codepointClass.isForbiddenControlChar(code, { forbidTab: true })) { // ASCII control codepoints
90
91
  throw new AuthError("auth-step-up/bad-challenge",
91
92
  "challenge value contains control character at index " + i);
92
93
  }
@@ -378,22 +379,23 @@ function parseChallenge(headerValue) {
378
379
  if (rest.length === 0) return null;
379
380
  var out = { error: null, scope: null, acrValues: null, maxAge: null, raw: {} };
380
381
  // Split on commas at top level, but respect quoted strings.
381
- var tokens = _splitWwwAuth(rest);
382
- for (var i = 0; i < tokens.length; i += 1) {
383
- var token = tokens[i].trim();
384
- var eq = token.indexOf("=");
385
- if (eq === -1) continue;
386
- var key = token.slice(0, eq).trim().toLowerCase();
387
- var val = token.slice(eq + 1).trim();
388
- if (val.length >= 2 && val.charAt(0) === '"' && val.charAt(val.length - 1) === '"') {
389
- val = val.slice(1, val.length - 1);
390
- }
382
+ var kvps = structuredFields.parseKeyValuePieces(_splitWwwAuth(rest));
383
+ structuredFields.forEachKeyValue(kvps, function (key, val) {
384
+ val = structuredFields.stripDoubleQuotes(val);
391
385
  out.raw[key] = val;
392
386
  if (key === "error") out.error = val;
393
387
  else if (key === "scope") out.scope = val;
394
388
  else if (key === "acr_values") out.acrValues = val.split(/\s+/);
395
- else if (key === "max_age") out.maxAge = parseInt(val, 10);
396
- }
389
+ else if (key === "max_age") {
390
+ // Defensive: a malformed max_age (non-numeric / negative) from the
391
+ // server's challenge must not land as NaN — a downstream `age > maxAge`
392
+ // comparison against NaN is always false and would silently mis-handle
393
+ // the freshness requirement. Omit it unless it parses to a non-negative
394
+ // integer, so callers fall back to their own default.
395
+ var ma = parseInt(val, 10);
396
+ if (isFinite(ma) && ma >= 0) out.maxAge = ma;
397
+ }
398
+ });
397
399
  return out;
398
400
  }
399
401
 
@@ -40,6 +40,7 @@ var C = require("./constants");
40
40
  var lazyRequire = require("./lazy-require");
41
41
  var requestHelpers = require("./request-helpers");
42
42
  var validateOpts = require("./validate-opts");
43
+ var numericBounds = require("./numeric-bounds");
43
44
  var { AuthBotChallengeError } = require("./framework-error");
44
45
 
45
46
  var observability = lazyRequire(function () { return require("./observability"); });
@@ -67,7 +68,7 @@ function _requireFunction(name, val) {
67
68
  }
68
69
 
69
70
  function _requirePositiveInt(name, val) {
70
- if (typeof val !== "number" || !isFinite(val) || val < 1 || Math.floor(val) !== val) {
71
+ if (!numericBounds.isPositiveFiniteInt(val)) {
71
72
  throw new AuthBotChallengeError("auth-bot-challenge/bad-opt",
72
73
  name + ": expected positive integer, got " + JSON.stringify(val));
73
74
  }
@@ -88,13 +89,8 @@ function _requireKey(key) {
88
89
  }
89
90
 
90
91
  function _requireSessionStore(store) {
91
- if (!store || typeof store !== "object" ||
92
- typeof store.get !== "function" ||
93
- typeof store.set !== "function" ||
94
- typeof store.del !== "function") {
95
- throw new AuthBotChallengeError("auth-bot-challenge/bad-opt",
96
- "sessionStore must be a b.cache-shaped object (get/set/del)");
97
- }
92
+ validateOpts.requireMethods(store, ["get", "set", "del"],
93
+ "sessionStore (b.cache-shaped)", AuthBotChallengeError, "auth-bot-challenge/bad-opt");
98
94
  }
99
95
 
100
96
  function _requireBotGuard(bg) {
@@ -105,14 +101,8 @@ function _requireBotGuard(bg) {
105
101
  }
106
102
 
107
103
  function _requireLockout(lk) {
108
- if (!lk || typeof lk !== "object" ||
109
- typeof lk.recordFailure !== "function" ||
110
- typeof lk.recordSuccess !== "function" ||
111
- typeof lk.check !== "function") {
112
- throw new AuthBotChallengeError("auth-bot-challenge/bad-opt",
113
- "lockout must be a b.auth.lockout-shaped instance " +
114
- "(recordFailure/recordSuccess/check)");
115
- }
104
+ validateOpts.requireMethods(lk, ["recordFailure", "recordSuccess", "check"],
105
+ "lockout (b.auth.lockout-shaped instance)", AuthBotChallengeError, "auth-bot-challenge/bad-opt");
116
106
  }
117
107
 
118
108
  function _defaultKeyExtractor(req) {
@@ -222,29 +212,9 @@ function create(opts) {
222
212
  var obsInst = opts.observability || null;
223
213
  var clock = opts.clock || Date.now;
224
214
 
225
- function _emitObs(name, labels) {
226
- var sink = obsInst || _safeGlobalObs();
227
- if (!sink) return;
228
- try { sink.event(name, 1, labels); } catch (_e) { /* drop-silent */ }
229
- }
230
-
231
- function _safeGlobalObs() {
232
- try { return observability(); } catch (_e) { return null; }
233
- }
215
+ var _emitObs = observability().makeCounterEmitter(obsInst);
234
216
 
235
- function _emitAudit(action, key, outcome, metadata, req) {
236
- if (!auditInst) return;
237
- try {
238
- var event = {
239
- action: action,
240
- outcome: outcome,
241
- resource: { kind: "auth.bot_challenge", id: key },
242
- metadata: metadata || {},
243
- };
244
- if (req) event.actor = requestHelpers.extractActorContext(req);
245
- auditInst.safeEmit(event);
246
- } catch (_e) { /* audit best-effort */ }
247
- }
217
+ var _emitAudit = requestHelpers.makeResourceAuditEmitter(auditInst, "auth.bot_challenge");
248
218
 
249
219
  async function _readState(key) {
250
220
  try {
@@ -303,7 +273,25 @@ function create(opts) {
303
273
 
304
274
  // ---- Internal staircase advance ----
305
275
 
306
- async function _advanceFailure(key, req) {
276
+ // Per-key serialization: the failure staircase is a read→increment→write on
277
+ // an async store, so concurrent recordFailure calls for the SAME key would
278
+ // both read N and both write N+1 (lost update), letting an attacker fire
279
+ // parallel failures to stay under the challenge / lockout thresholds. A
280
+ // per-key promise chain applies advances for a key sequentially. (Cross-node
281
+ // atomicity additionally needs an atomic store; this fixes the in-process
282
+ // race the gate actually depends on.)
283
+ var _advanceChains = new Map();
284
+ function _advanceFailure(key, req) {
285
+ var prev = _advanceChains.get(key) || Promise.resolve();
286
+ var run = prev.then(function () { return _doAdvanceFailure(key, req); },
287
+ function () { return _doAdvanceFailure(key, req); });
288
+ var tail = run.then(function () {}, function () {});
289
+ _advanceChains.set(key, tail);
290
+ tail.then(function () { if (_advanceChains.get(key) === tail) _advanceChains.delete(key); });
291
+ return run;
292
+ }
293
+
294
+ async function _doAdvanceFailure(key, req) {
307
295
  var now = clock();
308
296
  var state = await _readState(key) || {
309
297
  stage: STATE_NEW, failures: 0, challengedAt: null, passedAt: null,
@@ -148,33 +148,30 @@ async function create(opts) {
148
148
  // earlier required-vs-skip branch above (existsSync → continue when
149
149
  // not entry.required) is honored before we reach this point; the
150
150
  // dest path is then computed from entry.relativePath, not srcPath.
151
- var plain;
152
- var srcFd = nodeFs.openSync(srcPath, "r");
153
- try {
154
- var srcStat = nodeFs.fstatSync(srcFd);
155
- plain = Buffer.alloc(srcStat.size);
156
- var read = 0;
157
- while (read < srcStat.size) {
158
- var n = nodeFs.readSync(srcFd, plain, read, srcStat.size - read, null);
159
- if (n === 0) break;
160
- read += n;
161
- }
162
- if (read !== srcStat.size) {
163
- throw new BackupBundleError("backup-bundle/short-read",
164
- "create: short read on '" + entry.relativePath + "': " + read + " of " + srcStat.size + " bytes");
165
- }
166
- } finally {
167
- try { nodeFs.closeSync(srcFd); } catch (_c) { /* close best-effort */ }
168
- }
151
+ // TOCTOU-safe read via atomic-file; the short-read message keeps the
152
+ // per-entry relativePath context via errorFor.
153
+ var plain = atomicFile.fdSafeReadSync(srcPath, {
154
+ errorFor: function (kind, detail) {
155
+ if (kind === "short-read") {
156
+ return new BackupBundleError("backup-bundle/short-read",
157
+ "create: short read on '" + entry.relativePath + "': " + detail.read + " of " + detail.size + " bytes");
158
+ }
159
+ return undefined;
160
+ },
161
+ });
169
162
  var checksum = bCrypto.checksum(plain);
170
- var encResult = await bCrypto.encryptWithFreshSalt(plain, passphrase);
163
+ // Bind the ciphertext to this blob's canonical relativePath as AEAD
164
+ // associated data. A blob copied to a different manifest entry (the
165
+ // restore-corruption / blob-remap attack) then fails the Poly1305 tag on
166
+ // restore — tamper-evident even on an unsigned bundle (manifest.aadBound).
167
+ var encResult = await bCrypto.encryptWithFreshSalt(plain, passphrase, entry.relativePath);
171
168
  var encPath = _encryptedPathFor(entry.relativePath);
172
169
  var destFull = nodePath.join(outDir, encPath);
173
170
  atomicFile.ensureDir(nodePath.dirname(destFull));
174
171
  atomicFile.writeSync(destFull, encResult.encrypted, { fileMode: 0o600 });
175
172
 
176
173
  var kind = entry.kind || "raw";
177
- if (!backupManifest.VALID_KINDS[kind]) {
174
+ if (!Object.prototype.hasOwnProperty.call(backupManifest.VALID_KINDS, kind)) {
178
175
  throw new BackupBundleError("backup-bundle/bad-kind",
179
176
  "create: files[" + i + "].kind must be one of raw, vault-sealed, plaintext (got '" + kind + "')");
180
177
  }
@@ -212,6 +209,7 @@ async function create(opts) {
212
209
  vaultKeyEnc: wrappedVk.encrypted.toString("base64"),
213
210
  files: fileEntries,
214
211
  metadata: opts.metadata || undefined,
212
+ aadBound: true, // every blob above was sealed with its relativePath as AEAD AAD
215
213
  });
216
214
  // Sign the manifest with the audit-sign keypair so a tampered
217
215
  // manifest fails verification on restore. The signer is best-
@@ -117,7 +117,21 @@ async function deriveKey(passphrase, saltHex, opts) {
117
117
  return hash;
118
118
  }
119
119
 
120
- async function encryptWithPassphrase(plaintext, passphrase, saltHex) {
120
+ // Normalize optional associated-authenticated-data (AAD) into the
121
+ // Uint8Array the AEAD expects, or undefined when none. AAD is NOT encrypted
122
+ // but IS authenticated: ciphertext sealed under one AAD fails the Poly1305
123
+ // tag when decrypted under a different AAD. Backup file blobs pass their
124
+ // canonical relativePath so a blob remapped to a different manifest entry is
125
+ // cryptographically rejected (the blob-remap / restore-corruption defense).
126
+ function _aadBytes(aad) {
127
+ if (aad === undefined || aad === null) return undefined;
128
+ if (Buffer.isBuffer(aad)) return new Uint8Array(aad);
129
+ if (typeof aad === "string") return new Uint8Array(Buffer.from(aad, "utf8"));
130
+ throw new BackupCryptoError("backup-crypto/bad-aad",
131
+ "associated data must be a Buffer or string");
132
+ }
133
+
134
+ async function encryptWithPassphrase(plaintext, passphrase, saltHex, aad) {
121
135
  if (!Buffer.isBuffer(plaintext) && typeof plaintext !== "string") {
122
136
  throw new BackupCryptoError("backup-crypto/bad-plaintext",
123
137
  "encryptWithPassphrase: plaintext must be a Buffer or string");
@@ -125,11 +139,11 @@ async function encryptWithPassphrase(plaintext, passphrase, saltHex) {
125
139
  var plainBuf = Buffer.isBuffer(plaintext) ? plaintext : Buffer.from(plaintext, "utf8");
126
140
  var key = await deriveKey(passphrase, saltHex);
127
141
  var nonce = nodeCrypto.randomBytes(NONCE_BYTES);
128
- var ct = xchacha20poly1305(new Uint8Array(key), nonce).encrypt(new Uint8Array(plainBuf));
142
+ var ct = xchacha20poly1305(new Uint8Array(key), nonce, _aadBytes(aad)).encrypt(new Uint8Array(plainBuf));
129
143
  return Buffer.concat([nonce, Buffer.from(ct)]);
130
144
  }
131
145
 
132
- async function decryptWithPassphrase(encrypted, passphrase, saltHex) {
146
+ async function decryptWithPassphrase(encrypted, passphrase, saltHex, aad) {
133
147
  if (!Buffer.isBuffer(encrypted)) {
134
148
  throw new BackupCryptoError("backup-crypto/bad-input",
135
149
  "decryptWithPassphrase: encrypted must be a Buffer");
@@ -143,11 +157,11 @@ async function decryptWithPassphrase(encrypted, passphrase, saltHex) {
143
157
  var ct = encrypted.subarray(NONCE_BYTES);
144
158
  var plain;
145
159
  try {
146
- plain = xchacha20poly1305(new Uint8Array(key), new Uint8Array(nonce))
160
+ plain = xchacha20poly1305(new Uint8Array(key), new Uint8Array(nonce), _aadBytes(aad))
147
161
  .decrypt(new Uint8Array(ct));
148
162
  } catch (e) {
149
163
  throw new BackupCryptoError("backup-crypto/decrypt-failed",
150
- "XChaCha20-Poly1305 decryption failed (wrong passphrase or tampered ciphertext): " +
164
+ "XChaCha20-Poly1305 decryption failed (wrong passphrase, tampered ciphertext, or blob remapped to a different path): " +
151
165
  ((e && e.message) || String(e)));
152
166
  }
153
167
  return Buffer.from(plain);
@@ -155,11 +169,12 @@ async function decryptWithPassphrase(encrypted, passphrase, saltHex) {
155
169
 
156
170
  // Convenience for the common "encrypt this with a fresh salt" pattern.
157
171
  // Returns the salt as hex so callers can store it alongside the
158
- // ciphertext in the bundle manifest.
159
- async function encryptWithFreshSalt(plaintext, passphrase) {
172
+ // ciphertext in the bundle manifest. `aad` (optional) is bound as AEAD
173
+ // associated data (see _aadBytes).
174
+ async function encryptWithFreshSalt(plaintext, passphrase, aad) {
160
175
  var salt = nodeCrypto.randomBytes(SALT_BYTES);
161
176
  var saltHex = salt.toString("hex");
162
- var encrypted = await encryptWithPassphrase(plaintext, passphrase, saltHex);
177
+ var encrypted = await encryptWithPassphrase(plaintext, passphrase, saltHex, aad);
163
178
  return { encrypted: encrypted, salt: saltHex };
164
179
  }
165
180
 
@@ -53,13 +53,16 @@ var os = require("node:os");
53
53
  var nodePath = require("node:path");
54
54
  var bCrypto = require("../crypto");
55
55
  var atomicFile = require("../atomic-file");
56
+ var C = require("../constants");
56
57
  var backupBundle = require("./bundle");
57
58
  var frameworkFiles = require("../framework-files");
58
59
  var backupManifest = require("./manifest");
59
60
  var lazyRequire = require("../lazy-require");
60
61
  var validateOpts = require("../validate-opts");
61
62
  var numericBounds = require("../numeric-bounds");
63
+ var boundedMap = require("../bounded-map");
62
64
  var audit = lazyRequire(function () { return require("../audit"); });
65
+ var auditEmit = require("../audit-emit");
63
66
  var compliance = lazyRequire(function () { return require("../compliance"); });
64
67
  // lazyRequire ../db so backup stays a leaf module operators can use
65
68
  // without the rest of the framework's DB chain loaded in the same
@@ -228,17 +231,9 @@ function diskStorage(opts) {
228
231
  // ---- Engine ----
229
232
 
230
233
  function _validateStorage(storage) {
231
- if (!storage || typeof storage !== "object") {
232
- throw new BackupError("backup/bad-storage",
233
- "storage backend is required (use b.backup.diskStorage or pass a custom one)");
234
- }
235
- var required = ["writeBundle", "readBundle", "listBundles", "deleteBundle", "hasBundle"];
236
- for (var i = 0; i < required.length; i++) {
237
- if (typeof storage[required[i]] !== "function") {
238
- throw new BackupError("backup/bad-storage",
239
- "storage backend missing method '" + required[i] + "'");
240
- }
241
- }
234
+ validateOpts.requireMethods(storage,
235
+ ["writeBundle", "readBundle", "listBundles", "deleteBundle", "hasBundle"],
236
+ "storage backend", BackupError, "backup/bad-storage");
242
237
  }
243
238
 
244
239
  async function _resolveVaultKeyJson(vaultKeyJsonOpt) {
@@ -488,15 +483,7 @@ function create(opts) {
488
483
  } catch (_e) { /* db not available in this module graph — flush is a no-op */ }
489
484
  }
490
485
 
491
- function _emitAudit(action, info, outcome) {
492
- if (!auditOn) return;
493
- audit().safeEmit({
494
- action: action,
495
- outcome: outcome,
496
- metadata: info || {},
497
- reason: info && info.reason ? info.reason : null,
498
- });
499
- }
486
+ var _emitAudit = auditEmit.gatedReasonEmitter({ audit: auditOn });
500
487
 
501
488
  async function run(runOpts) {
502
489
  runOpts = runOpts || {};
@@ -735,11 +722,16 @@ function create(opts) {
735
722
  try {
736
723
  await storage.readBundle(bundleId, stagingDir);
737
724
  manifestPath = nodePath.join(stagingDir, "manifest.json");
738
- if (!nodeFs.existsSync(manifestPath)) {
739
- throw new BackupError("backup/test-no-manifest",
740
- "manifest.json missing under restored bundle " + bundleId);
741
- }
742
- manifest = backupManifest.parse(nodeFs.readFileSync(manifestPath, "utf8"));
725
+ // Capped fd-bound read inside the scheduled restore-drill tick: an
726
+ // oversized manifest must not OOM the scheduler worker.
727
+ manifest = backupManifest.parse(atomicFile.fdSafeReadSync(manifestPath, {
728
+ maxBytes: C.BYTES.mib(4), encoding: "utf8",
729
+ errorFor: function (kind) {
730
+ if (kind === "enoent") return new BackupError("backup/test-no-manifest", "manifest.json missing under restored bundle " + bundleId);
731
+ if (kind === "too-large") return new BackupError("backup/test-bad-manifest", "manifest.json too large under restored bundle " + bundleId);
732
+ return new BackupError("backup/test-no-manifest", "manifest.json unreadable under restored bundle " + bundleId + ": " + kind);
733
+ },
734
+ }));
743
735
  // Verify the manifest signature so a tampered backup test
744
736
  // surfaces here, not as a regulator finding later.
745
737
  sigVerification = backupManifest.verifySignature(manifest, {
@@ -851,11 +843,17 @@ function verifyManifestSignature(target, opts) {
851
843
  var manifest;
852
844
  if (typeof target === "string") {
853
845
  var manifestPath = nodePath.join(target, "manifest.json");
854
- if (!nodeFs.existsSync(manifestPath)) {
855
- throw new BackupError("backup/no-manifest",
856
- "verifyManifestSignature: manifest.json missing at " + manifestPath);
857
- }
858
- try { manifest = backupManifest.parse(nodeFs.readFileSync(manifestPath, "utf8")); }
846
+ // Capped fd-bound read OUTSIDE the parse try (so a missing/oversized manifest
847
+ // surfaces backup/no-manifest|bad-manifest, not a generic parse error).
848
+ var manifestRaw = atomicFile.fdSafeReadSync(manifestPath, {
849
+ maxBytes: C.BYTES.mib(4), encoding: "utf8",
850
+ errorFor: function (kind) {
851
+ if (kind === "enoent") return new BackupError("backup/no-manifest", "verifyManifestSignature: manifest.json missing at " + manifestPath);
852
+ if (kind === "too-large") return new BackupError("backup/bad-manifest", "verifyManifestSignature: manifest.json too large");
853
+ return new BackupError("backup/bad-manifest", "verifyManifestSignature: unreadable: " + kind);
854
+ },
855
+ });
856
+ try { manifest = backupManifest.parse(manifestRaw); }
859
857
  catch (e) {
860
858
  throw new BackupError("backup/bad-manifest",
861
859
  "verifyManifestSignature: parse failed: " + ((e && e.message) || String(e)));
@@ -1091,18 +1089,10 @@ module.exports = {
1091
1089
  */
1092
1090
  function bundleAdapterStorage(opts) {
1093
1091
  opts = opts || {};
1094
- if (!opts.adapter || typeof opts.adapter !== "object") {
1095
- throw new BackupError("backup/bad-adapter",
1096
- "bundleAdapterStorage: opts.adapter is required (an object with writeFile/readFile/listKeys/deleteKey/hasKey)");
1097
- }
1092
+ validateOpts.requireMethods(opts.adapter,
1093
+ ["writeFile", "readFile", "listKeys", "deleteKey", "hasKey"],
1094
+ "bundleAdapterStorage: opts.adapter", BackupError, "backup/bad-adapter");
1098
1095
  var adapter = opts.adapter;
1099
- var required = ["writeFile", "readFile", "listKeys", "deleteKey", "hasKey"];
1100
- for (var i = 0; i < required.length; i += 1) {
1101
- if (typeof adapter[required[i]] !== "function") {
1102
- throw new BackupError("backup/bad-adapter",
1103
- "bundleAdapterStorage: adapter missing method '" + required[i] + "'");
1104
- }
1105
- }
1106
1096
  // v0.12.8 — `format: "tar"` becomes the default for new bundles.
1107
1097
  // `format: "directory"` opts back into the v0.12.7 file-by-file
1108
1098
  // layout for operators with existing bundles. The format is
@@ -1435,11 +1425,9 @@ function bundleAdapterStorage(opts) {
1435
1425
  if (slash <= 0) continue;
1436
1426
  var bid = key.slice(0, slash);
1437
1427
  if (!_isValidBundleId(bid)) continue;
1438
- var stats = byBundle.get(bid);
1439
- if (!stats) {
1440
- stats = { count: 0, hasTar: false, hasTarGz: false, hasOther: false };
1441
- byBundle.set(bid, stats);
1442
- }
1428
+ var stats = boundedMap.getOrInsert(byBundle, bid, function () {
1429
+ return { count: 0, hasTar: false, hasTarGz: false, hasOther: false };
1430
+ });
1443
1431
  stats.count += 1;
1444
1432
  var rest = key.slice(slash + 1);
1445
1433
  if (rest === "bundle.tar") stats.hasTar = true;
@@ -2211,19 +2199,28 @@ bundleAdapterStorage.fsAdapter = function (fsOpts) {
2211
2199
  // mode 0o600 matches the v0.12.9 directory-format readback
2212
2200
  // discipline — backup payloads carry operator-owned bytes
2213
2201
  // (potentially PHI / PCI / GDPR-scoped); owner-only is the
2214
- // strict posture. wx is not set here because writeFile is
2215
- // the storage primitive (operators legitimately rewrite the
2216
- // same key, e.g. resuming a multipart upload); upper layers
2217
- // (writeBundle's `bundle-exists` check) enforce no-overwrite
2218
- // at the bundle level.
2219
- nodeFs.writeFileSync(path, bytes, { mode: 0o600 });
2202
+ // strict posture. Overwrite of an existing key stays allowed
2203
+ // (operators legitimately rewrite the same key, e.g. resuming a
2204
+ // multipart upload); upper layers (writeBundle's `bundle-exists`
2205
+ // check) enforce no-overwrite at the bundle level. writeSync's
2206
+ // atomic rename preserves that overwrite semantic while refusing a
2207
+ // symlink pre-planted at `path` (CWE-59) and never leaving a torn
2208
+ // payload — a bare writeFileSync did both.
2209
+ atomicFile.writeSync(path, bytes, { fileMode: 0o600 });
2220
2210
  },
2221
2211
  async readFile(key) {
2222
2212
  var path = _keyPath(key);
2223
- if (!nodeFs.existsSync(path)) {
2224
- throw new BackupError("backup/no-key", "fsAdapter: key not found: " + JSON.stringify(key));
2225
- }
2226
- return nodeFs.readFileSync(path);
2213
+ // Capped fd-bound read (no existsSync check-then-read window): fetches a
2214
+ // whole bundle payload, so an oversize/swapped file is an OOM lever. 8 GiB
2215
+ // matches the writeBundle maxBundleBytes ceiling.
2216
+ return atomicFile.fdSafeReadSync(path, {
2217
+ maxBytes: C.BYTES.gib(8),
2218
+ errorFor: function (kind) {
2219
+ if (kind === "enoent") return new BackupError("backup/no-key", "fsAdapter: key not found: " + JSON.stringify(key));
2220
+ if (kind === "too-large") return new BackupError("backup/key-too-large", "fsAdapter: payload for key " + JSON.stringify(key) + " exceeds the read cap");
2221
+ return new BackupError("backup/no-key", "fsAdapter: key " + JSON.stringify(key) + " unreadable: " + kind);
2222
+ },
2223
+ });
2227
2224
  },
2228
2225
  async listKeys(prefix) {
2229
2226
  var out = [];
@@ -2341,17 +2338,8 @@ bundleAdapterStorage.fsAdapter = function (fsOpts) {
2341
2338
  * // path composes through unwrap + read.gz + read.tar.
2342
2339
  */
2343
2340
  bundleAdapterStorage.objectStoreAdapter = function (client, osOpts) {
2344
- if (!client || typeof client !== "object") {
2345
- throw new BackupError("backup/bad-adapter",
2346
- "objectStoreAdapter: client is required (a b.objectStore-shaped object with put / get / head / delete / list)");
2347
- }
2348
- var required = ["put", "get", "head", "delete", "list"];
2349
- for (var i = 0; i < required.length; i += 1) {
2350
- if (typeof client[required[i]] !== "function") {
2351
- throw new BackupError("backup/bad-adapter",
2352
- "objectStoreAdapter: client missing method '" + required[i] + "'");
2353
- }
2354
- }
2341
+ validateOpts.requireMethods(client, ["put", "get", "head", "delete", "list"],
2342
+ "objectStoreAdapter: client", BackupError, "backup/bad-adapter");
2355
2343
  osOpts = osOpts || {};
2356
2344
  var prefix = "";
2357
2345
  if (osOpts.prefix !== undefined && osOpts.prefix !== null) {
@@ -140,7 +140,7 @@ function _validateFileEntry(f, idx, errors) {
140
140
  if (!_isHex(f.salt, true)) {
141
141
  errors.push("files[" + idx + "].salt: required hex string");
142
142
  }
143
- if (typeof f.kind !== "string" || !VALID_KINDS[f.kind]) {
143
+ if (typeof f.kind !== "string" || !Object.prototype.hasOwnProperty.call(VALID_KINDS, f.kind)) {
144
144
  errors.push("files[" + idx + "].kind: must be one of raw, vault-sealed, plaintext");
145
145
  }
146
146
  }
@@ -239,6 +239,11 @@ function create(opts) {
239
239
  if (opts.metadata && typeof opts.metadata === "object" && !Array.isArray(opts.metadata)) {
240
240
  manifest.metadata = Object.assign({}, opts.metadata);
241
241
  }
242
+ // Marks that every file blob was sealed with its relativePath as AEAD
243
+ // associated data (the blob-remap defense). Set on all bundles this version
244
+ // writes; absent on legacy bundles, which restore decrypts without AAD.
245
+ // Carried into the signed payload so it cannot be flipped on a signed bundle.
246
+ if (opts.aadBound === true) manifest.aadBound = true;
242
247
  var v = validate(manifest);
243
248
  if (!v.ok) {
244
249
  throw new BackupManifestError("backup-manifest/invalid",
@@ -271,6 +276,7 @@ function _canonical(manifest, includeSignature) {
271
276
  }),
272
277
  };
273
278
  if (manifest.metadata) canonical.metadata = manifest.metadata;
279
+ if (manifest.aadBound === true) canonical.aadBound = true;
274
280
  // Signature block lives alongside the rest of the manifest fields
275
281
  // and is itself stable-ordered. Sign-time canonicalization (the
276
282
  // bytes the audit-sign keypair signs) excludes the signature field