@codespar/cli 0.17.0 → 0.18.1

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 (482) hide show
  1. package/README.md +1 -0
  2. package/dist/surface.js +53 -1
  3. package/dist/surface.js.map +1 -1
  4. package/package.json +1 -1
  5. package/templates/bills-agent/README.md +1 -1
  6. package/templates/bills-agent/agents/bills-agent/AGENTS.md +1 -1
  7. package/templates/bills-agent/agents/bills-agent/CLAUDE.md +1 -1
  8. package/templates/bills-agent/agents/bills-agent/README.md +6 -5
  9. package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +5 -4
  10. package/templates/bills-agent/packages/agent-core/README.md +14 -1
  11. package/templates/bills-agent/packages/agent-core/package.json +1 -1
  12. package/templates/bills-agent/packages/agent-core/src/agent.ts +39 -1
  13. package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  14. package/templates/bills-agent/packages/agent-core/src/api/client.ts +28 -5
  15. package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  16. package/templates/bills-agent/packages/agent-core/src/api/rail.ts +62 -26
  17. package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  18. package/templates/bills-agent/packages/agent-core/src/approval.ts +14 -2
  19. package/templates/bills-agent/packages/agent-core/src/bundle.ts +35 -3
  20. package/templates/bills-agent/packages/agent-core/src/channels.ts +49 -3
  21. package/templates/bills-agent/packages/agent-core/src/check.ts +25 -2
  22. package/templates/bills-agent/packages/agent-core/src/engine.ts +235 -46
  23. package/templates/bills-agent/packages/agent-core/src/hash.ts +57 -1
  24. package/templates/bills-agent/packages/agent-core/src/index.ts +2 -0
  25. package/templates/bills-agent/packages/agent-core/src/quote.ts +50 -0
  26. package/templates/bills-agent/packages/agent-core/src/rail.ts +89 -4
  27. package/templates/bills-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  28. package/templates/bills-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  29. package/templates/bills-agent/packages/agent-core/src/revocation.ts +1 -1
  30. package/templates/bills-agent/packages/agent-core/src/state-machine.ts +10 -1
  31. package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  32. package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  33. package/templates/bills-agent/packages/agent-core/src/tools.ts +17 -6
  34. package/templates/bills-agent/packages/agent-core/src/types.ts +54 -1
  35. package/templates/bills-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  36. package/templates/bills-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  37. package/templates/bills-agent/packages/agent-core/test/composition.test.ts +204 -0
  38. package/templates/bills-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  39. package/templates/bills-agent/packages/agent-core/test/helpers.ts +4 -1
  40. package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  41. package/templates/bills-agent/packages/agent-core/test/quote.test.ts +198 -0
  42. package/templates/bills-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  43. package/templates/bills-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  44. package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +103 -3
  45. package/templates/bills-agent/packages/agent-runtime/README.md +12 -0
  46. package/templates/bills-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  47. package/templates/bills-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  48. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  49. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  50. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  51. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  52. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  53. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  54. package/templates/bills-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  55. package/templates/bills-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  56. package/templates/bills-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  57. package/templates/bills-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  58. package/templates/bills-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  59. package/templates/bills-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  60. package/templates/bills-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  61. package/templates/bills-agent/packages/agent-runtime/src/index.ts +1 -0
  62. package/templates/bills-agent/packages/agent-runtime/src/inspect.ts +5 -1
  63. package/templates/bills-agent/packages/agent-runtime/src/kit.ts +25 -2
  64. package/templates/bills-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  65. package/templates/bills-agent/packages/agent-runtime/src/setup.ts +13 -3
  66. package/templates/bills-agent/packages/agent-runtime/src/terminal.ts +127 -18
  67. package/templates/bills-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  68. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  69. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  70. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  71. package/templates/checkout-agent/LICENSE +21 -0
  72. package/templates/checkout-agent/README.md +28 -0
  73. package/templates/checkout-agent/_gitignore +10 -0
  74. package/templates/checkout-agent/agents/checkout-agent/.env.example +5 -0
  75. package/templates/checkout-agent/agents/checkout-agent/AGENTS.md +20 -0
  76. package/templates/checkout-agent/agents/checkout-agent/CLAUDE.md +20 -0
  77. package/templates/checkout-agent/agents/checkout-agent/README.md +94 -0
  78. package/templates/checkout-agent/agents/checkout-agent/SYSTEM_PROMPT.md +36 -0
  79. package/templates/checkout-agent/agents/checkout-agent/agent.yaml +30 -0
  80. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/README.md +28 -0
  81. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-beatriz.json +11 -0
  82. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina-retomada.json +10 -0
  83. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina.json +11 -0
  84. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/templates.json +49 -0
  85. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap-human.json +12 -0
  86. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.json +11 -0
  87. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +5 -0
  88. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.json +13 -0
  89. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.transcript.jsonl +4 -0
  90. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.json +26 -0
  91. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
  92. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.json +11 -0
  93. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.transcript.jsonl +5 -0
  94. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.json +12 -0
  95. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.transcript.jsonl +5 -0
  96. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.json +13 -0
  97. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.transcript.jsonl +5 -0
  98. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.json +15 -0
  99. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.transcript.jsonl +7 -0
  100. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.json +12 -0
  101. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.transcript.jsonl +5 -0
  102. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.json +13 -0
  103. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.transcript.jsonl +4 -0
  104. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.json +16 -0
  105. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.transcript.jsonl +9 -0
  106. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.json +15 -0
  107. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.transcript.jsonl +8 -0
  108. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/webhook-replay.json +11 -0
  109. package/templates/checkout-agent/agents/checkout-agent/evals/eval.yaml +11 -0
  110. package/templates/checkout-agent/agents/checkout-agent/guardrails.json +25 -0
  111. package/templates/checkout-agent/agents/checkout-agent/mandate.example.json +28 -0
  112. package/templates/checkout-agent/agents/checkout-agent/package.json +33 -0
  113. package/templates/checkout-agent/agents/checkout-agent/runbook.md +19 -0
  114. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.json +42 -0
  115. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +4 -0
  116. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.json +32 -0
  117. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.transcript.jsonl +4 -0
  118. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.json +61 -0
  119. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.transcript.jsonl +9 -0
  120. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.json +61 -0
  121. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.transcript.jsonl +9 -0
  122. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.json +36 -0
  123. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.transcript.jsonl +6 -0
  124. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.json +23 -0
  125. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.transcript.jsonl +4 -0
  126. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.json +72 -0
  127. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.transcript.jsonl +9 -0
  128. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.json +15 -0
  129. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.transcript.jsonl +6 -0
  130. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.json +23 -0
  131. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.transcript.jsonl +3 -0
  132. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.json +42 -0
  133. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.transcript.jsonl +12 -0
  134. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.json +14 -0
  135. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.transcript.jsonl +9 -0
  136. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.json +34 -0
  137. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.transcript.jsonl +7 -0
  138. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.json +33 -0
  139. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.transcript.jsonl +4 -0
  140. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.json +32 -0
  141. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.transcript.jsonl +4 -0
  142. package/templates/checkout-agent/agents/checkout-agent/src/catalog.ts +92 -0
  143. package/templates/checkout-agent/agents/checkout-agent/src/kit.ts +338 -0
  144. package/templates/checkout-agent/agents/checkout-agent/src/modules/bolepix-receivables.ts +179 -0
  145. package/templates/checkout-agent/agents/checkout-agent/src/modules/nfse-invoice.ts +374 -0
  146. package/templates/checkout-agent/agents/checkout-agent/src/modules/storefront-cart.ts +220 -0
  147. package/templates/checkout-agent/agents/checkout-agent/src/pricing.ts +297 -0
  148. package/templates/checkout-agent/agents/checkout-agent/test/adversarial.test.ts +117 -0
  149. package/templates/checkout-agent/agents/checkout-agent/test/check.test.ts +67 -0
  150. package/templates/checkout-agent/agents/checkout-agent/test/cli.test.ts +169 -0
  151. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/order-marina.transcript.jsonl +5 -0
  152. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/ordered-marina-awaiting-payer.transcript.jsonl +6 -0
  153. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/tap-falar-agora-marina.transcript.jsonl +2 -0
  154. package/templates/checkout-agent/agents/checkout-agent/test/nfse.test.ts +170 -0
  155. package/templates/checkout-agent/agents/checkout-agent/test/pricing.test.ts +138 -0
  156. package/templates/checkout-agent/agents/checkout-agent/test/scenarios.test.ts +113 -0
  157. package/templates/checkout-agent/agents/checkout-agent/test/whatsapp.test.ts +171 -0
  158. package/templates/checkout-agent/agents/checkout-agent/tools.json +68 -0
  159. package/templates/checkout-agent/agents/checkout-agent/tsconfig.json +10 -0
  160. package/templates/checkout-agent/package.json +38 -0
  161. package/templates/checkout-agent/packages/agent-core/README.md +34 -0
  162. package/templates/checkout-agent/packages/agent-core/package.json +24 -0
  163. package/templates/checkout-agent/packages/agent-core/src/agent.ts +163 -0
  164. package/templates/checkout-agent/packages/agent-core/src/api/charge-rail.ts +195 -0
  165. package/templates/checkout-agent/packages/agent-core/src/api/client.ts +76 -0
  166. package/templates/checkout-agent/packages/agent-core/src/api/mandate-status.ts +74 -0
  167. package/templates/checkout-agent/packages/agent-core/src/api/rail.ts +153 -0
  168. package/templates/checkout-agent/packages/agent-core/src/api/sandbox-payer.ts +43 -0
  169. package/templates/checkout-agent/packages/agent-core/src/approval.ts +135 -0
  170. package/templates/checkout-agent/packages/agent-core/src/bundle.ts +191 -0
  171. package/templates/checkout-agent/packages/agent-core/src/channels.ts +186 -0
  172. package/templates/checkout-agent/packages/agent-core/src/check.ts +276 -0
  173. package/templates/checkout-agent/packages/agent-core/src/clock.ts +42 -0
  174. package/templates/checkout-agent/packages/agent-core/src/engine.ts +1074 -0
  175. package/templates/checkout-agent/packages/agent-core/src/escalate.ts +85 -0
  176. package/templates/checkout-agent/packages/agent-core/src/events.ts +40 -0
  177. package/templates/checkout-agent/packages/agent-core/src/guardrails.ts +45 -0
  178. package/templates/checkout-agent/packages/agent-core/src/hash.ts +118 -0
  179. package/templates/checkout-agent/packages/agent-core/src/ids.ts +10 -0
  180. package/templates/checkout-agent/packages/agent-core/src/index.ts +34 -0
  181. package/templates/checkout-agent/packages/agent-core/src/mandate.ts +100 -0
  182. package/templates/checkout-agent/packages/agent-core/src/manifest.ts +76 -0
  183. package/templates/checkout-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
  184. package/templates/checkout-agent/packages/agent-core/src/providers/replay.ts +63 -0
  185. package/templates/checkout-agent/packages/agent-core/src/providers/types.ts +39 -0
  186. package/templates/checkout-agent/packages/agent-core/src/quote.ts +50 -0
  187. package/templates/checkout-agent/packages/agent-core/src/rail.ts +186 -0
  188. package/templates/checkout-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  189. package/templates/checkout-agent/packages/agent-core/src/receipt-verification.ts +577 -0
  190. package/templates/checkout-agent/packages/agent-core/src/revocation.ts +30 -0
  191. package/templates/checkout-agent/packages/agent-core/src/secrets.ts +30 -0
  192. package/templates/checkout-agent/packages/agent-core/src/state/store.ts +307 -0
  193. package/templates/checkout-agent/packages/agent-core/src/state-machine.ts +157 -0
  194. package/templates/checkout-agent/packages/agent-core/src/stubs/charge-rail.ts +195 -0
  195. package/templates/checkout-agent/packages/agent-core/src/stubs/mandate-status.ts +58 -0
  196. package/templates/checkout-agent/packages/agent-core/src/stubs/rail.ts +209 -0
  197. package/templates/checkout-agent/packages/agent-core/src/tools.ts +66 -0
  198. package/templates/checkout-agent/packages/agent-core/src/types.ts +187 -0
  199. package/templates/checkout-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  200. package/templates/checkout-agent/packages/agent-core/test/approval.test.ts +117 -0
  201. package/templates/checkout-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  202. package/templates/checkout-agent/packages/agent-core/test/bundle.test.ts +85 -0
  203. package/templates/checkout-agent/packages/agent-core/test/clock.test.ts +25 -0
  204. package/templates/checkout-agent/packages/agent-core/test/composition.test.ts +204 -0
  205. package/templates/checkout-agent/packages/agent-core/test/engine.test.ts +596 -0
  206. package/templates/checkout-agent/packages/agent-core/test/escalate.test.ts +42 -0
  207. package/templates/checkout-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  208. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
  209. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
  210. package/templates/checkout-agent/packages/agent-core/test/helpers.ts +142 -0
  211. package/templates/checkout-agent/packages/agent-core/test/mandate-status.test.ts +229 -0
  212. package/templates/checkout-agent/packages/agent-core/test/manifest.test.ts +45 -0
  213. package/templates/checkout-agent/packages/agent-core/test/quote.test.ts +198 -0
  214. package/templates/checkout-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  215. package/templates/checkout-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  216. package/templates/checkout-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
  217. package/templates/checkout-agent/packages/agent-core/test/receivables.test.ts +416 -0
  218. package/templates/checkout-agent/packages/agent-core/test/secrets.test.ts +28 -0
  219. package/templates/checkout-agent/packages/agent-core/test/state-machine.test.ts +67 -0
  220. package/templates/checkout-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
  221. package/templates/checkout-agent/packages/agent-core/tsconfig.json +10 -0
  222. package/templates/checkout-agent/packages/agent-runtime/README.md +80 -0
  223. package/templates/checkout-agent/packages/agent-runtime/bin.mjs +32 -0
  224. package/templates/checkout-agent/packages/agent-runtime/package.json +23 -0
  225. package/templates/checkout-agent/packages/agent-runtime/src/adversarial.ts +199 -0
  226. package/templates/checkout-agent/packages/agent-runtime/src/agent.ts +68 -0
  227. package/templates/checkout-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
  228. package/templates/checkout-agent/packages/agent-runtime/src/channels/index.ts +72 -0
  229. package/templates/checkout-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
  230. package/templates/checkout-agent/packages/agent-runtime/src/channels/types.ts +173 -0
  231. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +459 -0
  232. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +216 -0
  233. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
  234. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +350 -0
  235. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +180 -0
  236. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
  237. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +120 -0
  238. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +127 -0
  239. package/templates/checkout-agent/packages/agent-runtime/src/cli.ts +98 -0
  240. package/templates/checkout-agent/packages/agent-runtime/src/commands/check.ts +15 -0
  241. package/templates/checkout-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
  242. package/templates/checkout-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
  243. package/templates/checkout-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
  244. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +367 -0
  245. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll.ts +145 -0
  246. package/templates/checkout-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
  247. package/templates/checkout-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
  248. package/templates/checkout-agent/packages/agent-runtime/src/commands/resume.ts +49 -0
  249. package/templates/checkout-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +134 -0
  250. package/templates/checkout-agent/packages/agent-runtime/src/commands/start.ts +248 -0
  251. package/templates/checkout-agent/packages/agent-runtime/src/commands/verify.ts +330 -0
  252. package/templates/checkout-agent/packages/agent-runtime/src/commands/webhook.ts +40 -0
  253. package/templates/checkout-agent/packages/agent-runtime/src/default-kit.ts +83 -0
  254. package/templates/checkout-agent/packages/agent-runtime/src/index.ts +13 -0
  255. package/templates/checkout-agent/packages/agent-runtime/src/inspect.ts +801 -0
  256. package/templates/checkout-agent/packages/agent-runtime/src/kit.ts +229 -0
  257. package/templates/checkout-agent/packages/agent-runtime/src/poll.ts +105 -0
  258. package/templates/checkout-agent/packages/agent-runtime/src/scenarios.ts +274 -0
  259. package/templates/checkout-agent/packages/agent-runtime/src/setup.ts +239 -0
  260. package/templates/checkout-agent/packages/agent-runtime/src/terminal.ts +263 -0
  261. package/templates/checkout-agent/packages/agent-runtime/src/webhook.ts +131 -0
  262. package/templates/checkout-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
  263. package/templates/checkout-agent/packages/agent-runtime/test/verify-cli.test.ts +329 -0
  264. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +277 -0
  265. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
  266. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
  267. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-session.test.ts +167 -0
  268. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  269. package/templates/checkout-agent/packages/agent-runtime/tsconfig.json +9 -0
  270. package/templates/checkout-agent/scripts/whatsapp-emulator.mjs +84 -0
  271. package/templates/checkout-agent/tsconfig.base.json +16 -0
  272. package/templates/checkout-agent/vitest.config.ts +12 -0
  273. package/templates/collections-agent/README.md +6 -1
  274. package/templates/collections-agent/agents/collections-agent/README.md +12 -8
  275. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/README.md +8 -0
  276. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/acordo-1042-retomada.json +10 -0
  277. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/templates.json +27 -1
  278. package/templates/collections-agent/agents/collections-agent/src/kit.ts +2 -0
  279. package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +12 -2
  280. package/templates/collections-agent/agents/collections-agent/test/ambiguous-charge.test.ts +55 -0
  281. package/templates/collections-agent/agents/collections-agent/test/fixtures/tap-emitir-nova-1042.transcript.jsonl +2 -0
  282. package/templates/collections-agent/agents/collections-agent/test/whatsapp-poll.test.ts +12 -6
  283. package/templates/collections-agent/agents/collections-agent/test/whatsapp-status-replies.test.ts +148 -0
  284. package/templates/collections-agent/agents/collections-agent/test/whatsapp.test.ts +5 -0
  285. package/templates/collections-agent/package.json +1 -0
  286. package/templates/collections-agent/packages/agent-core/README.md +14 -1
  287. package/templates/collections-agent/packages/agent-core/package.json +1 -1
  288. package/templates/collections-agent/packages/agent-core/src/agent.ts +39 -1
  289. package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  290. package/templates/collections-agent/packages/agent-core/src/api/client.ts +28 -5
  291. package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  292. package/templates/collections-agent/packages/agent-core/src/api/rail.ts +62 -26
  293. package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  294. package/templates/collections-agent/packages/agent-core/src/approval.ts +14 -2
  295. package/templates/collections-agent/packages/agent-core/src/bundle.ts +35 -3
  296. package/templates/collections-agent/packages/agent-core/src/channels.ts +49 -3
  297. package/templates/collections-agent/packages/agent-core/src/check.ts +25 -2
  298. package/templates/collections-agent/packages/agent-core/src/engine.ts +235 -46
  299. package/templates/collections-agent/packages/agent-core/src/hash.ts +57 -1
  300. package/templates/collections-agent/packages/agent-core/src/index.ts +2 -0
  301. package/templates/collections-agent/packages/agent-core/src/quote.ts +50 -0
  302. package/templates/collections-agent/packages/agent-core/src/rail.ts +89 -4
  303. package/templates/collections-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  304. package/templates/collections-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  305. package/templates/collections-agent/packages/agent-core/src/revocation.ts +1 -1
  306. package/templates/collections-agent/packages/agent-core/src/state-machine.ts +10 -1
  307. package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  308. package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  309. package/templates/collections-agent/packages/agent-core/src/tools.ts +17 -6
  310. package/templates/collections-agent/packages/agent-core/src/types.ts +54 -1
  311. package/templates/collections-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  312. package/templates/collections-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  313. package/templates/collections-agent/packages/agent-core/test/composition.test.ts +204 -0
  314. package/templates/collections-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  315. package/templates/collections-agent/packages/agent-core/test/helpers.ts +4 -1
  316. package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  317. package/templates/collections-agent/packages/agent-core/test/quote.test.ts +198 -0
  318. package/templates/collections-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  319. package/templates/collections-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  320. package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +103 -3
  321. package/templates/collections-agent/packages/agent-runtime/README.md +12 -0
  322. package/templates/collections-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  323. package/templates/collections-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  324. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  325. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  326. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  327. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  328. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  329. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  330. package/templates/collections-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  331. package/templates/collections-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  332. package/templates/collections-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  333. package/templates/collections-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  334. package/templates/collections-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  335. package/templates/collections-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  336. package/templates/collections-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  337. package/templates/collections-agent/packages/agent-runtime/src/index.ts +1 -0
  338. package/templates/collections-agent/packages/agent-runtime/src/inspect.ts +5 -1
  339. package/templates/collections-agent/packages/agent-runtime/src/kit.ts +25 -2
  340. package/templates/collections-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  341. package/templates/collections-agent/packages/agent-runtime/src/setup.ts +13 -3
  342. package/templates/collections-agent/packages/agent-runtime/src/terminal.ts +127 -18
  343. package/templates/collections-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  344. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  345. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  346. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  347. package/templates/collections-agent/scripts/whatsapp-emulator.mjs +84 -0
  348. package/templates/hello-agent/README.md +1 -1
  349. package/templates/hello-agent/packages/agent-core/README.md +14 -1
  350. package/templates/hello-agent/packages/agent-core/package.json +1 -1
  351. package/templates/hello-agent/packages/agent-core/src/agent.ts +39 -1
  352. package/templates/hello-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  353. package/templates/hello-agent/packages/agent-core/src/api/client.ts +28 -5
  354. package/templates/hello-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  355. package/templates/hello-agent/packages/agent-core/src/api/rail.ts +62 -26
  356. package/templates/hello-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  357. package/templates/hello-agent/packages/agent-core/src/approval.ts +14 -2
  358. package/templates/hello-agent/packages/agent-core/src/bundle.ts +35 -3
  359. package/templates/hello-agent/packages/agent-core/src/channels.ts +49 -3
  360. package/templates/hello-agent/packages/agent-core/src/check.ts +25 -2
  361. package/templates/hello-agent/packages/agent-core/src/engine.ts +235 -46
  362. package/templates/hello-agent/packages/agent-core/src/hash.ts +57 -1
  363. package/templates/hello-agent/packages/agent-core/src/index.ts +2 -0
  364. package/templates/hello-agent/packages/agent-core/src/quote.ts +50 -0
  365. package/templates/hello-agent/packages/agent-core/src/rail.ts +89 -4
  366. package/templates/hello-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  367. package/templates/hello-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  368. package/templates/hello-agent/packages/agent-core/src/revocation.ts +1 -1
  369. package/templates/hello-agent/packages/agent-core/src/state-machine.ts +10 -1
  370. package/templates/hello-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  371. package/templates/hello-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  372. package/templates/hello-agent/packages/agent-core/src/tools.ts +17 -6
  373. package/templates/hello-agent/packages/agent-core/src/types.ts +54 -1
  374. package/templates/hello-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  375. package/templates/hello-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  376. package/templates/hello-agent/packages/agent-core/test/composition.test.ts +204 -0
  377. package/templates/hello-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  378. package/templates/hello-agent/packages/agent-core/test/helpers.ts +4 -1
  379. package/templates/hello-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  380. package/templates/hello-agent/packages/agent-core/test/quote.test.ts +198 -0
  381. package/templates/hello-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  382. package/templates/hello-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  383. package/templates/hello-agent/packages/agent-core/test/receivables.test.ts +103 -3
  384. package/templates/hello-agent/packages/agent-runtime/README.md +12 -0
  385. package/templates/hello-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  386. package/templates/hello-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  387. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  388. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  389. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  390. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  391. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  392. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  393. package/templates/hello-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  394. package/templates/hello-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  395. package/templates/hello-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  396. package/templates/hello-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  397. package/templates/hello-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  398. package/templates/hello-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  399. package/templates/hello-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  400. package/templates/hello-agent/packages/agent-runtime/src/index.ts +1 -0
  401. package/templates/hello-agent/packages/agent-runtime/src/inspect.ts +5 -1
  402. package/templates/hello-agent/packages/agent-runtime/src/kit.ts +25 -2
  403. package/templates/hello-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  404. package/templates/hello-agent/packages/agent-runtime/src/setup.ts +13 -3
  405. package/templates/hello-agent/packages/agent-runtime/src/terminal.ts +127 -18
  406. package/templates/hello-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  407. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  408. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  409. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  410. package/templates/kits.lock.json +29 -6
  411. package/templates/supplier-payments-agent/README.md +1 -1
  412. package/templates/supplier-payments-agent/agents/supplier-payments-agent/AGENTS.md +1 -1
  413. package/templates/supplier-payments-agent/agents/supplier-payments-agent/CLAUDE.md +1 -1
  414. package/templates/supplier-payments-agent/agents/supplier-payments-agent/README.md +11 -9
  415. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/kit.ts +1 -1
  416. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/modules/batch-payout.ts +67 -20
  417. package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/batch.test.ts +504 -5
  418. package/templates/supplier-payments-agent/packages/agent-core/README.md +14 -1
  419. package/templates/supplier-payments-agent/packages/agent-core/package.json +1 -1
  420. package/templates/supplier-payments-agent/packages/agent-core/src/agent.ts +39 -1
  421. package/templates/supplier-payments-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  422. package/templates/supplier-payments-agent/packages/agent-core/src/api/client.ts +28 -5
  423. package/templates/supplier-payments-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  424. package/templates/supplier-payments-agent/packages/agent-core/src/api/rail.ts +62 -26
  425. package/templates/supplier-payments-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  426. package/templates/supplier-payments-agent/packages/agent-core/src/approval.ts +14 -2
  427. package/templates/supplier-payments-agent/packages/agent-core/src/bundle.ts +35 -3
  428. package/templates/supplier-payments-agent/packages/agent-core/src/channels.ts +49 -3
  429. package/templates/supplier-payments-agent/packages/agent-core/src/check.ts +25 -2
  430. package/templates/supplier-payments-agent/packages/agent-core/src/engine.ts +235 -46
  431. package/templates/supplier-payments-agent/packages/agent-core/src/hash.ts +57 -1
  432. package/templates/supplier-payments-agent/packages/agent-core/src/index.ts +2 -0
  433. package/templates/supplier-payments-agent/packages/agent-core/src/quote.ts +50 -0
  434. package/templates/supplier-payments-agent/packages/agent-core/src/rail.ts +89 -4
  435. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  436. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  437. package/templates/supplier-payments-agent/packages/agent-core/src/revocation.ts +1 -1
  438. package/templates/supplier-payments-agent/packages/agent-core/src/state-machine.ts +10 -1
  439. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  440. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  441. package/templates/supplier-payments-agent/packages/agent-core/src/tools.ts +17 -6
  442. package/templates/supplier-payments-agent/packages/agent-core/src/types.ts +54 -1
  443. package/templates/supplier-payments-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  444. package/templates/supplier-payments-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  445. package/templates/supplier-payments-agent/packages/agent-core/test/composition.test.ts +204 -0
  446. package/templates/supplier-payments-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  447. package/templates/supplier-payments-agent/packages/agent-core/test/helpers.ts +4 -1
  448. package/templates/supplier-payments-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  449. package/templates/supplier-payments-agent/packages/agent-core/test/quote.test.ts +198 -0
  450. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  451. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  452. package/templates/supplier-payments-agent/packages/agent-core/test/receivables.test.ts +103 -3
  453. package/templates/supplier-payments-agent/packages/agent-runtime/README.md +12 -0
  454. package/templates/supplier-payments-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  455. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  456. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  457. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  458. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  459. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  460. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  461. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  462. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  463. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  464. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  465. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  466. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  467. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  468. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  469. package/templates/supplier-payments-agent/packages/agent-runtime/src/index.ts +1 -0
  470. package/templates/supplier-payments-agent/packages/agent-runtime/src/inspect.ts +5 -1
  471. package/templates/supplier-payments-agent/packages/agent-runtime/src/kit.ts +25 -2
  472. package/templates/supplier-payments-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  473. package/templates/supplier-payments-agent/packages/agent-runtime/src/setup.ts +13 -3
  474. package/templates/supplier-payments-agent/packages/agent-runtime/src/terminal.ts +127 -18
  475. package/templates/supplier-payments-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  476. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  477. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  478. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  479. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  480. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  481. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  482. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
@@ -0,0 +1,329 @@
1
+ /**
2
+ * `codespar-agent verify` at the process boundary: the contracts a person and
3
+ * a script depend on.
4
+ *
5
+ * It runs from a temporary directory with no `agent.yaml` anywhere above it
6
+ * and no key in the environment, which is the whole claim of wave 5 — the
7
+ * verifier is someone who has the receipt file and nothing else. The key set
8
+ * is a local copy, so no test opens a socket.
9
+ */
10
+ import { spawn, spawnSync } from "node:child_process";
11
+ import { createHash, generateKeyPairSync, sign as signDetached } from "node:crypto";
12
+ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { createServer, type Server } from "node:http";
14
+ import type { AddressInfo } from "node:net";
15
+ import { tmpdir } from "node:os";
16
+ import { join, resolve } from "node:path";
17
+ import { canonicalize, itemsHash, maskPayee, type ExecutionItem } from "@codespar/agent-core";
18
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
19
+
20
+ const BIN = resolve(import.meta.dirname, "../bin.mjs");
21
+ const KID = "did:web:id.codespar.dev#1";
22
+ const CHAIN = "9f2c4a1b6d0e8f3a5c7b9d1e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f01";
23
+ const RECEIPT_ID = "rcpt_V1StGXR8Z5jdHi6BmyT0sw";
24
+
25
+ function fixture(over: Record<string, unknown> = {}) {
26
+ const dir = mkdtempSync(join(tmpdir(), "verify-cli-"));
27
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
28
+ const x = Buffer.from(publicKey.export({ format: "der", type: "spki" }).subarray(-32)).toString("base64url");
29
+ const signature = signDetached(null, Buffer.from(`codespar-receipt:v1:${RECEIPT_ID}:${CHAIN}`, "utf8"), privateKey).toString("base64url");
30
+ const receipt = {
31
+ receipt_id: RECEIPT_ID,
32
+ state: "paid",
33
+ mandate: { id: "mnd_1" },
34
+ payment: { amount_minor: 12345, payee: "es***@exemplo.com.br", attempt_id: "att_1", money_moved: false, sandbox: true, at: "2026-09-24T12:00:00.000Z" },
35
+ chain: CHAIN,
36
+ receipt_sig: "hmac-half",
37
+ receipt_sig_ed25519: signature,
38
+ receipt_sig_kid: KID,
39
+ ...over,
40
+ };
41
+ const keys = { issuer: "did:web:id.codespar.dev", algorithm: "Ed25519", signing_string: "codespar-receipt:v1:<receipt_id>:<chain>", keys: [{ kid: KID, kty: "OKP", crv: "Ed25519", x, alg: "EdDSA", use: "sig", status: "active", created_at: "2026-09-01T00:00:00.000Z", retired_at: null }] };
42
+ const receiptPath = join(dir, "receipt.json");
43
+ const keysPath = join(dir, "codespar-receipt-keys.json");
44
+ writeFileSync(receiptPath, JSON.stringify(receipt, null, 2) + "\n");
45
+ writeFileSync(keysPath, JSON.stringify(keys, null, 2) + "\n");
46
+ return { dir, receiptPath, keysPath };
47
+ }
48
+
49
+ function run(args: string[], cwd: string) {
50
+ const result = spawnSync(process.execPath, [BIN, "verify", ...args], {
51
+ cwd,
52
+ // No CodeSpar key, no model key: a verifier holds nothing.
53
+ env: { ...process.env, CODESPAR_API_KEY: "", ANTHROPIC_API_KEY: "" },
54
+ encoding: "utf8",
55
+ timeout: 60_000,
56
+ });
57
+ return { code: result.status, stdout: result.stdout, stderr: result.stderr };
58
+ }
59
+
60
+ describe("codespar-agent verify", () => {
61
+ it("checks a run's copy from a directory with no agent and no key, and says it proves the signature only (exit 7)", () => {
62
+ const { dir, receiptPath, keysPath } = fixture();
63
+ const out = run([receiptPath, "--keys", keysPath], dir);
64
+ expect(out.code).toBe(7);
65
+ expect(out.stdout).toContain("SIGNATURE_ONLY");
66
+ expect(out.stdout).toContain(RECEIPT_ID);
67
+ expect(out.stdout).toContain("read_required");
68
+ expect(out.stderr).toContain("carries CodeSpar's signature");
69
+ expect(out.stderr).toContain("--from-api");
70
+ });
71
+
72
+ it("puts valid JSON and nothing else on stdout with --json; the sentence goes to stderr", () => {
73
+ const { dir, receiptPath, keysPath } = fixture();
74
+ const out = run([receiptPath, "--keys", keysPath, "--json"], dir);
75
+ const lines = out.stdout.split("\n").filter(Boolean);
76
+ expect(lines).toHaveLength(1);
77
+ const report = JSON.parse(lines[0]!) as { verdict: string; kid: string; signing_string: string; message: string };
78
+ expect(report.verdict).toBe("signature_only");
79
+ expect(report.kid).toBe(KID);
80
+ expect(report.signing_string).toBe(`codespar-receipt:v1:${RECEIPT_ID}:${CHAIN}`);
81
+ expect(out.stderr).toContain(report.message);
82
+ });
83
+
84
+ it("exits 1 and says so on a receipt that does not match its signature", () => {
85
+ const { dir, receiptPath, keysPath } = fixture({ chain: CHAIN.replace(/^9/, "8") });
86
+ const out = run([receiptPath, "--keys", keysPath], dir);
87
+ expect(out.code).toBe(1);
88
+ expect(out.stdout).toContain("TAMPERED");
89
+ });
90
+
91
+ it("exits 3 on a receipt sealed before Ed25519, and calls it what it is", () => {
92
+ const { dir, receiptPath, keysPath } = fixture({ receipt_sig_ed25519: null, receipt_sig_kid: null });
93
+ const out = run([receiptPath, "--keys", keysPath], dir);
94
+ expect(out.code).toBe(3);
95
+ expect(out.stdout).toContain("UNSIGNED");
96
+ expect(out.stderr).toContain("never will");
97
+ });
98
+
99
+ it("exits 4 when the key set does not publish the kid the receipt names", () => {
100
+ const { dir, receiptPath, keysPath } = fixture({ receipt_sig_kid: "did:web:id.codespar.dev#9" });
101
+ const out = run([receiptPath, "--keys", keysPath], dir);
102
+ expect(out.code).toBe(4);
103
+ expect(out.stdout).toContain("UNKNOWN_KEY");
104
+ });
105
+
106
+ it("exits 5 when the key set cannot be read: unknown, never invalid", () => {
107
+ const { dir, receiptPath } = fixture();
108
+ const empty = join(dir, "not-a-key-set.json");
109
+ writeFileSync(empty, '{"error":"not found"}\n');
110
+ const out = run([receiptPath, "--keys", empty], dir);
111
+ expect(out.code).toBe(5);
112
+ expect(out.stdout).toContain("UNREACHABLE");
113
+ });
114
+
115
+ it("exits 6 rather than calling an arbitrary JSON file an unsigned receipt", () => {
116
+ const { dir, keysPath } = fixture();
117
+ const other = join(dir, "something-else.json");
118
+ writeFileSync(other, '{"hello":"world"}\n');
119
+ const out = run([other, "--keys", keysPath], dir);
120
+ expect(out.code).toBe(6);
121
+ expect(out.stdout).toContain("MALFORMED");
122
+ });
123
+
124
+ it("refuses its arguments with 2, the way the other commands do", () => {
125
+ const { dir, receiptPath, keysPath } = fixture();
126
+ expect(run([], dir).code).toBe(2);
127
+ expect(run([receiptPath, "--keys", keysPath, "--url", "https://example.invalid/k.json"], dir).code).toBe(2);
128
+ expect(run([receiptPath, "--keys"], dir).code).toBe(2);
129
+ expect(run([join(dir, "missing.json"), "--keys", keysPath], dir).code).toBe(2);
130
+ expect(run(["--help"], dir).code).toBe(0);
131
+ });
132
+ });
133
+
134
+ /*
135
+ * The body and the approval (OPEN_QUESTIONS §3, §47). A v4 receipt read, its
136
+ * chain written out link by link and signed with a key generated here, under
137
+ * the recipe production published on 2026-09-26.
138
+ */
139
+ const PUBLISHED = JSON.parse(readFileSync(resolve(import.meta.dirname, "../../agent-core/test/fixtures/production-receipt-keys.json"), "utf8")) as Record<string, unknown>;
140
+ const V4_KID = "did:web:id.codespar.dev#production-2";
141
+ const PAYEE = "escola@exemplo.com.br";
142
+ const ITEMS: ExecutionItem[] = [{ alias: "escola", beneficiary: "Escola Aurora", payee: PAYEE, amount: 185000, currency: "BRL", description: "outubro" }];
143
+ const sha256 = (v: string) => createHash("sha256").update(v, "utf8").digest("hex");
144
+
145
+ function v4Fixture(sealedItemsHash = itemsHash(ITEMS)) {
146
+ const dir = mkdtempSync(join(tmpdir(), "verify-cli-v4-"));
147
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
148
+ const x = Buffer.from(publicKey.export({ format: "der", type: "spki" }).subarray(-32)).toString("base64url");
149
+ const mandate = { id: "cm_test_1", nonce: "nonce-1", scope: "contas do mes", currency: "BRL", sig_sha256: sha256("s".repeat(64)) };
150
+ const quote = { seller: "Escola Aurora", resource: "outubro", price_minor: 185000, payee: PAYEE, session_id: null, sig: "q".repeat(64), at: "2026-09-26T18:00:00.000Z" };
151
+ const approval = { items_hash: sealedItemsHash, batch_hash: null };
152
+ const payment = { rail: "pix-consent", provider: "pix", tx_id: "tx_1", amount_minor: 185000, amount_atomic: null, sandbox: true, attempt_id: "att_1_0", money_moved: false, at: "2026-09-26T18:00:01.000Z" };
153
+ const chain = sha256(
154
+ canonicalize({
155
+ v: 4,
156
+ links: [
157
+ mandate,
158
+ quote,
159
+ approval,
160
+ { rail: payment.rail, provider: payment.provider, tx_id: payment.tx_id, amount_minor: payment.amount_minor, sandbox: true, attempt_id: payment.attempt_id, money_moved: false, at: payment.at },
161
+ ],
162
+ }),
163
+ );
164
+ const signature = signDetached(null, Buffer.from(`codespar-receipt:v1:${RECEIPT_ID}:${chain}`, "utf8"), privateKey).toString("base64url");
165
+ const read = { receipt_id: RECEIPT_ID, state: "paid", chain_version: 4, mandate, quote, approval, payment, delivery: null, chain, receipt_sig: "hmac-not-read-here", receipt_sig_ed25519: signature, receipt_sig_kid: V4_KID, exceptions: [] };
166
+ const keys = { ...PUBLISHED, keys: [{ kid: V4_KID, kty: "OKP", crv: "Ed25519", x, alg: "EdDSA", use: "sig", status: "active" }] };
167
+ const artifact = { approval_id: "apr_1", execution_id: "exe_1", items: ITEMS, items_hash: itemsHash(ITEMS), approved_at: "2026-09-26T18:00:00.000Z" };
168
+
169
+ // A run's bundle, as the engine writes it: the masked copy naming its artifact, and approval.json beside receipts/.
170
+ const run = join(dir, "runs", "run_1");
171
+ mkdirSync(join(run, "receipts"), { recursive: true });
172
+ const copy = {
173
+ receipt_id: RECEIPT_ID,
174
+ state: "paid",
175
+ mandate: { id: mandate.id },
176
+ payment: { amount_minor: 185000, payee: maskPayee(PAYEE), attempt_id: payment.attempt_id, money_moved: false, sandbox: true, at: payment.at },
177
+ chain,
178
+ receipt_sig: "hmac-not-read-here",
179
+ receipt_sig_ed25519: signature,
180
+ receipt_sig_kid: V4_KID,
181
+ actor: { type: "agent", agent: "bills-agent@0.1.0", on_behalf_of: "usr_demo" },
182
+ approval_id: artifact.approval_id,
183
+ };
184
+ const paths = {
185
+ read: join(dir, "receipt-read.json"),
186
+ keys: join(dir, "codespar-receipt-keys.json"),
187
+ artifact: join(dir, "approval.json"),
188
+ copy: join(run, "receipts", `${RECEIPT_ID}.json`),
189
+ runApprovals: join(run, "approval.json"),
190
+ };
191
+ writeFileSync(paths.read, JSON.stringify(read, null, 2) + "\n");
192
+ writeFileSync(paths.keys, JSON.stringify(keys, null, 2) + "\n");
193
+ writeFileSync(paths.artifact, JSON.stringify([artifact], null, 2) + "\n");
194
+ writeFileSync(paths.copy, JSON.stringify(copy, null, 2) + "\n");
195
+ writeFileSync(paths.runApprovals, JSON.stringify([artifact], null, 2) + "\n");
196
+ return { dir, read, keys, copy, artifact, paths };
197
+ }
198
+
199
+ describe("codespar-agent verify, the body and the approval of a v4 receipt read", () => {
200
+ it("exits 0 when the chain recomputes from the read and the sealed approval is the artifact's", () => {
201
+ const f = v4Fixture();
202
+ const out = run([f.paths.read, "--keys", f.paths.keys, "--approval", f.paths.artifact], f.dir);
203
+ expect(out.code).toBe(0);
204
+ expect(out.stdout).toContain("VERIFIED");
205
+ expect(out.stdout).toContain("body recomputed (chain v4)");
206
+ expect(out.stdout).toContain("approval matched");
207
+ expect(out.stderr).toContain("paid against the list artifact apr_1 approved");
208
+ });
209
+
210
+ it("exits 9, approval_mismatch, when the receipt sealed another list than the artifact's", () => {
211
+ const f = v4Fixture(`sha256:${"9".repeat(64)}`);
212
+ const out = run([f.paths.read, "--keys", f.paths.keys, "--approval", f.paths.artifact, "--json"], f.dir);
213
+ expect(out.code).toBe(9);
214
+ const report = JSON.parse(out.stdout) as { verdict: string; reason: string; approval_check: { status: string } };
215
+ expect(report).toMatchObject({ verdict: "approval_mismatch", reason: "items_hash_differs", approval_check: { status: "mismatch" } });
216
+ });
217
+
218
+ it("exits 8, chain_mismatch, when the payee beside a genuine signature was changed", () => {
219
+ const f = v4Fixture();
220
+ writeFileSync(f.paths.read, JSON.stringify({ ...f.read, quote: { ...f.read.quote, payee: "outra@exemplo.com.br" } }));
221
+ const out = run([f.paths.read, "--keys", f.paths.keys, "--approval", f.paths.artifact], f.dir);
222
+ expect(out.code).toBe(8);
223
+ expect(out.stdout).toContain("CHAIN_MISMATCH");
224
+ });
225
+
226
+ it("exits 7 for a v2 read, and says the chain is not recomputable without the mandate signature", () => {
227
+ const f = v4Fixture();
228
+ writeFileSync(f.paths.read, JSON.stringify({ ...f.read, chain_version: 2 }));
229
+ const out = run([f.paths.read, "--keys", f.paths.keys], f.dir);
230
+ expect(out.code).toBe(7);
231
+ expect(out.stderr).toContain("not recomputable without the mandate signature");
232
+ });
233
+
234
+ it("refuses an --approval file with more than one artifact unless --approval-id names one", () => {
235
+ const f = v4Fixture();
236
+ writeFileSync(f.paths.artifact, JSON.stringify([f.artifact, { ...f.artifact, approval_id: "apr_2" }]));
237
+ expect(run([f.paths.read, "--keys", f.paths.keys, "--approval", f.paths.artifact], f.dir).code).toBe(2);
238
+ expect(run([f.paths.read, "--keys", f.paths.keys, "--approval", f.paths.artifact, "--approval-id", "apr_1"], f.dir).code).toBe(0);
239
+ expect(run([f.paths.read, "--keys", f.paths.keys, "--approval-id", "apr_1"], f.dir).code).toBe(2);
240
+ });
241
+ });
242
+
243
+ /*
244
+ * `--from-api`: a run's masked copy, bound through the API's read. A local
245
+ * server plays the deployment — the receipt route and its well-known key set —
246
+ * so the read and the key set come from ONE origin, as the command requires.
247
+ */
248
+ describe("codespar-agent verify --from-api", () => {
249
+ let server: Server;
250
+ let baseUrl: string;
251
+ let served: { read: unknown; keys: unknown } = { read: {}, keys: {} };
252
+ const asked: string[] = [];
253
+
254
+ beforeAll(async () => {
255
+ server = createServer((req, res) => {
256
+ asked.push(`${req.method} ${req.url} ${req.headers["authorization"] ? "auth" : "anon"}`);
257
+ const body = req.url === "/.well-known/codespar-receipt-keys.json" ? served.keys : req.url === `/v1/consumers/receipts/${RECEIPT_ID}` ? served.read : undefined;
258
+ res.writeHead(body === undefined ? 404 : 200, { "content-type": "application/json" });
259
+ res.end(JSON.stringify(body ?? { error: { code: "not_found", message: "no" } }));
260
+ });
261
+ await new Promise<void>((ok) => server.listen(0, "127.0.0.1", ok));
262
+ baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
263
+ });
264
+
265
+ afterAll(async () => {
266
+ server.closeAllConnections();
267
+ await new Promise<void>((ok) => server.close(() => ok()));
268
+ });
269
+
270
+ /** The server answers while the command runs, so the process is awaited, never spawned synchronously. */
271
+ function runAsync(args: string[], cwd: string): Promise<{ code: number | null; stdout: string; stderr: string }> {
272
+ return new Promise((done) => {
273
+ const child = spawn(process.execPath, [BIN, "verify", ...args], {
274
+ cwd,
275
+ // The placeholder passes the csk_test_ guard and is the one test-key-shaped string the secret scan allows.
276
+ env: { ...process.env, CODESPAR_API_KEY: "csk_test_your_key_here", CODESPAR_API_URL: baseUrl, ANTHROPIC_API_KEY: "" },
277
+ });
278
+ let stdout = "";
279
+ let stderr = "";
280
+ child.stdout.on("data", (d: Buffer) => (stdout += d.toString()));
281
+ child.stderr.on("data", (d: Buffer) => (stderr += d.toString()));
282
+ child.on("close", (code) => done({ code, stdout, stderr }));
283
+ });
284
+ }
285
+
286
+ it("binds the copy's body through the read and checks the run's own artifact: exit 0, and nothing unmasked is written", async () => {
287
+ const f = v4Fixture();
288
+ served = { read: f.read, keys: f.keys };
289
+ asked.length = 0;
290
+ const out = await runAsync([f.paths.copy, "--from-api"], f.dir);
291
+ expect(out.code).toBe(0);
292
+ expect(out.stdout).toContain("approval matched");
293
+ expect(asked).toEqual([`GET /.well-known/codespar-receipt-keys.json anon`, `GET /v1/consumers/receipts/${RECEIPT_ID} auth`]);
294
+ expect(readFileSync(f.paths.copy, "utf8")).not.toContain(PAYEE);
295
+ });
296
+
297
+ it("exits 9 when the run's artifact is not the list the receipt sealed", async () => {
298
+ const f = v4Fixture();
299
+ served = { read: f.read, keys: f.keys };
300
+ writeFileSync(f.paths.runApprovals, JSON.stringify([{ ...f.artifact, items: [{ ...ITEMS[0]!, amount: 1 }], items_hash: itemsHash([{ ...ITEMS[0]!, amount: 1 }]) }]));
301
+ const out = await runAsync([f.paths.copy, "--from-api", "--json"], f.dir);
302
+ expect(out.code).toBe(9);
303
+ expect(JSON.parse(out.stdout)).toMatchObject({ verdict: "approval_mismatch", reason: "items_hash_differs" });
304
+ });
305
+
306
+ it("exits 8 when the copy was edited after it was written: the signature does not cover its body, the read does", async () => {
307
+ const f = v4Fixture();
308
+ served = { read: f.read, keys: f.keys };
309
+ writeFileSync(f.paths.copy, JSON.stringify({ ...f.copy, payment: { ...f.copy.payment, amount_minor: 1 } }));
310
+ const out = await runAsync([f.paths.copy, "--from-api", "--json"], f.dir);
311
+ expect(out.code).toBe(8);
312
+ expect(JSON.parse(out.stdout)).toMatchObject({ verdict: "chain_mismatch", reason: "copy_differs_from_read" });
313
+ });
314
+
315
+ it("answers signature_only, never a failure, when the read cannot be fetched", async () => {
316
+ const f = v4Fixture();
317
+ served = { read: undefined as unknown, keys: f.keys };
318
+ const out = await runAsync([f.paths.copy, "--from-api", "--json"], f.dir);
319
+ expect(out.code).toBe(7);
320
+ expect(JSON.parse(out.stdout)).toMatchObject({ verdict: "signature_only", reason: "read_unavailable" });
321
+ });
322
+
323
+ it("refuses a key set from another deployment than the read", async () => {
324
+ const f = v4Fixture();
325
+ const out = await runAsync([f.paths.copy, "--from-api", "--url", "https://api.codespar.dev/.well-known/codespar-receipt-keys.json"], f.dir);
326
+ expect(out.code).toBe(2);
327
+ expect(out.stderr).toContain("same deployment");
328
+ });
329
+ });
@@ -0,0 +1,277 @@
1
+ /**
2
+ * The official backend, without Meta.
3
+ *
4
+ * Nothing here reaches the network. What is driven is the half where a
5
+ * mistake is silent: the signature check (a wrong one that passes is an open
6
+ * webhook), the webhook parse (a shape misread is a message never answered),
7
+ * the verification handshake, and the request builder. The send itself is one
8
+ * `fetch` over a request these functions built, and the one test that touches
9
+ * `deliver` injects a fetch that asserts what it was handed — so a regression
10
+ * that started calling Meta would fail here instead of dialling out.
11
+ *
12
+ * The shapes come from Meta's published documentation. Nobody on this repo
13
+ * has a Business account, so none of this has been run against Meta, and the
14
+ * README says so in those words rather than implying otherwise.
15
+ */
16
+ import { createHmac } from "node:crypto";
17
+ import { describe, expect, it } from "vitest";
18
+ import {
19
+ buildSendRequest,
20
+ isMetaBaseUrl,
21
+ loadCloudApiConfig,
22
+ parseInbound,
23
+ providerRefusal,
24
+ verifyChallenge,
25
+ verifyWebhookSignature,
26
+ WhatsAppCloudApi,
27
+ type CloudApiConfig,
28
+ } from "../src/channels/whatsapp/cloud-api.js";
29
+
30
+ const CONFIG: CloudApiConfig = {
31
+ baseUrl: "https://graph.facebook.com",
32
+ phoneNumberId: "111222333",
33
+ accessToken: "test-token-not-a-credential",
34
+ verifyToken: "verify-me",
35
+ appSecret: "app-secret-for-the-test",
36
+ apiVersion: "v21.0",
37
+ webhookPort: 3111,
38
+ };
39
+
40
+ const sign = (body: string, secret = CONFIG.appSecret) => `sha256=${createHmac("sha256", secret).update(body, "utf8").digest("hex")}`;
41
+
42
+ describe("credentials are absent by default", () => {
43
+ it("names every variable that is missing rather than saying 'misconfigured'", () => {
44
+ const { config, missing } = loadCloudApiConfig({});
45
+ expect(config).toBeUndefined();
46
+ expect(missing).toEqual(["WHATSAPP_PHONE_NUMBER_ID", "WHATSAPP_ACCESS_TOKEN", "WHATSAPP_VERIFY_TOKEN", "WHATSAPP_APP_SECRET"]);
47
+ });
48
+
49
+ it("treats an empty value as absent: a commented-out .env.example must not half-configure a channel", () => {
50
+ const { config, missing } = loadCloudApiConfig({ WHATSAPP_PHONE_NUMBER_ID: " ", WHATSAPP_ACCESS_TOKEN: "", WHATSAPP_VERIFY_TOKEN: "v", WHATSAPP_APP_SECRET: "s" });
51
+ expect(config).toBeUndefined();
52
+ expect(missing).toEqual(["WHATSAPP_PHONE_NUMBER_ID", "WHATSAPP_ACCESS_TOKEN"]);
53
+ });
54
+
55
+ it("defaults the version, the port and Meta's own host, and nothing else", () => {
56
+ const { config } = loadCloudApiConfig({ WHATSAPP_PHONE_NUMBER_ID: "1", WHATSAPP_ACCESS_TOKEN: "2", WHATSAPP_VERIFY_TOKEN: "3", WHATSAPP_APP_SECRET: "4" });
57
+ expect(config?.apiVersion).toBe("v21.0");
58
+ expect(config?.webhookPort).toBe(3111);
59
+ expect(config?.baseUrl).toBe("https://graph.facebook.com");
60
+ });
61
+ });
62
+
63
+ describe("live is decided by the base URL, and only by it", () => {
64
+ it("is Meta's host and nothing that merely looks like it", () => {
65
+ expect(isMetaBaseUrl("https://graph.facebook.com")).toBe(true);
66
+ expect(isMetaBaseUrl("https://graph.facebook.com/")).toBe(true);
67
+ expect(isMetaBaseUrl("http://127.0.0.1:4290")).toBe(false);
68
+ expect(isMetaBaseUrl("https://graph.facebook.com.evil.test")).toBe(false);
69
+ expect(isMetaBaseUrl("not a url")).toBe(false);
70
+ });
71
+
72
+ it("names itself `emulator` and reports `live: false` off Meta, which is what the evidence builder refuses on", () => {
73
+ const options = { conversation: { contact: "+5511987654321" }, say: () => undefined, fetchImpl: async () => new Response("{}") };
74
+ const emulator = new WhatsAppCloudApi({ ...options, config: { ...CONFIG, baseUrl: "http://127.0.0.1:4290" } });
75
+ expect(emulator.live).toBe(false);
76
+ expect(emulator.name).toBe("emulator");
77
+ const meta = new WhatsAppCloudApi({ ...options, config: CONFIG });
78
+ expect(meta.live).toBe(true);
79
+ expect(meta.name).toBe("cloud-api");
80
+ });
81
+
82
+ it("builds the send against whichever base URL it was given, with the path unchanged", () => {
83
+ const request = buildSendRequest({ ...CONFIG, baseUrl: "http://127.0.0.1:4290", apiVersion: "v22.0" }, "+5511987654321", { kind: "text", text: "oi" });
84
+ if ("unsupported" in request) throw new Error("expected a request");
85
+ expect(request.url).toBe("http://127.0.0.1:4290/v22.0/111222333/messages");
86
+ });
87
+ });
88
+
89
+ describe("a delivery is trusted only when it is signed", () => {
90
+ const body = JSON.stringify({ object: "whatsapp_business_account" });
91
+
92
+ it("accepts the app secret's own signature over the RAW body", () => {
93
+ expect(verifyWebhookSignature(CONFIG.appSecret, sign(body), body)).toEqual({ ok: true });
94
+ });
95
+
96
+ it("refuses a body that was re-serialized: the signature is over bytes", () => {
97
+ const reserialized = JSON.stringify(JSON.parse(body), null, 2);
98
+ expect(verifyWebhookSignature(CONFIG.appSecret, sign(body), reserialized).ok).toBe(false);
99
+ });
100
+
101
+ it("refuses another secret, a missing header and a malformed one", () => {
102
+ expect(verifyWebhookSignature(CONFIG.appSecret, sign(body, "someone-else"), body).ok).toBe(false);
103
+ expect(verifyWebhookSignature(CONFIG.appSecret, undefined, body)).toEqual({ ok: false, reason: "missing X-Hub-Signature-256" });
104
+ expect(verifyWebhookSignature(CONFIG.appSecret, "sha1=abc", body).ok).toBe(false);
105
+ expect(verifyWebhookSignature(CONFIG.appSecret, "nonsense", body).ok).toBe(false);
106
+ });
107
+ });
108
+
109
+ describe("the registration handshake", () => {
110
+ it("echoes the challenge for the right verify token", () => {
111
+ expect(verifyChallenge(CONFIG, { "hub.mode": "subscribe", "hub.verify_token": "verify-me", "hub.challenge": "12345" })).toEqual({ status: 200, body: "12345" });
112
+ });
113
+
114
+ it("refuses a wrong token and a wrong mode", () => {
115
+ expect(verifyChallenge(CONFIG, { "hub.mode": "subscribe", "hub.verify_token": "nope", "hub.challenge": "1" }).status).toBe(403);
116
+ expect(verifyChallenge(CONFIG, { "hub.mode": "unsubscribe", "hub.verify_token": "verify-me" }).status).toBe(400);
117
+ });
118
+ });
119
+
120
+ describe("reading what Meta delivers", () => {
121
+ const delivery = {
122
+ object: "whatsapp_business_account",
123
+ entry: [
124
+ {
125
+ changes: [
126
+ {
127
+ field: "messages",
128
+ value: {
129
+ messaging_product: "whatsapp",
130
+ metadata: { phone_number_id: "111222333" },
131
+ messages: [
132
+ { id: "wamid.HBgNNTUxMTk4NzY1NDMyMRUCABIYEjND", from: "5511987654321", timestamp: "1758643200", type: "text", text: { body: "fechado, pago à vista" } },
133
+ { id: "wamid.SECOND", from: "5511987654321", timestamp: "1758643260", type: "image", image: { id: "media_1" } },
134
+ ],
135
+ },
136
+ },
137
+ ],
138
+ },
139
+ ],
140
+ };
141
+
142
+ it("reads the text messages and puts the plus back on the number", () => {
143
+ const { messages } = parseInbound(delivery);
144
+ expect(messages).toEqual([{ id: "wamid.HBgNNTUxMTk4NzY1NDMyMRUCABIYEjND", from: "+5511987654321", text: "fechado, pago à vista", timestamp: 1758643200 }]);
145
+ });
146
+
147
+ it("names what it dropped instead of turning it into an empty message", () => {
148
+ expect(parseInbound(delivery).ignored).toEqual([{ id: "wamid.SECOND", type: "image" }]);
149
+ });
150
+
151
+ it("reads a delivery that carries only status updates as no messages", () => {
152
+ expect(parseInbound({ entry: [{ changes: [{ value: { statuses: [{ id: "wamid.X", status: "delivered" }] } }] }] }).messages).toEqual([]);
153
+ expect(parseInbound({}).messages).toEqual([]);
154
+ });
155
+ });
156
+
157
+ describe("the request that would go to Meta", () => {
158
+ it("builds the documented /messages call for a text", () => {
159
+ const request = buildSendRequest(CONFIG, "+5511987654321", { kind: "text", text: "Oi, Joana!" });
160
+ expect("unsupported" in request).toBe(false);
161
+ if ("unsupported" in request) return;
162
+ expect(request.url).toBe("https://graph.facebook.com/v21.0/111222333/messages");
163
+ expect(request.headers["authorization"]).toBe("Bearer test-token-not-a-credential");
164
+ expect(JSON.parse(request.body)).toEqual({ messaging_product: "whatsapp", recipient_type: "individual", to: "5511987654321", type: "text", text: { preview_url: false, body: "Oi, Joana!" } });
165
+ });
166
+
167
+ it("sends a copy-and-paste as its own plain text message: a code inside a paragraph cannot be tapped", () => {
168
+ const request = buildSendRequest(CONFIG, "+5511987654321", { kind: "instrument", instrument: "pix_copy_paste", value: "00020126580014br.gov.bcb.pix" });
169
+ if ("unsupported" in request) throw new Error("expected a request");
170
+ expect(JSON.parse(request.body).text.body).toBe("00020126580014br.gov.bcb.pix");
171
+ });
172
+
173
+ it("builds a template with its body parameters", () => {
174
+ const request = buildSendRequest(CONFIG, "+5511987654321", { kind: "template", template: "cobranca_lembrete", language: "pt_BR", variables: ["Joana", "1042"] });
175
+ if ("unsupported" in request) throw new Error("expected a request");
176
+ expect(JSON.parse(request.body).template).toEqual({
177
+ name: "cobranca_lembrete",
178
+ language: { code: "pt_BR" },
179
+ components: [{ type: "body", parameters: [{ type: "text", text: "Joana" }, { type: "text", text: "1042" }] }],
180
+ });
181
+ });
182
+
183
+ it("says outright that it cannot send an image, instead of pretending", () => {
184
+ expect(buildSendRequest(CONFIG, "+5511987654321", { kind: "media", media: "qr", data: "00020126" })).toEqual({ unsupported: "media_upload_unimplemented" });
185
+ });
186
+ });
187
+
188
+ describe("deliver, with the network replaced by an assertion", () => {
189
+ it("posts the request the builder produced and returns the provider's message id", async () => {
190
+ const calls: Array<{ url: string; init: RequestInit | undefined }> = [];
191
+ const backend = new WhatsAppCloudApi({
192
+ config: CONFIG,
193
+ conversation: { contact: "+5511987654321" },
194
+ say: () => undefined,
195
+ fetchImpl: async (url, init) => {
196
+ calls.push({ url: String(url), init });
197
+ return new Response(JSON.stringify({ messages: [{ id: "wamid.SENT" }] }), { status: 200 });
198
+ },
199
+ });
200
+ const sent = await backend.deliver("+5511987654321", { kind: "text", text: "Oi!" });
201
+ expect(sent).toEqual({ id: "wamid.SENT", state: "sent" });
202
+ expect(calls).toHaveLength(1);
203
+ expect(calls[0]!.url).toBe("https://graph.facebook.com/v21.0/111222333/messages");
204
+ });
205
+
206
+ it("refuses an image without reaching for the network at all", async () => {
207
+ let called = false;
208
+ const backend = new WhatsAppCloudApi({
209
+ config: CONFIG,
210
+ conversation: { contact: "+5511987654321" },
211
+ say: () => undefined,
212
+ fetchImpl: async () => {
213
+ called = true;
214
+ return new Response("{}", { status: 200 });
215
+ },
216
+ });
217
+ const sent = await backend.deliver("+5511987654321", { kind: "media", media: "qr", data: "00020126" });
218
+ expect(sent.refused?.rule).toBe("media_upload_unimplemented");
219
+ expect(called).toBe(false);
220
+ });
221
+
222
+ it("reports a provider refusal as a failure and never as a send", async () => {
223
+ const backend = new WhatsAppCloudApi({
224
+ config: CONFIG,
225
+ conversation: { contact: "+5511987654321" },
226
+ say: () => undefined,
227
+ fetchImpl: async () => new Response(JSON.stringify({ error: { message: "Invalid parameter" } }), { status: 400 }),
228
+ });
229
+ const sent = await backend.deliver("+5511987654321", { kind: "text", text: "Oi!" });
230
+ expect(sent.state).toBe("failed");
231
+ expect(sent.refused?.rule).toBe("provider_refused");
232
+ });
233
+ });
234
+
235
+ describe("what Meta's error body says, read rather than collapsed into a status", () => {
236
+ const body = (error: unknown) => JSON.stringify({ error });
237
+
238
+ it("reads 131047 as the window rule it is, so a caller can answer it with a template", () => {
239
+ const refusal = providerRefusal(400, body({ code: 131047, message: "(#131047) Message failed to send", error_data: { details: "the 24h customer service window closed" } }));
240
+ expect(refusal.rule).toBe("session_window_closed");
241
+ expect(refusal.detail).toContain("400/131047");
242
+ expect(refusal.detail).toContain("the 24h customer service window closed");
243
+ });
244
+
245
+ it("keeps any other code a provider refusal, with the code named", () => {
246
+ expect(providerRefusal(400, body({ code: 131026, message: "Message undeliverable" }))).toEqual({ rule: "provider_refused", detail: "400/131026 from the Cloud API" });
247
+ });
248
+
249
+ it("does not mistake a body without Meta's envelope for anything but the status", () => {
250
+ expect(providerRefusal(502, "<html>bad gateway</html>")).toEqual({ rule: "provider_refused", detail: "502 from the Cloud API" });
251
+ });
252
+ });
253
+
254
+ describe("a redelivered message is the same message", () => {
255
+ it("hands a message on once however many times it arrives, and says it dropped the rest", async () => {
256
+ const lines: string[] = [];
257
+ const port = 40000 + Math.floor(Math.random() * 20000);
258
+ const backend = new WhatsAppCloudApi({ config: { ...CONFIG, webhookPort: port }, conversation: { contact: "+5511987654321" }, say: (l) => lines.push(l) });
259
+ await backend.open();
260
+ try {
261
+ const raw = JSON.stringify({
262
+ entry: [{ changes: [{ value: { messages: [{ id: "wamid.ONCE", from: "5511987654321", timestamp: "1758643200", type: "text", text: { body: "fechado" } }] } }] }],
263
+ });
264
+ for (let i = 0; i < 2; i++) {
265
+ const response = await fetch(`http://127.0.0.1:${port}/`, { method: "POST", headers: { "x-hub-signature-256": sign(raw) }, body: raw });
266
+ // Still 200: Meta retries anything else, and a duplicate is not an error.
267
+ expect(response.status).toBe(200);
268
+ }
269
+ expect((await backend.next())?.id).toBe("wamid.ONCE");
270
+ const second = await Promise.race([backend.next(), new Promise((resolve) => setTimeout(() => resolve("nothing"), 200))]);
271
+ expect(second).toBe("nothing");
272
+ expect(lines).toContain("[whatsapp] duplicate delivery of wamid.ONCE dropped");
273
+ } finally {
274
+ await backend.close();
275
+ }
276
+ });
277
+ });