@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,216 @@
1
+ /**
2
+ * The house simulator, which is not ours: `dyvit-wa-sim`, the local Cloud API
3
+ * emulator from https://github.com/fabianocruz/whatsapp-simulator (MIT).
4
+ *
5
+ * WHY A SEPARATE PROCESS INSTEAD OF A FAKE IN THIS REPO. A mock we wrote would
6
+ * agree with us by construction — it would accept the payloads we send because
7
+ * we wrote both sides, and the day Meta refused one we would find out in
8
+ * production. The emulator answers `POST /v{version}/{phone-number-id}/messages`
9
+ * with the Cloud API's own response shape and posts back the same signed
10
+ * `x-hub-signature-256` webhooks, so the `simulator` backend is THE SAME CODE
11
+ * as the official one with a different base URL. That is the thing being
12
+ * proved, and it is not something a mock can prove.
13
+ *
14
+ * It also has a clock we can move (`POST /_sim/clock`), which a replay
15
+ * finishing in seconds cannot otherwise have: a collection runs over days, and
16
+ * without moving the clock the 24-hour window never closes, so the one rule
17
+ * that decides whether a message may be sent at all is never exercised.
18
+ *
19
+ * WHAT THIS FILE IS: the `_sim/*` side only — the routes that belong to the
20
+ * emulator and have no counterpart at Meta. The sending and receiving are
21
+ * `WhatsAppCloudApi`, unchanged.
22
+ *
23
+ * The emulator is a separate process and is not a dependency of this workspace:
24
+ * it is a development tool nothing here imports. `scripts/whatsapp-emulator.mjs`
25
+ * fetches `@dyvit/whatsapp-simulator-cli` at a pinned version and runs it;
26
+ * `npm run whatsapp:emulator` is the command.
27
+ */
28
+ import type { ConversationScript } from "@codespar/agent-core";
29
+ import type { ChannelBackend, InboundMessage, OutboundBody, SentMessage, StatusUpdate } from "../types.js";
30
+ import { WhatsAppCloudApi, type CloudApiOptions } from "./cloud-api.js";
31
+
32
+ export const EMULATOR_ENV = {
33
+ url: "WHATSAPP_SIM_URL",
34
+ appSecret: "WHATSAPP_SIM_APP_SECRET",
35
+ webhookPort: "WHATSAPP_SIM_WEBHOOK_PORT",
36
+ phoneNumberId: "WHATSAPP_SIM_PHONE_NUMBER_ID",
37
+ } as const;
38
+
39
+ export const EMULATOR_DEFAULTS = {
40
+ url: "http://127.0.0.1:4290",
41
+ /** Matches `--app-secret` in `npm run whatsapp:emulator`. A local development value, not a credential. */
42
+ appSecret: "dev",
43
+ webhookPort: 4399,
44
+ phoneNumberId: "900000000001",
45
+ apiVersion: "v22.0",
46
+ } as const;
47
+
48
+ export class EmulatorUnreachableError extends Error {
49
+ constructor(url: string, detail: string) {
50
+ super(
51
+ `the WhatsApp emulator is not answering at ${url} (${detail}).\n` +
52
+ ` Start it in another terminal: npm run whatsapp:emulator\n` +
53
+ ` It is dyvit-wa-sim, from https://github.com/fabianocruz/whatsapp-simulator — MIT, run from npm at a pinned version, no Meta account and no credential.`,
54
+ );
55
+ this.name = "EmulatorUnreachableError";
56
+ }
57
+ }
58
+
59
+ /** The `_sim/*` routes: what the emulator has and Meta does not. */
60
+ export class EmulatorDriver {
61
+ constructor(
62
+ private readonly url: string,
63
+ private readonly fetchImpl: typeof fetch = fetch,
64
+ ) {}
65
+
66
+ private async call(path: string, init?: RequestInit): Promise<unknown> {
67
+ let response: Response;
68
+ try {
69
+ response = await this.fetchImpl(`${this.url}${path}`, { ...init, signal: AbortSignal.timeout(5000) });
70
+ } catch (err) {
71
+ throw new EmulatorUnreachableError(this.url, err instanceof Error ? err.message : String(err));
72
+ }
73
+ const text = await response.text();
74
+ if (!response.ok) throw new Error(`emulator answered ${response.status} to ${path}: ${text.slice(0, 200)}`);
75
+ return text ? JSON.parse(text) : {};
76
+ }
77
+
78
+ private post(path: string, body: unknown): Promise<unknown> {
79
+ return this.call(path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
80
+ }
81
+
82
+ health(): Promise<unknown> {
83
+ return this.call("/health");
84
+ }
85
+
86
+ /** Pins the conversation clock, so a scripted run reads the same at 03:00 as at 15:00. */
87
+ pin(at: Date): Promise<unknown> {
88
+ return this.post("/_sim/clock", { now: at.toISOString() });
89
+ }
90
+
91
+ /** Moves the conversation clock forward. The only way to reach the far side of the 24-hour window. */
92
+ advanceHours(hours: number): Promise<unknown> {
93
+ return this.post("/_sim/clock", { advance_hours: hours });
94
+ }
95
+
96
+ /** The person writes, or taps a quick reply. The emulator answers with a signed inbound webhook to our receiver. */
97
+ inbound(options: { phoneNumberId: string; from: string; text: string; sentAt?: Date } | { phoneNumberId: string; from: string; reply: { id: string; title: string }; sentAt?: Date }): Promise<unknown> {
98
+ return this.post("/_sim/inbound", {
99
+ phone_number_id: options.phoneNumberId,
100
+ from: options.from,
101
+ ...("reply" in options
102
+ ? { type: "interactive", interactive: { type: "button_reply", button_reply: { id: options.reply.id, title: options.reply.title } } }
103
+ : { text: options.text }),
104
+ ...(options.sentAt ? { sent_at: options.sentAt.toISOString() } : {}),
105
+ });
106
+ }
107
+
108
+ /** Reports a delivery status for a message the business sent, as the provider would: `read`, or `failed` with a reason. */
109
+ status(options: { status: "read" | "failed"; messageId: string; reason?: string }): Promise<unknown> {
110
+ return this.post("/_sim/status", { status: options.status, message_id: options.messageId, ...(options.reason ? { reason: options.reason } : {}) });
111
+ }
112
+
113
+ /** The priced timeline, which is the emulator's own reason for existing. Read for the console, never asserted on. */
114
+ state(key: string): Promise<{ priced?: { total?: number; currency?: string } } | undefined> {
115
+ return this.call(`/_sim/state?key=${encodeURIComponent(key)}`).then(
116
+ (v) => v as { priced?: { total?: number; currency?: string } },
117
+ () => undefined,
118
+ );
119
+ }
120
+ }
121
+
122
+ export interface EmulatorBackendOptions extends CloudApiOptions {
123
+ driver: EmulatorDriver;
124
+ /** The person's turns. Absent means they are at the keyboard. */
125
+ script?: ConversationScript | undefined;
126
+ /** The instant the emulator's conversation clock is pinned to. */
127
+ pinAt?: Date | undefined;
128
+ /** Interactive only: reads the person's next line. */
129
+ ask?: ((question: string) => Promise<string>) | undefined;
130
+ /** Where the console draws the conversation as it goes. */
131
+ render: (line: string) => void;
132
+ }
133
+
134
+ /**
135
+ * The `simulator` backend: `WhatsAppCloudApi` pointed at the emulator, plus the
136
+ * one thing the Cloud API has no equivalent for — making the person write.
137
+ *
138
+ * Every outbound message goes through the same `deliver` the Meta backend uses,
139
+ * and every inbound one arrives as a signed webhook at the same receiver, so
140
+ * nothing here is a shortcut around the adapter.
141
+ */
142
+ export class WhatsAppEmulator implements ChannelBackend {
143
+ readonly name = "emulator";
144
+ readonly live = false;
145
+ private readonly api: WhatsAppCloudApi;
146
+ private turn = 0;
147
+ private closed = false;
148
+
149
+ constructor(private readonly options: EmulatorBackendOptions) {
150
+ const { driver: _driver, script: _script, pinAt: _pinAt, ask: _ask, render: _render, ...api } = options;
151
+ this.api = new WhatsAppCloudApi(api);
152
+ }
153
+
154
+ async open(): Promise<void> {
155
+ await this.api.open();
156
+ await this.options.driver.health();
157
+ if (this.options.pinAt) await this.options.driver.pin(this.options.pinAt);
158
+ this.options.render("");
159
+ this.options.render(` ┌─ WhatsApp (dyvit-wa-sim, emulador local da Cloud API — sem rede externa, sem conta Meta)`);
160
+ }
161
+
162
+ async next(): Promise<InboundMessage | undefined> {
163
+ if (this.closed) return undefined;
164
+ const turn = await this.nextTurn();
165
+ if (turn === undefined) return undefined;
166
+ // The person writes (or taps) THROUGH the emulator: it mints the message id
167
+ // and posts the signed webhook our receiver verifies, so a scripted turn
168
+ // takes the same path a real one would.
169
+ const who = { phoneNumberId: this.options.config.phoneNumberId, from: this.options.conversation.contact };
170
+ await this.options.driver.inbound(typeof turn === "string" ? { ...who, text: turn } : { ...who, reply: turn });
171
+ return this.api.next();
172
+ }
173
+
174
+ onStatus(listener: (status: StatusUpdate) => void): void {
175
+ this.api.onStatus(listener);
176
+ }
177
+
178
+ private async nextTurn(): Promise<string | { id: string; title: string } | undefined> {
179
+ const script = this.options.script;
180
+ if (script) {
181
+ const turn = script.turns[this.turn];
182
+ if (!turn) return undefined;
183
+ this.turn += 1;
184
+ // The person's own pause, on the emulator's clock. A conversation that
185
+ // happens over days is what closes the 24-hour window.
186
+ if (turn.after_seconds > 0) await this.options.driver.advanceHours(turn.after_seconds / 3600);
187
+ return turn.reply ? { id: turn.reply.id, title: turn.reply.title ?? turn.reply.id } : turn.text;
188
+ }
189
+ return this.nextText();
190
+ }
191
+
192
+ private async nextText(): Promise<string | undefined> {
193
+ if (!this.options.ask) return undefined;
194
+ let answer: string;
195
+ try {
196
+ answer = await this.options.ask(" │ voce> ");
197
+ } catch {
198
+ return undefined;
199
+ }
200
+ const text = answer.trim();
201
+ if (!text) return this.nextText();
202
+ if (["sair", "exit", "quit"].includes(text.toLowerCase())) return undefined;
203
+ return text;
204
+ }
205
+
206
+ deliver(to: string, body: OutboundBody): Promise<SentMessage> {
207
+ return this.api.deliver(to, body);
208
+ }
209
+
210
+ async close(): Promise<void> {
211
+ if (this.closed) return;
212
+ this.closed = true;
213
+ await this.api.close();
214
+ this.options.render(" └─ fim da conversa");
215
+ }
216
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Decision 19a, the part a channel can carry: `attestation.evidence` on
3
+ * `POST /v1/consents/{token}/submit` (codespar-enterprise ent#1615).
4
+ *
5
+ * In the sandbox a mandate is born at the terminal with
6
+ * `attestation: { method: "in_person" }` — the titular is at the keyboard and
7
+ * the partner is the process. In production the act happens in a
8
+ * CONVERSATION, so the method is `partner_session` and the submit may carry
9
+ * what the partner OBSERVED of it. This builds that object.
10
+ *
11
+ * The API's rule, restated here because it is the whole point: a WhatsApp
12
+ * conversation hands the partner a message and a sender, not a socket. The
13
+ * channel's allowed keys are therefore `contact`, `message_id`, `session_id`
14
+ * and `provider_ts`, and an `ip`, a `user_agent`, a `device_id_hash` or a
15
+ * `geo` on this channel would have been inferred rather than observed. The
16
+ * API refuses them (400 `attestation_evidence_invalid`), and so does this,
17
+ * one layer earlier, because a value inferred inside a signed record reads as
18
+ * observed and is worse than an absent one.
19
+ *
20
+ * WHY THE EMULATOR CANNOT PRODUCE ONE. Everything here is a claim about what a
21
+ * provider observed. A conversation carried by an emulator on the developer's
22
+ * own machine was observed by nobody: its message id was minted locally, its
23
+ * timestamp came from a clock we move ourselves, and no phone was involved.
24
+ * Signing that as evidence would be a false declaration — the same one the
25
+ * API's own schema warns about for a partner that declares `other` for a
26
+ * WhatsApp act. So `evidenceFor` returns a REFUSAL for any backend that is not
27
+ * live, with the reason on it, and the caller either submits without evidence
28
+ * or does not submit at all. That is not a gap in the seam; it is the seam
29
+ * working.
30
+ *
31
+ * `live` is not a flag anyone sets: it is whether the base URL is Meta's host.
32
+ * A run cannot claim to be live by being configured to say so.
33
+ */
34
+ import type { InboundMessage } from "../types.js";
35
+
36
+ /** The keys the API's `whatsapp` channel accepts. Mirrored from `CONSENT_EVIDENCE_CHANNEL_KEYS`. */
37
+ export const WHATSAPP_EVIDENCE_KEYS = ["contact", "message_id", "session_id", "provider_ts"] as const;
38
+
39
+ /** Opaque provider ids: a `wamid.` carries `.`, `:`, `=` and `+`, and all four are allowed. */
40
+ const OPAQUE_ID = /^[A-Za-z0-9_.:=+-]{1,120}$/;
41
+
42
+ /**
43
+ * The evidence as the PARTNER sends it. The wire carries `contact` in the
44
+ * clear and the API hashes it into `contact_hash` under the org's own key —
45
+ * a partner cannot compute that hash, and a partner-computed one would match
46
+ * nothing.
47
+ */
48
+ export interface WhatsAppConsentEvidence {
49
+ channel: "whatsapp";
50
+ contact?: string;
51
+ message_id?: string;
52
+ session_id?: string;
53
+ /** Unix seconds on the PROVIDER's clock, which is not the partner's. */
54
+ provider_ts?: number;
55
+ }
56
+
57
+ export interface ConsentAttestation {
58
+ method: "partner_session";
59
+ /** Unix seconds: when the PARTNER says the human authorized. */
60
+ asserted_at: number;
61
+ /** The partner's own record id. `^[A-Za-z0-9_-]{1,120}$` — no dots, so a `wamid` goes in `message_id`. */
62
+ reference?: string;
63
+ evidence: WhatsAppConsentEvidence;
64
+ }
65
+
66
+ export type EvidenceResult = { attestable: true; attestation: ConsentAttestation } | { attestable: false; reason: string };
67
+
68
+ export interface EvidenceInput {
69
+ /** Whether the backend that carried the message talks to a real provider. */
70
+ live: boolean;
71
+ /** The message in which the person said yes. */
72
+ message: InboundMessage;
73
+ /** The partner's id for this conversation, when it has one. */
74
+ sessionId?: string | undefined;
75
+ /** The partner's clock, for `asserted_at`. */
76
+ now: () => Date;
77
+ reference?: string | undefined;
78
+ }
79
+
80
+ export function evidenceFor(input: EvidenceInput): EvidenceResult {
81
+ if (!input.live) {
82
+ return {
83
+ attestable: false,
84
+ reason:
85
+ "this act was observed by an emulator on this machine, and a simulated observation is not evidence: " +
86
+ "the message id was minted locally, the timestamp came from a clock we move ourselves, and no phone was involved",
87
+ };
88
+ }
89
+ if (!OPAQUE_ID.test(input.message.id)) return { attestable: false, reason: `the provider message id does not fit the API's opaque-id shape: ${input.message.id.slice(0, 24)}` };
90
+ if (!Number.isInteger(input.message.timestamp) || input.message.timestamp <= 0) return { attestable: false, reason: "the provider gave no usable timestamp for the message" };
91
+ if (input.sessionId !== undefined && !OPAQUE_ID.test(input.sessionId)) return { attestable: false, reason: "the session id does not fit the API's opaque-id shape" };
92
+ if (input.reference !== undefined && !/^[A-Za-z0-9_-]{1,120}$/.test(input.reference)) return { attestable: false, reason: "the attestation reference must be [A-Za-z0-9_-]{1,120}" };
93
+
94
+ return {
95
+ attestable: true,
96
+ attestation: {
97
+ method: "partner_session",
98
+ asserted_at: Math.floor(input.now().getTime() / 1000),
99
+ ...(input.reference !== undefined ? { reference: input.reference } : {}),
100
+ evidence: {
101
+ channel: "whatsapp",
102
+ contact: input.message.from,
103
+ message_id: input.message.id,
104
+ ...(input.sessionId !== undefined ? { session_id: input.sessionId } : {}),
105
+ provider_ts: input.message.timestamp,
106
+ },
107
+ },
108
+ };
109
+ }
@@ -0,0 +1,350 @@
1
+ /**
2
+ * The WhatsApp channel: one adapter, two backends.
3
+ *
4
+ * Everything that decides what may be said lives HERE and not in a backend,
5
+ * which is what makes "the rules are code" true rather than aspirational.
6
+ * Picking the simulator or the official API changes who carries the bytes; it
7
+ * does not change the hours, the bound contact, the secrecy of the debt, the
8
+ * document rule or the session window. A test that tries to get around a rule
9
+ * by choosing a backend is in `test/whatsapp-rules.test.ts` and fails.
10
+ *
11
+ * It also keeps the two audiences apart. `send` reaches the person who owes.
12
+ * The operator's console and the approval question are wired to stderr by the
13
+ * caller and have no path to this object at all — not a check, a shape: there
14
+ * is no method here that an operator line could be handed to.
15
+ */
16
+ import { declaredReplies, type ProofBundle, type QuickReply, type WhatsAppTemplate } from "@codespar/agent-core";
17
+ import { checkOutbound, type HoursRule, type RuleContext } from "../rules.js";
18
+ import type { Channel, ChannelBackend, ChannelLogLine, Conversation, DeliveryState, InboundMessage, OutboundBody, SentMessage, StatusUpdate } from "../types.js";
19
+ import { maskContact } from "../contact.js";
20
+ import { SessionWindow, type SessionState } from "./session.js";
21
+
22
+ export interface WhatsAppChannelOptions {
23
+ backend: ChannelBackend;
24
+ conversation: Conversation;
25
+ now: () => Date;
26
+ hours?: HoursRule | undefined;
27
+ knownSubjects?: readonly string[] | undefined;
28
+ /** The run's bundle, so the conversation is part of the proof. */
29
+ bundle?: ProofBundle | undefined;
30
+ /** Templates the agent declares it uses, from `channels/whatsapp/templates.json`. Meta's approval of them is not knowable from here. */
31
+ templates?: readonly WhatsAppTemplate[] | undefined;
32
+ /**
33
+ * Where the window stood when an earlier run left this conversation. A run
34
+ * that JOINS a conversation — a poll looking at a charge agreed yesterday —
35
+ * has no inbound message of its own to open it from, and without this it
36
+ * would read every conversation as shut.
37
+ */
38
+ session?: SessionState | undefined;
39
+ /** The operator's console. Refusals are reported here, never to the conversation. */
40
+ say?: ((line: string) => void) | undefined;
41
+ /**
42
+ * Draws the conversation for whoever is watching the run. It is a VIEW of
43
+ * what went over the channel, written after the fact, and never a way to put
44
+ * something in front of the person — that is `send`, and it is the only one.
45
+ */
46
+ render?: ((line: string) => void) | undefined;
47
+ /**
48
+ * The provider reported that a message the agent sent was NOT delivered.
49
+ * Called with the status line, which names the outcome the message told
50
+ * when it told one — that outcome was not told, whatever the send answered.
51
+ */
52
+ onDeliveryFailed?: ((line: ChannelLogLine) => void) | undefined;
53
+ /** Lines an earlier run wrote for this conversation, so a status about a message it sent can be tied back to what that message was. */
54
+ priorLines?: ReadonlyArray<ChannelLogLine> | undefined;
55
+ }
56
+
57
+ export class WhatsAppChannel implements Channel {
58
+ readonly name = "whatsapp" as const;
59
+ readonly conversation: Conversation;
60
+ private readonly session: SessionWindow;
61
+ private readonly lines: ChannelLogLine[] = [];
62
+ private lastInbound: InboundMessage | undefined;
63
+ private readonly replies: Map<string, QuickReply>;
64
+ /** The latest state the provider reported per message id, this process. */
65
+ private readonly delivery = new Map<string, DeliveryState>();
66
+ /**
67
+ * Failures reported for a message not yet in the log. A provider can post
68
+ * the status before the send that minted the id has answered (the emulator
69
+ * does, synchronously); the failure is acted on once the message is logged
70
+ * and it is known what the message was.
71
+ */
72
+ private readonly earlyFailures = new Map<string, ChannelLogLine>();
73
+
74
+ constructor(private readonly options: WhatsAppChannelOptions) {
75
+ this.conversation = options.conversation;
76
+ this.session = new SessionWindow(options.templates ?? [], options.session);
77
+ this.replies = declaredReplies(options.templates ?? []);
78
+ }
79
+
80
+ get backend(): string {
81
+ return this.options.backend.name;
82
+ }
83
+
84
+ /** Whether the backend talks to a real provider. What the consent-evidence seam turns on. */
85
+ get live(): boolean {
86
+ return this.options.backend.live;
87
+ }
88
+
89
+ /** The message the person last sent, for the caller that needs to attest to an act. */
90
+ get lastInboundMessage(): InboundMessage | undefined {
91
+ return this.lastInbound;
92
+ }
93
+
94
+ /**
95
+ * Whether a free-form message may go out right now. A caller that has one
96
+ * thing to say and two ways to say it asks HERE which one the provider
97
+ * would carry — the alternative is composing a message, watching the
98
+ * channel refuse it, and calling that a decision.
99
+ */
100
+ get sessionOpen(): boolean {
101
+ return this.session.open(this.options.now());
102
+ }
103
+
104
+ /** Seconds of free-form left, for the console line that explains the choice. */
105
+ get sessionRemainingSeconds(): number {
106
+ return this.session.remainingSeconds(this.options.now());
107
+ }
108
+
109
+ /** The registry's fallback template, the one sent for an outcome the kit has no copy for. */
110
+ fallbackTemplate(): string | undefined {
111
+ return (this.options.templates ?? []).find((t) => t.fallback)?.name;
112
+ }
113
+
114
+ /** What the agent declared about a template it is about to send. The language is the registry's, never the sender's guess. */
115
+ declaredTemplate(name: string): WhatsAppTemplate | undefined {
116
+ return this.session.declared(name);
117
+ }
118
+
119
+ async open(): Promise<void> {
120
+ this.options.backend.onStatus?.((status) => this.observeStatus(status));
121
+ await this.options.backend.open();
122
+ }
123
+
124
+ /**
125
+ * The person's next turn. A typed message is the turn. A TAPPED quick reply
126
+ * is a turn only when its id is one a declared template offers, and then the
127
+ * turn is that reply's declared intent, never the button's title and never
128
+ * anything the model makes of it. A tap on an id nobody declared — or on a
129
+ * message this conversation did not send, when the provider names the
130
+ * message — is recorded, reported to the operator and skipped: a button the
131
+ * agent never offered has no meaning to hand on.
132
+ */
133
+ async next(): Promise<InboundMessage | undefined> {
134
+ for (;;) {
135
+ const message = await this.options.backend.next();
136
+ if (!message) return undefined;
137
+ // Any inbound, tap or text, reopens the 24-hour window: the person wrote.
138
+ this.lastInbound = message;
139
+ this.session.observeInbound(message.timestamp);
140
+ const base = {
141
+ at: this.options.now().toISOString(),
142
+ direction: "in" as const,
143
+ contact: maskContact(message.from),
144
+ message_id: message.id,
145
+ state: "delivered" as const,
146
+ // The provider's clock, which is the only one the 24-hour window is
147
+ // counted on. A later run reads the window back from here.
148
+ provider_timestamp: message.timestamp,
149
+ };
150
+ if (!message.reply) {
151
+ this.record({ ...base, kind: "text", text: message.text });
152
+ return message;
153
+ }
154
+ const tapped = { type: message.reply.type, id: message.reply.id, title: message.reply.title };
155
+ const declared = this.replies.get(message.reply.id);
156
+ const refusal = !declared
157
+ ? { rule: "reply_not_offered", detail: `the person tapped ${message.reply.id}, which no template this agent declares offers; it is not a turn` }
158
+ : message.reply.context_id && !this.offeredOn(message.reply.context_id, message.reply.id)
159
+ ? { rule: "reply_not_offered", detail: `the tap names message ${message.reply.context_id}, which this conversation did not send with ${message.reply.id}` }
160
+ : undefined;
161
+ if (refusal) {
162
+ this.record({ ...base, kind: "reply", reply: tapped, refused: refusal });
163
+ this.options.say?.(` [whatsapp] toque ignorado (${refusal.rule}): ${refusal.detail}`);
164
+ continue;
165
+ }
166
+ this.record({ ...base, kind: "reply", reply: tapped, text: declared!.intent });
167
+ return { ...message, text: declared!.intent };
168
+ }
169
+ }
170
+
171
+ /** Whether the message the provider named is one this conversation sent carrying that reply. */
172
+ private offeredOn(messageId: string, replyId: string): boolean {
173
+ return [...(this.options.priorLines ?? []), ...this.lines].some((l) => l.direction === "out" && l.message_id === messageId && (l.offered ?? []).includes(replyId));
174
+ }
175
+
176
+ /**
177
+ * A status webhook. Recorded in the conversation; a `failed` is also the
178
+ * operator's business and, when the message told an outcome, the outcome's:
179
+ * the provider just said the person never got it. A `read` is recorded and
180
+ * nothing more — it says a device displayed the message, which is not
181
+ * consent, not acknowledgement of a debt or an order, and not a reply.
182
+ */
183
+ private observeStatus(status: StatusUpdate): void {
184
+ const sent = [...(this.options.priorLines ?? []), ...this.lines].find((l) => l.direction === "out" && l.message_id === status.message_id);
185
+ this.delivery.set(status.message_id, status.status);
186
+ const line: ChannelLogLine = {
187
+ at: this.options.now().toISOString(),
188
+ direction: "status",
189
+ contact: maskContact(this.conversation.contact),
190
+ kind: "status",
191
+ message_id: status.message_id,
192
+ state: status.status,
193
+ provider_timestamp: status.timestamp,
194
+ ...(sent?.about ? { about: sent.about } : {}),
195
+ ...(status.errors.length ? { errors: status.errors } : {}),
196
+ };
197
+ this.record(line);
198
+ if (status.status !== "failed") return;
199
+ if (!sent) {
200
+ this.earlyFailures.set(status.message_id, line);
201
+ return;
202
+ }
203
+ this.reportFailure(line);
204
+ }
205
+
206
+ private reportFailure(line: ChannelLogLine): void {
207
+ const status = { message_id: line.message_id, errors: line.errors ?? [] };
208
+ const codes = status.errors.map((e) => `${e.code ?? "?"}${e.details ? ` ${e.details}` : ""}`).join("; ") || "no error given";
209
+ this.options.say?.(
210
+ ` [operador] ENTREGA FALHOU da mensagem ${status.message_id} (${codes})${line.about ? ` — era o aviso de ${line.about.execution_id} (${line.about.state}): a pessoa NAO foi avisada` : ""}`,
211
+ );
212
+ this.options.onDeliveryFailed?.(line);
213
+ }
214
+
215
+ /** The latest status the provider reported for a message this process saw a status for. */
216
+ deliveryOf(messageId: string): DeliveryState | undefined {
217
+ return this.delivery.get(messageId);
218
+ }
219
+
220
+ /**
221
+ * Waits for status webhooks that may still be in flight. A provider reports
222
+ * a failure after the send answered, and a process that exits first never
223
+ * hears it; this is the short, bounded wait a caller that has just told an
224
+ * outcome gives the provider before it reports the outcome as told.
225
+ */
226
+ async settle(ms: number): Promise<void> {
227
+ if (ms > 0) await new Promise((resolve) => setTimeout(resolve, ms));
228
+ }
229
+
230
+ async send(body: OutboundBody): Promise<SentMessage> {
231
+ const to = this.conversation.contact;
232
+ const ctx: RuleContext = {
233
+ conversation: this.conversation,
234
+ hours: this.options.hours,
235
+ now: this.options.now,
236
+ knownSubjects: this.options.knownSubjects,
237
+ };
238
+
239
+ // The house rules first: a message the law refuses is not a message the provider should ever see.
240
+ const refusal = checkOutbound(to, body, ctx) ?? this.providerRefusal(body);
241
+ if (refusal) {
242
+ const sent: SentMessage = { id: "", state: "failed", refused: refusal };
243
+ this.logOutbound(body, sent);
244
+ this.options.say?.(` [whatsapp] recusado (${refusal.rule}): ${refusal.detail}`);
245
+ return sent;
246
+ }
247
+
248
+ // A template carries the quick replies its declaration offers, and only those.
249
+ const buttons = body.kind === "template" ? this.session.declared(body.template)?.buttons : undefined;
250
+ const outgoing: OutboundBody = body.kind === "template" && buttons?.length ? { ...body, buttons: buttons.map((b) => ({ id: b.id, title: b.title })) } : body;
251
+ const sent = await this.options.backend.deliver(to, outgoing);
252
+ if (sent.refused) this.options.say?.(` [whatsapp] recusado pelo backend (${sent.refused.rule}): ${sent.refused.detail}`);
253
+ if (sent.refused?.rule === "session_window_closed") this.session.observeProviderShut();
254
+ this.logOutbound(outgoing, sent);
255
+ return sent;
256
+ }
257
+
258
+ say(text: string): Promise<SentMessage> {
259
+ return this.send({ kind: "text", text });
260
+ }
261
+
262
+ async close(): Promise<void> {
263
+ await this.options.backend.close();
264
+ }
265
+
266
+ /** Every message of this conversation, in order. */
267
+ log(): ReadonlyArray<ChannelLogLine> {
268
+ return this.lines;
269
+ }
270
+
271
+ private logOutbound(body: OutboundBody, sent: SentMessage): void {
272
+ const early = sent.id ? this.earlyFailures.get(sent.id) : undefined;
273
+ if (early) {
274
+ this.earlyFailures.delete(sent.id);
275
+ const about = (body.kind === "text" || body.kind === "template") && body.about ? body.about : undefined;
276
+ queueMicrotask(() => this.reportFailure({ ...early, ...(about ? { about } : {}) }));
277
+ }
278
+ this.record({
279
+ at: this.options.now().toISOString(),
280
+ direction: "out",
281
+ contact: maskContact(this.conversation.contact),
282
+ kind: body.kind,
283
+ message_id: sent.id,
284
+ state: sent.state,
285
+ ...(textOf(body) !== undefined ? { text: textOf(body)! } : {}),
286
+ ...(sent.refused ? { refused: sent.refused } : {}),
287
+ ...((body.kind === "text" || body.kind === "template") && body.about ? { about: body.about } : {}),
288
+ ...(body.kind === "template" && body.buttons?.length ? { offered: body.buttons.map((b) => b.id) } : {}),
289
+ });
290
+ }
291
+
292
+ /**
293
+ * WhatsApp's own rule, enforced on both backends: outside the 24 hours
294
+ * after the person's last message only an approved template may go out.
295
+ * The simulator obeys it because a rule that only bites in production is a
296
+ * rule you meet in production.
297
+ */
298
+ private providerRefusal(body: OutboundBody): { rule: string; detail: string } | undefined {
299
+ const open = this.session.open(this.options.now());
300
+ if (body.kind === "template") return this.session.refuse(body);
301
+ if (!open) {
302
+ return {
303
+ rule: "session_window_closed",
304
+ detail: "more than 24h since the person's last message: WhatsApp only carries an approved template now, not a free-form message",
305
+ };
306
+ }
307
+ return undefined;
308
+ }
309
+
310
+ private record(line: ChannelLogLine): void {
311
+ this.lines.push(line);
312
+ this.options.bundle?.channel({ ...line, channel: "whatsapp", backend: this.backend });
313
+ this.options.render?.(` │ ${draw(line)}`);
314
+ }
315
+ }
316
+
317
+ /** One console line for one message. The refusals are visible: a message nobody got is part of the conversation's story. */
318
+ function draw(line: ChannelLogLine): string {
319
+ if (line.direction === "status") return `(status ${line.message_id}: ${line.state}${line.errors?.length ? ` ${line.errors.map((e) => e.code).join(",")}` : ""})`;
320
+ if (line.kind === "reply") return `${line.contact}: [toque: ${line.reply?.title ?? "?"}]${line.refused ? ` (ignorado: ${line.refused.rule})` : ` -> ${line.text ?? ""}`}`;
321
+ const who = line.direction === "in" ? `${line.contact}:` : "loja:";
322
+ const what =
323
+ line.kind === "instrument"
324
+ ? `[copia e cola] ${line.text ?? ""}`
325
+ : line.kind === "media"
326
+ ? "[imagem: QR Pix]"
327
+ : line.kind === "template"
328
+ ? line.text ?? "[template]"
329
+ : line.text ?? "";
330
+ return line.refused ? `${who} (nao enviada: ${line.refused.rule}) ${what}` : `${who} ${what}`;
331
+ }
332
+
333
+ function textOf(body: OutboundBody): string | undefined {
334
+ switch (body.kind) {
335
+ case "text":
336
+ return body.text;
337
+ case "instrument":
338
+ return body.value;
339
+ case "media":
340
+ return body.caption;
341
+ case "template":
342
+ return `[template ${body.template}] ${body.variables.join(" | ")}${body.buttons?.length ? ` [${body.buttons.map((b) => b.title).join("] [")}]` : ""}`;
343
+ }
344
+ }
345
+
346
+ export * from "../contact.js";
347
+ export * from "./cloud-api.js";
348
+ export * from "./emulator.js";
349
+ export * from "./evidence.js";
350
+ export * from "./session.js";