@blamejs/blamejs-shop 0.5.12 → 0.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (521) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/vendor/MANIFEST.json +632 -416
  4. package/lib/vendor/blamejs/.clusterfuzzlite/Dockerfile +21 -10
  5. package/lib/vendor/blamejs/.clusterfuzzlite/build.sh +30 -9
  6. package/lib/vendor/blamejs/.github/dependabot.yml +8 -0
  7. package/lib/vendor/blamejs/.github/workflows/actions-lint.yml +3 -3
  8. package/lib/vendor/blamejs/.github/workflows/cflite_batch.yml +7 -2
  9. package/lib/vendor/blamejs/.github/workflows/cflite_pr.yml +7 -2
  10. package/lib/vendor/blamejs/.github/workflows/ci.yml +21 -21
  11. package/lib/vendor/blamejs/.github/workflows/codeql.yml +3 -3
  12. package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +4 -4
  13. package/lib/vendor/blamejs/.github/workflows/release-container.yml +7 -7
  14. package/lib/vendor/blamejs/.github/workflows/scorecard.yml +3 -3
  15. package/lib/vendor/blamejs/.github/workflows/sha-to-tag-verify.yml +1 -1
  16. package/lib/vendor/blamejs/.gitignore +2 -0
  17. package/lib/vendor/blamejs/CHANGELOG.md +122 -0
  18. package/lib/vendor/blamejs/NOTICE +26 -1
  19. package/lib/vendor/blamejs/SECURITY.md +1 -1
  20. package/lib/vendor/blamejs/api-snapshot.json +207 -5
  21. package/lib/vendor/blamejs/docker/keycloak/realm-blamejs-test.json +29 -0
  22. package/lib/vendor/blamejs/examples/wiki/DEPLOY.md +1 -0
  23. package/lib/vendor/blamejs/examples/wiki/lib/build-app.js +31 -0
  24. package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +109 -0
  25. package/lib/vendor/blamejs/examples/wiki/nginx.conf +106 -0
  26. package/lib/vendor/blamejs/examples/wiki/package-lock.json +1 -1
  27. package/lib/vendor/blamejs/examples/wiki/test/e2e.js +92 -0
  28. package/lib/vendor/blamejs/fuzz/asn1-der.fuzz.js +28 -0
  29. package/lib/vendor/blamejs/fuzz/cms-codec.fuzz.js +17 -0
  30. package/lib/vendor/blamejs/fuzz/link-header.fuzz.js +18 -0
  31. package/lib/vendor/blamejs/index.js +6 -0
  32. package/lib/vendor/blamejs/lib/a2a.js +4 -4
  33. package/lib/vendor/blamejs/lib/agent-idempotency.js +18 -2
  34. package/lib/vendor/blamejs/lib/agent-saga.js +36 -8
  35. package/lib/vendor/blamejs/lib/agent-snapshot.js +63 -6
  36. package/lib/vendor/blamejs/lib/app-shutdown.js +23 -20
  37. package/lib/vendor/blamejs/lib/archive-read.js +8 -0
  38. package/lib/vendor/blamejs/lib/archive-tar.js +12 -2
  39. package/lib/vendor/blamejs/lib/arg-parser.js +49 -5
  40. package/lib/vendor/blamejs/lib/asn1-der.js +12 -0
  41. package/lib/vendor/blamejs/lib/audit-sign.js +47 -1
  42. package/lib/vendor/blamejs/lib/audit.js +71 -8
  43. package/lib/vendor/blamejs/lib/auth/ciba.js +14 -2
  44. package/lib/vendor/blamejs/lib/auth/fido-mds3.js +58 -47
  45. package/lib/vendor/blamejs/lib/auth/jwt.js +34 -0
  46. package/lib/vendor/blamejs/lib/auth/oauth.js +90 -13
  47. package/lib/vendor/blamejs/lib/auth/oid4vci.js +20 -17
  48. package/lib/vendor/blamejs/lib/auth/openid-federation.js +214 -23
  49. package/lib/vendor/blamejs/lib/auth/saml.js +38 -20
  50. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +10 -4
  51. package/lib/vendor/blamejs/lib/backup/bundle.js +13 -3
  52. package/lib/vendor/blamejs/lib/backup/index.js +16 -5
  53. package/lib/vendor/blamejs/lib/backup/manifest.js +18 -5
  54. package/lib/vendor/blamejs/lib/base32.js +21 -0
  55. package/lib/vendor/blamejs/lib/break-glass.js +19 -10
  56. package/lib/vendor/blamejs/lib/bundler.js +5 -1
  57. package/lib/vendor/blamejs/lib/cache-status.js +3 -7
  58. package/lib/vendor/blamejs/lib/calendar.js +21 -1
  59. package/lib/vendor/blamejs/lib/cbor.js +12 -1
  60. package/lib/vendor/blamejs/lib/cert.js +29 -11
  61. package/lib/vendor/blamejs/lib/cli.js +94 -24
  62. package/lib/vendor/blamejs/lib/cloud-events.js +2 -2
  63. package/lib/vendor/blamejs/lib/cluster-provider-db.js +13 -3
  64. package/lib/vendor/blamejs/lib/codepoint-class.js +85 -0
  65. package/lib/vendor/blamejs/lib/compliance-ai-act-prohibited.js +8 -4
  66. package/lib/vendor/blamejs/lib/compliance-ai-act-transparency.js +19 -4
  67. package/lib/vendor/blamejs/lib/compliance-ai-act.js +6 -8
  68. package/lib/vendor/blamejs/lib/content-credentials.js +1 -1
  69. package/lib/vendor/blamejs/lib/content-digest.js +2 -1
  70. package/lib/vendor/blamejs/lib/cookies.js +13 -3
  71. package/lib/vendor/blamejs/lib/cose.js +31 -16
  72. package/lib/vendor/blamejs/lib/crypto-field.js +32 -0
  73. package/lib/vendor/blamejs/lib/crypto-oprf.js +11 -2
  74. package/lib/vendor/blamejs/lib/crypto-xwing.js +22 -4
  75. package/lib/vendor/blamejs/lib/crypto.js +314 -19
  76. package/lib/vendor/blamejs/lib/csp.js +11 -3
  77. package/lib/vendor/blamejs/lib/daemon.js +270 -24
  78. package/lib/vendor/blamejs/lib/dark-patterns.js +5 -2
  79. package/lib/vendor/blamejs/lib/db-declare-view.js +2 -2
  80. package/lib/vendor/blamejs/lib/db-query.js +34 -7
  81. package/lib/vendor/blamejs/lib/db.js +18 -6
  82. package/lib/vendor/blamejs/lib/dbsc.js +17 -0
  83. package/lib/vendor/blamejs/lib/dev.js +21 -2
  84. package/lib/vendor/blamejs/lib/dsr.js +157 -102
  85. package/lib/vendor/blamejs/lib/external-db.js +41 -2
  86. package/lib/vendor/blamejs/lib/file-upload.js +25 -7
  87. package/lib/vendor/blamejs/lib/flag-providers.js +27 -15
  88. package/lib/vendor/blamejs/lib/forms.js +113 -27
  89. package/lib/vendor/blamejs/lib/gate-contract.js +23 -10
  90. package/lib/vendor/blamejs/lib/gdpr-ropa.js +8 -1
  91. package/lib/vendor/blamejs/lib/guard-agent-registry.js +1 -1
  92. package/lib/vendor/blamejs/lib/guard-archive.js +2 -2
  93. package/lib/vendor/blamejs/lib/guard-cidr.js +27 -1
  94. package/lib/vendor/blamejs/lib/guard-graphql.js +30 -11
  95. package/lib/vendor/blamejs/lib/guard-html.js +26 -53
  96. package/lib/vendor/blamejs/lib/guard-image.js +24 -5
  97. package/lib/vendor/blamejs/lib/guard-imap-command.js +10 -2
  98. package/lib/vendor/blamejs/lib/guard-jmap.js +19 -3
  99. package/lib/vendor/blamejs/lib/guard-jwt.js +18 -2
  100. package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +2 -2
  101. package/lib/vendor/blamejs/lib/guard-managesieve-command.js +10 -2
  102. package/lib/vendor/blamejs/lib/guard-markdown.js +11 -24
  103. package/lib/vendor/blamejs/lib/guard-pdf.js +18 -2
  104. package/lib/vendor/blamejs/lib/guard-pop3-command.js +10 -2
  105. package/lib/vendor/blamejs/lib/guard-regex.js +8 -5
  106. package/lib/vendor/blamejs/lib/guard-svg.js +31 -32
  107. package/lib/vendor/blamejs/lib/guard-time.js +1 -1
  108. package/lib/vendor/blamejs/lib/guard-uuid.js +2 -2
  109. package/lib/vendor/blamejs/lib/html-balance.js +3 -3
  110. package/lib/vendor/blamejs/lib/http-client-cache.js +82 -4
  111. package/lib/vendor/blamejs/lib/http-client-cookie-jar.js +16 -1
  112. package/lib/vendor/blamejs/lib/http-client.js +39 -7
  113. package/lib/vendor/blamejs/lib/http-message-signature.js +35 -1
  114. package/lib/vendor/blamejs/lib/i18n-messageformat.js +31 -6
  115. package/lib/vendor/blamejs/lib/i18n.js +35 -21
  116. package/lib/vendor/blamejs/lib/incident-report.js +8 -1
  117. package/lib/vendor/blamejs/lib/ip-utils.js +11 -1
  118. package/lib/vendor/blamejs/lib/json-path.js +23 -1
  119. package/lib/vendor/blamejs/lib/jwk.js +20 -3
  120. package/lib/vendor/blamejs/lib/link-header.js +2 -1
  121. package/lib/vendor/blamejs/lib/log-stream-otlp-grpc.js +5 -1
  122. package/lib/vendor/blamejs/lib/log-stream-otlp.js +5 -1
  123. package/lib/vendor/blamejs/lib/log-stream.js +8 -3
  124. package/lib/vendor/blamejs/lib/mail-auth.js +123 -22
  125. package/lib/vendor/blamejs/lib/mail-bimi.js +1 -1
  126. package/lib/vendor/blamejs/lib/mail-bounce.js +8 -0
  127. package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +46 -10
  128. package/lib/vendor/blamejs/lib/mail-dav.js +22 -3
  129. package/lib/vendor/blamejs/lib/mail-dkim.js +96 -30
  130. package/lib/vendor/blamejs/lib/mail-helo.js +6 -1
  131. package/lib/vendor/blamejs/lib/mail-scan.js +23 -16
  132. package/lib/vendor/blamejs/lib/mail-send-deliver.js +34 -8
  133. package/lib/vendor/blamejs/lib/mail-server-imap.js +3 -4
  134. package/lib/vendor/blamejs/lib/mail-server-managesieve.js +7 -11
  135. package/lib/vendor/blamejs/lib/mail-server-mx.js +93 -7
  136. package/lib/vendor/blamejs/lib/mail-server-registry.js +8 -2
  137. package/lib/vendor/blamejs/lib/mail.js +15 -5
  138. package/lib/vendor/blamejs/lib/metrics.js +268 -63
  139. package/lib/vendor/blamejs/lib/middleware/age-gate.js +7 -1
  140. package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +1 -1
  141. package/lib/vendor/blamejs/lib/middleware/body-parser.js +19 -4
  142. package/lib/vendor/blamejs/lib/middleware/bot-guard.js +5 -9
  143. package/lib/vendor/blamejs/lib/middleware/csp-report.js +40 -8
  144. package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +24 -10
  145. package/lib/vendor/blamejs/lib/middleware/dpop.js +10 -5
  146. package/lib/vendor/blamejs/lib/middleware/index.js +6 -1
  147. package/lib/vendor/blamejs/lib/middleware/nel.js +1 -1
  148. package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +1 -1
  149. package/lib/vendor/blamejs/lib/middleware/rate-limit.js +12 -1
  150. package/lib/vendor/blamejs/lib/middleware/require-bound-key.js +1 -1
  151. package/lib/vendor/blamejs/lib/middleware/scim-server.js +1 -1
  152. package/lib/vendor/blamejs/lib/middleware/tus-upload.js +6 -2
  153. package/lib/vendor/blamejs/lib/mtls-ca.js +18 -2
  154. package/lib/vendor/blamejs/lib/network-dns-resolver.js +11 -1
  155. package/lib/vendor/blamejs/lib/network-dns.js +21 -2
  156. package/lib/vendor/blamejs/lib/network-tls.js +69 -7
  157. package/lib/vendor/blamejs/lib/nonce-store.js +9 -1
  158. package/lib/vendor/blamejs/lib/object-store/gcs.js +7 -0
  159. package/lib/vendor/blamejs/lib/object-store/sigv4.js +31 -0
  160. package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +1 -1
  161. package/lib/vendor/blamejs/lib/outbox.js +1 -1
  162. package/lib/vendor/blamejs/lib/parsers/safe-ini.js +9 -1
  163. package/lib/vendor/blamejs/lib/parsers/safe-toml.js +10 -1
  164. package/lib/vendor/blamejs/lib/permissions.js +6 -3
  165. package/lib/vendor/blamejs/lib/pid-probe.js +55 -0
  166. package/lib/vendor/blamejs/lib/pqc-agent.js +8 -1
  167. package/lib/vendor/blamejs/lib/pqc-software.js +3 -3
  168. package/lib/vendor/blamejs/lib/pubsub.js +20 -9
  169. package/lib/vendor/blamejs/lib/queue-local.js +8 -1
  170. package/lib/vendor/blamejs/lib/queue-redis.js +5 -0
  171. package/lib/vendor/blamejs/lib/redact.js +54 -0
  172. package/lib/vendor/blamejs/lib/restore-bundle.js +10 -2
  173. package/lib/vendor/blamejs/lib/restore.js +19 -0
  174. package/lib/vendor/blamejs/lib/retention.js +12 -4
  175. package/lib/vendor/blamejs/lib/router.js +60 -6
  176. package/lib/vendor/blamejs/lib/safe-buffer.js +69 -0
  177. package/lib/vendor/blamejs/lib/safe-decompress.js +8 -2
  178. package/lib/vendor/blamejs/lib/safe-dns.js +25 -20
  179. package/lib/vendor/blamejs/lib/safe-ical.js +7 -3
  180. package/lib/vendor/blamejs/lib/safe-mime.js +12 -1
  181. package/lib/vendor/blamejs/lib/safe-object.js +80 -0
  182. package/lib/vendor/blamejs/lib/safe-path.js +52 -4
  183. package/lib/vendor/blamejs/lib/safe-redirect.js +10 -2
  184. package/lib/vendor/blamejs/lib/safe-schema.js +4 -1
  185. package/lib/vendor/blamejs/lib/safe-sieve.js +33 -1
  186. package/lib/vendor/blamejs/lib/safe-sql.js +88 -0
  187. package/lib/vendor/blamejs/lib/safe-vcard.js +7 -3
  188. package/lib/vendor/blamejs/lib/scheduler.js +8 -1
  189. package/lib/vendor/blamejs/lib/security-assert.js +6 -6
  190. package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +74 -27
  191. package/lib/vendor/blamejs/lib/self-update.js +517 -87
  192. package/lib/vendor/blamejs/lib/server-timing.js +2 -4
  193. package/lib/vendor/blamejs/lib/session.js +34 -12
  194. package/lib/vendor/blamejs/lib/sql.js +15 -1
  195. package/lib/vendor/blamejs/lib/ssrf-guard.js +52 -0
  196. package/lib/vendor/blamejs/lib/static.js +55 -13
  197. package/lib/vendor/blamejs/lib/storage.js +8 -2
  198. package/lib/vendor/blamejs/lib/subject.js +6 -1
  199. package/lib/vendor/blamejs/lib/tenant-quota.js +52 -13
  200. package/lib/vendor/blamejs/lib/tls-exporter.js +3 -3
  201. package/lib/vendor/blamejs/lib/tsa.js +34 -13
  202. package/lib/vendor/blamejs/lib/uri-template.js +8 -1
  203. package/lib/vendor/blamejs/lib/vault/index.js +13 -0
  204. package/lib/vendor/blamejs/lib/vc.js +44 -5
  205. package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +26 -26
  206. package/lib/vendor/blamejs/lib/vendor/noble-ciphers.cjs +792 -1
  207. package/lib/vendor/blamejs/lib/vendor/noble-curves.cjs +3443 -1
  208. package/lib/vendor/blamejs/lib/vendor/noble-post-quantum.cjs +2737 -1
  209. package/lib/vendor/blamejs/lib/vendor/pki.cjs +39550 -13
  210. package/lib/vendor/blamejs/lib/vendor/public-suffix-list.dat +55 -53
  211. package/lib/vendor/blamejs/lib/vendor/public-suffix-list.data.js +5551 -5554
  212. package/lib/vendor/blamejs/lib/vendor/simplewebauthn-server.cjs +22459 -32
  213. package/lib/vendor/blamejs/lib/watcher.js +89 -17
  214. package/lib/vendor/blamejs/lib/webhook-dispatcher.js +25 -3
  215. package/lib/vendor/blamejs/lib/webhook.js +12 -2
  216. package/lib/vendor/blamejs/lib/ws-client.js +32 -1
  217. package/lib/vendor/blamejs/lib/xml-c14n.js +29 -1
  218. package/lib/vendor/blamejs/oss-fuzz/projects/blamejs/Dockerfile +1 -1
  219. package/lib/vendor/blamejs/package-lock.json +2 -2
  220. package/lib/vendor/blamejs/package.json +1 -1
  221. package/lib/vendor/blamejs/release-notes/v0.16.x.json +1462 -0
  222. package/lib/vendor/blamejs/release-notes/v0.17.0.json +31 -0
  223. package/lib/vendor/blamejs/release-notes/v0.17.1.json +31 -0
  224. package/lib/vendor/blamejs/release-notes/v0.17.10.json +30 -0
  225. package/lib/vendor/blamejs/release-notes/v0.17.11.json +22 -0
  226. package/lib/vendor/blamejs/release-notes/v0.17.12.json +18 -0
  227. package/lib/vendor/blamejs/release-notes/v0.17.13.json +77 -0
  228. package/lib/vendor/blamejs/release-notes/v0.17.14.json +40 -0
  229. package/lib/vendor/blamejs/release-notes/v0.17.15.json +18 -0
  230. package/lib/vendor/blamejs/release-notes/v0.17.16.json +27 -0
  231. package/lib/vendor/blamejs/release-notes/v0.17.17.json +31 -0
  232. package/lib/vendor/blamejs/release-notes/v0.17.18.json +18 -0
  233. package/lib/vendor/blamejs/release-notes/v0.17.19.json +18 -0
  234. package/lib/vendor/blamejs/release-notes/v0.17.2.json +26 -0
  235. package/lib/vendor/blamejs/release-notes/v0.17.20.json +43 -0
  236. package/lib/vendor/blamejs/release-notes/v0.17.21.json +26 -0
  237. package/lib/vendor/blamejs/release-notes/v0.17.22.json +18 -0
  238. package/lib/vendor/blamejs/release-notes/v0.17.3.json +43 -0
  239. package/lib/vendor/blamejs/release-notes/v0.17.4.json +35 -0
  240. package/lib/vendor/blamejs/release-notes/v0.17.5.json +26 -0
  241. package/lib/vendor/blamejs/release-notes/v0.17.6.json +26 -0
  242. package/lib/vendor/blamejs/release-notes/v0.17.7.json +35 -0
  243. package/lib/vendor/blamejs/release-notes/v0.17.8.json +22 -0
  244. package/lib/vendor/blamejs/release-notes/v0.17.9.json +26 -0
  245. package/lib/vendor/blamejs/scripts/validate-source-comment-blocks.js +4 -0
  246. package/lib/vendor/blamejs/scripts/vendor-update.sh +293 -9
  247. package/lib/vendor/blamejs/socket.yml +94 -0
  248. package/lib/vendor/blamejs/test/00-primitives.js +605 -0
  249. package/lib/vendor/blamejs/test/helpers/drivers.js +12 -9
  250. package/lib/vendor/blamejs/test/integration/audit-stack-mysql.test.js +90 -1
  251. package/lib/vendor/blamejs/test/integration/audit-stack-postgres.test.js +122 -1
  252. package/lib/vendor/blamejs/test/integration/data-layer-mysql.test.js +30 -0
  253. package/lib/vendor/blamejs/test/integration/data-layer-pg.test.js +29 -0
  254. package/lib/vendor/blamejs/test/integration/dsr-cluster-mysql.test.js +479 -0
  255. package/lib/vendor/blamejs/test/integration/dsr-cluster-pg.test.js +487 -0
  256. package/lib/vendor/blamejs/test/integration/federation-auth.test.js +313 -5
  257. package/lib/vendor/blamejs/test/integration/mail-dkim.test.js +80 -0
  258. package/lib/vendor/blamejs/test/integration/openid-federation-chain.test.js +275 -0
  259. package/lib/vendor/blamejs/test/integration/webhook-dispatcher-pg.test.js +96 -0
  260. package/lib/vendor/blamejs/test/layer-0-primitives/a2a-tasks.test.js +401 -0
  261. package/lib/vendor/blamejs/test/layer-0-primitives/acme.test.js +1649 -2
  262. package/lib/vendor/blamejs/test/layer-0-primitives/age-gate.test.js +33 -0
  263. package/lib/vendor/blamejs/test/layer-0-primitives/agent-idempotency.test.js +0 -0
  264. package/lib/vendor/blamejs/test/layer-0-primitives/agent-saga.test.js +60 -0
  265. package/lib/vendor/blamejs/test/layer-0-primitives/agent-snapshot.test.js +524 -0
  266. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +891 -0
  267. package/lib/vendor/blamejs/test/layer-0-primitives/app-shutdown.test.js +128 -1
  268. package/lib/vendor/blamejs/test/layer-0-primitives/archive-adapters.test.js +271 -0
  269. package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +628 -1
  270. package/lib/vendor/blamejs/test/layer-0-primitives/archive-tar-read.test.js +0 -0
  271. package/lib/vendor/blamejs/test/layer-0-primitives/archive-wrap.test.js +227 -0
  272. package/lib/vendor/blamejs/test/layer-0-primitives/arg-parser.test.js +15 -0
  273. package/lib/vendor/blamejs/test/layer-0-primitives/asn1-der.test.js +73 -0
  274. package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file.test.js +114 -0
  275. package/lib/vendor/blamejs/test/layer-0-primitives/audit-sign-anchor.test.js +141 -0
  276. package/lib/vendor/blamejs/test/layer-0-primitives/audit-tools.test.js +624 -0
  277. package/lib/vendor/blamejs/test/layer-0-primitives/audit.test.js +769 -0
  278. package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +436 -0
  279. package/lib/vendor/blamejs/test/layer-0-primitives/auth-oauth.test.js +1697 -0
  280. package/lib/vendor/blamejs/test/layer-0-primitives/auth-oid4vp.test.js +495 -0
  281. package/lib/vendor/blamejs/test/layer-0-primitives/auth-password.test.js +500 -0
  282. package/lib/vendor/blamejs/test/layer-0-primitives/auth-saml.test.js +1809 -0
  283. package/lib/vendor/blamejs/test/layer-0-primitives/auth-status-list.test.js +243 -1
  284. package/lib/vendor/blamejs/test/layer-0-primitives/backup-index.test.js +2226 -0
  285. package/lib/vendor/blamejs/test/layer-0-primitives/backup-manifest-signature.test.js +183 -0
  286. package/lib/vendor/blamejs/test/layer-0-primitives/base32.test.js +105 -0
  287. package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +693 -0
  288. package/lib/vendor/blamejs/test/layer-0-primitives/cache.test.js +603 -0
  289. package/lib/vendor/blamejs/test/layer-0-primitives/calendar.test.js +557 -0
  290. package/lib/vendor/blamejs/test/layer-0-primitives/case-insensitive-security-token-guard.test.js +214 -0
  291. package/lib/vendor/blamejs/test/layer-0-primitives/cbor.test.js +25 -0
  292. package/lib/vendor/blamejs/test/layer-0-primitives/cert.test.js +843 -6
  293. package/lib/vendor/blamejs/test/layer-0-primitives/ciba-authreqid-binding.test.js +77 -2
  294. package/lib/vendor/blamejs/test/layer-0-primitives/ciba.test.js +866 -0
  295. package/lib/vendor/blamejs/test/layer-0-primitives/clear-site-data.test.js +18 -0
  296. package/lib/vendor/blamejs/test/layer-0-primitives/cli-erase.test.js +19 -0
  297. package/lib/vendor/blamejs/test/layer-0-primitives/cli.test.js +2790 -0
  298. package/lib/vendor/blamejs/test/layer-0-primitives/cluster-lease-renewal-ttl.test.js +114 -0
  299. package/lib/vendor/blamejs/test/layer-0-primitives/cluster.test.js +1032 -0
  300. package/lib/vendor/blamejs/test/layer-0-primitives/cms-codec.test.js +575 -0
  301. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +739 -1
  302. package/lib/vendor/blamejs/test/layer-0-primitives/codepoint-class.test.js +8 -0
  303. package/lib/vendor/blamejs/test/layer-0-primitives/compliance-ai-act.test.js +194 -0
  304. package/lib/vendor/blamejs/test/layer-0-primitives/compliance-lifecycle-proto-key.test.js +104 -0
  305. package/lib/vendor/blamejs/test/layer-0-primitives/compliance-sanctions.test.js +254 -0
  306. package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials.test.js +613 -3
  307. package/lib/vendor/blamejs/test/layer-0-primitives/cookies.test.js +140 -0
  308. package/lib/vendor/blamejs/test/layer-0-primitives/cose.test.js +407 -0
  309. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-adversarial.test.js +352 -0
  310. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +32 -0
  311. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field.test.js +575 -0
  312. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-hash-files-parallel.test.js +10 -0
  313. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-hash-stream.test.js +24 -0
  314. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-oprf.test.js +17 -0
  315. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-spki-pin.test.js +183 -0
  316. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-xwing.test.js +48 -0
  317. package/lib/vendor/blamejs/test/layer-0-primitives/crypto.test.js +345 -0
  318. package/lib/vendor/blamejs/test/layer-0-primitives/csp-report.test.js +58 -0
  319. package/lib/vendor/blamejs/test/layer-0-primitives/csv.test.js +48 -0
  320. package/lib/vendor/blamejs/test/layer-0-primitives/daemon.test.js +459 -1
  321. package/lib/vendor/blamejs/test/layer-0-primitives/dark-patterns.test.js +19 -0
  322. package/lib/vendor/blamejs/test/layer-0-primitives/db-query.test.js +1019 -0
  323. package/lib/vendor/blamejs/test/layer-0-primitives/db-raw-residency-gate.test.js +77 -0
  324. package/lib/vendor/blamejs/test/layer-0-primitives/db.test.js +1621 -0
  325. package/lib/vendor/blamejs/test/layer-0-primitives/ddl-change-control.test.js +462 -3
  326. package/lib/vendor/blamejs/test/layer-0-primitives/dnssec.test.js +323 -0
  327. package/lib/vendor/blamejs/test/layer-0-primitives/dpop-middleware-verify.test.js +562 -0
  328. package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +901 -1
  329. package/lib/vendor/blamejs/test/layer-0-primitives/dual-control.test.js +249 -0
  330. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-migrate.test.js +561 -0
  331. package/lib/vendor/blamejs/test/layer-0-primitives/external-db.test.js +1214 -0
  332. package/lib/vendor/blamejs/test/layer-0-primitives/fal.test.js +21 -0
  333. package/lib/vendor/blamejs/test/layer-0-primitives/fedcm-dbsc.test.js +46 -20
  334. package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +147 -3
  335. package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3.test.js +907 -38
  336. package/lib/vendor/blamejs/test/layer-0-primitives/file-upload-content-safety-skip-audit.test.js +29 -0
  337. package/lib/vendor/blamejs/test/layer-0-primitives/file-upload.test.js +1135 -0
  338. package/lib/vendor/blamejs/test/layer-0-primitives/flag.test.js +30 -0
  339. package/lib/vendor/blamejs/test/layer-0-primitives/forms.test.js +747 -0
  340. package/lib/vendor/blamejs/test/layer-0-primitives/gate-contract.test.js +1855 -0
  341. package/lib/vendor/blamejs/test/layer-0-primitives/gdpr-ropa.test.js +30 -0
  342. package/lib/vendor/blamejs/test/layer-0-primitives/guard-archive.test.js +86 -0
  343. package/lib/vendor/blamejs/test/layer-0-primitives/guard-auth.test.js +117 -0
  344. package/lib/vendor/blamejs/test/layer-0-primitives/guard-cidr.test.js +151 -0
  345. package/lib/vendor/blamejs/test/layer-0-primitives/guard-domain.test.js +422 -0
  346. package/lib/vendor/blamejs/test/layer-0-primitives/guard-filename.test.js +424 -0
  347. package/lib/vendor/blamejs/test/layer-0-primitives/{guard-gate-disposition-coverage.test.js → guard-gate-disposition.test.js} +1 -1
  348. package/lib/vendor/blamejs/test/layer-0-primitives/guard-graphql.test.js +340 -0
  349. package/lib/vendor/blamejs/test/layer-0-primitives/guard-html.test.js +29 -0
  350. package/lib/vendor/blamejs/test/layer-0-primitives/guard-idempotency-key.test.js +28 -0
  351. package/lib/vendor/blamejs/test/layer-0-primitives/guard-image.test.js +519 -45
  352. package/lib/vendor/blamejs/test/layer-0-primitives/guard-imap-command.test.js +0 -0
  353. package/lib/vendor/blamejs/test/layer-0-primitives/guard-jmap.test.js +51 -0
  354. package/lib/vendor/blamejs/test/layer-0-primitives/guard-jsonpath.test.js +66 -0
  355. package/lib/vendor/blamejs/test/layer-0-primitives/guard-jwt.test.js +436 -0
  356. package/lib/vendor/blamejs/test/layer-0-primitives/guard-managesieve-command.test.js +25 -0
  357. package/lib/vendor/blamejs/test/layer-0-primitives/guard-markdown.test.js +16 -0
  358. package/lib/vendor/blamejs/test/layer-0-primitives/guard-mime.test.js +86 -0
  359. package/lib/vendor/blamejs/test/layer-0-primitives/guard-oauth.test.js +140 -0
  360. package/lib/vendor/blamejs/test/layer-0-primitives/guard-pdf.test.js +80 -0
  361. package/lib/vendor/blamejs/test/layer-0-primitives/guard-pop3-command.test.js +27 -0
  362. package/lib/vendor/blamejs/test/layer-0-primitives/guard-regex.test.js +99 -0
  363. package/lib/vendor/blamejs/test/layer-0-primitives/guard-shell.test.js +76 -0
  364. package/lib/vendor/blamejs/test/layer-0-primitives/{guard-sql-coverage.test.js → guard-sql.test.js} +3 -3
  365. package/lib/vendor/blamejs/test/layer-0-primitives/guard-svg.test.js +524 -0
  366. package/lib/vendor/blamejs/test/layer-0-primitives/guard-template.test.js +71 -0
  367. package/lib/vendor/blamejs/test/layer-0-primitives/guard-time.test.js +92 -0
  368. package/lib/vendor/blamejs/test/layer-0-primitives/guard-uuid.test.js +86 -0
  369. package/lib/vendor/blamejs/test/layer-0-primitives/html-balance.test.js +38 -0
  370. package/lib/vendor/blamejs/test/layer-0-primitives/http-client-cache-authorization.test.js +347 -0
  371. package/lib/vendor/blamejs/test/layer-0-primitives/http-client-cache.test.js +908 -0
  372. package/lib/vendor/blamejs/test/layer-0-primitives/http-client.test.js +2874 -0
  373. package/lib/vendor/blamejs/test/layer-0-primitives/http-message-signature.test.js +643 -0
  374. package/lib/vendor/blamejs/test/layer-0-primitives/i18n-messageformat.test.js +38 -0
  375. package/lib/vendor/blamejs/test/layer-0-primitives/i18n.test.js +1153 -1
  376. package/lib/vendor/blamejs/test/layer-0-primitives/idempotency-key.test.js +494 -0
  377. package/lib/vendor/blamejs/test/layer-0-primitives/ip-utils.test.js +30 -0
  378. package/lib/vendor/blamejs/test/layer-0-primitives/jsdoc-example-execution.test.js +177 -0
  379. package/lib/vendor/blamejs/test/layer-0-primitives/json-path.test.js +103 -0
  380. package/lib/vendor/blamejs/test/layer-0-primitives/json-schema.test.js +214 -0
  381. package/lib/vendor/blamejs/test/layer-0-primitives/jwk.test.js +41 -0
  382. package/lib/vendor/blamejs/test/layer-0-primitives/jwt-external.test.js +276 -0
  383. package/lib/vendor/blamejs/test/layer-0-primitives/keychain.test.js +0 -0
  384. package/lib/vendor/blamejs/test/layer-0-primitives/log-stream.test.js +241 -0
  385. package/lib/vendor/blamejs/test/layer-0-primitives/log.test.js +106 -0
  386. package/lib/vendor/blamejs/test/layer-0-primitives/mail-agent.test.js +441 -0
  387. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +2136 -4
  388. package/lib/vendor/blamejs/test/layer-0-primitives/mail-bimi.test.js +759 -12
  389. package/lib/vendor/blamejs/test/layer-0-primitives/mail-bounce.test.js +811 -1
  390. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-pgp-experimental.test.js +401 -3
  391. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-pgp.test.js +751 -8
  392. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +864 -0
  393. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto.test.js +110 -0
  394. package/lib/vendor/blamejs/test/layer-0-primitives/mail-dav.test.js +830 -1
  395. package/lib/vendor/blamejs/test/layer-0-primitives/mail-deploy.test.js +635 -86
  396. package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim.test.js +818 -0
  397. package/lib/vendor/blamejs/test/layer-0-primitives/mail-helo.test.js +27 -0
  398. package/lib/vendor/blamejs/test/layer-0-primitives/mail-scan.test.js +199 -0
  399. package/lib/vendor/blamejs/test/layer-0-primitives/mail-send-deliver.test.js +686 -2
  400. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-imap.test.js +778 -0
  401. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-jmap.test.js +1438 -0
  402. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-managesieve.test.js +449 -6
  403. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +773 -2
  404. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-pop3.test.js +623 -3
  405. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-rate-limit.test.js +55 -0
  406. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-registry.test.js +13 -0
  407. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-submission.test.js +877 -1
  408. package/lib/vendor/blamejs/test/layer-0-primitives/mail-sieve.test.js +326 -0
  409. package/lib/vendor/blamejs/test/layer-0-primitives/mail-store-fts.test.js +55 -0
  410. package/lib/vendor/blamejs/test/layer-0-primitives/mail-store.test.js +242 -0
  411. package/lib/vendor/blamejs/test/layer-0-primitives/mail.test.js +1596 -2
  412. package/lib/vendor/blamejs/test/layer-0-primitives/mcp.test.js +440 -4
  413. package/lib/vendor/blamejs/test/layer-0-primitives/metrics-snapshot.test.js +89 -0
  414. package/lib/vendor/blamejs/test/layer-0-primitives/metrics.test.js +1230 -0
  415. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-assetlinks.test.js +113 -0
  416. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-body-parser.test.js +1169 -0
  417. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-bot-disclose.test.js +123 -0
  418. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-cookies.test.js +108 -0
  419. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-gpc.test.js +121 -0
  420. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-headers.test.js +163 -0
  421. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-host-allowlist.test.js +154 -0
  422. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-require-content-type.test.js +127 -0
  423. package/lib/vendor/blamejs/test/layer-0-primitives/{middleware-tus-upload-coverage.test.js → middleware-tus-upload.test.js} +45 -0
  424. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-web-app-manifest.test.js +132 -0
  425. package/lib/vendor/blamejs/test/layer-0-primitives/mtls-ca-crl-fingerprint-only.test.js +92 -0
  426. package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-resolver.test.js +65 -0
  427. package/lib/vendor/blamejs/test/layer-0-primitives/network-dns.test.js +1727 -1
  428. package/lib/vendor/blamejs/test/layer-0-primitives/network-nts.test.js +699 -0
  429. package/lib/vendor/blamejs/test/layer-0-primitives/network-smtp-policy.test.js +1181 -0
  430. package/lib/vendor/blamejs/test/layer-0-primitives/network-tls.test.js +1876 -3
  431. package/lib/vendor/blamejs/test/layer-0-primitives/network.test.js +32 -0
  432. package/lib/vendor/blamejs/test/layer-0-primitives/nonce-store-enforce-replay-nonboolean.test.js +81 -0
  433. package/lib/vendor/blamejs/test/layer-0-primitives/ntp-check.test.js +122 -0
  434. package/lib/vendor/blamejs/test/layer-0-primitives/observability-tracing.test.js +378 -0
  435. package/lib/vendor/blamejs/test/layer-0-primitives/observability.test.js +77 -0
  436. package/lib/vendor/blamejs/test/layer-0-primitives/oid4vci.test.js +1016 -0
  437. package/lib/vendor/blamejs/test/layer-0-primitives/openid-federation.test.js +946 -0
  438. package/lib/vendor/blamejs/test/layer-0-primitives/outbox.test.js +818 -0
  439. package/lib/vendor/blamejs/test/layer-0-primitives/own-property-membership-guards.test.js +109 -0
  440. package/lib/vendor/blamejs/test/layer-0-primitives/parsers-safe-toml.test.js +543 -0
  441. package/lib/vendor/blamejs/test/layer-0-primitives/{parsers-safe-yaml-coverage.test.js → parsers-safe-yaml.test.js} +2 -2
  442. package/lib/vendor/blamejs/test/layer-0-primitives/parsers-standalone.test.js +73 -0
  443. package/lib/vendor/blamejs/test/layer-0-primitives/passkey.test.js +649 -0
  444. package/lib/vendor/blamejs/test/layer-0-primitives/permissions.test.js +524 -0
  445. package/lib/vendor/blamejs/test/layer-0-primitives/pqc-agent-curve.test.js +22 -0
  446. package/lib/vendor/blamejs/test/layer-0-primitives/pubsub.test.js +295 -0
  447. package/lib/vendor/blamejs/test/layer-0-primitives/queue-flow-repeat.test.js +36 -0
  448. package/lib/vendor/blamejs/test/layer-0-primitives/queue.test.js +83 -0
  449. package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-xff-spoofing.test.js +34 -0
  450. package/lib/vendor/blamejs/test/layer-0-primitives/redact.test.js +1031 -0
  451. package/lib/vendor/blamejs/test/layer-0-primitives/request-helpers.test.js +94 -0
  452. package/lib/vendor/blamejs/test/layer-0-primitives/require-bound-key.test.js +401 -0
  453. package/lib/vendor/blamejs/test/layer-0-primitives/restore-empty-manifest-wipe.test.js +154 -0
  454. package/lib/vendor/blamejs/test/layer-0-primitives/retention.test.js +600 -0
  455. package/lib/vendor/blamejs/test/layer-0-primitives/router-host-path-injection.test.js +231 -0
  456. package/lib/vendor/blamejs/test/layer-0-primitives/router-tls0rtt.test.js +21 -0
  457. package/lib/vendor/blamejs/test/layer-0-primitives/router.test.js +1277 -0
  458. package/lib/vendor/blamejs/test/layer-0-primitives/safe-async.test.js +133 -0
  459. package/lib/vendor/blamejs/test/layer-0-primitives/safe-buffer-linear-scans.test.js +14 -0
  460. package/lib/vendor/blamejs/test/layer-0-primitives/safe-buffer.test.js +80 -0
  461. package/lib/vendor/blamejs/test/layer-0-primitives/safe-decompress.test.js +28 -0
  462. package/lib/vendor/blamejs/test/layer-0-primitives/safe-dns.test.js +245 -0
  463. package/lib/vendor/blamejs/test/layer-0-primitives/safe-ical.test.js +32 -0
  464. package/lib/vendor/blamejs/test/layer-0-primitives/safe-ini.test.js +376 -0
  465. package/lib/vendor/blamejs/test/layer-0-primitives/safe-json.test.js +175 -0
  466. package/lib/vendor/blamejs/test/layer-0-primitives/safe-mime.test.js +518 -0
  467. package/lib/vendor/blamejs/test/layer-0-primitives/safe-object.test.js +62 -0
  468. package/lib/vendor/blamejs/test/layer-0-primitives/safe-path.test.js +30 -0
  469. package/lib/vendor/blamejs/test/layer-0-primitives/safe-schema.test.js +104 -0
  470. package/lib/vendor/blamejs/test/layer-0-primitives/safe-sql.test.js +142 -0
  471. package/lib/vendor/blamejs/test/layer-0-primitives/safe-url.test.js +112 -0
  472. package/lib/vendor/blamejs/test/layer-0-primitives/safe-vcard.test.js +26 -0
  473. package/lib/vendor/blamejs/test/layer-0-primitives/scheduler.test.js +157 -0
  474. package/lib/vendor/blamejs/test/layer-0-primitives/scim-server.test.js +396 -0
  475. package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +930 -0
  476. package/lib/vendor/blamejs/test/layer-0-primitives/security-assert.test.js +755 -82
  477. package/lib/vendor/blamejs/test/layer-0-primitives/self-update.test.js +1053 -0
  478. package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding.test.js +200 -0
  479. package/lib/vendor/blamejs/test/layer-0-primitives/session-strict-binding-missing-failclosed.test.js +110 -0
  480. package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +636 -0
  481. package/lib/vendor/blamejs/test/layer-0-primitives/sql-offset-without-limit.test.js +94 -0
  482. package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +1190 -0
  483. package/lib/vendor/blamejs/test/layer-0-primitives/ssrf-guard.test.js +21 -0
  484. package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +850 -0
  485. package/lib/vendor/blamejs/test/layer-0-primitives/storage-presigned-url.test.js +146 -0
  486. package/lib/vendor/blamejs/test/layer-0-primitives/storage.test.js +621 -0
  487. package/lib/vendor/blamejs/test/layer-0-primitives/subject.test.js +47 -0
  488. package/lib/vendor/blamejs/test/layer-0-primitives/template.test.js +582 -0
  489. package/lib/vendor/blamejs/test/layer-0-primitives/tenant-quota.test.js +587 -25
  490. package/lib/vendor/blamejs/test/layer-0-primitives/time.test.js +11 -0
  491. package/lib/vendor/blamejs/test/layer-0-primitives/tsa.test.js +524 -63
  492. package/lib/vendor/blamejs/test/layer-0-primitives/uri-template.test.js +27 -0
  493. package/lib/vendor/blamejs/test/layer-0-primitives/vault-rotate.test.js +866 -0
  494. package/lib/vendor/blamejs/test/layer-0-primitives/vault.test.js +74 -0
  495. package/lib/vendor/blamejs/test/layer-0-primitives/vc.test.js +215 -0
  496. package/lib/vendor/blamejs/test/layer-0-primitives/watcher.test.js +550 -0
  497. package/lib/vendor/blamejs/test/layer-0-primitives/webhook-dispatcher.test.js +388 -0
  498. package/lib/vendor/blamejs/test/layer-0-primitives/webhook.test.js +262 -0
  499. package/lib/vendor/blamejs/test/layer-0-primitives/websocket.test.js +1294 -0
  500. package/lib/vendor/blamejs/test/layer-0-primitives/ws-client.test.js +629 -0
  501. package/lib/vendor/blamejs/test/layer-0-primitives/xml-c14n-attr-normalization.test.js +164 -0
  502. package/lib/vendor/blamejs/test/smoke.js +2 -0
  503. package/package.json +1 -1
  504. package/lib/vendor/blamejs/release-notes/v0.16.0.json +0 -44
  505. package/lib/vendor/blamejs/release-notes/v0.16.1.json +0 -36
  506. package/lib/vendor/blamejs/release-notes/v0.16.2.json +0 -71
  507. package/lib/vendor/blamejs/release-notes/v0.16.3.json +0 -71
  508. package/lib/vendor/blamejs/test/layer-0-primitives/acme-coverage.test.js +0 -441
  509. package/lib/vendor/blamejs/test/layer-0-primitives/auth-oauth-coverage.test.js +0 -482
  510. package/lib/vendor/blamejs/test/layer-0-primitives/auth-saml-coverage.test.js +0 -671
  511. package/lib/vendor/blamejs/test/layer-0-primitives/backup-index-coverage.test.js +0 -690
  512. package/lib/vendor/blamejs/test/layer-0-primitives/cli-coverage.test.js +0 -238
  513. package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials-coverage.test.js +0 -319
  514. package/lib/vendor/blamejs/test/layer-0-primitives/keychain-coverage.test.js +0 -0
  515. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth-coverage.test.js +0 -437
  516. package/lib/vendor/blamejs/test/layer-0-primitives/mcp-coverage.test.js +0 -443
  517. package/lib/vendor/blamejs/test/layer-0-primitives/metrics-coverage.test.js +0 -395
  518. package/lib/vendor/blamejs/test/layer-0-primitives/network-smtp-policy-coverage.test.js +0 -565
  519. package/lib/vendor/blamejs/test/layer-0-primitives/router-coverage.test.js +0 -592
  520. package/lib/vendor/blamejs/test/layer-0-primitives/sql-coverage.test.js +0 -422
  521. package/lib/vendor/blamejs/test/layer-0-primitives/test-coverage.test.js +0 -573
@@ -0,0 +1,2790 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+
5
+ // Exercises lib/cli.js branches not driven by the per-command
6
+ // cli-*.test.js files or test/00-primitives.js:
7
+ // - the `seed` subcommand (arg validation + a local-sqlite status run)
8
+ // - `dev --grace-ms` numeric validation
9
+ // - `api-snapshot` operator-supplied `--module` (capture + compare
10
+ // failure paths)
11
+ // - top-level `--help` / `-h` routing + unknown help topic fall-through
12
+ // Everything drives the public b.cli.main(argv, ctx) surface with a
13
+ // captured-output ctx; nothing here needs a live network/db backend
14
+ // (the sqlite handle is a local temp file, same shape the existing
15
+ // cli-audit-verify-chain / migrate tests use).
16
+ // It also drives the seed RUN path (apply / re-skip / --only / --force /
17
+ // broken-seeder catch), an api-snapshot compare that detects a breaking
18
+ // change, audit archive/export/verify-bundle/purge arg validation plus
19
+ // verify-chain FAIL and --max-rows branches, file-type edges, every
20
+ // `<cmd> help` positional, cheap arg-validation returns for the remaining
21
+ // commands, booted api-key / erase / retention / mtls / security edges,
22
+ // and repeatable-flag accumulation (--arg / --watch / --ignore).
23
+
24
+ var fs = require("node:fs");
25
+ var os = require("node:os");
26
+ var path = require("node:path");
27
+ var sqlite = require("node:sqlite");
28
+ var helpers = require("../helpers");
29
+ var b = helpers.b;
30
+ var check = helpers.check;
31
+ var cli = require("../../lib/cli");
32
+
33
+ function _captureCtx() {
34
+ var stdout = [];
35
+ var stderr = [];
36
+ return {
37
+ stdout: { write: function (s) { stdout.push(String(s)); } },
38
+ stderr: { write: function (s) { stderr.push(String(s)); } },
39
+ env: {},
40
+ cwd: process.cwd(),
41
+ out: function () { return stdout.join(""); },
42
+ err: function () { return stderr.join(""); },
43
+ };
44
+ }
45
+
46
+ function _tmpDir(prefix) {
47
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix + "-"));
48
+ }
49
+
50
+ function _rm(dir) {
51
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_e) { /* best-effort */ }
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // top-level dispatch edges
56
+ // ---------------------------------------------------------------------------
57
+ async function sectionTopLevel() {
58
+ // `-v` short flag → version (covers the `args.flags.v` operand of `||`)
59
+ var cV = _captureCtx();
60
+ var rcV = await cli.main(["-v"], cV);
61
+ check("-v short flag → exit 0", rcV === 0);
62
+ check("-v short flag → prints version", /\d+\.\d+\.\d+/.test(cV.out()));
63
+
64
+ // A stray -v / --version ALONGSIDE a subcommand must NOT short-circuit to
65
+ // the version print — the subcommand runs (or fails), never a silent
66
+ // version no-op that would hand automation a false exit 0.
67
+ var cVSub = _captureCtx();
68
+ var rcVSub = await cli.main(["migrate", "-v"], cVSub);
69
+ check("subcommand + stray -v → runs subcommand (exit 2 usage, not version no-op)",
70
+ rcVSub === 2 && /Usage: blamejs migrate/.test(cVSub.err()));
71
+ var cVerSub = _captureCtx();
72
+ var rcVerSub = await cli.main(["audit", "--version"], cVerSub);
73
+ check("subcommand + stray --version → runs subcommand (exit 2 usage, not version no-op)",
74
+ rcVerSub === 2 && /Usage: blamejs audit/.test(cVerSub.err()));
75
+ var cVerBare = _captureCtx();
76
+ var rcVerBare = await cli.main(["--version"], cVerBare);
77
+ check("bare --version → exit 0 + version", rcVerBare === 0 && /\d+\.\d+\.\d+/.test(cVerBare.out()));
78
+
79
+ // ...and version BEFORE the subcommand: _parseArgs treats --version as a
80
+ // value flag and swallows the subcommand as its value, so the stray flag is
81
+ // stripped from the raw argv and the command still dispatches.
82
+ var cVLead = _captureCtx();
83
+ var rcVLead = await cli.main(["--version", "audit"], cVLead);
84
+ check("leading --version + subcommand → dispatches subcommand (exit 2 usage, not version)",
85
+ rcVLead === 2 && /Usage: blamejs audit/.test(cVLead.err()) && !/^\d+\.\d+\.\d+\s*$/.test(cVLead.out().trim()));
86
+ var cVLead2 = _captureCtx();
87
+ var rcVLead2 = await cli.main(["-v", "migrate"], cVLead2);
88
+ check("leading -v + subcommand → dispatches subcommand (migrate usage, not version)",
89
+ rcVLead2 === 2 && /Usage: blamejs migrate/.test(cVLead2.err()));
90
+
91
+ // ...and BETWEEN a command and its subcommand: `--version` must not swallow
92
+ // the subcommand, and the subcommand must keep its position after the
93
+ // command (audit purge, not the unknown top-level command "purge").
94
+ var cVMid = _captureCtx();
95
+ var rcVMid = await cli.main(["audit", "--version", "purge"], cVMid);
96
+ check("mid-argv --version → command keeps its subcommand (audit purge dispatched, not version)",
97
+ rcVMid === 2 && /blamejs audit purge:/.test(cVMid.err()) &&
98
+ !/unknown/.test(cVMid.err()) && !/^\d+\.\d+\.\d+\s*$/.test(cVMid.out().trim()));
99
+
100
+ // A version token AFTER the `--` option terminator is literal positional
101
+ // data (a file NAMED --version), not a version flag — it must reach the
102
+ // command unstripped.
103
+ var vTermDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-cli-verterm-"));
104
+ fs.writeFileSync(path.join(vTermDir, "--version"),
105
+ Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]));
106
+ var cVTerm = _captureCtx();
107
+ cVTerm.cwd = vTermDir;
108
+ var rcVTerm = await cli.main(["file-type", "detect", "--", "--version"], cVTerm);
109
+ check("version token after -- is a literal file arg, not stripped (detects the file)",
110
+ rcVTerm === 0 && /mime:\s+image\/png/.test(cVTerm.out()));
111
+ try { fs.rmSync(vTermDir, { recursive: true, force: true }); } catch (_e) { /* best-effort */ }
112
+
113
+ // `migrate help` positional → _runMigrate's own help branch (distinct from
114
+ // the top-level `migrate --help` synth, which never enters _runMigrate).
115
+ var cMh = _captureCtx();
116
+ var rcMh = await cli.main(["migrate", "help"], cMh);
117
+ check("migrate help positional → exit 0", rcMh === 0);
118
+ check("migrate help positional → usage", /Usage: blamejs migrate/.test(cMh.out()));
119
+
120
+ // bare `audit` → AUDIT_USAGE on stderr, exit 2
121
+ var cAudit = _captureCtx();
122
+ var rcAudit = await cli.main(["audit"], cAudit);
123
+ check("bare audit → exit 2", rcAudit === 2);
124
+ check("bare audit → usage on stderr", /Usage: blamejs audit/.test(cAudit.err()));
125
+
126
+ // unknown audit subcommand → exit 2
127
+ var cAuditUnk = _captureCtx();
128
+ var rcAuditUnk = await cli.main(["audit", "frobnicate"], cAuditUnk);
129
+ check("audit unknown sub → exit 2", rcAuditUnk === 2);
130
+ check("audit unknown sub → names sub", /unknown subcommand 'frobnicate'/.test(cAuditUnk.err()));
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // seed run — apply / re-skip / --only / --force / broken-seeder catch
135
+ // ---------------------------------------------------------------------------
136
+ async function sectionSeedRun() {
137
+ var dir = _tmpDir("blamejs-cli-seed");
138
+ try {
139
+ var dbPath = path.join(dir, "seed.db");
140
+ var seedRoot = path.join(dir, "seeders");
141
+ var devDir = path.join(seedRoot, "dev");
142
+ fs.mkdirSync(devDir, { recursive: true });
143
+ fs.writeFileSync(path.join(devDir, "0001-alpha.js"),
144
+ "module.exports = { description: \"a\", run: async function (db) {" +
145
+ " db.exec(\"CREATE TABLE IF NOT EXISTS m_alpha (id INTEGER)\"); } };");
146
+ fs.writeFileSync(path.join(devDir, "0002-beta.js"),
147
+ "module.exports = { description: \"b\", rerunnable: true, run: async function (db) {" +
148
+ " db.exec(\"CREATE TABLE IF NOT EXISTS m_beta (id INTEGER)\"); } };");
149
+
150
+ // first run → both apply
151
+ var c1 = _captureCtx();
152
+ var r1 = await cli.main(["seed", "run", "--db", dbPath, "--env", "dev", "--dir", seedRoot], c1);
153
+ check("seed run: first run → exit 0", r1 === 0);
154
+ check("seed run: applies 2 seeds", /applied 2 seed\(s\)/.test(c1.out()));
155
+
156
+ // status after run → applied rows loop + rerunnable listing
157
+ var c2 = _captureCtx();
158
+ var r2 = await cli.main(["seed", "status", "--db", dbPath, "--env", "dev", "--dir", seedRoot], c2);
159
+ check("seed status (post-run): exit 0", r2 === 0);
160
+ check("seed status (post-run): env line", /env: dev/.test(c2.out()));
161
+ check("seed status (post-run): rerunnable", /rerunnable:/.test(c2.out()));
162
+
163
+ // second run → 0001 skipped, 0002 (rerunnable) re-applies
164
+ var c3 = _captureCtx();
165
+ var r3 = await cli.main(["seed", "run", "--db", dbPath, "--env", "dev", "--dir", seedRoot], c3);
166
+ check("seed run: second run → exit 0", r3 === 0);
167
+ check("seed run: reports a skipped seed", /skipped 1/.test(c3.out()));
168
+
169
+ // --only an already-applied seed → nothing applied (only-branch)
170
+ var c4 = _captureCtx();
171
+ var r4 = await cli.main(
172
+ ["seed", "run", "--db", dbPath, "--env", "dev", "--dir", seedRoot, "--only", "0001-alpha.js"], c4);
173
+ check("seed run --only: exit 0", r4 === 0);
174
+ check("seed run --only: no re-apply", /no seeds applied/.test(c4.out()));
175
+
176
+ // --force → re-applies already-applied seeds (force-branch)
177
+ var c5 = _captureCtx();
178
+ var r5 = await cli.main(
179
+ ["seed", "run", "--db", dbPath, "--env", "dev", "--dir", seedRoot, "--force"], c5);
180
+ check("seed run --force: exit 0", r5 === 0);
181
+ check("seed run --force: applies again", /applied \d+ seed\(s\)/.test(c5.out()));
182
+
183
+ // broken seeder → run throws → the run catch returns exit 1
184
+ var dir2 = _tmpDir("blamejs-cli-seed-broken");
185
+ try {
186
+ var db2 = path.join(dir2, "seed.db");
187
+ var devDir2 = path.join(dir2, "seeders", "dev");
188
+ fs.mkdirSync(devDir2, { recursive: true });
189
+ fs.writeFileSync(path.join(devDir2, "0001-boom.js"),
190
+ "module.exports = { description: \"x\", run: async function () {" +
191
+ " throw new Error(\"boom-seed\"); } };");
192
+ var c6 = _captureCtx();
193
+ var r6 = await cli.main(
194
+ ["seed", "run", "--db", db2, "--env", "dev", "--dir", path.join(dir2, "seeders")], c6);
195
+ check("seed run: broken seeder → exit 1", r6 === 1);
196
+ check("seed run: broken seeder → stderr", /blamejs seed run:/.test(c6.err()));
197
+ } finally { _rm(dir2); }
198
+ } finally { _rm(dir); }
199
+ }
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // api-snapshot compare that DETECTS a breaking change (exit 1)
203
+ // ---------------------------------------------------------------------------
204
+ async function sectionApiSnapshotBreaking() {
205
+ var dir = _tmpDir("blamejs-cli-apisnap");
206
+ try {
207
+ var modA = path.join(dir, "mod-a.js");
208
+ var modB = path.join(dir, "mod-b.js");
209
+ var snap = path.join(dir, "snap.json");
210
+ fs.writeFileSync(modA,
211
+ "module.exports = { version: \"1.0.0\", greet: function greet() {}, extra: function extra() {} };");
212
+ fs.writeFileSync(modB,
213
+ "module.exports = { version: \"1.0.0\", greet: function greet() {} };");
214
+
215
+ var cCap = _captureCtx();
216
+ var rcCap = await cli.main(
217
+ ["api-snapshot", "capture", "--file", snap, "--module", modA], cCap);
218
+ check("api-snapshot capture (mod-a): exit 0", rcCap === 0);
219
+
220
+ // mod-b dropped `extra` → removed export → breaking → exit 1
221
+ var cCmp = _captureCtx();
222
+ var rcCmp = await cli.main(
223
+ ["api-snapshot", "compare", "--file", snap, "--module", modB], cCmp);
224
+ check("api-snapshot compare breaking → exit 1", rcCmp === 1);
225
+ check("api-snapshot compare breaking → reports it",
226
+ /BREAKING|removed|extra/i.test(cCmp.out()));
227
+ } finally { _rm(dir); }
228
+ }
229
+
230
+ // ---------------------------------------------------------------------------
231
+ // audit — arg-validation returns + env passphrase + verify-chain FAIL / max-rows
232
+ // ---------------------------------------------------------------------------
233
+ async function sectionAudit() {
234
+ var dir = _tmpDir("blamejs-cli-audit");
235
+ try {
236
+ var outDir = path.join(dir, "bundle-out");
237
+
238
+ // archive: missing passphrase → 2
239
+ var a1 = _captureCtx();
240
+ check("audit archive no-pass → exit 2",
241
+ (await cli.main(["audit", "archive", "--out", outDir, "--before", "2020-01-01"], a1)) === 2);
242
+ check("audit archive no-pass → message", /--passphrase or BLAMEJS_AUDIT_PASSPHRASE/.test(a1.err()));
243
+
244
+ // archive: passphrase but no --out → 2
245
+ var a2 = _captureCtx();
246
+ check("audit archive no-out → exit 2",
247
+ (await cli.main(["audit", "archive", "--passphrase", "p", "--before", "2020-01-01"], a2)) === 2);
248
+ check("audit archive no-out → message", /--out is required/.test(a2.err()));
249
+
250
+ // archive: passphrase + out but no --before → 2
251
+ var a3 = _captureCtx();
252
+ check("audit archive no-before → exit 2",
253
+ (await cli.main(["audit", "archive", "--passphrase", "p", "--out", outDir], a3)) === 2);
254
+ check("audit archive no-before → message", /--before is required/.test(a3.err()));
255
+
256
+ // export: passphrase but no --out → 2
257
+ var e1 = _captureCtx();
258
+ check("audit export no-out → exit 2",
259
+ (await cli.main(["audit", "export", "--passphrase", "p"], e1)) === 2);
260
+ check("audit export no-out → message", /--out is required/.test(e1.err()));
261
+
262
+ // export: passphrase + out but no from/to/action → 2
263
+ var e2 = _captureCtx();
264
+ check("audit export no-range → exit 2",
265
+ (await cli.main(["audit", "export", "--passphrase", "p", "--out", outDir], e2)) === 2);
266
+ check("audit export no-range → message", /at least one of --from/.test(e2.err()));
267
+
268
+ // verify-bundle: passphrase but no --in → 2
269
+ var vb = _captureCtx();
270
+ check("audit verify-bundle no-in → exit 2",
271
+ (await cli.main(["audit", "verify-bundle", "--passphrase", "p"], vb)) === 2);
272
+ check("audit verify-bundle no-in → message", /--in is required/.test(vb.err()));
273
+
274
+ // purge: passphrase but no --archive → 2
275
+ var p1 = _captureCtx();
276
+ check("audit purge no-archive → exit 2",
277
+ (await cli.main(["audit", "purge", "--passphrase", "p"], p1)) === 2);
278
+ check("audit purge no-archive → message", /--archive .* is required/.test(p1.err()));
279
+
280
+ // purge: passphrase + archive but no --confirm → 2
281
+ var p2 = _captureCtx();
282
+ check("audit purge no-confirm → exit 2",
283
+ (await cli.main(["audit", "purge", "--passphrase", "p", "--archive", outDir], p2)) === 2);
284
+ check("audit purge no-confirm → message", /--confirm is REQUIRED/.test(p2.err()));
285
+
286
+ // env passphrase resolution: BLAMEJS_AUDIT_PASSPHRASE satisfies passRequired,
287
+ // then archive fails on the missing --out (exit 2). Exercises the env branch
288
+ // of _resolvePassphrase.
289
+ var envCtx = _captureCtx();
290
+ envCtx.env = { BLAMEJS_AUDIT_PASSPHRASE: "from-env" };
291
+ var rcEnv = await cli.main(["audit", "archive", "--before", "2020-01-01"], envCtx);
292
+ check("audit archive env-pass → past pass-gate to --out → exit 2", rcEnv === 2);
293
+ check("audit archive env-pass → --out message", /--out is required/.test(envCtx.err()));
294
+
295
+ // verify-chain: valid --max-rows applied against an empty audit_log (exit 0)
296
+ var okDb = path.join(dir, "ok.db");
297
+ var okHandle = new sqlite.DatabaseSync(okDb);
298
+ okHandle.prepare("CREATE TABLE audit_log (_id INTEGER PRIMARY KEY, monotonicCounter INTEGER," +
299
+ " prevHash TEXT, rowHash TEXT, nonce BLOB)").run();
300
+ okHandle.close();
301
+ var mr = _captureCtx();
302
+ var rcMr = await cli.main(["audit", "verify-chain", "--db", okDb, "--max-rows", "5"], mr);
303
+ check("audit verify-chain --max-rows valid → exit 0", rcMr === 0);
304
+ check("audit verify-chain --max-rows valid → verified", /rowsVerified=0/.test(mr.out()));
305
+
306
+ // verify-chain: a tampered first row (prevHash != ZERO_HASH) → FAIL, exit 1
307
+ var badDb = path.join(dir, "bad.db");
308
+ var badHandle = new sqlite.DatabaseSync(badDb);
309
+ badHandle.prepare("CREATE TABLE audit_consent (_id INTEGER PRIMARY KEY, monotonicCounter INTEGER," +
310
+ " prevHash TEXT, rowHash TEXT, nonce BLOB)").run();
311
+ var ins = badHandle.prepare("INSERT INTO audit_consent" +
312
+ " (_id, monotonicCounter, prevHash, rowHash, nonce) VALUES (?, ?, ?, ?, ?)");
313
+ // prevHash of the FIRST row must be the all-zero sentinel; "ff"*64 is a
314
+ // valid 128-hex string that is NOT the sentinel → guaranteed chain break.
315
+ ins.run(1, 1, "ff".repeat(64), "aa".repeat(64), Buffer.alloc(16));
316
+ badHandle.close();
317
+ var fc = _captureCtx();
318
+ var rcFc = await cli.main(
319
+ ["audit", "verify-chain", "--db", badDb, "--table", "audit_consent"], fc);
320
+ check("audit verify-chain tampered → exit 1", rcFc === 1);
321
+ check("audit verify-chain tampered → FAIL line", /FAIL —/.test(fc.err()));
322
+ check("audit verify-chain tampered → prints expected/actual", /expected prevHash:/.test(fc.err()));
323
+ } finally { _rm(dir); }
324
+ }
325
+
326
+ // ---------------------------------------------------------------------------
327
+ // file-type — bare / help / read-failure / json+allowlist / json-null
328
+ // ---------------------------------------------------------------------------
329
+ async function sectionFileType() {
330
+ var dir = _tmpDir("blamejs-cli-filetype");
331
+ try {
332
+ var pngBytes = Buffer.concat([
333
+ Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
334
+ Buffer.alloc(20),
335
+ ]);
336
+ var pngFile = path.join(dir, "img.png");
337
+ fs.writeFileSync(pngFile, pngBytes);
338
+ var unkFile = path.join(dir, "unk.bin");
339
+ fs.writeFileSync(unkFile, Buffer.from("not-a-known-format-byte-stream-xyz"));
340
+
341
+ // bare `file-type` → usage exit 2
342
+ var bare = _captureCtx();
343
+ check("file-type bare → exit 2", (await cli.main(["file-type"], bare)) === 2);
344
+ check("file-type bare → usage", /Usage: blamejs file-type/.test(bare.err()));
345
+
346
+ // `file-type help` positional → helpStdout exit 0
347
+ var fh = _captureCtx();
348
+ check("file-type help → exit 0", (await cli.main(["file-type", "help"], fh)) === 0);
349
+ check("file-type help → usage", /Usage: blamejs file-type/.test(fh.out()));
350
+
351
+ // detect a nonexistent path → read failure catch → exit 1
352
+ var rf = _captureCtx();
353
+ var rcRf = await cli.main(["file-type", "detect", path.join(dir, "nope.bin")], rf);
354
+ check("file-type detect missing file → exit 1", rcRf === 1);
355
+ check("file-type detect missing file → read failed", /read failed/.test(rf.err()));
356
+
357
+ // json + allowlist match → the json arm of the allowlist branch
358
+ var ja = _captureCtx();
359
+ var rcJa = await cli.main(
360
+ ["file-type", "detect", pngFile, "--json", "--allowlist", "image/png,application/pdf"], ja);
361
+ check("file-type allowlist+json → exit 0", rcJa === 0);
362
+ check("file-type allowlist+json → JSON body", /"mime":"image\/png"/.test(ja.out()));
363
+
364
+ // json + unknown signature → prints "null" then errors (exit 1)
365
+ var jn = _captureCtx();
366
+ var rcJn = await cli.main(["file-type", "detect", unkFile, "--json"], jn);
367
+ check("file-type json-null → exit 1", rcJn === 1);
368
+ check("file-type json-null → 'null' on stdout", /null/.test(jn.out()));
369
+ } finally { _rm(dir); }
370
+ }
371
+
372
+ // ---------------------------------------------------------------------------
373
+ // `<cmd> help` positional — report.helpStdout for every reporter-backed command
374
+ // ---------------------------------------------------------------------------
375
+ async function sectionPositionalHelp() {
376
+ var cmds = [
377
+ ["restore", /Usage: blamejs restore/],
378
+ ["backup", /Usage: blamejs backup/],
379
+ ["mtls", /Usage: blamejs mtls/],
380
+ ["vault", /Usage: blamejs vault/],
381
+ ["api-key", /Usage: blamejs api-key/],
382
+ ["security", /Usage: blamejs security/],
383
+ ["config-drift", /Usage: blamejs config-drift/],
384
+ ["retention", /Usage: blamejs retention/],
385
+ ["password", /Usage: blamejs password/],
386
+ ];
387
+ for (var i = 0; i < cmds.length; i++) {
388
+ var ctx = _captureCtx();
389
+ var rc = await cli.main([cmds[i][0], "help"], ctx);
390
+ check(cmds[i][0] + " help → exit 0", rc === 0);
391
+ check(cmds[i][0] + " help → usage", cmds[i][1].test(ctx.out()));
392
+ }
393
+ }
394
+
395
+ // ---------------------------------------------------------------------------
396
+ // cheap arg-validation returns (no boot)
397
+ // ---------------------------------------------------------------------------
398
+ async function sectionArgValidation() {
399
+ // bare subcommand-less usages → exit 2
400
+ var bares = ["security", "config-drift", "retention", "password"];
401
+ for (var i = 0; i < bares.length; i++) {
402
+ var cb = _captureCtx();
403
+ check("bare " + bares[i] + " → exit 2", (await cli.main([bares[i]], cb)) === 2);
404
+ }
405
+
406
+ // retention: missing --data-dir → 2
407
+ var rNoDir = _captureCtx();
408
+ check("retention preview no data-dir → exit 2",
409
+ (await cli.main(["retention", "preview", "--table", "t", "--age-field", "a", "--ttl-ms", "1"], rNoDir)) === 2);
410
+
411
+ // retention: missing --ttl-ms → 2
412
+ var rNoTtl = _captureCtx();
413
+ check("retention preview no ttl-ms → exit 2",
414
+ (await cli.main(["retention", "preview", "--data-dir", "/tmp/x", "--table", "t", "--age-field", "a"], rNoTtl)) === 2);
415
+
416
+ // api-key: missing --namespace → 2 (data-dir present)
417
+ var kNoNs = _captureCtx();
418
+ check("api-key issue no namespace → exit 2",
419
+ (await cli.main(["api-key", "issue", "--data-dir", "/tmp/x"], kNoNs)) === 2);
420
+ check("api-key issue no namespace → message", /--namespace/.test(kNoNs.err()));
421
+
422
+ // api-key: bad --vault-mode → 2
423
+ var kBadVm = _captureCtx();
424
+ check("api-key bad vault-mode → exit 2",
425
+ (await cli.main(["api-key", "issue", "--data-dir", "/tmp/x", "--namespace", "n", "--vault-mode", "yolo"], kBadVm)) === 2);
426
+ check("api-key bad vault-mode → message", /--vault-mode/.test(kBadVm.err()));
427
+
428
+ // erase: missing --data-dir (table/row/confirm all present) → 2
429
+ var eNoDir = _captureCtx();
430
+ check("erase no data-dir → exit 2",
431
+ (await cli.main(["erase", "--table", "users", "--row-id", "r-1", "--confirm", "--vault-mode", "plaintext"], eNoDir)) === 2);
432
+ check("erase no data-dir → message", /--data-dir/.test(eNoDir.err()));
433
+ }
434
+
435
+ // ---------------------------------------------------------------------------
436
+ // backup / restore validation + error catches (mostly no boot)
437
+ // ---------------------------------------------------------------------------
438
+ async function sectionBackupRestoreValidation() {
439
+ var dir = _tmpDir("blamejs-cli-br");
440
+ try {
441
+ // --- backup ---
442
+ // inspect: missing --bundle → 2
443
+ var b1 = _captureCtx();
444
+ check("backup inspect no bundle → exit 2", (await cli.main(["backup", "inspect"], b1)) === 2);
445
+ check("backup inspect no bundle → message", /--bundle <dir> is required/.test(b1.err()));
446
+
447
+ // inspect a nonexistent bundle → restoreBundle.inspect throws → catch → 1
448
+ var b2 = _captureCtx();
449
+ var rcB2 = await cli.main(["backup", "inspect", "--bundle", path.join(dir, "no-such-bundle")], b2);
450
+ check("backup inspect bad bundle → exit 1", rcB2 === 1);
451
+
452
+ // verify: missing passphrase → 2 (bundle present so passphrase gate is reached)
453
+ var b3 = _captureCtx();
454
+ check("backup verify no passphrase → exit 2",
455
+ (await cli.main(["backup", "verify", "--bundle", path.join(dir, "b")], b3)) === 2);
456
+ check("backup verify no passphrase → message", /--passphrase or BLAMEJS_BACKUP_PASSPHRASE/.test(b3.err()));
457
+
458
+ // extract: passphrase present but missing --to → 2
459
+ var b4 = _captureCtx();
460
+ check("backup extract no --to → exit 2",
461
+ (await cli.main(["backup", "extract", "--bundle", path.join(dir, "b"), "--passphrase", "p"], b4)) === 2);
462
+ check("backup extract no --to → message", /--to <stagingDir> is required/.test(b4.err()));
463
+
464
+ // --- restore ---
465
+ // list with no selector → "--storage-root is required" → 2
466
+ var r1 = _captureCtx();
467
+ check("restore list no selector → exit 2", (await cli.main(["restore", "list"], r1)) === 2);
468
+ check("restore list no selector → message", /--storage-root/.test(r1.err()));
469
+
470
+ // inspect --storage-root without --bundle-id → 2
471
+ var r2 = _captureCtx();
472
+ check("restore inspect storage-root sans bundle-id → exit 2",
473
+ (await cli.main(["restore", "inspect", "--storage-root", dir], r2)) === 2);
474
+ check("restore inspect storage-root sans bundle-id → message", /--bundle-id is required/.test(r2.err()));
475
+
476
+ // rollback with no --data-dir → 2
477
+ var r3 = _captureCtx();
478
+ check("restore rollback no data-dir → exit 2", (await cli.main(["restore", "rollback"], r3)) === 2);
479
+
480
+ // list-rollbacks with no --data-dir → 2
481
+ var r4 = _captureCtx();
482
+ check("restore list-rollbacks no data-dir → exit 2", (await cli.main(["restore", "list-rollbacks"], r4)) === 2);
483
+
484
+ // apply: valid selector + passphrase, but invalid --max-pulled-bytes → 2
485
+ var r5 = _captureCtx();
486
+ check("restore apply bad max-bytes → exit 2",
487
+ (await cli.main(["restore", "apply", "--data-dir", path.join(dir, "dd"),
488
+ "--bundle", path.join(dir, "bun"), "--passphrase", "p", "--max-pulled-bytes", "-1"], r5)) === 2);
489
+ check("restore apply bad max-bytes → message", /--max-pulled-bytes/.test(r5.err()));
490
+
491
+ // apply: invalid --max-pulled-files → 2
492
+ var r6 = _captureCtx();
493
+ check("restore apply bad max-files → exit 2",
494
+ (await cli.main(["restore", "apply", "--data-dir", path.join(dir, "dd"),
495
+ "--bundle", path.join(dir, "bun"), "--passphrase", "p", "--max-pulled-files", "-1"], r6)) === 2);
496
+ check("restore apply bad max-files → message", /--max-pulled-files/.test(r6.err()));
497
+
498
+ // rollback with an explicit --rollback that does not exist → primitive
499
+ // throws → catch → non-zero (exercises the explicit-target branch).
500
+ var ddEmpty = path.join(dir, "empty-data");
501
+ fs.mkdirSync(ddEmpty, { recursive: true });
502
+ var r7 = _captureCtx();
503
+ var rcR7 = await cli.main(
504
+ ["restore", "rollback", "--data-dir", ddEmpty, "--rollback", path.join(dir, "no-such-point")], r7);
505
+ check("restore rollback bad explicit target → non-zero", rcR7 !== 0);
506
+ } finally { _rm(dir); }
507
+ }
508
+
509
+ // ---------------------------------------------------------------------------
510
+ // booted: api-key rotate + revoke no-op + empty-scopes
511
+ // ---------------------------------------------------------------------------
512
+ async function sectionBootedApiKey() {
513
+ var dataDir = _tmpDir("blamejs-cli-apikey");
514
+ try {
515
+ var base = ["--data-dir", dataDir, "--vault-mode", "plaintext", "--namespace", "api"];
516
+
517
+ // issue a key so rotate has something to rotate
518
+ var ci = _captureCtx();
519
+ var rci = await cli.main(["api-key", "issue"].concat(base).concat(
520
+ ["--owner-id", "dave", "--scopes", "a:read"]), ci);
521
+ check("apikey issue (for rotate): exit 0", rci === 0);
522
+ var idMatch = ci.out().match(/^id:\s+([a-f0-9]+)/m);
523
+ check("apikey issue: id captured", !!idMatch);
524
+ var id = idMatch ? idMatch[1] : "deadbeef";
525
+ var keyMatch = ci.out().match(/^key:\s+(\S+)/m);
526
+
527
+ // list → owner header + active-key row loop (the list subcommand body)
528
+ var cl = _captureCtx();
529
+ var rcl = await cli.main(["api-key", "list"].concat(base).concat(["--owner-id", "dave"]), cl);
530
+ check("apikey list: exit 0", rcl === 0);
531
+ check("apikey list: owner header", /owner: dave \(\d+ active keys\)/.test(cl.out()));
532
+
533
+ // verify a VALID composite token BEFORE rotate (rotate with default
534
+ // gracePeriodMs=0 immediately invalidates the old secret) → success
535
+ // path prints the resolved id / ownerId / scopes.
536
+ if (keyMatch) {
537
+ var cvf = _captureCtx();
538
+ var rcvf = await cli.main(["api-key", "verify"].concat(base).concat(["--token", keyMatch[1]]), cvf);
539
+ check("apikey verify valid → exit 0", rcvf === 0);
540
+ check("apikey verify valid → prints ownerId", /ownerId:\s+dave/.test(cvf.out()));
541
+ }
542
+
543
+ // verify a syntactically-bogus token → registry.verify returns null →
544
+ // report.error("rejected: ...") → non-zero.
545
+ var cvb = _captureCtx();
546
+ var rcvb = await cli.main(["api-key", "verify"].concat(base).concat(["--token", "not-a-real-token"]), cvb);
547
+ check("apikey verify bogus → non-zero", rcvb !== 0);
548
+ check("apikey verify bogus → rejected msg", /rejected: token does not verify/.test(cvb.err()));
549
+
550
+ // rotate → new secret, exit 0 (the rotate subcommand is otherwise untested)
551
+ var cr = _captureCtx();
552
+ var rcr = await cli.main(["api-key", "rotate"].concat(base).concat(["--id", id]), cr);
553
+ check("apikey rotate: exit 0", rcr === 0);
554
+ check("apikey rotate: prints new key", /key \(new\):/.test(cr.out()));
555
+
556
+ // revoke a nonexistent id → registry.revoke returns false → no-op error path
557
+ var cx = _captureCtx();
558
+ var rcx = await cli.main(["api-key", "revoke"].concat(base).concat(["--id", "00ffffffffffffff"]), cx);
559
+ check("apikey revoke no-op → non-zero", rcx !== 0);
560
+ check("apikey revoke no-op → message", /no-op/.test(cx.err()));
561
+
562
+ // issue with scopes that parse to an empty list → exit 2 (post-boot check)
563
+ var cs = _captureCtx();
564
+ var rcs = await cli.main(["api-key", "issue"].concat(base).concat(
565
+ ["--owner-id", "erin", "--scopes", ","]), cs);
566
+ check("apikey issue empty-scopes → exit 2", rcs === 2);
567
+ check("apikey issue empty-scopes → message", /at least one non-empty scope/.test(cs.err()));
568
+
569
+ // post-boot required-flag checks that only fire AFTER a successful boot:
570
+ // issue with no --owner-id and issue with no --scopes each return exit 2.
571
+ var cNoOwner = _captureCtx();
572
+ var rcNoOwner = await cli.main(["api-key", "issue"].concat(base).concat(["--scopes", "a:read"]), cNoOwner);
573
+ check("apikey issue no owner-id (post-boot) → exit 2", rcNoOwner === 2);
574
+ check("apikey issue no owner-id → message", /--owner-id <id> is required/.test(cNoOwner.err()));
575
+
576
+ var cNoScopes = _captureCtx();
577
+ var rcNoScopes = await cli.main(["api-key", "issue"].concat(base).concat(["--owner-id", "frank"]), cNoScopes);
578
+ check("apikey issue no scopes (post-boot) → exit 2", rcNoScopes === 2);
579
+ check("apikey issue no scopes → message", /--scopes <comma-separated> is required/.test(cNoScopes.err()));
580
+
581
+ // issue WITH --label + --expires-ms → the metadata-label arm, the
582
+ // expiresAt-mapping arm, and the "expires:" stdout line. The subsequent
583
+ // list surfaces the per-row "expires=" branch, and verifying the composite
584
+ // token twice stamps then prints "last-used:" + "expires:".
585
+ var future = Date.now() + 1000 * 60 * 60 * 24;
586
+ var ce = _captureCtx();
587
+ var rce = await cli.main(["api-key", "issue"].concat(base).concat(
588
+ ["--owner-id", "grace", "--scopes", "a:read,b:write", "--label", "ci-token",
589
+ "--expires-ms", String(future)]), ce);
590
+ check("apikey issue --label --expires-ms → exit 0", rce === 0);
591
+ check("apikey issue --expires-ms → prints expires line", /^expires:\s+\d{4}-/m.test(ce.out()));
592
+ var graceKey = ce.out().match(/^key:\s+(\S+)/m);
593
+
594
+ var cle = _captureCtx();
595
+ await cli.main(["api-key", "list"].concat(base).concat(["--owner-id", "grace"]), cle);
596
+ check("apikey list → per-row expires= branch", /expires=\d{4}-/.test(cle.out()));
597
+
598
+ if (graceKey) {
599
+ // first verify stamps lastUsedAt; second verify prints both the
600
+ // "last-used:" and "expires:" lines (v.lastUsedAt / v.expiresAt truthy).
601
+ await cli.main(["api-key", "verify"].concat(base).concat(["--token", graceKey[1]]), _captureCtx());
602
+ var cve = _captureCtx();
603
+ var rcve = await cli.main(["api-key", "verify"].concat(base).concat(["--token", graceKey[1]]), cve);
604
+ check("apikey verify (2nd) → exit 0", rcve === 0);
605
+ check("apikey verify → prints last-used line", /last-used:\s+\d{4}-/.test(cve.out()));
606
+ check("apikey verify → prints expires line", /expires:\s+\d{4}-/.test(cve.out()));
607
+ }
608
+ } finally { _rm(dataDir); }
609
+ }
610
+
611
+ // ---------------------------------------------------------------------------
612
+ // booted: erase row-lookup-failure catch
613
+ // ---------------------------------------------------------------------------
614
+ async function sectionBootedErase() {
615
+ var dataDir = _tmpDir("blamejs-cli-erase");
616
+ try {
617
+ // A syntactically-valid table identifier that does not exist → the SELECT
618
+ // prepare throws → the "row lookup failed" catch returns exit 1.
619
+ var ctx = _captureCtx();
620
+ var rc = await cli.main(
621
+ ["erase", "--data-dir", dataDir, "--table", "nonexistent_tbl", "--row-id", "r-1",
622
+ "--confirm", "--vault-mode", "plaintext"], ctx);
623
+ check("erase missing-table lookup → exit 1", rc === 1);
624
+ check("erase missing-table lookup → catch message", /row lookup failed/.test(ctx.err()));
625
+ } finally { _rm(dataDir); }
626
+ }
627
+
628
+ // ---------------------------------------------------------------------------
629
+ // booted: retention run against a missing table (boot + declare + run + error path)
630
+ // ---------------------------------------------------------------------------
631
+ async function sectionBootedRetention() {
632
+ var dataDir = _tmpDir("blamejs-cli-retention");
633
+ try {
634
+ var ctx = _captureCtx();
635
+ var rc = await cli.main(
636
+ ["retention", "preview", "--data-dir", dataDir, "--vault-mode", "plaintext",
637
+ "--table", "no_such_retention_tbl", "--age-field", "ts", "--ttl-ms", "1", "--action", "delete"], ctx);
638
+ // The rule boots + declares + runs; a missing table surfaces as either a
639
+ // per-row error summary (exit 1) or the outer catch (exit 1) — never a
640
+ // bad-invocation (exit 2). Assert it got PAST validation into the run.
641
+ check("retention preview missing-table → past validation (not exit 2)", rc !== 2);
642
+ check("retention preview missing-table → exit 0 or 1", rc === 0 || rc === 1);
643
+ } finally { _rm(dataDir); }
644
+ }
645
+
646
+ // ---------------------------------------------------------------------------
647
+ // booted: mtls status(exists) / show-cert(success) / issue --days / issue-p12 stdout
648
+ // ---------------------------------------------------------------------------
649
+ async function sectionBootedMtls() {
650
+ var dataDir = _tmpDir("blamejs-cli-mtls");
651
+ try {
652
+ var base = ["--data-dir", dataDir, "--vault-mode", "plaintext"];
653
+
654
+ var ci = _captureCtx();
655
+ check("mtls init: exit 0", (await cli.main(["mtls", "init"].concat(base), ci)) === 0);
656
+
657
+ // status against an EXISTING CA → the `s.exists` true branch (generation + paths)
658
+ var cs = _captureCtx();
659
+ var rcs = await cli.main(["mtls", "status"].concat(base), cs);
660
+ check("mtls status (CA exists): exit 0", rcs === 0);
661
+ check("mtls status (CA exists): reports yes", /CA exists:\s+yes/.test(cs.out()));
662
+ check("mtls status (CA exists): prints paths", /cert:/.test(cs.out()));
663
+
664
+ // show-cert against an existing CA → success PEM branch
665
+ var cc = _captureCtx();
666
+ var rcc = await cli.main(["mtls", "show-cert"].concat(base), cc);
667
+ check("mtls show-cert (CA exists): exit 0", rcc === 0);
668
+ check("mtls show-cert (CA exists): prints PEM", /BEGIN CERTIFICATE/.test(cc.out()));
669
+
670
+ // issue with --days → the days-truthy branch
671
+ var cd = _captureCtx();
672
+ var rcd = await cli.main(["mtls", "issue"].concat(base).concat(["--subject", "cn-a", "--days", "30"]), cd);
673
+ check("mtls issue --days: exit 0", rcd === 0);
674
+ check("mtls issue --days: prints cert", /BEGIN CERTIFICATE/.test(cd.out()));
675
+
676
+ // issue-p12 WITHOUT --out → streams the p12 bytes to stdout
677
+ var cp = _captureCtx();
678
+ var rcp = await cli.main(["mtls", "issue-p12"].concat(base).concat(
679
+ ["--subject", "cn-b", "--password", "p12-passphrase-abc"]), cp);
680
+ check("mtls issue-p12 (stdout): exit 0", rcp === 0);
681
+ check("mtls issue-p12 (stdout): prints fingerprint", /fingerprint \(sha3-512\)/.test(cp.out()));
682
+
683
+ // issue-p12 WITH --out AND --days → atomicFile.writeSync writes the bundle
684
+ // to disk (the `outPath` branch) and the `--days` truthy arm sets the leaf
685
+ // validityDays (distinct from the default-validity stdout call above).
686
+ var p12Out = path.join(dataDir, "client.p12");
687
+ var co = _captureCtx();
688
+ var rco = await cli.main(["mtls", "issue-p12"].concat(base).concat(
689
+ ["--subject", "cn-c", "--password", "p12-passphrase-xyz", "--days", "45", "--out", p12Out]), co);
690
+ check("mtls issue-p12 --out --days: exit 0", rco === 0);
691
+ check("mtls issue-p12 --out --days: reports written path", /p12 written: /.test(co.out()));
692
+ check("mtls issue-p12 --out --days: file exists on disk", fs.existsSync(p12Out));
693
+ } finally { _rm(dataDir); }
694
+ }
695
+
696
+ // ---------------------------------------------------------------------------
697
+ // booted: security assert with --require-env / --forbid-env
698
+ // ---------------------------------------------------------------------------
699
+ async function sectionBootedSecurity() {
700
+ var dataDir = _tmpDir("blamejs-cli-security");
701
+ try {
702
+ var ctx = _captureCtx();
703
+ var rc = await cli.main(
704
+ ["security", "assert", "--data-dir", dataDir, "--vault-mode", "plaintext",
705
+ "--no-audit-signing", "--no-db-at-rest", "--no-ntp-strict",
706
+ "--require-env", "BLAMEJS_DEFINITELY_UNSET_XYZ",
707
+ "--forbid-env", "BLAMEJS_ANOTHER_UNSET_ABC"], ctx);
708
+ // plaintext vault vs the asserted "wrapped" posture + the unset required
709
+ // env fail the assertion → FAIL summary, exit 1.
710
+ check("security assert require/forbid-env → exit 1", rc === 1);
711
+ check("security assert require/forbid-env → FAIL summary", /FAIL:/.test(ctx.out()));
712
+ } finally { _rm(dataDir); }
713
+ }
714
+
715
+ // ---------------------------------------------------------------------------
716
+ // repeatable flags — a duplicated --arg / --watch / --ignore must accumulate
717
+ // every occurrence into an array, not collapse to the last one. Regression
718
+ // for the dev-command repeatable-flag drop: parseRaw overwrote flags[name] on
719
+ // repeat, so only the LAST --watch dir was monitored, the LAST --ignore
720
+ // applied, and only the LAST --arg reached the spawned child — every earlier
721
+ // occurrence was silently dropped even though DEV_USAGE documents all three
722
+ // as "(repeatable)".
723
+ // ---------------------------------------------------------------------------
724
+ function sectionRepeatableFlags() {
725
+ // Public primitive: b.argParser.parseRaw (the splitter lib/cli.js runs).
726
+ var r = b.argParser.parseRaw(
727
+ ["dev", "--command", "node",
728
+ "--arg", "x", "--arg", "y", "--arg", "z",
729
+ "--watch", "./a", "--watch", "./b",
730
+ "--ignore", "p1", "--ignore", "p2"]);
731
+ check("parseRaw: repeated --arg accumulates in order",
732
+ Array.isArray(r.flags.arg) && r.flags.arg.join(",") === "x,y,z");
733
+ check("parseRaw: repeated --watch accumulates in order",
734
+ Array.isArray(r.flags.watch) && r.flags.watch.join(",") === "./a,./b");
735
+ check("parseRaw: repeated --ignore accumulates in order",
736
+ Array.isArray(r.flags.ignore) && r.flags.ignore.join(",") === "p1,p2");
737
+ // A flag seen once stays a scalar — non-repeatable flags are unaffected.
738
+ check("parseRaw: single --command stays a scalar string", r.flags.command === "node");
739
+
740
+ // Consumer path: cli._parseArgs is the exact splitter main() runs before
741
+ // dispatching to _runDev, which reads args.flags.watch / .arg / .ignore and
742
+ // funnels each through _coerceList. An array here is what _runDev needs so
743
+ // it watches BOTH dirs and forwards BOTH args to the child.
744
+ var a = cli._parseArgs(
745
+ ["--command", "node", "--watch", "one", "--watch", "two",
746
+ "--arg", "a1", "--arg", "a2"]);
747
+ check("_parseArgs: --watch reaches _runDev as an ordered array",
748
+ Array.isArray(a.flags.watch) && a.flags.watch.length === 2 &&
749
+ a.flags.watch[0] === "one" && a.flags.watch[1] === "two");
750
+ check("_parseArgs: --arg reaches _runDev as an ordered array",
751
+ Array.isArray(a.flags.arg) && a.flags.arg.length === 2 &&
752
+ a.flags.arg[0] === "a1" && a.flags.arg[1] === "a2");
753
+
754
+ // The `--flag=value` form repeats accumulate too — including child flags
755
+ // passed as `--arg=--inspect` (a value that itself starts with "--").
756
+ var eqArg = b.argParser.parseRaw(["--arg=--inspect", "--arg=--port=3000"]);
757
+ check("parseRaw: repeated --arg=VALUE accumulates (dash-valued children)",
758
+ Array.isArray(eqArg.flags.arg) &&
759
+ eqArg.flags.arg.join("|") === "--inspect|--port=3000");
760
+
761
+ // Single occurrence of a value flag stays a plain string, not a 1-element
762
+ // array — _coerceList wraps it, so the shape contract downstream is scalar.
763
+ var one = b.argParser.parseRaw(["--watch", "./only"]);
764
+ check("parseRaw: single --watch stays a string", one.flags.watch === "./only");
765
+ }
766
+
767
+ // ---------------------------------------------------------------------------
768
+ // migrate — the whole _runMigrate body (up / down / status / no-op / bad-steps
769
+ // / cannot-open / broken-migration catch) against a local temp sqlite file.
770
+ // Structurally identical to the seed section already in this file; the CLI
771
+ // opens its own raw node:sqlite handle via _openSqlite, so no framework boot.
772
+ // ---------------------------------------------------------------------------
773
+ async function sectionMigrate() {
774
+ var dir = _tmpDir("blamejs-cli-migrate");
775
+ try {
776
+ var dbPath = path.join(dir, "mig.db");
777
+ var migDir = path.join(dir, "migrations");
778
+ fs.mkdirSync(migDir, { recursive: true });
779
+ fs.writeFileSync(path.join(migDir, "0001-foo.js"),
780
+ "module.exports = { up: function (db) { db['exec'](\"CREATE TABLE foo (id INTEGER)\"); }," +
781
+ " down: function (db) { db['exec'](\"DROP TABLE foo\"); } };");
782
+ fs.writeFileSync(path.join(migDir, "0002-bar.js"),
783
+ "module.exports = { up: function (db) { db['exec'](\"CREATE TABLE bar (id INTEGER)\"); }," +
784
+ " down: function (db) { db['exec'](\"DROP TABLE bar\"); } };");
785
+
786
+ // bare `migrate` (no subcommand) → usage on stderr, exit 2
787
+ var cbare = _captureCtx();
788
+ check("migrate bare: exit 2", (await cli.main(["migrate"], cbare)) === 2);
789
+ check("migrate bare: usage", /Usage: blamejs migrate/.test(cbare.err()));
790
+
791
+ // unknown subcommand → error + usage, exit 2
792
+ var cunk = _captureCtx();
793
+ check("migrate unknown sub: exit 2", (await cli.main(["migrate", "frobnicate"], cunk)) === 2);
794
+ check("migrate unknown sub: names sub", /unknown subcommand 'frobnicate'/.test(cunk.err()));
795
+
796
+ // known subcommand but missing --db → exit 2
797
+ var cnodb = _captureCtx();
798
+ check("migrate status no --db: exit 2", (await cli.main(["migrate", "status"], cnodb)) === 2);
799
+ check("migrate status no --db: message", /--db <path> is required/.test(cnodb.err()));
800
+
801
+ // status before any apply → 0/2 applied + pending listing loop
802
+ var cs = _captureCtx();
803
+ var rcs = await cli.main(["migrate", "status", "--db", dbPath, "--dir", migDir], cs);
804
+ check("migrate status (pre): exit 0", rcs === 0);
805
+ check("migrate status (pre): 0/2 applied", /applied: 0 \/ 2/.test(cs.out()));
806
+ check("migrate status (pre): lists pending", /0001-foo\.js/.test(cs.out()) && /0002-bar\.js/.test(cs.out()));
807
+
808
+ // up → applies both (the applied-count loop)
809
+ var cu = _captureCtx();
810
+ var rcu = await cli.main(["migrate", "up", "--db", dbPath, "--dir", migDir], cu);
811
+ check("migrate up: exit 0", rcu === 0);
812
+ check("migrate up: applies 2", /applied 2 migration/.test(cu.out()));
813
+
814
+ // status after up → applied-rows loop (the ✓ lines)
815
+ var cs2 = _captureCtx();
816
+ await cli.main(["migrate", "status", "--db", dbPath, "--dir", migDir], cs2);
817
+ check("migrate status (post): 2/2 applied", /applied: 2 \/ 2/.test(cs2.out()));
818
+
819
+ // up again → no-pending branch
820
+ var cu2 = _captureCtx();
821
+ var rcu2 = await cli.main(["migrate", "up", "--db", dbPath, "--dir", migDir], cu2);
822
+ check("migrate up again: exit 0", rcu2 === 0);
823
+ check("migrate up again: no pending", /no pending migrations/.test(cu2.out()));
824
+
825
+ // down --steps 1 → reverts most-recent (the reverted-count loop)
826
+ var cd = _captureCtx();
827
+ var rcd = await cli.main(["migrate", "down", "--db", dbPath, "--dir", migDir, "--steps", "1"], cd);
828
+ check("migrate down --steps 1: exit 0", rcd === 0);
829
+ check("migrate down --steps 1: reverts bar", /reverted 1 migration/.test(cd.out()) && /0002-bar\.js/.test(cd.out()));
830
+
831
+ // down with no --steps → default 1
832
+ var cd2 = _captureCtx();
833
+ var rcd2 = await cli.main(["migrate", "down", "--db", dbPath, "--dir", migDir], cd2);
834
+ check("migrate down (default steps): reverts foo", rcd2 === 0 && /0001-foo\.js/.test(cd2.out()));
835
+
836
+ // down again on a fully-reverted db → nothing-to-revert branch
837
+ var cd3 = _captureCtx();
838
+ var rcd3 = await cli.main(["migrate", "down", "--db", dbPath, "--dir", migDir], cd3);
839
+ check("migrate down (empty): nothing to revert", rcd3 === 0 && /nothing to revert/.test(cd3.out()));
840
+
841
+ // --steps 0 → positive-integer validation → exit 2
842
+ var cbad = _captureCtx();
843
+ var rcbad = await cli.main(["migrate", "down", "--db", dbPath, "--dir", migDir, "--steps", "0"], cbad);
844
+ check("migrate down --steps 0: exit 2", rcbad === 2);
845
+ check("migrate down --steps 0: message", /--steps must be a positive integer/.test(cbad.err()));
846
+
847
+ // unopenable db (parent dir absent) → _openSqlite throws → exit 1
848
+ var copen = _captureCtx();
849
+ var rcopen = await cli.main(
850
+ ["migrate", "status", "--db", path.join(dir, "no-such-dir", "x.db"), "--dir", migDir], copen);
851
+ check("migrate status unopenable db: exit 1", rcopen === 1);
852
+ check("migrate status unopenable db: message", /cannot open db/.test(copen.err()));
853
+
854
+ // a migration whose up() throws → runner.up() rejects → the run catch
855
+ // returns exit 1 with the code:message shape.
856
+ var dir2 = _tmpDir("blamejs-cli-migrate-boom");
857
+ try {
858
+ var db2 = path.join(dir2, "mig.db");
859
+ var migDir2 = path.join(dir2, "migrations");
860
+ fs.mkdirSync(migDir2, { recursive: true });
861
+ fs.writeFileSync(path.join(migDir2, "0001-boom.js"),
862
+ "module.exports = { up: function () { throw new Error(\"boom-mig\"); } };");
863
+ var cboom = _captureCtx();
864
+ var rcboom = await cli.main(["migrate", "up", "--db", db2, "--dir", migDir2], cboom);
865
+ check("migrate up broken migration: exit 1", rcboom === 1);
866
+ check("migrate up broken migration: stderr", /blamejs migrate up:/.test(cboom.err()));
867
+ } finally { _rm(dir2); }
868
+ } finally { _rm(dir); }
869
+ }
870
+
871
+ // ---------------------------------------------------------------------------
872
+ // api-snapshot — bare usage / unknown subcommand / non-breaking compare (exit 0)
873
+ // / compare where the saved-snapshot READ itself fails.
874
+ // ---------------------------------------------------------------------------
875
+ async function sectionApiSnapshotEdges() {
876
+ var dir = _tmpDir("blamejs-cli-apisnap2");
877
+ try {
878
+ // bare `api-snapshot` (no subcommand) → usage on stderr, exit 2
879
+ var cbare = _captureCtx();
880
+ var rcbare = await cli.main(["api-snapshot"], cbare);
881
+ check("api-snapshot bare: exit 2", rcbare === 2);
882
+ check("api-snapshot bare: usage on stderr", /Usage: blamejs api-snapshot/.test(cbare.err()));
883
+
884
+ // unknown subcommand → exit 2
885
+ var cunk = _captureCtx();
886
+ var rcunk = await cli.main(["api-snapshot", "frobnicate"], cunk);
887
+ check("api-snapshot unknown sub: exit 2", rcunk === 2);
888
+ check("api-snapshot unknown sub: names sub", /unknown subcommand 'frobnicate'/.test(cunk.err()));
889
+
890
+ // capture then compare against the SAME module → no diff → exit 0 (the
891
+ // non-breaking return path, distinct from the breaking exit-1 case).
892
+ var mod = path.join(dir, "mod.js");
893
+ fs.writeFileSync(mod, "module.exports = { version: \"2.0.0\", greet: function greet() {} };");
894
+ var snap = path.join(dir, "snap.json");
895
+ var ccap = _captureCtx();
896
+ check("api-snapshot capture (same-mod): exit 0",
897
+ (await cli.main(["api-snapshot", "capture", "--file", snap, "--module", mod], ccap)) === 0);
898
+ var ccmp = _captureCtx();
899
+ var rccmp = await cli.main(["api-snapshot", "compare", "--file", snap, "--module", mod], ccmp);
900
+ check("api-snapshot compare no-change: exit 0", rccmp === 0);
901
+
902
+ // capture with NO --module → the default-module branch loads the
903
+ // framework root index.js (operator-omitted-flag default path).
904
+ var defSnap = path.join(dir, "default-snap.json");
905
+ var cdef = _captureCtx();
906
+ var rcdef = await cli.main(["api-snapshot", "capture", "--file", defSnap], cdef);
907
+ check("api-snapshot capture (default module): exit 0", rcdef === 0);
908
+ check("api-snapshot capture (default module): wrote file", fs.existsSync(defSnap));
909
+
910
+ // compare where the saved snapshot file cannot be READ → the read catch
911
+ // (distinct from the capture-current-surface catch) → exit 1.
912
+ var cread = _captureCtx();
913
+ var rcread = await cli.main(
914
+ ["api-snapshot", "compare", "--file", path.join(dir, "no-such-snap.json"), "--module", mod], cread);
915
+ check("api-snapshot compare unreadable snap: exit 1", rcread === 1);
916
+ check("api-snapshot compare unreadable snap: message", /api-snapshot compare:/.test(cread.err()));
917
+ } finally { _rm(dir); }
918
+ }
919
+
920
+ // ---------------------------------------------------------------------------
921
+ // audit verify-chain — arg-validation + defensive open/query branches.
922
+ // ---------------------------------------------------------------------------
923
+ async function sectionAuditVerifyChainEdges() {
924
+ var dir = _tmpDir("blamejs-cli-audit-vc");
925
+ try {
926
+ // missing --db → exit 2
927
+ var cnodb = _captureCtx();
928
+ var rcnodb = await cli.main(["audit", "verify-chain"], cnodb);
929
+ check("verify-chain no --db: exit 2", rcnodb === 2);
930
+ check("verify-chain no --db: message", /--db <path> is required/.test(cnodb.err()));
931
+
932
+ // --max-rows 0 → positive-integer validation → exit 2
933
+ var okDb = path.join(dir, "ok.db");
934
+ var h = new sqlite.DatabaseSync(okDb);
935
+ h.prepare("CREATE TABLE audit_log (_id INTEGER PRIMARY KEY, monotonicCounter INTEGER," +
936
+ " prevHash TEXT, rowHash TEXT, nonce BLOB)").run();
937
+ h.close();
938
+ var cmr = _captureCtx();
939
+ var rcmr = await cli.main(["audit", "verify-chain", "--db", okDb, "--max-rows", "0"], cmr);
940
+ check("verify-chain --max-rows 0: exit 2", rcmr === 2);
941
+ check("verify-chain --max-rows 0: message", /--max-rows must be a positive integer/.test(cmr.err()));
942
+
943
+ // --max-rows 2.5 → the error message promises "a positive integer", so a
944
+ // fractional value MUST be refused with exit 2 — matching the sibling
945
+ // `migrate down --steps 2.5` guard. Before the fix, verify-chain only
946
+ // screened `< 1` (not non-integer), so 2.5 slipped through, walked the
947
+ // chain, and reported the nonsensical `rowsVerified=2.5` from
948
+ // Math.min(rows.length, 2.5). Drive the real b.cli consumer path.
949
+ var cfrac = _captureCtx();
950
+ var rcfrac = await cli.main(["audit", "verify-chain", "--db", okDb, "--max-rows", "2.5"], cfrac);
951
+ check("verify-chain --max-rows 2.5: exit 2", rcfrac === 2);
952
+ check("verify-chain --max-rows 2.5: rejected (never verifies)", !/rowsVerified/.test(cfrac.out()));
953
+ check("verify-chain --max-rows 2.5: message", /--max-rows must be a positive integer/.test(cfrac.err()));
954
+
955
+ // a whole-number float string like "3.0" is an integer value → still valid
956
+ // (Math.floor(3) === 3), exercising the accepted-integer arm after the fix.
957
+ var cwhole = _captureCtx();
958
+ var rcwhole = await cli.main(["audit", "verify-chain", "--db", okDb, "--max-rows", "3"], cwhole);
959
+ check("verify-chain --max-rows 3 (integer): exit 0", rcwhole === 0);
960
+ check("verify-chain --max-rows 3 (integer): verifies", /rowsVerified=0/.test(cwhole.out()));
961
+
962
+ // unopenable db (parent dir absent) → _openSqlite throws → exit 1
963
+ var copen = _captureCtx();
964
+ var rcopen = await cli.main(
965
+ ["audit", "verify-chain", "--db", path.join(dir, "no-dir", "x.db")], copen);
966
+ check("verify-chain unopenable db: exit 1", rcopen === 1);
967
+ check("verify-chain unopenable db: message", /cannot open db at/.test(copen.err()));
968
+
969
+ // db opens but the audit table does not exist → the queryAllAsync prepare
970
+ // throws inside the try → the outer catch returns exit 1.
971
+ var emptyDb = path.join(dir, "empty.db");
972
+ var h2 = new sqlite.DatabaseSync(emptyDb);
973
+ h2.prepare("CREATE TABLE unrelated (id INTEGER)").run();
974
+ h2.close();
975
+ var cq = _captureCtx();
976
+ var rcq = await cli.main(
977
+ ["audit", "verify-chain", "--db", emptyDb, "--table", "audit_log"], cq);
978
+ check("verify-chain missing table: exit 1", rcq === 1);
979
+ check("verify-chain missing table: message", /blamejs audit verify-chain:/.test(cq.err()));
980
+ } finally { _rm(dir); }
981
+ }
982
+
983
+ // ---------------------------------------------------------------------------
984
+ // restore — bare / unknown-sub / list(empty) / inspect(bad-bundle catch) /
985
+ // apply(no-passphrase) / rollback(default-no-points) / list-rollbacks(empty).
986
+ // Everything below drives the real b.backup.diskStorage + b.restoreRollback
987
+ // primitives; none needs a live network or a decryptable bundle.
988
+ // ---------------------------------------------------------------------------
989
+ async function sectionRestoreEdges() {
990
+ var dir = _tmpDir("blamejs-cli-restore2");
991
+ try {
992
+ // bare `restore` → usage, exit 2
993
+ var cbare = _captureCtx();
994
+ check("restore bare: exit 2", (await cli.main(["restore"], cbare)) === 2);
995
+ check("restore bare: usage", /Usage: blamejs restore/.test(cbare.err()));
996
+
997
+ // unknown subcommand → error + usage, exit 2
998
+ var cunk = _captureCtx();
999
+ check("restore unknown sub: exit 2", (await cli.main(["restore", "frobnicate"], cunk)) === 2);
1000
+ check("restore unknown sub: names sub", /unknown subcommand 'frobnicate'/.test(cunk.err()));
1001
+
1002
+ // inspect with NEITHER --bundle NOR --storage-root → the requireBundle
1003
+ // "--bundle OR --storage-root ..." selector error, exit 2.
1004
+ var cnosel = _captureCtx();
1005
+ check("restore inspect (no selector): exit 2", (await cli.main(["restore", "inspect"], cnosel)) === 2);
1006
+ check("restore inspect (no selector): message",
1007
+ /--bundle <dir> OR --storage-root/.test(cnosel.err()));
1008
+
1009
+ // list against an empty storage root → "no bundles" success path
1010
+ var store = path.join(dir, "store");
1011
+ fs.mkdirSync(store, { recursive: true });
1012
+ var cl = _captureCtx();
1013
+ var rcl = await cli.main(["restore", "list", "--storage-root", store], cl);
1014
+ check("restore list (empty store): exit 0", rcl === 0);
1015
+ check("restore list (empty store): no bundles line", /no bundles in /.test(cl.out()));
1016
+
1017
+ // inspect a nonexistent bundle-id under a valid storage root → the
1018
+ // restore.create().inspect() rejects → the catch returns exit 1.
1019
+ var ci = _captureCtx();
1020
+ var rci = await cli.main(
1021
+ ["restore", "inspect", "--storage-root", store, "--bundle-id", "nope"], ci);
1022
+ check("restore inspect (missing bundle): exit 1", rci === 1);
1023
+ check("restore inspect (missing bundle): reports it", /blamejs restore inspect:/.test(ci.err()));
1024
+
1025
+ // apply with a valid selector + data-dir but NO passphrase → the
1026
+ // passphrase gate returns exit 2 (distinct from the max-pulled-* gates).
1027
+ var ca = _captureCtx();
1028
+ var rca = await cli.main(
1029
+ ["restore", "apply", "--data-dir", path.join(dir, "dd"),
1030
+ "--bundle", path.join(dir, "bun")], ca);
1031
+ check("restore apply (no passphrase): exit 2", rca === 2);
1032
+ check("restore apply (no passphrase): message",
1033
+ /--passphrase or BLAMEJS_BACKUP_PASSPHRASE is required/.test(ca.err()));
1034
+
1035
+ // rollback WITHOUT --rollback, against a data-dir with no rollback points →
1036
+ // the default "most-recent" branch finds none → exit 2 with the explicit
1037
+ // "pass --rollback" hint.
1038
+ var dd = path.join(dir, "dd-empty");
1039
+ fs.mkdirSync(dd, { recursive: true });
1040
+ var crb = _captureCtx();
1041
+ var rcrb = await cli.main(["restore", "rollback", "--data-dir", dd], crb);
1042
+ check("restore rollback (no points): exit 2", rcrb === 2);
1043
+ check("restore rollback (no points): hint", /no rollback points at .* pass --rollback/.test(crb.err()));
1044
+
1045
+ // list-rollbacks against a data-dir with no rollback points → empty path,
1046
+ // exit 0.
1047
+ var clr = _captureCtx();
1048
+ var rclr = await cli.main(["restore", "list-rollbacks", "--data-dir", dd], clr);
1049
+ check("restore list-rollbacks (empty): exit 0", rclr === 0);
1050
+ check("restore list-rollbacks (empty): no points line", /no rollback points at /.test(clr.out()));
1051
+ } finally { _rm(dir); }
1052
+ }
1053
+
1054
+ // ---------------------------------------------------------------------------
1055
+ // backup — bare usage + unknown subcommand (cheap dispatch edges; the
1056
+ // decrypt/inspect success paths need a real encrypted bundle → out of scope
1057
+ // for an in-process test).
1058
+ // ---------------------------------------------------------------------------
1059
+ async function sectionBackupEdges() {
1060
+ var cbare = _captureCtx();
1061
+ check("backup bare: exit 2", (await cli.main(["backup"], cbare)) === 2);
1062
+ check("backup bare: usage", /Usage: blamejs backup/.test(cbare.err()));
1063
+
1064
+ var cunk = _captureCtx();
1065
+ check("backup unknown sub: exit 2", (await cli.main(["backup", "frobnicate"], cunk)) === 2);
1066
+ check("backup unknown sub: names sub", /unknown subcommand 'frobnicate'/.test(cunk.err()));
1067
+ }
1068
+
1069
+ // ---------------------------------------------------------------------------
1070
+ // mtls — dispatch + flag validation + boot-failure + not-yet-initialised CA
1071
+ // branches. status(no-CA) / show-cert(no-CA) boot a plaintext app on a fresh
1072
+ // data dir; the wrapped-boot-failure path uses an empty ctx.env so bootApp
1073
+ // throws on the missing passphrase and the catch reports "boot failed".
1074
+ // ---------------------------------------------------------------------------
1075
+ async function sectionMtlsEdges() {
1076
+ var dir = _tmpDir("blamejs-cli-mtls2");
1077
+ try {
1078
+ // bare → usage, exit 2
1079
+ var cbare = _captureCtx();
1080
+ check("mtls bare: exit 2", (await cli.main(["mtls"], cbare)) === 2);
1081
+ check("mtls bare: usage", /Usage: blamejs mtls/.test(cbare.err()));
1082
+
1083
+ // unknown subcommand → exit 2
1084
+ var cunk = _captureCtx();
1085
+ check("mtls unknown sub: exit 2", (await cli.main(["mtls", "frobnicate"], cunk)) === 2);
1086
+
1087
+ // missing --data-dir → exit 2
1088
+ var cnodir = _captureCtx();
1089
+ check("mtls no data-dir: exit 2", (await cli.main(["mtls", "status"], cnodir)) === 2);
1090
+ check("mtls no data-dir: message", /--data-dir <path> is required/.test(cnodir.err()));
1091
+
1092
+ // bad --vault-mode → exit 2
1093
+ var cvm = _captureCtx();
1094
+ check("mtls bad vault-mode: exit 2",
1095
+ (await cli.main(["mtls", "status", "--data-dir", dir, "--vault-mode", "yolo"], cvm)) === 2);
1096
+ check("mtls bad vault-mode: message", /--vault-mode must be/.test(cvm.err()));
1097
+
1098
+ // bad --sealed-mode → exit 2
1099
+ var csm = _captureCtx();
1100
+ check("mtls bad sealed-mode: exit 2",
1101
+ (await cli.main(["mtls", "status", "--data-dir", dir, "--vault-mode", "plaintext",
1102
+ "--sealed-mode", "bogus"], csm)) === 2);
1103
+ check("mtls bad sealed-mode: message", /--sealed-mode must be/.test(csm.err()));
1104
+
1105
+ // default vault-mode is wrapped; empty ctx.env → bootApp throws → catch
1106
+ // reports "boot failed" (exit 1).
1107
+ var cboot = _captureCtx();
1108
+ var rcboot = await cli.main(["mtls", "status", "--data-dir", dir], cboot);
1109
+ check("mtls wrapped-boot-fail: exit 1", rcboot === 1);
1110
+ check("mtls wrapped-boot-fail: message", /boot failed/.test(cboot.err()));
1111
+
1112
+ // status on a fresh (never-initialised) data dir → the CA-absent branch.
1113
+ var fresh = _tmpDir("blamejs-cli-mtls2-fresh");
1114
+ try {
1115
+ var cst = _captureCtx();
1116
+ var rcst = await cli.main(["mtls", "status", "--data-dir", fresh, "--vault-mode", "plaintext"], cst);
1117
+ check("mtls status (no CA): exit 0", rcst === 0);
1118
+ check("mtls status (no CA): reports no", /CA exists:\s+no/.test(cst.out()));
1119
+ check("mtls status (no CA): hints init", /run 'blamejs mtls init'/.test(cst.out()));
1120
+
1121
+ // show-cert with no CA on disk → error, exit 1.
1122
+ var csc = _captureCtx();
1123
+ var rcsc = await cli.main(["mtls", "show-cert", "--data-dir", fresh, "--vault-mode", "plaintext"], csc);
1124
+ check("mtls show-cert (no CA): exit 1", rcsc === 1);
1125
+ check("mtls show-cert (no CA): message", /no CA on disk/.test(csc.err()));
1126
+ } finally { _rm(fresh); }
1127
+ } finally { _rm(dir); }
1128
+ }
1129
+
1130
+ // ---------------------------------------------------------------------------
1131
+ // security — dispatch + flag validation + boot-failure.
1132
+ // ---------------------------------------------------------------------------
1133
+ async function sectionSecurityEdges() {
1134
+ var dir = _tmpDir("blamejs-cli-sec2");
1135
+ try {
1136
+ var cunk = _captureCtx();
1137
+ check("security unknown sub: exit 2", (await cli.main(["security", "frobnicate"], cunk)) === 2);
1138
+ check("security unknown sub: names sub", /unknown subcommand 'frobnicate'/.test(cunk.err()));
1139
+
1140
+ var cnodir = _captureCtx();
1141
+ check("security no data-dir: exit 2", (await cli.main(["security", "assert"], cnodir)) === 2);
1142
+ check("security no data-dir: message", /--data-dir <path> is required/.test(cnodir.err()));
1143
+
1144
+ var cvm = _captureCtx();
1145
+ check("security bad vault-mode: exit 2",
1146
+ (await cli.main(["security", "assert", "--data-dir", dir, "--vault-mode", "yolo"], cvm)) === 2);
1147
+
1148
+ // wrapped (default) + empty env → boot fails → exit 1
1149
+ var cboot = _captureCtx();
1150
+ var rcboot = await cli.main(["security", "assert", "--data-dir", dir], cboot);
1151
+ check("security wrapped-boot-fail: exit 1", rcboot === 1);
1152
+ check("security wrapped-boot-fail: message", /boot failed/.test(cboot.err()));
1153
+ } finally { _rm(dir); }
1154
+ }
1155
+
1156
+ // ---------------------------------------------------------------------------
1157
+ // config-drift — dispatch + flag validation + boot-failure + the no-sidecar
1158
+ // read branch (verify → exit 1, inspect → exit 0) on a freshly-booted app.
1159
+ // ---------------------------------------------------------------------------
1160
+ async function sectionConfigDrift() {
1161
+ var dir = _tmpDir("blamejs-cli-drift");
1162
+ try {
1163
+ var cunk = _captureCtx();
1164
+ check("config-drift unknown sub: exit 2", (await cli.main(["config-drift", "frobnicate"], cunk)) === 2);
1165
+ check("config-drift unknown sub: names sub", /unknown subcommand 'frobnicate'/.test(cunk.err()));
1166
+
1167
+ var cnodir = _captureCtx();
1168
+ check("config-drift no data-dir: exit 2", (await cli.main(["config-drift", "verify"], cnodir)) === 2);
1169
+ check("config-drift no data-dir: message", /--data-dir <path> is required/.test(cnodir.err()));
1170
+
1171
+ // wrapped (default) + empty env → boot fails → exit 1
1172
+ var cboot = _captureCtx();
1173
+ var rcboot = await cli.main(["config-drift", "verify", "--data-dir", dir], cboot);
1174
+ check("config-drift wrapped-boot-fail: exit 1", rcboot === 1);
1175
+ check("config-drift wrapped-boot-fail: message", /boot failed/.test(cboot.err()));
1176
+
1177
+ // Fresh plaintext boot with no captured sidecar → verify returns exit 1,
1178
+ // inspect returns exit 0, both printing "no sidecar present".
1179
+ var fresh = _tmpDir("blamejs-cli-drift-fresh");
1180
+ try {
1181
+ var cv = _captureCtx();
1182
+ var rcv = await cli.main(["config-drift", "verify", "--data-dir", fresh, "--vault-mode", "plaintext"], cv);
1183
+ check("config-drift verify (no sidecar): exit 1", rcv === 1);
1184
+ check("config-drift verify (no sidecar): message", /no sidecar present/.test(cv.out()));
1185
+
1186
+ // inspect with an explicit --baseline exercises the baseline-opt branch
1187
+ // (still no sidecar under that baseline → exit 0).
1188
+ var cin = _captureCtx();
1189
+ var rcin = await cli.main(
1190
+ ["config-drift", "inspect", "--data-dir", fresh, "--vault-mode", "plaintext", "--baseline", "custom"], cin);
1191
+ check("config-drift inspect (no sidecar): exit 0", rcin === 0);
1192
+ check("config-drift inspect (no sidecar): message", /no sidecar present/.test(cin.out()));
1193
+ } finally { _rm(fresh); }
1194
+ } finally { _rm(dir); }
1195
+ }
1196
+
1197
+ // ---------------------------------------------------------------------------
1198
+ // config-drift — the sidecar-PRESENT branches (verified-yes + inspect JSON +
1199
+ // inspect snapshot text). A signed baseline is captured out-of-band through
1200
+ // the real b.configDrift primitive on the same data-dir, then fully shut down
1201
+ // before the CLI boots the same dir (the CLI loads the persisted audit-signing
1202
+ // key, so the sidecar verifies). Covers the block the no-sidecar section
1203
+ // (which returns early at "no sidecar present") never reaches.
1204
+ // ---------------------------------------------------------------------------
1205
+ async function sectionConfigDriftSidecar() {
1206
+ var dataDir = _tmpDir("blamejs-cli-drift-sc");
1207
+ try {
1208
+ var booted = await b.cliHelpers.bootApp({ dataDir: dataDir, vaultMode: "plaintext", env: {} });
1209
+ try {
1210
+ var drift = booted.b.configDrift.create({ dataDir: dataDir, audit: booted.b.audit });
1211
+ var res = await drift.checkpoint({ service: "web", replicas: 3 });
1212
+ check("config-drift sidecar: baseline captured", res && res.signed === true && res.tamper === false);
1213
+ } finally {
1214
+ try { await booted.app.shutdown(); } catch (_e) { /* best-effort */ }
1215
+ }
1216
+
1217
+ // inspect (text) → verified-yes branch + capturedAt/digest/snapshot printing
1218
+ var ci = _captureCtx();
1219
+ var rci = await cli.main(
1220
+ ["config-drift", "inspect", "--data-dir", dataDir, "--vault-mode", "plaintext"], ci);
1221
+ check("config-drift inspect (sidecar): exit 0", rci === 0);
1222
+ check("config-drift inspect (sidecar): capturedAt line", /capturedAt:/.test(ci.out()));
1223
+ check("config-drift inspect (sidecar): verified yes", /verified:\s+yes/.test(ci.out()));
1224
+ check("config-drift inspect (sidecar): prints snapshot", /"service": "web"/.test(ci.out()));
1225
+
1226
+ // inspect --json → the JSON arm (machine-readable verified flag)
1227
+ var cj = _captureCtx();
1228
+ var rcj = await cli.main(
1229
+ ["config-drift", "inspect", "--data-dir", dataDir, "--vault-mode", "plaintext", "--json"], cj);
1230
+ check("config-drift inspect --json (sidecar): exit 0", rcj === 0);
1231
+ check("config-drift inspect --json (sidecar): verified true", /"verified":\s*true/.test(cj.out()));
1232
+
1233
+ // verify → the verified-success branch (exit 0 + "sidecar verified at")
1234
+ var cv = _captureCtx();
1235
+ var rcv = await cli.main(
1236
+ ["config-drift", "verify", "--data-dir", dataDir, "--vault-mode", "plaintext"], cv);
1237
+ check("config-drift verify (sidecar): exit 0", rcv === 0);
1238
+ check("config-drift verify (sidecar): verified message", /sidecar verified at/.test(cv.out()));
1239
+ } finally { _rm(dataDir); }
1240
+ }
1241
+
1242
+ // ---------------------------------------------------------------------------
1243
+ // vault — the whole _runVault body: dispatch, status(fresh), seal(no-pass /
1244
+ // success / keep-plaintext / already-sealed catch), unseal(no-pass / success),
1245
+ // rotate(missing-new-pass / success). Drives b.vaultPassphraseOps through the
1246
+ // CLI. A plaintext vault.key is created up-front via b.vault.init({ mode:
1247
+ // "plaintext" }); the vault singleton is reset afterward so later booted
1248
+ // sections re-init cleanly.
1249
+ // ---------------------------------------------------------------------------
1250
+ async function sectionVault() {
1251
+ var dir = _tmpDir("blamejs-cli-vault");
1252
+ var savedPass = process.env.BLAMEJS_VAULT_PASSPHRASE;
1253
+ var PP = "vault-cli-old-passphrase-123456";
1254
+ var NP = "vault-cli-new-passphrase-654321";
1255
+ try {
1256
+ // bare → usage, exit 2
1257
+ var cbare = _captureCtx();
1258
+ check("vault bare: exit 2", (await cli.main(["vault"], cbare)) === 2);
1259
+ check("vault bare: usage", /Usage: blamejs vault/.test(cbare.err()));
1260
+
1261
+ // unknown subcommand → exit 2
1262
+ var cunk = _captureCtx();
1263
+ check("vault unknown sub: exit 2", (await cli.main(["vault", "frobnicate"], cunk)) === 2);
1264
+ check("vault unknown sub: names sub", /unknown subcommand 'frobnicate'/.test(cunk.err()));
1265
+
1266
+ // status on a dir with neither key → both "absent", exit 0
1267
+ var cst = _captureCtx();
1268
+ var rcst = await cli.main(["vault", "status", "--data-dir", dir], cst);
1269
+ check("vault status (fresh): exit 0", rcst === 0);
1270
+ check("vault status (fresh): plaintext absent", /vault\.key \(plaintext\):\s+absent/.test(cst.out()));
1271
+
1272
+ // seal with no passphrase → exit 2 (before any key exists)
1273
+ var cnp = _captureCtx();
1274
+ var rcnp = await cli.main(["vault", "seal", "--data-dir", dir], cnp);
1275
+ check("vault seal (no pass): exit 2", rcnp === 2);
1276
+ check("vault seal (no pass): message", /--passphrase or BLAMEJS_VAULT_PASSPHRASE is required/.test(cnp.err()));
1277
+
1278
+ // create a plaintext vault.key to seal (init in plaintext mode writes it)
1279
+ delete process.env.BLAMEJS_VAULT_PASSPHRASE;
1280
+ b.vault._resetForTest();
1281
+ await b.vault.init({ dataDir: dir, mode: "plaintext" });
1282
+ b.vault._resetForTest();
1283
+
1284
+ // seal → wraps + deletes plaintext, exit 0
1285
+ var cse = _captureCtx();
1286
+ var rcse = await cli.main(["vault", "seal", "--data-dir", dir, "--passphrase", PP], cse);
1287
+ check("vault seal: exit 0", rcse === 0);
1288
+ check("vault seal: reports sealed path", /sealed: /.test(cse.out()));
1289
+ check("vault seal: removed plaintext", /removed plaintext vault\.key/.test(cse.out()));
1290
+
1291
+ // status now reports the sealed file present
1292
+ var cst2 = _captureCtx();
1293
+ await cli.main(["vault", "status", "--data-dir", dir], cst2);
1294
+ check("vault status (sealed): sealed present", /vault\.key\.sealed \(wrapped\): present/.test(cst2.out()));
1295
+
1296
+ // seal AGAIN (plaintext already deleted) → preflight fails → catch → exit 1
1297
+ var cse2 = _captureCtx();
1298
+ var rcse2 = await cli.main(["vault", "seal", "--data-dir", dir, "--passphrase", PP], cse2);
1299
+ check("vault seal (already sealed): exit 1", rcse2 === 1);
1300
+ check("vault seal (already sealed): message", /nothing to seal/.test(cse2.err()));
1301
+
1302
+ // unseal with no passphrase → exit 2
1303
+ var cunp = _captureCtx();
1304
+ var rcunp = await cli.main(["vault", "unseal", "--data-dir", dir], cunp);
1305
+ check("vault unseal (no pass): exit 2", rcunp === 2);
1306
+
1307
+ // unseal with the WRONG passphrase → unwrap rejects → catch → exit 1
1308
+ var cuw = _captureCtx();
1309
+ var rcuw = await cli.main(["vault", "unseal", "--data-dir", dir, "--passphrase", "totally-wrong-passphrase"], cuw);
1310
+ check("vault unseal (wrong pass): exit 1", rcuw === 1);
1311
+ check("vault unseal (wrong pass): message", /blamejs vault unseal:/.test(cuw.err()));
1312
+
1313
+ // unseal → recreates plaintext vault.key, exit 0
1314
+ var cun = _captureCtx();
1315
+ var rcun = await cli.main(["vault", "unseal", "--data-dir", dir, "--passphrase", PP], cun);
1316
+ check("vault unseal: exit 0", rcun === 0);
1317
+ check("vault unseal: reports plaintext path", /unsealed: /.test(cun.out()));
1318
+
1319
+ // re-seal keeping the plaintext (the --keep-plaintext branch), giving us a
1320
+ // sealed file for rotate.
1321
+ var cke = _captureCtx();
1322
+ var rcke = await cli.main(
1323
+ ["vault", "seal", "--data-dir", dir, "--passphrase", PP, "--keep-plaintext"], cke);
1324
+ check("vault seal --keep-plaintext: exit 0", rcke === 0);
1325
+ check("vault seal --keep-plaintext: kept plaintext", /kept plaintext vault\.key/.test(cke.out()));
1326
+
1327
+ // rotate missing --new-passphrase → exit 2
1328
+ var crn = _captureCtx();
1329
+ var rcrn = await cli.main(["vault", "rotate", "--data-dir", dir, "--passphrase", PP], crn);
1330
+ check("vault rotate (no new-pass): exit 2", rcrn === 2);
1331
+ check("vault rotate (no new-pass): message", /both --passphrase \(old\) and --new-passphrase/.test(crn.err()));
1332
+
1333
+ // rotate old→new → exit 0
1334
+ var cro = _captureCtx();
1335
+ var rcro = await cli.main(
1336
+ ["vault", "rotate", "--data-dir", dir, "--passphrase", PP, "--new-passphrase", NP], cro);
1337
+ check("vault rotate: exit 0", rcro === 0);
1338
+ check("vault rotate: reports rotated path", /rotated: /.test(cro.out()));
1339
+
1340
+ // rotate with the WRONG old passphrase (sealed is now under NP) → the
1341
+ // unwrap in rotate rejects → catch → exit 1.
1342
+ var crw = _captureCtx();
1343
+ var rcrw = await cli.main(
1344
+ ["vault", "rotate", "--data-dir", dir, "--passphrase", PP, "--new-passphrase", "another-new-pass-999"], crw);
1345
+ check("vault rotate (wrong old pass): exit 1", rcrw === 1);
1346
+ check("vault rotate (wrong old pass): message", /blamejs vault rotate:/.test(crw.err()));
1347
+ } finally {
1348
+ b.vault._resetForTest();
1349
+ if (savedPass === undefined) delete process.env.BLAMEJS_VAULT_PASSPHRASE;
1350
+ else process.env.BLAMEJS_VAULT_PASSPHRASE = savedPass;
1351
+ _rm(dir);
1352
+ }
1353
+ }
1354
+
1355
+ // ---------------------------------------------------------------------------
1356
+ // password — the whole _runPassword body. No framework boot: it runs
1357
+ // b.auth.password.policy(...).check(...) directly. --breach-check is NEVER
1358
+ // exercised (it touches the network); every path here is offline.
1359
+ // ---------------------------------------------------------------------------
1360
+ async function sectionPassword() {
1361
+ // unknown subcommand → exit 2
1362
+ var cunk = _captureCtx();
1363
+ check("password unknown sub: exit 2", (await cli.main(["password", "frobnicate"], cunk)) === 2);
1364
+ check("password unknown sub: names sub", /unknown subcommand 'frobnicate'/.test(cunk.err()));
1365
+
1366
+ // check with neither --plaintext nor --stdin → exit 2
1367
+ var cnop = _captureCtx();
1368
+ check("password check (no plaintext): exit 2", (await cli.main(["password", "check"], cnop)) === 2);
1369
+ check("password check (no plaintext): message", /--plaintext <s> or --stdin is required/.test(cnop.err()));
1370
+
1371
+ // a strong passphrase → policy passes → "ok", exit 0
1372
+ var cok = _captureCtx();
1373
+ var rcok = await cli.main(
1374
+ ["password", "check", "--plaintext", "correct-horse-battery-staple-9273-Zx!"], cok);
1375
+ check("password check (strong): exit 0", rcok === 0);
1376
+ check("password check (strong): prints ok", /^ok/m.test(cok.out()));
1377
+
1378
+ // a too-short password → policy rejects → REJECTED line, exit 1
1379
+ var crej = _captureCtx();
1380
+ var rcrej = await cli.main(["password", "check", "--plaintext", "x"], crej);
1381
+ check("password check (weak): exit 1", rcrej === 1);
1382
+ check("password check (weak): REJECTED line", /REJECTED: policy\/too-short/.test(crej.out()));
1383
+
1384
+ // --json arm → machine-readable verdict; exit code still reflects ok
1385
+ var cjs = _captureCtx();
1386
+ var rcjs = await cli.main(
1387
+ ["password", "check", "--plaintext", "correct-horse-battery-staple-9273-Zx!", "--json"], cjs);
1388
+ check("password check --json (strong): exit 0", rcjs === 0);
1389
+ check("password check --json (strong): JSON body", /"ok":true/.test(cjs.out()));
1390
+
1391
+ // --min-length + --max-length overrides + --email / --username context
1392
+ // flags all exercised.
1393
+ var cml = _captureCtx();
1394
+ var rcml = await cli.main(
1395
+ ["password", "check", "--plaintext", "abcd", "--min-length", "4", "--max-length", "128",
1396
+ "--email", "a@b.com", "--username", "alice"], cml);
1397
+ check("password check --min/max-length: exits 0/1 (ran policy)", rcml === 0 || rcml === 1);
1398
+
1399
+ // an unknown --profile → policy() throws → "bad policy" exit 2
1400
+ var cbp = _captureCtx();
1401
+ var rcbp = await cli.main(
1402
+ ["password", "check", "--plaintext", "whatever-long-enough-string", "--profile", "no-such-profile"], cbp);
1403
+ check("password check bad profile: exit 2", rcbp === 2);
1404
+ check("password check bad profile: message", /bad policy:/.test(cbp.err()));
1405
+ }
1406
+
1407
+ // ---------------------------------------------------------------------------
1408
+ // file-type — unknown subcommand, allowlist match (non-json), allowlist
1409
+ // rejection, and the plain (non-allowlist) detect success arms (text + json).
1410
+ // ---------------------------------------------------------------------------
1411
+ async function sectionFileTypeEdges() {
1412
+ var dir = _tmpDir("blamejs-cli-ft2");
1413
+ try {
1414
+ var pngFile = path.join(dir, "img.png");
1415
+ fs.writeFileSync(pngFile, Buffer.concat([
1416
+ Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]), Buffer.alloc(20),
1417
+ ]));
1418
+
1419
+ // unknown subcommand → exit 2
1420
+ var cunk = _captureCtx();
1421
+ check("file-type unknown sub: exit 2", (await cli.main(["file-type", "frobnicate"], cunk)) === 2);
1422
+ check("file-type unknown sub: names sub", /unknown subcommand 'frobnicate'/.test(cunk.err()));
1423
+
1424
+ // allowlist match, NON-json → the human-readable mime/extension/category arm
1425
+ var cam = _captureCtx();
1426
+ var rcam = await cli.main(
1427
+ ["file-type", "detect", pngFile, "--allowlist", "image/png,application/pdf"], cam);
1428
+ check("file-type allowlist match (text): exit 0", rcam === 0);
1429
+ check("file-type allowlist match (text): mime line", /mime:\s+image\/png/.test(cam.out()));
1430
+
1431
+ // allowlist that EXCLUDES the detected type → assertOneOf throws → exit 1
1432
+ var car = _captureCtx();
1433
+ var rcar = await cli.main(
1434
+ ["file-type", "detect", pngFile, "--allowlist", "application/pdf"], car);
1435
+ check("file-type allowlist reject: exit 1", rcar === 1);
1436
+ check("file-type allowlist reject: error message", /blamejs file-type detect:/.test(car.err()));
1437
+
1438
+ // plain detect (no allowlist), NON-json → the mime/extension text arm
1439
+ var cpd = _captureCtx();
1440
+ var rcpd = await cli.main(["file-type", "detect", pngFile], cpd);
1441
+ check("file-type plain detect (text): exit 0", rcpd === 0);
1442
+ check("file-type plain detect (text): mime line", /mime:\s+image\/png/.test(cpd.out()));
1443
+
1444
+ // plain detect (no allowlist), json → the json arm
1445
+ var cpj = _captureCtx();
1446
+ var rcpj = await cli.main(["file-type", "detect", pngFile, "--json"], cpj);
1447
+ check("file-type plain detect (json): exit 0", rcpj === 0);
1448
+ check("file-type plain detect (json): JSON body", /"mime":"image\/png"/.test(cpj.out()));
1449
+ } finally { _rm(dir); }
1450
+ }
1451
+
1452
+ // ---------------------------------------------------------------------------
1453
+ // retention — unknown-sub + the arg-validation branches that fire BEFORE the
1454
+ // framework boot (bad ttl / bad action / soft-delete-without-field) + the
1455
+ // wrapped-boot-failure catch.
1456
+ // ---------------------------------------------------------------------------
1457
+ async function sectionRetentionEdges() {
1458
+ var dir = _tmpDir("blamejs-cli-ret2");
1459
+ try {
1460
+ var cunk = _captureCtx();
1461
+ check("retention unknown sub: exit 2", (await cli.main(["retention", "frobnicate"], cunk)) === 2);
1462
+ check("retention unknown sub: names sub", /unknown subcommand 'frobnicate'/.test(cunk.err()));
1463
+
1464
+ // --ttl-ms 0 → non-positive → exit 2
1465
+ var cttl = _captureCtx();
1466
+ check("retention bad ttl: exit 2",
1467
+ (await cli.main(["retention", "preview", "--data-dir", dir, "--table", "t",
1468
+ "--age-field", "ts", "--ttl-ms", "0"], cttl)) === 2);
1469
+ check("retention bad ttl: message", /--ttl-ms must be a positive finite number/.test(cttl.err()));
1470
+
1471
+ // unknown --action → exit 2
1472
+ var cact = _captureCtx();
1473
+ check("retention bad action: exit 2",
1474
+ (await cli.main(["retention", "preview", "--data-dir", dir, "--table", "t",
1475
+ "--age-field", "ts", "--ttl-ms", "1", "--action", "bogus"], cact)) === 2);
1476
+ check("retention bad action: message", /--action must be erase \/ delete \/ soft-delete/.test(cact.err()));
1477
+
1478
+ // action=soft-delete without --soft-delete-field → exit 2
1479
+ var csd = _captureCtx();
1480
+ check("retention soft-delete no field: exit 2",
1481
+ (await cli.main(["retention", "preview", "--data-dir", dir, "--table", "t",
1482
+ "--age-field", "ts", "--ttl-ms", "1", "--action", "soft-delete"], csd)) === 2);
1483
+ check("retention soft-delete no field: message",
1484
+ /--soft-delete-field <col> required when --action=soft-delete/.test(csd.err()));
1485
+
1486
+ // wrapped (default) vault-mode + empty env → boot fails → exit 1
1487
+ var cboot = _captureCtx();
1488
+ var rcboot = await cli.main(
1489
+ ["retention", "preview", "--data-dir", dir, "--table", "t",
1490
+ "--age-field", "ts", "--ttl-ms", "1"], cboot);
1491
+ check("retention wrapped-boot-fail: exit 1", rcboot === 1);
1492
+ check("retention wrapped-boot-fail: message", /boot failed/.test(cboot.err()));
1493
+ } finally { _rm(dir); }
1494
+ }
1495
+
1496
+ // ---------------------------------------------------------------------------
1497
+ // erase — help, missing --confirm, wrapped-boot-failure, and the post-boot
1498
+ // bad-identifier rejection (the safeTable-strip guard).
1499
+ // ---------------------------------------------------------------------------
1500
+ async function sectionEraseEdges() {
1501
+ var dir = _tmpDir("blamejs-cli-erase2");
1502
+ try {
1503
+ // erase --help → usage, exit 0 (the flags.help branch of the handler)
1504
+ var chelp = _captureCtx();
1505
+ var rchelp = await cli.main(["erase", "--help"], chelp);
1506
+ check("erase --help: exit 0", rchelp === 0);
1507
+ check("erase --help: usage", /Usage: blamejs erase/.test(chelp.out()));
1508
+
1509
+ // missing --confirm (table + row present) → exit 2
1510
+ var cnc = _captureCtx();
1511
+ var rcnc = await cli.main(["erase", "--table", "users", "--row-id", "r-1"], cnc);
1512
+ check("erase no confirm: exit 2", rcnc === 2);
1513
+ check("erase no confirm: message", /--confirm is required/.test(cnc.err()));
1514
+
1515
+ // default wrapped vault-mode + empty env → boot fails → exit 1
1516
+ var cboot = _captureCtx();
1517
+ var rcboot = await cli.main(
1518
+ ["erase", "--table", "users", "--row-id", "r-1", "--confirm", "--data-dir", dir], cboot);
1519
+ check("erase wrapped-boot-fail: exit 1", rcboot === 1);
1520
+ check("erase wrapped-boot-fail: message", /boot failed/.test(cboot.err()));
1521
+
1522
+ // plaintext boot succeeds, then a table name with non-identifier chars is
1523
+ // rejected by the safeTable-strip guard → exit 2.
1524
+ var fresh = _tmpDir("blamejs-cli-erase2-id");
1525
+ try {
1526
+ var cid = _captureCtx();
1527
+ var rcid = await cli.main(
1528
+ ["erase", "--table", "bad-name!", "--row-id", "r-1", "--confirm",
1529
+ "--data-dir", fresh, "--vault-mode", "plaintext"], cid);
1530
+ check("erase bad-identifier table: exit 2", rcid === 2);
1531
+ check("erase bad-identifier table: message", /--table must be a valid identifier/.test(cid.err()));
1532
+ } finally { _rm(fresh); }
1533
+ } finally { _rm(dir); }
1534
+ }
1535
+
1536
+ // ---------------------------------------------------------------------------
1537
+ // api-key — bare / unknown-sub / missing-data-dir dispatch + the wrapped-boot
1538
+ // failure catch.
1539
+ // ---------------------------------------------------------------------------
1540
+ async function sectionApiKeyEdges() {
1541
+ var dir = _tmpDir("blamejs-cli-apikey2");
1542
+ try {
1543
+ var cbare = _captureCtx();
1544
+ check("api-key bare: exit 2", (await cli.main(["api-key"], cbare)) === 2);
1545
+ check("api-key bare: usage", /Usage: blamejs api-key/.test(cbare.err()));
1546
+
1547
+ var cunk = _captureCtx();
1548
+ check("api-key unknown sub: exit 2", (await cli.main(["api-key", "frobnicate"], cunk)) === 2);
1549
+ check("api-key unknown sub: names sub", /unknown subcommand 'frobnicate'/.test(cunk.err()));
1550
+
1551
+ var cnodir = _captureCtx();
1552
+ check("api-key no data-dir: exit 2", (await cli.main(["api-key", "issue"], cnodir)) === 2);
1553
+ check("api-key no data-dir: message", /--data-dir <path> is required/.test(cnodir.err()));
1554
+
1555
+ // data-dir + namespace present, default wrapped vault-mode, empty env →
1556
+ // bootApp throws → the catch reports "boot failed" (exit 1).
1557
+ var cboot = _captureCtx();
1558
+ var rcboot = await cli.main(
1559
+ ["api-key", "issue", "--data-dir", dir, "--namespace", "api",
1560
+ "--owner-id", "x", "--scopes", "a:read"], cboot);
1561
+ check("api-key wrapped-boot-fail: exit 1", rcboot === 1);
1562
+ check("api-key wrapped-boot-fail: message", /boot failed/.test(cboot.err()));
1563
+ } finally { _rm(dir); }
1564
+ }
1565
+
1566
+ // ---------------------------------------------------------------------------
1567
+ // booted raw-table paths: retention run (json delete + non-json soft-delete
1568
+ // with legal-hold) and erase (row-not-found + no-sealed-columns). The CLI
1569
+ // boots its OWN app per invocation, so the table is created out-of-band with a
1570
+ // raw node:sqlite handle after a first boot materialises the db file.
1571
+ // ---------------------------------------------------------------------------
1572
+ async function sectionBootedRawTable() {
1573
+ var dir = _tmpDir("blamejs-cli-rawtbl");
1574
+ try {
1575
+ // First plaintext boot (against a missing table) materialises blamejs.db.
1576
+ await cli.main(
1577
+ ["retention", "preview", "--data-dir", dir, "--vault-mode", "plaintext",
1578
+ "--table", "seed_missing", "--age-field", "ts", "--ttl-ms", "1", "--action", "delete"],
1579
+ _captureCtx());
1580
+ var dbFile = path.join(dir, "blamejs.db");
1581
+ check("rawtbl: db file materialised", fs.existsSync(dbFile));
1582
+
1583
+ // Create a plain table with one old row + a soft-delete table with a
1584
+ // legal-hold row, via a raw handle while no app holds the db.
1585
+ var h = new sqlite.DatabaseSync(dbFile);
1586
+ h.exec("CREATE TABLE ret_del (_id TEXT PRIMARY KEY, ts INTEGER)");
1587
+ h.prepare("INSERT INTO ret_del (_id, ts) VALUES (?, ?)").run("d-old", 1);
1588
+ h.exec("CREATE TABLE ret_soft (_id TEXT PRIMARY KEY, ts INTEGER, deletedAt INTEGER, hold INTEGER)");
1589
+ var ins = h.prepare("INSERT INTO ret_soft (_id, ts, deletedAt, hold) VALUES (?, ?, ?, ?)");
1590
+ ins.run("s-1", 1, null, null);
1591
+ ins.run("s-2", 1, null, 1);
1592
+ h.exec("CREATE TABLE er_plain (_id TEXT PRIMARY KEY, ts INTEGER)");
1593
+ h.prepare("INSERT INTO er_plain (_id, ts) VALUES (?, ?)").run("er-1", 5);
1594
+ h.close();
1595
+
1596
+ // retention run --json against the delete table → success, json summary arm
1597
+ var cj = _captureCtx();
1598
+ var rcj = await cli.main(
1599
+ ["retention", "run", "--data-dir", dir, "--vault-mode", "plaintext",
1600
+ "--table", "ret_del", "--age-field", "ts", "--ttl-ms", "1", "--action", "delete", "--json"], cj);
1601
+ check("retention run (json delete): exit 0", rcj === 0);
1602
+ check("retention run (json delete): json summary", /"processed":\s*1/.test(cj.out()));
1603
+
1604
+ // retention run non-json soft-delete + legal-hold → the text summary arm,
1605
+ // the soft-delete-field / legal-hold-field ruleSpec wiring, and a honored
1606
+ // legal hold.
1607
+ var cs = _captureCtx();
1608
+ var rcs = await cli.main(
1609
+ ["retention", "run", "--data-dir", dir, "--vault-mode", "plaintext",
1610
+ "--table", "ret_soft", "--age-field", "ts", "--ttl-ms", "1", "--action", "soft-delete",
1611
+ "--soft-delete-field", "deletedAt", "--legal-hold-field", "hold"], cs);
1612
+ check("retention run (soft-delete): exit 0", rcs === 0);
1613
+ check("retention run (soft-delete): text summary", /processed:1/.test(cs.out()));
1614
+ check("retention run (soft-delete): honored legal hold", /legalHoldsHonored: 1/.test(cs.out()));
1615
+
1616
+ // erase against an EXISTING table but a missing row id → the "no row" branch
1617
+ var cnr = _captureCtx();
1618
+ var rcnr = await cli.main(
1619
+ ["erase", "--data-dir", dir, "--vault-mode", "plaintext",
1620
+ "--table", "er_plain", "--row-id", "does-not-exist", "--confirm"], cnr);
1621
+ check("erase (row not found): exit 1", rcnr === 1);
1622
+ check("erase (row not found): message", /no row with _id=/.test(cnr.err()));
1623
+
1624
+ // erase a REAL row in a table with no sealed columns / derived hashes → the
1625
+ // "nothing sealed to erase" guard.
1626
+ var cns = _captureCtx();
1627
+ var rcns = await cli.main(
1628
+ ["erase", "--data-dir", dir, "--vault-mode", "plaintext",
1629
+ "--table", "er_plain", "--row-id", "er-1", "--confirm"], cns);
1630
+ check("erase (no sealed columns): exit 1", rcns === 1);
1631
+ check("erase (no sealed columns): message", /has no sealed columns or derived hashes/.test(cns.err()));
1632
+ } finally { _rm(dir); }
1633
+ }
1634
+
1635
+ // ---------------------------------------------------------------------------
1636
+ // top-level dispatch — an unknown command prints the "unknown command" error
1637
+ // plus the top usage, exit 2.
1638
+ // ---------------------------------------------------------------------------
1639
+ async function sectionTopUnknownCommand() {
1640
+ var c = _captureCtx();
1641
+ var rc = await cli.main(["totally-unknown-command"], c);
1642
+ check("unknown top-level command: exit 2", rc === 2);
1643
+ check("unknown top-level command: names it", /unknown command 'totally-unknown-command'/.test(c.err()));
1644
+ check("unknown top-level command: prints top usage", /blamejs <command>/.test(c.out()));
1645
+ }
1646
+
1647
+ // ---------------------------------------------------------------------------
1648
+ // top-level dispatch — the option-default fallbacks in main(), the non-array
1649
+ // argv guard, the `--version` (long) + `version` command paths, and the
1650
+ // per-command `help <topic>` fan-out block (one line per subcommand USAGE).
1651
+ // ---------------------------------------------------------------------------
1652
+ async function sectionTopLevelDispatch2() {
1653
+ // main() with a NON-ARRAY (undefined) argv AND no opts object at all.
1654
+ // Exercises `if (!Array.isArray(argv)) argv = []`, every `opts.<x> ||
1655
+ // process.<x>` default, and the `cmd === undefined` top-help early return.
1656
+ // Output goes to the real process.stdout (no ctx supplied) — a single
1657
+ // usage block, harmless to the check tally.
1658
+ var rcUndef = await cli.main(undefined);
1659
+ check("main(undefined) → exit 0 (non-array guard + opts defaults + cmd-undefined)", rcUndef === 0);
1660
+
1661
+ // `--version` (long flag) → the LEFT operand of `args.flags.version ||
1662
+ // args.flags.v`; only `-v` (the right operand) was covered before.
1663
+ var cVer = _captureCtx();
1664
+ var rcVer = await cli.main(["--version"], cVer);
1665
+ check("--version long flag → exit 0", rcVer === 0);
1666
+ check("--version long flag → prints version", /\d+\.\d+\.\d+/.test(cVer.out()));
1667
+
1668
+ // `version` positional command → the dedicated `cmd === "version"` branch
1669
+ // (distinct from the top-level --version/-v flag handling above).
1670
+ var cVerCmd = _captureCtx();
1671
+ var rcVerCmd = await cli.main(["version"], cVerCmd);
1672
+ check("version command → exit 0", rcVerCmd === 0);
1673
+ check("version command → prints version", /\d+\.\d+\.\d+/.test(cVerCmd.out()));
1674
+
1675
+ // `help <topic>` for every reporter-/writeLine-backed subcommand → the
1676
+ // per-topic dispatch block (each prints that command's USAGE, exit 0).
1677
+ var topics = [
1678
+ ["dev", /Usage: blamejs dev/],
1679
+ ["api-snapshot", /Usage: blamejs api-snapshot/],
1680
+ ["api-key", /Usage: blamejs api-key/],
1681
+ ["audit", /Usage: blamejs audit/],
1682
+ ["backup", /Usage: blamejs backup/],
1683
+ ["restore", /Usage: blamejs restore/],
1684
+ ["mtls", /Usage: blamejs mtls/],
1685
+ ["vault", /Usage: blamejs vault/],
1686
+ ["security", /Usage: blamejs security/],
1687
+ ["config-drift", /Usage: blamejs config-drift/],
1688
+ ["file-type", /Usage: blamejs file-type/],
1689
+ ["password", /Usage: blamejs password/],
1690
+ ["retention", /Usage: blamejs retention/],
1691
+ ];
1692
+ for (var i = 0; i < topics.length; i++) {
1693
+ var ctx = _captureCtx();
1694
+ var rc = await cli.main(["help", topics[i][0]], ctx);
1695
+ check("help " + topics[i][0] + " → exit 0", rc === 0);
1696
+ check("help " + topics[i][0] + " → prints its USAGE", topics[i][1].test(ctx.out()));
1697
+ }
1698
+ }
1699
+
1700
+ // ---------------------------------------------------------------------------
1701
+ // migrate — default --dir (omitted) + adversarial --steps values (fractional /
1702
+ // non-numeric). The fractional guard mirrors the sibling `verify-chain
1703
+ // --max-rows 2.5` refusal: the message promises "a positive integer", so 2.5
1704
+ // must be refused at the entry point, never floored-and-run.
1705
+ // ---------------------------------------------------------------------------
1706
+ async function sectionMigrateMore() {
1707
+ var dir = _tmpDir("blamejs-cli-migrate-more");
1708
+ try {
1709
+ var dbPath = path.join(dir, "mig.db");
1710
+ // status with --db but NO --dir → the `args.flags.dir || DEFAULT_MIG_DIR`
1711
+ // default arm (resolves ./migrations relative to cwd). Whether that dir
1712
+ // exists or not, dispatch gets past arg-validation into the runner.
1713
+ var cdef = _captureCtx();
1714
+ var rcdef = await cli.main(["migrate", "status", "--db", dbPath], cdef);
1715
+ check("migrate status (default --dir): not a bad-invocation", rcdef !== 2);
1716
+
1717
+ // --steps 2.5 → fractional → positive-integer guard → exit 2. Before this,
1718
+ // migrate only exercised --steps 0 (the `< 1` clause); the non-integer
1719
+ // clause (`Math.floor(steps) !== steps`) went untested for migrate.
1720
+ var cfrac = _captureCtx();
1721
+ var rcfrac = await cli.main(["migrate", "down", "--db", dbPath, "--steps", "2.5"], cfrac);
1722
+ check("migrate down --steps 2.5 → exit 2", rcfrac === 2);
1723
+ check("migrate down --steps 2.5 → message", /--steps must be a positive integer/.test(cfrac.err()));
1724
+
1725
+ // --steps not-a-number → NaN → !Number.isFinite clause → exit 2.
1726
+ var cnan = _captureCtx();
1727
+ var rcnan = await cli.main(["migrate", "down", "--db", dbPath, "--steps", "not-a-number"], cnan);
1728
+ check("migrate down --steps non-numeric → exit 2", rcnan === 2);
1729
+ check("migrate down --steps non-numeric → message", /--steps must be a positive integer/.test(cnan.err()));
1730
+ } finally { _rm(dir); }
1731
+ }
1732
+
1733
+ // ---------------------------------------------------------------------------
1734
+ // api-key — the post-boot REQUIRED-flag guards for revoke / list / rotate /
1735
+ // verify (each fires only after a successful boot, mirroring the issue guards
1736
+ // already covered) + rotate WITH --grace-ms (the Number(graceMs) arm) + an
1737
+ // adversarial non-numeric --expires-ms (downstream validation rejects it).
1738
+ // ---------------------------------------------------------------------------
1739
+ async function sectionApiKeyMore() {
1740
+ var dataDir = _tmpDir("blamejs-cli-apikey3");
1741
+ try {
1742
+ var base = ["--data-dir", dataDir, "--vault-mode", "plaintext", "--namespace", "api"];
1743
+
1744
+ // revoke with no --id (post-boot) → exit 2
1745
+ var crv = _captureCtx();
1746
+ var rcrv = await cli.main(["api-key", "revoke"].concat(base), crv);
1747
+ check("apikey revoke no --id (post-boot) → exit 2", rcrv === 2);
1748
+ check("apikey revoke no --id → message", /--id <idHex> is required/.test(crv.err()));
1749
+
1750
+ // list with no --owner-id (post-boot) → exit 2
1751
+ var cls = _captureCtx();
1752
+ var rcls = await cli.main(["api-key", "list"].concat(base), cls);
1753
+ check("apikey list no --owner-id (post-boot) → exit 2", rcls === 2);
1754
+ check("apikey list no --owner-id → message", /--owner-id <id> is required/.test(cls.err()));
1755
+
1756
+ // rotate with no --id (post-boot) → exit 2
1757
+ var crt = _captureCtx();
1758
+ var rcrt = await cli.main(["api-key", "rotate"].concat(base), crt);
1759
+ check("apikey rotate no --id (post-boot) → exit 2", rcrt === 2);
1760
+ check("apikey rotate no --id → message", /--id <idHex> is required/.test(crt.err()));
1761
+
1762
+ // verify with no --token (post-boot) → exit 2
1763
+ var cvt = _captureCtx();
1764
+ var rcvt = await cli.main(["api-key", "verify"].concat(base), cvt);
1765
+ check("apikey verify no --token (post-boot) → exit 2", rcvt === 2);
1766
+ check("apikey verify no --token → message", /--token <key> is required/.test(cvt.err()));
1767
+
1768
+ // issue a key, then rotate WITH --grace-ms → the `Number(graceMs)` arm of
1769
+ // the gracePeriodMs ternary (the default `: 0` arm is covered elsewhere).
1770
+ var ci = _captureCtx();
1771
+ var rci = await cli.main(["api-key", "issue"].concat(base).concat(
1772
+ ["--owner-id", "gm", "--scopes", "a:read"]), ci);
1773
+ check("apikey issue (for grace rotate): exit 0", rci === 0);
1774
+ var idMatch = ci.out().match(/^id:\s+([a-f0-9]+)/m);
1775
+ if (idMatch) {
1776
+ var cgr = _captureCtx();
1777
+ var rcgr = await cli.main(["api-key", "rotate"].concat(base).concat(
1778
+ ["--id", idMatch[1], "--grace-ms", "60000"]), cgr);
1779
+ check("apikey rotate --grace-ms → exit 0", rcgr === 0);
1780
+ check("apikey rotate --grace-ms → prints new key", /key \(new\):/.test(cgr.out()));
1781
+ }
1782
+
1783
+ // issue a key then revoke it by id → the revoke SUCCESS arm
1784
+ // (`revoked ? report.ok("revoked: ...") : ...`); the no-op/false arm is
1785
+ // covered by sectionBootedApiKey.
1786
+ var ck = _captureCtx();
1787
+ await cli.main(["api-key", "issue"].concat(base).concat(
1788
+ ["--owner-id", "rv", "--scopes", "a:read"]), ck);
1789
+ var rvId = ck.out().match(/^id:\s+([a-f0-9]+)/m);
1790
+ if (rvId) {
1791
+ var crvk = _captureCtx();
1792
+ var rcrvk = await cli.main(["api-key", "revoke"].concat(base).concat(["--id", rvId[1]]), crvk);
1793
+ check("apikey revoke (real key) → exit 0", rcrvk === 0);
1794
+ check("apikey revoke (real key) → revoked message", /revoked: /.test(crvk.out()));
1795
+ }
1796
+
1797
+ // adversarial: --expires-ms with a non-numeric value → Number() → NaN →
1798
+ // apiKey.issue's downstream numeric validation rejects (exit 1, not a
1799
+ // silently-stored NaN expiry). Drives the real consumer reject path.
1800
+ var cbad = _captureCtx();
1801
+ var rcbad = await cli.main(["api-key", "issue"].concat(base).concat(
1802
+ ["--owner-id", "nanx", "--scopes", "a:read", "--expires-ms", "not-ms"]), cbad);
1803
+ check("apikey issue --expires-ms non-numeric → rejected (exit 1)", rcbad === 1);
1804
+ check("apikey issue --expires-ms non-numeric → validation message",
1805
+ /expiresAt must be a non-negative finite number/.test(cbad.err()));
1806
+ } finally { _rm(dataDir); }
1807
+ }
1808
+
1809
+ // ---------------------------------------------------------------------------
1810
+ // security — assert WITHOUT --no-vault / --no-db-at-rest / --no-audit-signing
1811
+ // negations toggled off for two of the three postures, so the default posture
1812
+ // arms ("wrapped" / "encrypted") are chosen, plus --no-vault selected so the
1813
+ // `false` (skip) arm of the vault ternary is taken. The plaintext boot fails
1814
+ // the asserted postures → FAIL summary, exit 1.
1815
+ // ---------------------------------------------------------------------------
1816
+ async function sectionSecurityMore() {
1817
+ var dataDir = _tmpDir("blamejs-cli-sec3");
1818
+ try {
1819
+ var ctx = _captureCtx();
1820
+ var rc = await cli.main(
1821
+ ["security", "assert", "--data-dir", dataDir, "--vault-mode", "plaintext",
1822
+ "--no-vault", "--no-ntp-strict"], ctx);
1823
+ // --no-vault → vault:false (skip); dbAtRest defaults to "encrypted" and
1824
+ // auditSigning to "wrapped" (both asserted against the plaintext boot) →
1825
+ // the assertion fails → exit 1 with a FAIL summary.
1826
+ check("security assert (default postures) → exit 1", rc === 1);
1827
+ check("security assert (default postures) → FAIL summary", /FAIL:/.test(ctx.out()));
1828
+ } finally { _rm(dataDir); }
1829
+ }
1830
+
1831
+ // ---------------------------------------------------------------------------
1832
+ // misc cheap arg-guards that fire before any boot:
1833
+ // file-type detect with NO file positional → "file path is required"
1834
+ // erase with no --table / no --row-id → the two ordered guards
1835
+ // retention with no --table / no --age-field → the two ordered guards
1836
+ // vault status with NO --data-dir → the "./data" default arm
1837
+ // password check --json on a WEAK plaintext → the json arm's `? 0 : 1`
1838
+ // ---------------------------------------------------------------------------
1839
+ async function sectionCheapGuards() {
1840
+ // file-type detect with no positional file → exit 2
1841
+ var cft = _captureCtx();
1842
+ var rcft = await cli.main(["file-type", "detect"], cft);
1843
+ check("file-type detect (no file) → exit 2", rcft === 2);
1844
+ check("file-type detect (no file) → message", /file path is required/.test(cft.err()));
1845
+
1846
+ // erase with nothing → the --table guard fires first (before --row-id) → 2
1847
+ var cet = _captureCtx();
1848
+ var rcet = await cli.main(["erase"], cet);
1849
+ check("erase (no --table) → exit 2", rcet === 2);
1850
+ check("erase (no --table) → message", /--table <name> is required/.test(cet.err()));
1851
+
1852
+ // erase --table present, no --row-id → the --row-id guard → 2
1853
+ var cer = _captureCtx();
1854
+ var rcer = await cli.main(["erase", "--table", "t"], cer);
1855
+ check("erase (no --row-id) → exit 2", rcer === 2);
1856
+ check("erase (no --row-id) → message", /--row-id <id> is required/.test(cer.err()));
1857
+
1858
+ // retention --data-dir present, no --table → the --table guard → 2
1859
+ var crt = _captureCtx();
1860
+ var rcrt = await cli.main(["retention", "preview", "--data-dir", "/tmp/x"], crt);
1861
+ check("retention (no --table) → exit 2", rcrt === 2);
1862
+ check("retention (no --table) → message", /--table <name> is required/.test(crt.err()));
1863
+
1864
+ // retention --table present, no --age-field → the --age-field guard → 2
1865
+ var cra = _captureCtx();
1866
+ var rcra = await cli.main(["retention", "preview", "--data-dir", "/tmp/x", "--table", "t"], cra);
1867
+ check("retention (no --age-field) → exit 2", rcra === 2);
1868
+ check("retention (no --age-field) → message", /--age-field <col> is required/.test(cra.err()));
1869
+
1870
+ // vault status with NO --data-dir → the `args.flags["data-dir"] || "./data"`
1871
+ // default arm is resolved before the status block runs (covers that arm
1872
+ // regardless of whether ./data exists in the cwd).
1873
+ var cvd = _captureCtx();
1874
+ var rcvd = await cli.main(["vault", "status"], cvd);
1875
+ check("vault status (default ./data) → resolved default, no throw", rcvd === 0 || rcvd === 1);
1876
+
1877
+ // BUG (fixed in cli.js): `vault status` against a NON-EXISTENT data-dir must
1878
+ // surface the standard clean reporter error (exit 1) — NOT reject main()
1879
+ // with an uncaught VaultPassphraseError (which the bin shim would render as
1880
+ // a stack trace). preflightSealable/preflightUnsealable throw when the dir
1881
+ // is absent; the status block now catches it exactly like seal/unseal/rotate.
1882
+ var missingDir = path.join(os.tmpdir(), "blamejs-cli-vault-nonexistent-" + Date.now());
1883
+ var cvm = _captureCtx();
1884
+ var threwVaultStatus = false;
1885
+ var rcvm;
1886
+ try { rcvm = await cli.main(["vault", "status", "--data-dir", missingDir], cvm); }
1887
+ catch (_e) { threwVaultStatus = true; }
1888
+ check("vault status (missing data-dir) → never rejects main()", threwVaultStatus === false);
1889
+ check("vault status (missing data-dir) → clean exit 1", rcvm === 1);
1890
+ check("vault status (missing data-dir) → data-dir-does-not-exist message",
1891
+ /does not exist/.test(cvm.err()));
1892
+
1893
+ // password check --json on a WEAK plaintext → the json arm returns
1894
+ // `verdict.ok ? 0 : 1`; the strong-json case (0) was covered, this is the 1.
1895
+ var cpj = _captureCtx();
1896
+ var rcpj = await cli.main(["password", "check", "--plaintext", "x", "--json"], cpj);
1897
+ check("password check --json (weak) → exit 1", rcpj === 1);
1898
+ check("password check --json (weak) → JSON ok:false", /"ok":false/.test(cpj.out()));
1899
+ }
1900
+
1901
+ // ---------------------------------------------------------------------------
1902
+ // mtls — post-boot required-flag guards (issue / issue-p12 missing --subject /
1903
+ // --password, checked BEFORE cert generation) + a --sealed-mode disabled init
1904
+ // (the plaintext-key-on-disk path, distinct from the default "required" seal).
1905
+ // ---------------------------------------------------------------------------
1906
+ async function sectionMtlsMore() {
1907
+ var dataDir = _tmpDir("blamejs-cli-mtls3");
1908
+ try {
1909
+ var base = ["--data-dir", dataDir, "--vault-mode", "plaintext"];
1910
+
1911
+ // issue with no --subject (post-boot) → exit 2
1912
+ var cis = _captureCtx();
1913
+ var rcis = await cli.main(["mtls", "issue"].concat(base), cis);
1914
+ check("mtls issue no --subject (post-boot) → exit 2", rcis === 2);
1915
+ check("mtls issue no --subject → message", /--subject <CN> is required/.test(cis.err()));
1916
+
1917
+ // issue-p12 with no --subject → exit 2
1918
+ var cps = _captureCtx();
1919
+ var rcps = await cli.main(["mtls", "issue-p12"].concat(base), cps);
1920
+ check("mtls issue-p12 no --subject → exit 2", rcps === 2);
1921
+ check("mtls issue-p12 no --subject → message", /--subject <CN> is required/.test(cps.err()));
1922
+
1923
+ // issue-p12 with --subject but no --password → exit 2
1924
+ var cpp = _captureCtx();
1925
+ var rcpp = await cli.main(["mtls", "issue-p12"].concat(base).concat(["--subject", "cn"]), cpp);
1926
+ check("mtls issue-p12 no --password → exit 2", rcpp === 2);
1927
+ check("mtls issue-p12 no --password → message", /--password <pkcs12-passphrase> is required/.test(cpp.err()));
1928
+
1929
+ // init with --sealed-mode disabled → the CA key is written PLAINTEXT to
1930
+ // disk (ca.paths.caKey), the non-default arm of the init key-path report.
1931
+ var cin = _captureCtx();
1932
+ var rcin = await cli.main(["mtls", "init"].concat(base).concat(["--sealed-mode", "disabled"]), cin);
1933
+ check("mtls init --sealed-mode disabled → exit 0", rcin === 0);
1934
+ check("mtls init --sealed-mode disabled → reports plaintext ca-key", /ca-key:\s+\S+/.test(cin.out()));
1935
+
1936
+ // status against the disabled-mode CA → exercises the exists-true + paths
1937
+ // block once more under the plaintext-key layout.
1938
+ var cst = _captureCtx();
1939
+ var rcst = await cli.main(["mtls", "status"].concat(base).concat(["--sealed-mode", "disabled"]), cst);
1940
+ check("mtls status (disabled CA) → exit 0", rcst === 0);
1941
+ check("mtls status (disabled CA) → CA exists yes", /CA exists:\s+yes/.test(cst.out()));
1942
+ } finally { _rm(dataDir); }
1943
+ }
1944
+
1945
+ // ---------------------------------------------------------------------------
1946
+ // restore — the success-side list / list-rollbacks loop bodies (which only run
1947
+ // when storage / rollback-root actually contain entries) + an apply that gets
1948
+ // PAST the max-pulled-* numeric gates with valid values and a --rollback-root,
1949
+ // building the restore engine before run() rejects on the absent bundle.
1950
+ // The storage + rollback fixtures are crafted directly on disk in the exact
1951
+ // shapes b.backup.diskStorage.listBundles + b.restoreRollback.list read
1952
+ // (a timestamp+suffix bundle dir; a non-"discarded-" rollback point dir).
1953
+ // ---------------------------------------------------------------------------
1954
+ async function sectionRestoreMore() {
1955
+ var dir = _tmpDir("blamejs-cli-restore3");
1956
+ try {
1957
+ // --- list with a bundle present → the "bundles in <root>: N" + per-bundle
1958
+ // row loop (only reached when listBundles() returns > 0). ---
1959
+ var store = path.join(dir, "store");
1960
+ var bundleId = "2026-05-24T15-00-00-000Z-aabb1100"; // valid timestamp+suffix
1961
+ fs.mkdirSync(path.join(store, bundleId), { recursive: true });
1962
+ fs.writeFileSync(path.join(store, bundleId, "manifest.json"), "{}");
1963
+ var cl = _captureCtx();
1964
+ var rcl = await cli.main(["restore", "list", "--storage-root", store], cl);
1965
+ check("restore list (populated) → exit 0", rcl === 0);
1966
+ check("restore list (populated) → bundle-count header", /bundles in .*: 1/.test(cl.out()));
1967
+ check("restore list (populated) → lists the bundle id", new RegExp(bundleId).test(cl.out()));
1968
+
1969
+ // --- list-rollbacks with a point present → the "rollback points at ...: N"
1970
+ // + per-point row loop (only reached when list() returns > 0). ---
1971
+ var dd = path.join(dir, "dd");
1972
+ fs.mkdirSync(dd, { recursive: true });
1973
+ var rbRoot = path.join(dir, "rbroot");
1974
+ fs.mkdirSync(path.join(rbRoot, "point-0001"), { recursive: true });
1975
+ // A sibling <point>.marker.json carries the operator metadata the listing
1976
+ // annotates each point with (bundleId / reason on marker.operator, swappedAt
1977
+ // top-level) -- the row previously read a non-existent recordedAt/bundleId.
1978
+ fs.writeFileSync(path.join(rbRoot, "point-0001.marker.json"),
1979
+ JSON.stringify({ swappedAt: "2026-05-24T15:00:00.000Z", operator: { bundleId: "bk-test-123", reason: "unit-test" } }));
1980
+ var clr = _captureCtx();
1981
+ var rclr = await cli.main(
1982
+ ["restore", "list-rollbacks", "--data-dir", dd, "--rollback-root", rbRoot], clr);
1983
+ check("restore list-rollbacks (populated) → exit 0", rclr === 0);
1984
+ check("restore list-rollbacks (populated) → point-count header", /rollback points at .*: 1/.test(clr.out()));
1985
+ check("restore list-rollbacks (populated) → lists the point", /point-0001/.test(clr.out()));
1986
+ check("restore list-rollbacks (populated) → renders swappedAt from the marker",
1987
+ /swappedAt=2026-05-24T15:00:00\.000Z/.test(clr.out()));
1988
+ check("restore list-rollbacks (populated) → renders bundleId from marker.operator",
1989
+ /bundleId=bk-test-123/.test(clr.out()));
1990
+
1991
+ // --- apply past the numeric gates: valid --max-pulled-bytes / -files +
1992
+ // --rollback-root build the restore engine; run() then rejects because the
1993
+ // bundle can't be pulled → the catch returns non-zero (exit 1). This
1994
+ // covers the Number(maxBytes)/Number(maxFiles) opt-mapping arms and the
1995
+ // rollback-root resolution arm, distinct from the earlier bad-value gates. ---
1996
+ var ca = _captureCtx();
1997
+ var rca = await cli.main(
1998
+ ["restore", "apply", "--data-dir", path.join(dir, "dd2"),
1999
+ "--bundle", path.join(store, bundleId), "--passphrase", "p",
2000
+ "--max-pulled-bytes", "1048576", "--max-pulled-files", "100",
2001
+ "--rollback-root", path.join(dir, "rb2")], ca);
2002
+ check("restore apply (valid gates, unpullable bundle) → exit 1", rca === 1);
2003
+ } finally { _rm(dir); }
2004
+ }
2005
+
2006
+ // ---------------------------------------------------------------------------
2007
+ // audit — drive each of archive / export / verify-bundle / purge with ALL of
2008
+ // its required flags present, so dispatch gets PAST the arg-validation returns
2009
+ // and into the auditTools.* call (and its surrounding try/catch). Against a
2010
+ // fresh process with no live audit chain the call fails and the catch reports
2011
+ // it — but the point is coverage of the operation body + its error path, which
2012
+ // the existing tests (each omitting one required flag) never reach. Assert
2013
+ // only that we got past validation (exit != 2), robust to whether an ambient
2014
+ // booted db makes the call succeed or fail.
2015
+ // ---------------------------------------------------------------------------
2016
+ async function sectionAuditOps() {
2017
+ var dir = _tmpDir("blamejs-cli-auditops");
2018
+ try {
2019
+ // archive: passphrase + out + before all present → into auditTools.archive
2020
+ var ca = _captureCtx();
2021
+ var rca = await cli.main(
2022
+ ["audit", "archive", "--passphrase", "p", "--out", path.join(dir, "arc-out"),
2023
+ "--before", "2000-01-01"], ca);
2024
+ check("audit archive (all flags) → past validation (exit != 2)", rca !== 2);
2025
+
2026
+ // export: passphrase + out + a range flag present → into exportSlice
2027
+ var ce = _captureCtx();
2028
+ var rce = await cli.main(
2029
+ ["audit", "export", "--passphrase", "p", "--out", path.join(dir, "exp-out"),
2030
+ "--from", "2000-01-01"], ce);
2031
+ check("audit export (all flags) → past validation (exit != 2)", rce !== 2);
2032
+
2033
+ // verify-bundle: passphrase + in present → into verifyBundle
2034
+ var cv = _captureCtx();
2035
+ var rcv = await cli.main(
2036
+ ["audit", "verify-bundle", "--passphrase", "p", "--in", path.join(dir, "no-bundle")], cv);
2037
+ check("audit verify-bundle (all flags) → past validation (exit != 2)", rcv !== 2);
2038
+
2039
+ // purge: passphrase + archive + confirm present → into purge
2040
+ var cp = _captureCtx();
2041
+ var rcp = await cli.main(
2042
+ ["audit", "purge", "--passphrase", "p", "--archive", path.join(dir, "no-arc"), "--confirm"], cp);
2043
+ check("audit purge (all flags) → past validation (exit != 2)", rcp !== 2);
2044
+ } finally { _rm(dir); }
2045
+ }
2046
+
2047
+ // ---------------------------------------------------------------------------
2048
+ // retention — a booted PREVIEW (dry-run) with non-JSON text output against a
2049
+ // real table, plus an explicit valid --batch-size, so the "DRY-RUN " summary
2050
+ // prefix arm and the Number(batchSize) opt arm are both exercised (the JSON
2051
+ // arm + the non-preview text arm are covered by sectionBootedRawTable).
2052
+ // ---------------------------------------------------------------------------
2053
+ async function sectionRetentionPreviewText() {
2054
+ var dir = _tmpDir("blamejs-cli-ret3");
2055
+ try {
2056
+ // First plaintext boot (missing table) materialises blamejs.db.
2057
+ await cli.main(
2058
+ ["retention", "preview", "--data-dir", dir, "--vault-mode", "plaintext",
2059
+ "--table", "seed_missing", "--age-field", "ts", "--ttl-ms", "1", "--action", "delete"],
2060
+ _captureCtx());
2061
+ var dbFile = path.join(dir, "blamejs.db");
2062
+ check("ret-preview-text: db file materialised", fs.existsSync(dbFile));
2063
+
2064
+ var h = new sqlite.DatabaseSync(dbFile);
2065
+ h.exec("CREATE TABLE ret_prev (_id TEXT PRIMARY KEY, ts INTEGER)");
2066
+ h.prepare("INSERT INTO ret_prev (_id, ts) VALUES (?, ?)").run("p-old", 1);
2067
+ h.close();
2068
+
2069
+ // preview (dry-run) non-JSON + explicit --batch-size → DRY-RUN text arm +
2070
+ // Number(batchSize) arm.
2071
+ var cp = _captureCtx();
2072
+ var rcp = await cli.main(
2073
+ ["retention", "preview", "--data-dir", dir, "--vault-mode", "plaintext",
2074
+ "--table", "ret_prev", "--age-field", "ts", "--ttl-ms", "1", "--action", "delete",
2075
+ "--batch-size", "250"], cp);
2076
+ check("retention preview (text): exit 0", rcp === 0);
2077
+ check("retention preview (text): DRY-RUN prefix", /DRY-RUN rule:/.test(cp.out()));
2078
+ } finally { _rm(dir); }
2079
+ }
2080
+
2081
+ // ---------------------------------------------------------------------------
2082
+ // restore — the error catches that only fire when the underlying primitive
2083
+ // THROWS (not returns empty): listBundles / restoreRollback.list against a
2084
+ // path that is a regular FILE (ENOTDIR), plus the default-most-recent rollback
2085
+ // branch when a rollback point actually exists (picks pts[0] then rolls back).
2086
+ // ---------------------------------------------------------------------------
2087
+ async function sectionRestoreCatches() {
2088
+ var dir = _tmpDir("blamejs-cli-restore-catch");
2089
+ try {
2090
+ // list with --storage-root pointing at a FILE → diskStorage.listBundles
2091
+ // does a scandir that throws ENOTDIR → the list catch returns exit 1.
2092
+ var storeFile = path.join(dir, "not-a-dir");
2093
+ fs.writeFileSync(storeFile, "x");
2094
+ var cl = _captureCtx();
2095
+ var rcl = await cli.main(["restore", "list", "--storage-root", storeFile], cl);
2096
+ check("restore list (storage-root is a file) → exit 1", rcl === 1);
2097
+ check("restore list (storage-root is a file) → ENOTDIR message", /blamejs restore list:/.test(cl.err()));
2098
+
2099
+ // list-rollbacks with --rollback-root pointing at a FILE → restoreRollback.list
2100
+ // scandir throws ENOTDIR → the list-rollbacks catch returns exit 1.
2101
+ var dd = path.join(dir, "dd");
2102
+ fs.mkdirSync(dd, { recursive: true });
2103
+ var rbFile = path.join(dir, "rb-not-a-dir");
2104
+ fs.writeFileSync(rbFile, "x");
2105
+ var clr = _captureCtx();
2106
+ var rclr = await cli.main(
2107
+ ["restore", "list-rollbacks", "--data-dir", dd, "--rollback-root", rbFile], clr);
2108
+ check("restore list-rollbacks (rollback-root is a file) → exit 1", rclr === 1);
2109
+ check("restore list-rollbacks (rollback-root is a file) → catch message",
2110
+ /blamejs restore list-rollbacks:/.test(clr.err()));
2111
+
2112
+ // rollback WITHOUT --rollback, default-most-recent branch, but rollback-root
2113
+ // is a FILE → restoreRollback.list inside the default branch throws → the
2114
+ // "listing rollbacks at ..." catch returns exit 1.
2115
+ var crb = _captureCtx();
2116
+ var rcrb = await cli.main(
2117
+ ["restore", "rollback", "--data-dir", dd, "--rollback-root", rbFile], crb);
2118
+ check("restore rollback (default, rollback-root is a file) → exit 1", rcrb === 1);
2119
+ check("restore rollback (default, rollback-root is a file) → listing message",
2120
+ /listing rollbacks at /.test(crb.err()));
2121
+
2122
+ // rollback WITHOUT --rollback, a rollback point IS present → the default
2123
+ // branch picks pts[0].rollbackPath and restoreRollback.rollback swaps the
2124
+ // empty point in → the OK-rolled-back success arm (exit 0).
2125
+ var dd2 = path.join(dir, "dd2");
2126
+ fs.mkdirSync(dd2, { recursive: true });
2127
+ var rbRoot = path.join(dir, "rbroot");
2128
+ fs.mkdirSync(path.join(rbRoot, "point-0001"), { recursive: true });
2129
+ fs.writeFileSync(path.join(rbRoot, "point-0001.marker.json"),
2130
+ JSON.stringify({ swappedAt: "2026-05-24T15:00:00.000Z", operator: { bundleId: "bk-1", reason: "unit" } }));
2131
+ var crd = _captureCtx();
2132
+ var rcrd = await cli.main(
2133
+ ["restore", "rollback", "--data-dir", dd2, "--rollback-root", rbRoot], crd);
2134
+ check("restore rollback (default most-recent, point present) → exit 0", rcrd === 0);
2135
+ check("restore rollback (default most-recent, point present) → OK line", /OK — rolled back/.test(crd.out()));
2136
+ check("restore rollback (default most-recent, point present) → used the point",
2137
+ /point-0001/.test(crd.out()));
2138
+ } finally { _rm(dir); }
2139
+ }
2140
+
2141
+ // ---------------------------------------------------------------------------
2142
+ // api-snapshot capture where the module LOADS fine but is not a plain object
2143
+ // (it exports a function) → apiSnapshot.capture throws AFTER the module resolves
2144
+ // → the capture catch (distinct from the module-load catch) returns exit 1.
2145
+ // ---------------------------------------------------------------------------
2146
+ async function sectionApiSnapshotCaptureThrow() {
2147
+ var dir = _tmpDir("blamejs-cli-apisnap-cap");
2148
+ try {
2149
+ var fnMod = path.join(dir, "fn-mod.js");
2150
+ fs.writeFileSync(fnMod, "module.exports = function fn() {};");
2151
+ var snap = path.join(dir, "snap.json");
2152
+ var c = _captureCtx();
2153
+ var rc = await cli.main(["api-snapshot", "capture", "--file", snap, "--module", fnMod], c);
2154
+ check("api-snapshot capture (function module) → exit 1", rc === 1);
2155
+ check("api-snapshot capture (function module) → capture (not load) error",
2156
+ /blamejs api-snapshot capture: capture:/.test(c.err()));
2157
+ } finally { _rm(dir); }
2158
+ }
2159
+
2160
+ // ---------------------------------------------------------------------------
2161
+ // backup extract — the extract catch: a --to is supplied (past the required-flag
2162
+ // gate) but the bundle can't be extracted (nonexistent bundle dir) → restoreBundle
2163
+ // .extract throws → the extract catch returns exit 1.
2164
+ // ---------------------------------------------------------------------------
2165
+ async function sectionBackupExtractCatch() {
2166
+ var dir = _tmpDir("blamejs-cli-backup-extract");
2167
+ try {
2168
+ var c = _captureCtx();
2169
+ var rc = await cli.main(
2170
+ ["backup", "extract", "--bundle", path.join(dir, "no-such-bundle"),
2171
+ "--to", path.join(dir, "fresh-staging"), "--passphrase", "p"], c);
2172
+ check("backup extract (bad bundle, --to present) → exit 1", rc === 1);
2173
+ check("backup extract (bad bundle, --to present) → catch message",
2174
+ /blamejs backup extract:/.test(c.err()));
2175
+ } finally { _rm(dir); }
2176
+ }
2177
+
2178
+ // ---------------------------------------------------------------------------
2179
+ // mtls — the operation catches that fire AFTER a successful boot: init whose
2180
+ // atomic CA write fails (ca.crt pre-exists as a DIRECTORY the rename can't
2181
+ // clobber), and issue / issue-p12 against a CA whose sealed key on disk was
2182
+ // corrupted (the unwrap fails inside generateClientCert* → the catch → exit 1).
2183
+ // ---------------------------------------------------------------------------
2184
+ async function sectionMtlsCorruptCA() {
2185
+ // init: ca.crt pre-exists as a directory → atomicFile rename fails → init catch
2186
+ var initDir = _tmpDir("blamejs-cli-mtls-initfail");
2187
+ try {
2188
+ fs.mkdirSync(path.join(initDir, "ca.crt"), { recursive: true });
2189
+ var ci = _captureCtx();
2190
+ var rci = await cli.main(
2191
+ ["mtls", "init", "--data-dir", initDir, "--vault-mode", "plaintext"], ci);
2192
+ check("mtls init (ca.crt is a dir) → exit 1", rci === 1);
2193
+ check("mtls init (ca.crt is a dir) → catch message", /blamejs mtls init:/.test(ci.err()));
2194
+ } finally { _rm(initDir); }
2195
+
2196
+ // issue / issue-p12 against a corrupted sealed CA key → generateClientCert*
2197
+ // throws while loading the key → the respective catch returns exit 1.
2198
+ var caDir = _tmpDir("blamejs-cli-mtls-corrupt");
2199
+ try {
2200
+ var base = ["--data-dir", caDir, "--vault-mode", "plaintext"];
2201
+ var cInit = _captureCtx();
2202
+ check("mtls init (for corrupt-key) → exit 0",
2203
+ (await cli.main(["mtls", "init"].concat(base), cInit)) === 0);
2204
+ var sealedKey = path.join(caDir, "ca.key.sealed");
2205
+ check("mtls init wrote a sealed CA key", fs.existsSync(sealedKey));
2206
+ fs.writeFileSync(sealedKey, "corrupted-not-a-sealed-key-blob");
2207
+
2208
+ var cIssue = _captureCtx();
2209
+ var rcIssue = await cli.main(["mtls", "issue"].concat(base).concat(["--subject", "cn-x"]), cIssue);
2210
+ check("mtls issue (corrupt CA key) → exit 1", rcIssue === 1);
2211
+ check("mtls issue (corrupt CA key) → catch message", /blamejs mtls issue:/.test(cIssue.err()));
2212
+
2213
+ var cP12 = _captureCtx();
2214
+ var rcP12 = await cli.main(["mtls", "issue-p12"].concat(base).concat(
2215
+ ["--subject", "cn-y", "--password", "p12-passphrase-abc"]), cP12);
2216
+ check("mtls issue-p12 (corrupt CA key) → exit 1", rcP12 === 1);
2217
+ check("mtls issue-p12 (corrupt CA key) → catch message", /blamejs mtls issue-p12:/.test(cP12.err()));
2218
+ } finally { _rm(caDir); }
2219
+ }
2220
+
2221
+ // ---------------------------------------------------------------------------
2222
+ // security assert — the CLEAN (exit 0) success arm: every posture check skipped
2223
+ // via --no-vault / --no-db-at-rest / --no-audit-signing / --no-ntp-strict, and
2224
+ // no require/forbid-env, so assertProduction passes and report.ok("production
2225
+ // posture clean") returns 0 (the FAIL summary is covered elsewhere).
2226
+ // ---------------------------------------------------------------------------
2227
+ async function sectionSecurityAssertClean() {
2228
+ var dataDir = _tmpDir("blamejs-cli-sec-clean");
2229
+ try {
2230
+ var ctx = _captureCtx();
2231
+ var rc = await cli.main(
2232
+ ["security", "assert", "--data-dir", dataDir, "--vault-mode", "plaintext",
2233
+ "--no-vault", "--no-db-at-rest", "--no-audit-signing", "--no-ntp-strict"], ctx);
2234
+ check("security assert (all postures skipped) → exit 0", rc === 0);
2235
+ check("security assert (all postures skipped) → clean message", /production posture clean/.test(ctx.out()));
2236
+ } finally { _rm(dataDir); }
2237
+ }
2238
+
2239
+ // ---------------------------------------------------------------------------
2240
+ // config-drift verify — the tamper branch: capture a signed baseline out-of-band
2241
+ // on the same data-dir, then corrupt the sidecar's digestHex so the recorded
2242
+ // signature no longer verifies. read() returns a sidecar with verified:false, so
2243
+ // `verify` takes the tamper-detected arm (report.error + exit 1).
2244
+ // ---------------------------------------------------------------------------
2245
+ async function sectionConfigDriftTamper() {
2246
+ var dataDir = _tmpDir("blamejs-cli-drift-tamper");
2247
+ try {
2248
+ var sidecarPath;
2249
+ var booted = await b.cliHelpers.bootApp({ dataDir: dataDir, vaultMode: "plaintext", env: {} });
2250
+ try {
2251
+ var drift = booted.b.configDrift.create({ dataDir: dataDir, audit: booted.b.audit });
2252
+ sidecarPath = drift.sidecarPath;
2253
+ var res = await drift.checkpoint({ service: "api", replicas: 2 });
2254
+ check("config-drift tamper: baseline captured", res && res.signed === true && res.tamper === false);
2255
+ } finally {
2256
+ try { await booted.app.shutdown(); } catch (_e) { /* best-effort */ }
2257
+ }
2258
+
2259
+ // Corrupt the recorded digest — the signature is over digestHex, so flipping
2260
+ // one hex char makes _verifySidecar's signature check fail (tamper) while the
2261
+ // file stays parseable + shape-valid so read() returns a sidecar object.
2262
+ var sc = JSON.parse(fs.readFileSync(sidecarPath, "utf8"));
2263
+ sc.digestHex = sc.digestHex.slice(0, -1) + (sc.digestHex.slice(-1) === "a" ? "b" : "a");
2264
+ fs.writeFileSync(sidecarPath, JSON.stringify(sc, null, 2));
2265
+
2266
+ var cv = _captureCtx();
2267
+ var rcv = await cli.main(
2268
+ ["config-drift", "verify", "--data-dir", dataDir, "--vault-mode", "plaintext"], cv);
2269
+ check("config-drift verify (tampered sidecar) → exit 1", rcv === 1);
2270
+ check("config-drift verify (tampered sidecar) → tamper message",
2271
+ /sidecar tamper detected:/.test(cv.err()));
2272
+ } finally { _rm(dataDir); }
2273
+ }
2274
+
2275
+ // ---------------------------------------------------------------------------
2276
+ // password — the --breach-check opt-wiring arm (policyOpts.breachCheck +
2277
+ // failClosed). A too-short plaintext is rejected on length BEFORE any network
2278
+ // call, so the HIBP path is never reached: the opt wiring is exercised, the
2279
+ // verdict is policy/too-short, exit 1. No network touched.
2280
+ // ---------------------------------------------------------------------------
2281
+ async function sectionPasswordBreachOpt() {
2282
+ var c = _captureCtx();
2283
+ var rc = await cli.main(
2284
+ ["password", "check", "--plaintext", "x", "--breach-check", "--fail-closed"], c);
2285
+ check("password --breach-check (short plaintext) → exit 1 (rejected pre-network)", rc === 1);
2286
+ check("password --breach-check (short plaintext) → too-short verdict", /REJECTED: policy\/too-short/.test(c.out()));
2287
+ }
2288
+
2289
+ // ---------------------------------------------------------------------------
2290
+ // retention run — the per-row ERROR summary arm (non-JSON). A BEFORE DELETE
2291
+ // trigger that RAISE(ABORT)s makes the scan succeed but each per-row delete
2292
+ // throw, so _runAction fails per-row and summary.errors is populated → the
2293
+ // "errors: N" + per-error lines print and the run returns exit 1.
2294
+ // ---------------------------------------------------------------------------
2295
+ async function sectionRetentionRowErrors() {
2296
+ var dir = _tmpDir("blamejs-cli-ret-rowerr");
2297
+ try {
2298
+ // First plaintext boot materialises blamejs.db.
2299
+ await cli.main(
2300
+ ["retention", "preview", "--data-dir", dir, "--vault-mode", "plaintext",
2301
+ "--table", "seed_missing", "--age-field", "ts", "--ttl-ms", "1", "--action", "delete"],
2302
+ _captureCtx());
2303
+ var dbFile = path.join(dir, "blamejs.db");
2304
+ check("ret-rowerr: db file materialised", fs.existsSync(dbFile));
2305
+
2306
+ var h = new sqlite.DatabaseSync(dbFile);
2307
+ h.exec("CREATE TABLE ret_trig (_id TEXT PRIMARY KEY, ts INTEGER)");
2308
+ h.prepare("INSERT INTO ret_trig (_id, ts) VALUES (?, ?)").run("t-old", 1);
2309
+ h.exec("CREATE TRIGGER ret_trig_no_del BEFORE DELETE ON ret_trig " +
2310
+ "BEGIN SELECT RAISE(ABORT, 'trigger-blocks-delete'); END;");
2311
+ h.close();
2312
+
2313
+ var c = _captureCtx();
2314
+ var rc = await cli.main(
2315
+ ["retention", "run", "--data-dir", dir, "--vault-mode", "plaintext",
2316
+ "--table", "ret_trig", "--age-field", "ts", "--ttl-ms", "1", "--action", "delete"], c);
2317
+ check("retention run (per-row delete error) → exit 1", rc === 1);
2318
+ check("retention run (per-row delete error) → errors count line", /errors:\s+1/.test(c.out()));
2319
+ check("retention run (per-row delete error) → per-error row line",
2320
+ /t-old: trigger-blocks-delete/.test(c.out()));
2321
+ } finally { _rm(dir); }
2322
+ }
2323
+
2324
+ // ---------------------------------------------------------------------------
2325
+ // erase — the SUCCESS body: a table with a registered cryptoField sealed schema
2326
+ // and a real row. The lookup finds the row, getSchema reports sealed columns, the
2327
+ // b.sql UPDATE nulls them, the audit event emits, and the "erased: ..." summary
2328
+ // prints (exit 0). cryptoField schemas are a process-global registry the CLI's
2329
+ // booted app shares, so registering the schema here makes it visible to the boot.
2330
+ // ---------------------------------------------------------------------------
2331
+ async function sectionEraseSuccess() {
2332
+ var dir = _tmpDir("blamejs-cli-erase-ok");
2333
+ try {
2334
+ // First plaintext boot materialises blamejs.db.
2335
+ await cli.main(
2336
+ ["retention", "preview", "--data-dir", dir, "--vault-mode", "plaintext",
2337
+ "--table", "seed_missing", "--age-field", "ts", "--ttl-ms", "1", "--action", "delete"],
2338
+ _captureCtx());
2339
+ var dbFile = path.join(dir, "blamejs.db");
2340
+ check("erase-ok: db file materialised", fs.existsSync(dbFile));
2341
+
2342
+ var h = new sqlite.DatabaseSync(dbFile);
2343
+ // er_sealed carries a sealed column AND a derived-hash column so erase nulls
2344
+ // both — exercising the "derived hashes nulled" report line.
2345
+ h.exec("CREATE TABLE er_sealed (_id TEXT PRIMARY KEY, secret TEXT, secretHash TEXT, other TEXT)");
2346
+ h.prepare("INSERT INTO er_sealed (_id, secret, secretHash, other) VALUES (?, ?, ?, ?)")
2347
+ .run("row-1", "cleartext-secret", "deadbeef", "keepme");
2348
+ // er_badcol registers a sealed column that does NOT exist on the table, so
2349
+ // the row lookup (SELECT *) succeeds but the erase UPDATE fails.
2350
+ h.exec("CREATE TABLE er_badcol (_id TEXT PRIMARY KEY, other TEXT)");
2351
+ h.prepare("INSERT INTO er_badcol (_id, other) VALUES (?, ?)").run("row-2", "x");
2352
+ h.close();
2353
+
2354
+ // Register the sealed schemas in the shared cryptoField registry so the CLI's
2355
+ // boot resolves them. No posture is pinned, so the seal-envelope floor is a
2356
+ // no-op and registration passes.
2357
+ b.cryptoField.registerTable("er_sealed", {
2358
+ sealedFields: ["secret"],
2359
+ derivedHashes: { secretHash: { from: "secret", normalize: function (v) { return String(v); } } },
2360
+ rowIdField: "_id",
2361
+ });
2362
+ b.cryptoField.registerTable("er_badcol", { sealedFields: ["ghost_col"], rowIdField: "_id" });
2363
+ try {
2364
+ var c = _captureCtx();
2365
+ var rc = await cli.main(
2366
+ ["erase", "--data-dir", dir, "--vault-mode", "plaintext",
2367
+ "--table", "er_sealed", "--row-id", "row-1", "--confirm", "--reason", "gdpr-art-17"], c);
2368
+ check("erase (sealed schema, real row) → exit 0", rc === 0);
2369
+ check("erase (sealed schema, real row) → erased summary", /erased: er_sealed\/row-1/.test(c.out()));
2370
+ check("erase (sealed schema, real row) → names nulled column", /sealed columns nulled: secret/.test(c.out()));
2371
+ check("erase (sealed schema, real row) → names nulled derived hash", /derived hashes nulled: secretHash/.test(c.out()));
2372
+
2373
+ // A sealed column the table doesn't have → the erase UPDATE throws → the
2374
+ // "UPDATE failed" catch returns exit 1 (distinct from the row-lookup catch).
2375
+ var c2 = _captureCtx();
2376
+ var rc2 = await cli.main(
2377
+ ["erase", "--data-dir", dir, "--vault-mode", "plaintext",
2378
+ "--table", "er_badcol", "--row-id", "row-2", "--confirm"], c2);
2379
+ check("erase (sealed col missing on table) → exit 1", rc2 === 1);
2380
+ check("erase (sealed col missing on table) → UPDATE failed message", /UPDATE failed:/.test(c2.err()));
2381
+ } finally {
2382
+ if (typeof b.cryptoField._resetForTest === "function") b.cryptoField._resetForTest();
2383
+ }
2384
+ } finally { _rm(dir); }
2385
+ }
2386
+
2387
+ // ---------------------------------------------------------------------------
2388
+ // audit — the OPERATION-BODY success arms of archive / export / verify-bundle /
2389
+ // purge, driven against a REAL booted db + audit chain + ML-DSA-signed checkpoint
2390
+ // (setupTestDb, the same fixture the b.auditTools canonical suite uses). The
2391
+ // omit-one-flag tests only reach the arg-validation returns; these drive the
2392
+ // auditTools.* calls and their success reporting, plus the verify-bundle FAIL
2393
+ // arm on a manifest-tampered copy.
2394
+ // ---------------------------------------------------------------------------
2395
+ async function sectionAuditSuccess() {
2396
+ var dbDir = _tmpDir("blamejs-cli-audit-live");
2397
+ var bundleRoot = _tmpDir("blamejs-cli-audit-bundles");
2398
+ var savedVault = process.env.BLAMEJS_VAULT_PASSPHRASE;
2399
+ var savedSign = process.env.BLAMEJS_AUDIT_SIGNING_PASSPHRASE;
2400
+ var savedNtp = process.env.BLAMEJS_SKIP_NTP_CHECK;
2401
+ var tornDown = false;
2402
+ var PASS = "audit-cli-bundle-passphrase";
2403
+ try {
2404
+ await helpers.setupTestDb(dbDir);
2405
+ b.audit.registerNamespace("test");
2406
+ for (var i = 0; i < 4; i++) {
2407
+ await b.audit.record({ actor: { userId: "u-" + i }, action: "test.seeded", outcome: "success", metadata: { i: i } });
2408
+ }
2409
+ await b.audit.flush();
2410
+ await b.audit.checkpoint();
2411
+
2412
+ var archiveDir = path.join(bundleRoot, "arc");
2413
+ var exportDir = path.join(bundleRoot, "exp");
2414
+ var future = new Date(Date.now() + 3600000).toISOString();
2415
+
2416
+ // archive — the success arm (wrote archive bundle + return 0).
2417
+ var ca = _captureCtx();
2418
+ var rca = await cli.main(
2419
+ ["audit", "archive", "--passphrase", PASS, "--out", archiveDir, "--before", future], ca);
2420
+ check("audit archive (live chain) → exit 0", rca === 0);
2421
+ check("audit archive (live chain) → wrote-bundle line", /wrote archive bundle to /.test(ca.out()));
2422
+
2423
+ // export — the success arm (wrote export bundle + return 0).
2424
+ var ce = _captureCtx();
2425
+ var rce = await cli.main(
2426
+ ["audit", "export", "--passphrase", PASS, "--out", exportDir,
2427
+ "--from", "1970-01-01T00:00:00Z", "--to", future], ce);
2428
+ check("audit export (live chain) → exit 0", rce === 0);
2429
+ check("audit export (live chain) → wrote-bundle line", /wrote export bundle to /.test(ce.out()));
2430
+
2431
+ // verify-bundle — the OK success arm on the archive bundle.
2432
+ var cvb = _captureCtx();
2433
+ var rcvb = await cli.main(
2434
+ ["audit", "verify-bundle", "--passphrase", PASS, "--in", archiveDir], cvb);
2435
+ check("audit verify-bundle (valid archive) → exit 0", rcvb === 0);
2436
+ check("audit verify-bundle (valid archive) → OK bundle-verified line", /bundle verified/.test(cvb.out()));
2437
+
2438
+ // verify-bundle — the FAIL arm: a manifest-tampered COPY (predecessor witness
2439
+ // rewritten) fails the chain walk → v.ok false → "FAIL —" on stderr, exit 1.
2440
+ var tamperedDir = path.join(bundleRoot, "arc-tampered");
2441
+ fs.cpSync(archiveDir, tamperedDir, { recursive: true });
2442
+ var mPath = path.join(tamperedDir, "manifest.json");
2443
+ var manifest = JSON.parse(fs.readFileSync(mPath, "utf8"));
2444
+ manifest.range.predecessorRowHash = "f".repeat(128);
2445
+ fs.writeFileSync(mPath, JSON.stringify(manifest));
2446
+ var cvf = _captureCtx();
2447
+ var rcvf = await cli.main(
2448
+ ["audit", "verify-bundle", "--passphrase", PASS, "--in", tamperedDir], cvf);
2449
+ check("audit verify-bundle (tampered archive) → exit 1", rcvf === 1);
2450
+ check("audit verify-bundle (tampered archive) → FAIL line", /FAIL —/.test(cvf.err()));
2451
+
2452
+ // purge — the success arm: verifies the (untampered) archive, deletes the
2453
+ // captured live rows, writes the anchor, prints "OK — purged N rows".
2454
+ var cp = _captureCtx();
2455
+ var rcp = await cli.main(
2456
+ ["audit", "purge", "--passphrase", PASS, "--archive", archiveDir, "--confirm"], cp);
2457
+ check("audit purge (verified archive) → exit 0", rcp === 0);
2458
+ check("audit purge (verified archive) → purged-rows line", /purged \d+ rows/.test(cp.out()));
2459
+
2460
+ await helpers.teardownTestDb(dbDir);
2461
+ tornDown = true;
2462
+ } finally {
2463
+ if (!tornDown) { try { await helpers.teardownTestDb(dbDir); } catch (_e) { /* best-effort */ } }
2464
+ _rm(bundleRoot);
2465
+ if (savedVault === undefined) delete process.env.BLAMEJS_VAULT_PASSPHRASE; else process.env.BLAMEJS_VAULT_PASSPHRASE = savedVault;
2466
+ if (savedSign === undefined) delete process.env.BLAMEJS_AUDIT_SIGNING_PASSPHRASE; else process.env.BLAMEJS_AUDIT_SIGNING_PASSPHRASE = savedSign;
2467
+ if (savedNtp === undefined) delete process.env.BLAMEJS_SKIP_NTP_CHECK; else process.env.BLAMEJS_SKIP_NTP_CHECK = savedNtp;
2468
+ }
2469
+ }
2470
+
2471
+ async function run() {
2472
+ var dir = _tmpDir("blamejs-cli");
2473
+ try {
2474
+ // =====================================================================
2475
+ // seed — arg validation
2476
+ // =====================================================================
2477
+
2478
+ // bare `seed` (no subcommand) → usage on stderr, exit 2
2479
+ var cBare = _captureCtx();
2480
+ var rcBare = await cli.main(["seed"], cBare);
2481
+ check("seed: bare → exit 2", rcBare === 2);
2482
+ check("seed: bare → usage on stderr", /Usage: blamejs seed/.test(cBare.err()));
2483
+
2484
+ // `seed help` → usage on stdout, exit 0
2485
+ var cHelp = _captureCtx();
2486
+ var rcHelp = await cli.main(["seed", "help"], cHelp);
2487
+ check("seed help: exit 0", rcHelp === 0);
2488
+ check("seed help: usage on stdout", /Usage: blamejs seed/.test(cHelp.out()));
2489
+ check("seed help: lists run + status", /\brun\b/.test(cHelp.out()) && /\bstatus\b/.test(cHelp.out()));
2490
+
2491
+ // `help seed` (top-level help dispatch → SEED_USAGE)
2492
+ var cTopHelpSeed = _captureCtx();
2493
+ var rcTopHelpSeed = await cli.main(["help", "seed"], cTopHelpSeed);
2494
+ check("help seed: exit 0", rcTopHelpSeed === 0);
2495
+ check("help seed: prints SEED_USAGE", /Usage: blamejs seed/.test(cTopHelpSeed.out()));
2496
+
2497
+ // unknown subcommand → exit 2 + usage
2498
+ var cUnk = _captureCtx();
2499
+ var rcUnk = await cli.main(["seed", "frobnicate"], cUnk);
2500
+ check("seed unknown sub: exit 2", rcUnk === 2);
2501
+ check("seed unknown sub: names the sub", /unknown subcommand 'frobnicate'/.test(cUnk.err()));
2502
+
2503
+ // run without --db → exit 2
2504
+ var cNoDb = _captureCtx();
2505
+ var rcNoDb = await cli.main(["seed", "run"], cNoDb);
2506
+ check("seed run: missing --db → exit 2", rcNoDb === 2);
2507
+ check("seed run: missing --db message", /--db <path> is required/.test(cNoDb.err()));
2508
+
2509
+ // --db present but bare (boolean flag, no value) → treated as missing
2510
+ var cDbBool = _captureCtx();
2511
+ var rcDbBool = await cli.main(["seed", "run", "--db"], cDbBool);
2512
+ check("seed run: --db with no value → exit 2", rcDbBool === 2);
2513
+ check("seed run: --db no value message", /--db <path> is required/.test(cDbBool.err()));
2514
+
2515
+ // --db present, --env missing → exit 2
2516
+ var dbPath = path.join(dir, "seed.db");
2517
+ var cNoEnv = _captureCtx();
2518
+ var rcNoEnv = await cli.main(["seed", "status", "--db", dbPath], cNoEnv);
2519
+ check("seed status: missing --env → exit 2", rcNoEnv === 2);
2520
+ check("seed status: missing --env message", /--env <name> is required/.test(cNoEnv.err()));
2521
+
2522
+ // bad db path (parent dir doesn't exist) → cannot open db, exit 1
2523
+ var cBadDb = _captureCtx();
2524
+ var rcBadDb = await cli.main(
2525
+ ["seed", "status", "--db", path.join(dir, "no-such-dir", "x.db"), "--env", "dev"],
2526
+ cBadDb);
2527
+ check("seed status: unopenable db → exit 1", rcBadDb === 1);
2528
+ check("seed status: cannot-open message", /cannot open db/.test(cBadDb.err()));
2529
+
2530
+ // =====================================================================
2531
+ // seed — status happy path against a local sqlite file + empty dir
2532
+ // =====================================================================
2533
+ var seedersDir = path.join(dir, "seeders-empty");
2534
+ var cStatus = _captureCtx();
2535
+ var rcStatus = await cli.main(
2536
+ ["seed", "status", "--db", dbPath, "--env", "dev", "--dir", seedersDir],
2537
+ cStatus);
2538
+ check("seed status: empty dir → exit 0", rcStatus === 0);
2539
+ check("seed status: prints env line", /env: dev/.test(cStatus.out()));
2540
+ check("seed status: 0 applied of 0 total", /applied: 0 \/ 0/.test(cStatus.out()));
2541
+ check("seed status: 0 pending", /pending: 0/.test(cStatus.out()));
2542
+
2543
+ // =====================================================================
2544
+ // dev — --grace-ms numeric validation (returns before spawning a child)
2545
+ // =====================================================================
2546
+ var cGraceNeg = _captureCtx();
2547
+ var rcGraceNeg = await cli.main(
2548
+ ["dev", "--command", "node", "--grace-ms", "-5"], cGraceNeg);
2549
+ check("dev --grace-ms negative → exit 2", rcGraceNeg === 2);
2550
+ check("dev --grace-ms negative message", /--grace-ms must be a non-negative number/.test(cGraceNeg.err()));
2551
+
2552
+ var cGraceNaN = _captureCtx();
2553
+ var rcGraceNaN = await cli.main(
2554
+ ["dev", "--command", "node", "--grace-ms", "not-a-number"], cGraceNaN);
2555
+ check("dev --grace-ms non-numeric → exit 2", rcGraceNaN === 2);
2556
+ check("dev --grace-ms non-numeric message", /--grace-ms must be a non-negative number/.test(cGraceNaN.err()));
2557
+
2558
+ // missing --command still exits 2 (guards the `dev` entry independent
2559
+ // of the grace-ms path above)
2560
+ var cDevNoCmd = _captureCtx();
2561
+ var rcDevNoCmd = await cli.main(["dev"], cDevNoCmd);
2562
+ check("dev: missing --command → exit 2", rcDevNoCmd === 2);
2563
+ check("dev: missing --command message", /--command <cmd> is required/.test(cDevNoCmd.err()));
2564
+
2565
+ // =====================================================================
2566
+ // dev — --ignore pattern refusal takes the standard exit-2 + stderr
2567
+ // path, never rejects main(). An over-length pattern, a ReDoS-shape
2568
+ // pattern guardRegex refuses, and a pattern RegExp() can't compile are
2569
+ // all operator-input validation failures: they must return exit 2 like
2570
+ // every other dev validation, not reject the awaited promise (which the
2571
+ // bin shim would surface as a stack-trace + exit 1).
2572
+ // =====================================================================
2573
+ async function _devIgnore(pattern) {
2574
+ var c = _captureCtx();
2575
+ var rejected = false;
2576
+ var rc;
2577
+ try {
2578
+ rc = await cli.main(["dev", "--command", "node", "--ignore", pattern], c);
2579
+ } catch (_e) {
2580
+ rejected = true;
2581
+ }
2582
+ return { rc: rc, rejected: rejected, err: c.err() };
2583
+ }
2584
+
2585
+ var igLong = await _devIgnore("a".repeat(300));
2586
+ check("dev --ignore over-length → never rejects main()", igLong.rejected === false);
2587
+ check("dev --ignore over-length → exit 2", igLong.rc === 2);
2588
+ check("dev --ignore over-length message", /exceeds max length/.test(igLong.err));
2589
+
2590
+ var igReDoS = await _devIgnore("(a+)+$");
2591
+ check("dev --ignore ReDoS-shape → never rejects main()", igReDoS.rejected === false);
2592
+ check("dev --ignore ReDoS-shape → exit 2", igReDoS.rc === 2);
2593
+ check("dev --ignore ReDoS-shape message", /refused by guardRegex/.test(igReDoS.err));
2594
+
2595
+ var igBadRe = await _devIgnore("(");
2596
+ check("dev --ignore uncompilable → never rejects main()", igBadRe.rejected === false);
2597
+ check("dev --ignore uncompilable → exit 2", igBadRe.rc === 2);
2598
+ check("dev --ignore uncompilable message", /--ignore pattern/.test(igBadRe.err));
2599
+
2600
+ // =====================================================================
2601
+ // api-snapshot — operator-supplied --module
2602
+ // =====================================================================
2603
+
2604
+ // capture from a non-existent module → cannot load, exit 1
2605
+ var cModBad = _captureCtx();
2606
+ var rcModBad = await cli.main(
2607
+ ["api-snapshot", "capture", "--module", path.join(dir, "no-module.js")], cModBad);
2608
+ check("api-snapshot capture bad --module → exit 1", rcModBad === 1);
2609
+ check("api-snapshot capture bad --module message",
2610
+ /cannot load module/.test(cModBad.err()));
2611
+
2612
+ // capture from a valid operator module → wrote snapshot, exit 0
2613
+ var customMod = path.join(dir, "custom-mod.js");
2614
+ fs.writeFileSync(customMod,
2615
+ "module.exports = { version: \"9.9.9\", greet: function greet() {} };");
2616
+ var customSnap = path.join(dir, "custom-snap.json");
2617
+ var cModOk = _captureCtx();
2618
+ var rcModOk = await cli.main(
2619
+ ["api-snapshot", "capture", "--file", customSnap, "--module", customMod], cModOk);
2620
+ check("api-snapshot capture custom --module → exit 0", rcModOk === 0);
2621
+ check("api-snapshot capture custom --module wrote file", fs.existsSync(customSnap));
2622
+ check("api-snapshot capture custom --module reports version",
2623
+ /frameworkVersion 9\.9\.9/.test(cModOk.out()));
2624
+
2625
+ // capture with a RELATIVE --module path → the `nodePath.resolve(ctx.cwd,
2626
+ // modulePath)` arm of _resolveTargetModule (the absolute-path arm is what
2627
+ // every other --module test exercises). ctx.cwd is the repo root, so a
2628
+ // repo-relative module path resolves.
2629
+ var relSnap = path.join(dir, "rel-snap.json");
2630
+ var cRel = _captureCtx();
2631
+ var rcRel = await cli.main(
2632
+ ["api-snapshot", "capture", "--file", relSnap, "--module", "lib/constants.js"], cRel);
2633
+ check("api-snapshot capture relative --module → exit 0", rcRel === 0);
2634
+ check("api-snapshot capture relative --module wrote file", fs.existsSync(relSnap));
2635
+
2636
+ // compare where the saved snapshot reads fine but capturing the CURRENT
2637
+ // surface fails (bad --module) → "cannot capture current surface", exit 1
2638
+ var cCmpBad = _captureCtx();
2639
+ var rcCmpBad = await cli.main(
2640
+ ["api-snapshot", "compare", "--file", customSnap, "--module", path.join(dir, "gone.js")],
2641
+ cCmpBad);
2642
+ check("api-snapshot compare uncapturable current → exit 1", rcCmpBad === 1);
2643
+ check("api-snapshot compare uncapturable current message",
2644
+ /cannot capture current surface/.test(cCmpBad.err()));
2645
+
2646
+ // =====================================================================
2647
+ // top-level dispatch — help routing edges
2648
+ // =====================================================================
2649
+
2650
+ // `<cmd> --help` synthesizes `help <cmd>` → that command's usage
2651
+ var cMigHelp = _captureCtx();
2652
+ var rcMigHelp = await cli.main(["migrate", "--help"], cMigHelp);
2653
+ check("migrate --help → exit 0", rcMigHelp === 0);
2654
+ check("migrate --help prints migrate usage", /Usage: blamejs migrate/.test(cMigHelp.out()));
2655
+
2656
+ // `-h` short flag takes the same route
2657
+ var cMigH = _captureCtx();
2658
+ var rcMigH = await cli.main(["migrate", "-h"], cMigH);
2659
+ check("migrate -h → exit 0", rcMigH === 0);
2660
+ check("migrate -h prints migrate usage", /Usage: blamejs migrate/.test(cMigH.out()));
2661
+
2662
+ // `help <unknown-topic>` falls through to the top-level usage
2663
+ var cBogus = _captureCtx();
2664
+ var rcBogus = await cli.main(["help", "totally-bogus-topic"], cBogus);
2665
+ check("help unknown-topic → exit 0", rcBogus === 0);
2666
+ check("help unknown-topic prints top usage", /blamejs <command>/.test(cBogus.out()));
2667
+ } finally {
2668
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_e) { /* best-effort */ }
2669
+ }
2670
+
2671
+ sectionRepeatableFlags();
2672
+ await sectionTopLevel();
2673
+ await sectionSeedRun();
2674
+ await sectionApiSnapshotBreaking();
2675
+ await sectionAudit();
2676
+ await sectionFileType();
2677
+ await sectionPositionalHelp();
2678
+ await sectionArgValidation();
2679
+ await sectionBackupRestoreValidation();
2680
+ await sectionBootedApiKey();
2681
+ await sectionBootedErase();
2682
+ await sectionBootedRetention();
2683
+ await sectionBootedMtls();
2684
+ await sectionBootedSecurity();
2685
+
2686
+ // Additional branch coverage for the remaining error / defensive / option-
2687
+ // default paths across every subcommand.
2688
+ await sectionMigrate();
2689
+ await sectionApiSnapshotEdges();
2690
+ await sectionAuditVerifyChainEdges();
2691
+ await sectionRestoreEdges();
2692
+ await sectionBackupEdges();
2693
+ await sectionMtlsEdges();
2694
+ await sectionSecurityEdges();
2695
+ await sectionConfigDrift();
2696
+ await sectionConfigDriftSidecar();
2697
+ await sectionVault();
2698
+ await sectionPassword();
2699
+ await sectionFileTypeEdges();
2700
+ await sectionRetentionEdges();
2701
+ await sectionEraseEdges();
2702
+ await sectionApiKeyEdges();
2703
+ await sectionBootedRawTable();
2704
+ await sectionTopUnknownCommand();
2705
+
2706
+ // Additional branch coverage: top-level dispatch defaults + per-command
2707
+ // help fan-out, adversarial migrate --steps, post-boot required-flag guards
2708
+ // (api-key / mtls), default-arg arms, and the operation-body + error paths
2709
+ // (audit ops / restore populated-list loops) the omit-one-flag tests miss.
2710
+ await sectionTopLevelDispatch2();
2711
+ await sectionMigrateMore();
2712
+ await sectionApiKeyMore();
2713
+ await sectionSecurityMore();
2714
+ await sectionCheapGuards();
2715
+ await sectionMtlsMore();
2716
+ await sectionRestoreMore();
2717
+ await sectionAuditOps();
2718
+ await sectionRetentionPreviewText();
2719
+
2720
+ // Operation-body success arms + error catches the omit-one-flag / cheap-guard
2721
+ // tests never reach: restore primitive-throws catches, api-snapshot capture
2722
+ // throwing post-load, backup extract catch, mtls init / issue operation
2723
+ // catches, security assert clean, config-drift tamper, password breach-check
2724
+ // opt wiring, retention per-row error summary, erase success against a sealed
2725
+ // schema, and the audit archive/export/verify/purge bodies on a live chain.
2726
+ await sectionRestoreCatches();
2727
+ await sectionApiSnapshotCaptureThrow();
2728
+ await sectionBackupExtractCatch();
2729
+ await sectionMtlsCorruptCA();
2730
+ await sectionSecurityAssertClean();
2731
+ await sectionConfigDriftTamper();
2732
+ await sectionPasswordBreachOpt();
2733
+ await sectionRetentionRowErrors();
2734
+ await sectionEraseSuccess();
2735
+ await sectionAuditSuccess();
2736
+ await sectionDevSupervisor();
2737
+ }
2738
+
2739
+ // The `dev` supervisor's run loop, driven in-process via the _dev / _onDevRunning
2740
+ // test seams: main() must await stop() to COMPLETION on shutdown (never resolve
2741
+ // on a polled flag while a child kill is still in flight), so the bin shim can't
2742
+ // process.exit() mid-teardown and orphan the app child.
2743
+ async function sectionDevSupervisor() {
2744
+ var startCalls = 0, stopCalls = 0;
2745
+ var stopResolve;
2746
+ var stopDone = new Promise(function (r) { stopResolve = r; });
2747
+ var fakeDev = {
2748
+ start: function () { startCalls += 1; return Promise.resolve(); },
2749
+ stop: function () { stopCalls += 1; return stopDone; },
2750
+ };
2751
+ var shutdownFn = null;
2752
+ var capturedOpts = null;
2753
+ var c = _captureCtx();
2754
+ c._dev = function (opts) { capturedOpts = opts; return fakeDev; };
2755
+ c._onDevRunning = function (fn) { shutdownFn = fn; };
2756
+
2757
+ // `-v` here is the VALUE of --arg (run `node -v`), NOT the top-level
2758
+ // version flag — it must reach the command, not be mistaken for --version.
2759
+ var runP = cli.main(["dev", "--command", "node", "--arg", "-v"], c);
2760
+ await helpers.waitUntil(function () { return shutdownFn !== null; }, {
2761
+ timeoutMs: 5000, label: "cli dev: supervisor started + running",
2762
+ });
2763
+ check("dev: start() called once", startCalls === 1);
2764
+ check("dev: -v as an --arg value reaches the command (not stripped as version)",
2765
+ !!capturedOpts && Array.isArray(capturedOpts.args) &&
2766
+ capturedOpts.args.length === 1 && capturedOpts.args[0] === "-v");
2767
+
2768
+ var resolved = false;
2769
+ runP.then(function () { resolved = true; });
2770
+ shutdownFn(); // trigger graceful shutdown → stop() (still pending)
2771
+ check("dev: stop() invoked on shutdown", stopCalls === 1);
2772
+
2773
+ // Longer than any plausible poll interval: on a fire-and-forget regression
2774
+ // main() would resolve here; the fix keeps it awaiting stop() completion.
2775
+ await helpers.passiveObserve(400, "cli dev: main awaits stop() completion, not a polled flag");
2776
+ check("dev: main does NOT resolve while stop() is pending", resolved === false);
2777
+
2778
+ stopResolve(); // stop() completes
2779
+ var rc = await runP;
2780
+ check("dev: main resolves 0 after stop() completes", rc === 0);
2781
+ }
2782
+
2783
+ module.exports = { run: run };
2784
+
2785
+ if (require.main === module) {
2786
+ run().then(
2787
+ function () { console.log("OK — " + helpers.getChecks() + " checks passed"); },
2788
+ function (e) { console.error("FAIL:", e.stack || e); process.exit(1); }
2789
+ );
2790
+ }