@codespar/cli 0.16.0 → 0.18.0

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 (486) hide show
  1. package/README.md +1 -0
  2. package/dist/commands/mandate-verify.js +98 -19
  3. package/dist/commands/mandate-verify.js.map +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/mandate-codec.js +31 -20
  7. package/dist/mandate-codec.js.map +1 -1
  8. package/package.json +1 -1
  9. package/templates/bills-agent/README.md +1 -1
  10. package/templates/bills-agent/agents/bills-agent/AGENTS.md +1 -1
  11. package/templates/bills-agent/agents/bills-agent/CLAUDE.md +1 -1
  12. package/templates/bills-agent/agents/bills-agent/README.md +6 -5
  13. package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +5 -4
  14. package/templates/bills-agent/packages/agent-core/README.md +14 -1
  15. package/templates/bills-agent/packages/agent-core/package.json +1 -1
  16. package/templates/bills-agent/packages/agent-core/src/agent.ts +39 -1
  17. package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  18. package/templates/bills-agent/packages/agent-core/src/api/client.ts +28 -5
  19. package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  20. package/templates/bills-agent/packages/agent-core/src/api/rail.ts +62 -26
  21. package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  22. package/templates/bills-agent/packages/agent-core/src/approval.ts +14 -2
  23. package/templates/bills-agent/packages/agent-core/src/bundle.ts +35 -3
  24. package/templates/bills-agent/packages/agent-core/src/channels.ts +49 -3
  25. package/templates/bills-agent/packages/agent-core/src/check.ts +25 -2
  26. package/templates/bills-agent/packages/agent-core/src/engine.ts +235 -46
  27. package/templates/bills-agent/packages/agent-core/src/hash.ts +57 -1
  28. package/templates/bills-agent/packages/agent-core/src/index.ts +2 -0
  29. package/templates/bills-agent/packages/agent-core/src/quote.ts +50 -0
  30. package/templates/bills-agent/packages/agent-core/src/rail.ts +89 -4
  31. package/templates/bills-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  32. package/templates/bills-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  33. package/templates/bills-agent/packages/agent-core/src/revocation.ts +1 -1
  34. package/templates/bills-agent/packages/agent-core/src/state-machine.ts +10 -1
  35. package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  36. package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  37. package/templates/bills-agent/packages/agent-core/src/tools.ts +17 -6
  38. package/templates/bills-agent/packages/agent-core/src/types.ts +54 -1
  39. package/templates/bills-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  40. package/templates/bills-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  41. package/templates/bills-agent/packages/agent-core/test/composition.test.ts +204 -0
  42. package/templates/bills-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  43. package/templates/bills-agent/packages/agent-core/test/helpers.ts +4 -1
  44. package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  45. package/templates/bills-agent/packages/agent-core/test/quote.test.ts +198 -0
  46. package/templates/bills-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  47. package/templates/bills-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  48. package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +103 -3
  49. package/templates/bills-agent/packages/agent-runtime/README.md +12 -0
  50. package/templates/bills-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  51. package/templates/bills-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  52. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  53. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  54. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  55. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  56. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  57. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  58. package/templates/bills-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  59. package/templates/bills-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  60. package/templates/bills-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  61. package/templates/bills-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  62. package/templates/bills-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  63. package/templates/bills-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  64. package/templates/bills-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  65. package/templates/bills-agent/packages/agent-runtime/src/index.ts +1 -0
  66. package/templates/bills-agent/packages/agent-runtime/src/inspect.ts +5 -1
  67. package/templates/bills-agent/packages/agent-runtime/src/kit.ts +25 -2
  68. package/templates/bills-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  69. package/templates/bills-agent/packages/agent-runtime/src/setup.ts +13 -3
  70. package/templates/bills-agent/packages/agent-runtime/src/terminal.ts +127 -18
  71. package/templates/bills-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  72. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  73. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  74. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  75. package/templates/checkout-agent/LICENSE +21 -0
  76. package/templates/checkout-agent/README.md +28 -0
  77. package/templates/checkout-agent/_gitignore +10 -0
  78. package/templates/checkout-agent/agents/checkout-agent/.env.example +5 -0
  79. package/templates/checkout-agent/agents/checkout-agent/AGENTS.md +20 -0
  80. package/templates/checkout-agent/agents/checkout-agent/CLAUDE.md +20 -0
  81. package/templates/checkout-agent/agents/checkout-agent/README.md +94 -0
  82. package/templates/checkout-agent/agents/checkout-agent/SYSTEM_PROMPT.md +36 -0
  83. package/templates/checkout-agent/agents/checkout-agent/agent.yaml +30 -0
  84. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/README.md +28 -0
  85. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-beatriz.json +11 -0
  86. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina-retomada.json +10 -0
  87. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina.json +11 -0
  88. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/templates.json +49 -0
  89. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap-human.json +12 -0
  90. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.json +11 -0
  91. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +5 -0
  92. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.json +13 -0
  93. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.transcript.jsonl +4 -0
  94. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.json +26 -0
  95. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
  96. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.json +11 -0
  97. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.transcript.jsonl +5 -0
  98. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.json +12 -0
  99. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.transcript.jsonl +5 -0
  100. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.json +13 -0
  101. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.transcript.jsonl +5 -0
  102. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.json +15 -0
  103. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.transcript.jsonl +7 -0
  104. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.json +12 -0
  105. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.transcript.jsonl +5 -0
  106. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.json +13 -0
  107. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.transcript.jsonl +4 -0
  108. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.json +16 -0
  109. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.transcript.jsonl +9 -0
  110. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.json +15 -0
  111. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.transcript.jsonl +8 -0
  112. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/webhook-replay.json +11 -0
  113. package/templates/checkout-agent/agents/checkout-agent/evals/eval.yaml +11 -0
  114. package/templates/checkout-agent/agents/checkout-agent/guardrails.json +25 -0
  115. package/templates/checkout-agent/agents/checkout-agent/mandate.example.json +28 -0
  116. package/templates/checkout-agent/agents/checkout-agent/package.json +33 -0
  117. package/templates/checkout-agent/agents/checkout-agent/runbook.md +19 -0
  118. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.json +42 -0
  119. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +4 -0
  120. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.json +32 -0
  121. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.transcript.jsonl +4 -0
  122. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.json +61 -0
  123. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.transcript.jsonl +9 -0
  124. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.json +61 -0
  125. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.transcript.jsonl +9 -0
  126. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.json +36 -0
  127. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.transcript.jsonl +6 -0
  128. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.json +23 -0
  129. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.transcript.jsonl +4 -0
  130. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.json +72 -0
  131. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.transcript.jsonl +9 -0
  132. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.json +15 -0
  133. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.transcript.jsonl +6 -0
  134. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.json +23 -0
  135. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.transcript.jsonl +3 -0
  136. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.json +42 -0
  137. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.transcript.jsonl +12 -0
  138. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.json +14 -0
  139. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.transcript.jsonl +9 -0
  140. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.json +34 -0
  141. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.transcript.jsonl +7 -0
  142. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.json +33 -0
  143. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.transcript.jsonl +4 -0
  144. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.json +32 -0
  145. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.transcript.jsonl +4 -0
  146. package/templates/checkout-agent/agents/checkout-agent/src/catalog.ts +92 -0
  147. package/templates/checkout-agent/agents/checkout-agent/src/kit.ts +338 -0
  148. package/templates/checkout-agent/agents/checkout-agent/src/modules/bolepix-receivables.ts +179 -0
  149. package/templates/checkout-agent/agents/checkout-agent/src/modules/nfse-invoice.ts +374 -0
  150. package/templates/checkout-agent/agents/checkout-agent/src/modules/storefront-cart.ts +220 -0
  151. package/templates/checkout-agent/agents/checkout-agent/src/pricing.ts +297 -0
  152. package/templates/checkout-agent/agents/checkout-agent/test/adversarial.test.ts +117 -0
  153. package/templates/checkout-agent/agents/checkout-agent/test/check.test.ts +67 -0
  154. package/templates/checkout-agent/agents/checkout-agent/test/cli.test.ts +169 -0
  155. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/order-marina.transcript.jsonl +5 -0
  156. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/ordered-marina-awaiting-payer.transcript.jsonl +6 -0
  157. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/tap-falar-agora-marina.transcript.jsonl +2 -0
  158. package/templates/checkout-agent/agents/checkout-agent/test/nfse.test.ts +170 -0
  159. package/templates/checkout-agent/agents/checkout-agent/test/pricing.test.ts +138 -0
  160. package/templates/checkout-agent/agents/checkout-agent/test/scenarios.test.ts +113 -0
  161. package/templates/checkout-agent/agents/checkout-agent/test/whatsapp.test.ts +171 -0
  162. package/templates/checkout-agent/agents/checkout-agent/tools.json +68 -0
  163. package/templates/checkout-agent/agents/checkout-agent/tsconfig.json +10 -0
  164. package/templates/checkout-agent/package.json +38 -0
  165. package/templates/checkout-agent/packages/agent-core/README.md +34 -0
  166. package/templates/checkout-agent/packages/agent-core/package.json +24 -0
  167. package/templates/checkout-agent/packages/agent-core/src/agent.ts +163 -0
  168. package/templates/checkout-agent/packages/agent-core/src/api/charge-rail.ts +195 -0
  169. package/templates/checkout-agent/packages/agent-core/src/api/client.ts +76 -0
  170. package/templates/checkout-agent/packages/agent-core/src/api/mandate-status.ts +74 -0
  171. package/templates/checkout-agent/packages/agent-core/src/api/rail.ts +153 -0
  172. package/templates/checkout-agent/packages/agent-core/src/api/sandbox-payer.ts +43 -0
  173. package/templates/checkout-agent/packages/agent-core/src/approval.ts +135 -0
  174. package/templates/checkout-agent/packages/agent-core/src/bundle.ts +191 -0
  175. package/templates/checkout-agent/packages/agent-core/src/channels.ts +186 -0
  176. package/templates/checkout-agent/packages/agent-core/src/check.ts +276 -0
  177. package/templates/checkout-agent/packages/agent-core/src/clock.ts +42 -0
  178. package/templates/checkout-agent/packages/agent-core/src/engine.ts +1074 -0
  179. package/templates/checkout-agent/packages/agent-core/src/escalate.ts +85 -0
  180. package/templates/checkout-agent/packages/agent-core/src/events.ts +40 -0
  181. package/templates/checkout-agent/packages/agent-core/src/guardrails.ts +45 -0
  182. package/templates/checkout-agent/packages/agent-core/src/hash.ts +118 -0
  183. package/templates/checkout-agent/packages/agent-core/src/ids.ts +10 -0
  184. package/templates/checkout-agent/packages/agent-core/src/index.ts +34 -0
  185. package/templates/checkout-agent/packages/agent-core/src/mandate.ts +100 -0
  186. package/templates/checkout-agent/packages/agent-core/src/manifest.ts +76 -0
  187. package/templates/checkout-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
  188. package/templates/checkout-agent/packages/agent-core/src/providers/replay.ts +63 -0
  189. package/templates/checkout-agent/packages/agent-core/src/providers/types.ts +39 -0
  190. package/templates/checkout-agent/packages/agent-core/src/quote.ts +50 -0
  191. package/templates/checkout-agent/packages/agent-core/src/rail.ts +186 -0
  192. package/templates/checkout-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  193. package/templates/checkout-agent/packages/agent-core/src/receipt-verification.ts +577 -0
  194. package/templates/checkout-agent/packages/agent-core/src/revocation.ts +30 -0
  195. package/templates/checkout-agent/packages/agent-core/src/secrets.ts +30 -0
  196. package/templates/checkout-agent/packages/agent-core/src/state/store.ts +307 -0
  197. package/templates/checkout-agent/packages/agent-core/src/state-machine.ts +157 -0
  198. package/templates/checkout-agent/packages/agent-core/src/stubs/charge-rail.ts +195 -0
  199. package/templates/checkout-agent/packages/agent-core/src/stubs/mandate-status.ts +58 -0
  200. package/templates/checkout-agent/packages/agent-core/src/stubs/rail.ts +209 -0
  201. package/templates/checkout-agent/packages/agent-core/src/tools.ts +66 -0
  202. package/templates/checkout-agent/packages/agent-core/src/types.ts +187 -0
  203. package/templates/checkout-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  204. package/templates/checkout-agent/packages/agent-core/test/approval.test.ts +117 -0
  205. package/templates/checkout-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  206. package/templates/checkout-agent/packages/agent-core/test/bundle.test.ts +85 -0
  207. package/templates/checkout-agent/packages/agent-core/test/clock.test.ts +25 -0
  208. package/templates/checkout-agent/packages/agent-core/test/composition.test.ts +204 -0
  209. package/templates/checkout-agent/packages/agent-core/test/engine.test.ts +596 -0
  210. package/templates/checkout-agent/packages/agent-core/test/escalate.test.ts +42 -0
  211. package/templates/checkout-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  212. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
  213. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
  214. package/templates/checkout-agent/packages/agent-core/test/helpers.ts +142 -0
  215. package/templates/checkout-agent/packages/agent-core/test/mandate-status.test.ts +229 -0
  216. package/templates/checkout-agent/packages/agent-core/test/manifest.test.ts +45 -0
  217. package/templates/checkout-agent/packages/agent-core/test/quote.test.ts +198 -0
  218. package/templates/checkout-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  219. package/templates/checkout-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  220. package/templates/checkout-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
  221. package/templates/checkout-agent/packages/agent-core/test/receivables.test.ts +416 -0
  222. package/templates/checkout-agent/packages/agent-core/test/secrets.test.ts +28 -0
  223. package/templates/checkout-agent/packages/agent-core/test/state-machine.test.ts +67 -0
  224. package/templates/checkout-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
  225. package/templates/checkout-agent/packages/agent-core/tsconfig.json +10 -0
  226. package/templates/checkout-agent/packages/agent-runtime/README.md +80 -0
  227. package/templates/checkout-agent/packages/agent-runtime/bin.mjs +32 -0
  228. package/templates/checkout-agent/packages/agent-runtime/package.json +23 -0
  229. package/templates/checkout-agent/packages/agent-runtime/src/adversarial.ts +199 -0
  230. package/templates/checkout-agent/packages/agent-runtime/src/agent.ts +68 -0
  231. package/templates/checkout-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
  232. package/templates/checkout-agent/packages/agent-runtime/src/channels/index.ts +72 -0
  233. package/templates/checkout-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
  234. package/templates/checkout-agent/packages/agent-runtime/src/channels/types.ts +173 -0
  235. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +459 -0
  236. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +216 -0
  237. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
  238. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +350 -0
  239. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +180 -0
  240. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
  241. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +120 -0
  242. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +127 -0
  243. package/templates/checkout-agent/packages/agent-runtime/src/cli.ts +98 -0
  244. package/templates/checkout-agent/packages/agent-runtime/src/commands/check.ts +15 -0
  245. package/templates/checkout-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
  246. package/templates/checkout-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
  247. package/templates/checkout-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
  248. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +367 -0
  249. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll.ts +145 -0
  250. package/templates/checkout-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
  251. package/templates/checkout-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
  252. package/templates/checkout-agent/packages/agent-runtime/src/commands/resume.ts +49 -0
  253. package/templates/checkout-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +134 -0
  254. package/templates/checkout-agent/packages/agent-runtime/src/commands/start.ts +248 -0
  255. package/templates/checkout-agent/packages/agent-runtime/src/commands/verify.ts +330 -0
  256. package/templates/checkout-agent/packages/agent-runtime/src/commands/webhook.ts +40 -0
  257. package/templates/checkout-agent/packages/agent-runtime/src/default-kit.ts +83 -0
  258. package/templates/checkout-agent/packages/agent-runtime/src/index.ts +13 -0
  259. package/templates/checkout-agent/packages/agent-runtime/src/inspect.ts +801 -0
  260. package/templates/checkout-agent/packages/agent-runtime/src/kit.ts +229 -0
  261. package/templates/checkout-agent/packages/agent-runtime/src/poll.ts +105 -0
  262. package/templates/checkout-agent/packages/agent-runtime/src/scenarios.ts +274 -0
  263. package/templates/checkout-agent/packages/agent-runtime/src/setup.ts +239 -0
  264. package/templates/checkout-agent/packages/agent-runtime/src/terminal.ts +263 -0
  265. package/templates/checkout-agent/packages/agent-runtime/src/webhook.ts +131 -0
  266. package/templates/checkout-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
  267. package/templates/checkout-agent/packages/agent-runtime/test/verify-cli.test.ts +329 -0
  268. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +277 -0
  269. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
  270. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
  271. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-session.test.ts +167 -0
  272. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  273. package/templates/checkout-agent/packages/agent-runtime/tsconfig.json +9 -0
  274. package/templates/checkout-agent/scripts/whatsapp-emulator.mjs +84 -0
  275. package/templates/checkout-agent/tsconfig.base.json +16 -0
  276. package/templates/checkout-agent/vitest.config.ts +12 -0
  277. package/templates/collections-agent/README.md +6 -1
  278. package/templates/collections-agent/agents/collections-agent/README.md +12 -8
  279. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/README.md +8 -0
  280. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/acordo-1042-retomada.json +10 -0
  281. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/templates.json +27 -1
  282. package/templates/collections-agent/agents/collections-agent/src/kit.ts +2 -0
  283. package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +12 -2
  284. package/templates/collections-agent/agents/collections-agent/test/ambiguous-charge.test.ts +55 -0
  285. package/templates/collections-agent/agents/collections-agent/test/fixtures/tap-emitir-nova-1042.transcript.jsonl +2 -0
  286. package/templates/collections-agent/agents/collections-agent/test/whatsapp-poll.test.ts +12 -6
  287. package/templates/collections-agent/agents/collections-agent/test/whatsapp-status-replies.test.ts +148 -0
  288. package/templates/collections-agent/agents/collections-agent/test/whatsapp.test.ts +5 -0
  289. package/templates/collections-agent/package.json +1 -0
  290. package/templates/collections-agent/packages/agent-core/README.md +14 -1
  291. package/templates/collections-agent/packages/agent-core/package.json +1 -1
  292. package/templates/collections-agent/packages/agent-core/src/agent.ts +39 -1
  293. package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  294. package/templates/collections-agent/packages/agent-core/src/api/client.ts +28 -5
  295. package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  296. package/templates/collections-agent/packages/agent-core/src/api/rail.ts +62 -26
  297. package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  298. package/templates/collections-agent/packages/agent-core/src/approval.ts +14 -2
  299. package/templates/collections-agent/packages/agent-core/src/bundle.ts +35 -3
  300. package/templates/collections-agent/packages/agent-core/src/channels.ts +49 -3
  301. package/templates/collections-agent/packages/agent-core/src/check.ts +25 -2
  302. package/templates/collections-agent/packages/agent-core/src/engine.ts +235 -46
  303. package/templates/collections-agent/packages/agent-core/src/hash.ts +57 -1
  304. package/templates/collections-agent/packages/agent-core/src/index.ts +2 -0
  305. package/templates/collections-agent/packages/agent-core/src/quote.ts +50 -0
  306. package/templates/collections-agent/packages/agent-core/src/rail.ts +89 -4
  307. package/templates/collections-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  308. package/templates/collections-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  309. package/templates/collections-agent/packages/agent-core/src/revocation.ts +1 -1
  310. package/templates/collections-agent/packages/agent-core/src/state-machine.ts +10 -1
  311. package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  312. package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  313. package/templates/collections-agent/packages/agent-core/src/tools.ts +17 -6
  314. package/templates/collections-agent/packages/agent-core/src/types.ts +54 -1
  315. package/templates/collections-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  316. package/templates/collections-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  317. package/templates/collections-agent/packages/agent-core/test/composition.test.ts +204 -0
  318. package/templates/collections-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  319. package/templates/collections-agent/packages/agent-core/test/helpers.ts +4 -1
  320. package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  321. package/templates/collections-agent/packages/agent-core/test/quote.test.ts +198 -0
  322. package/templates/collections-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  323. package/templates/collections-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  324. package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +103 -3
  325. package/templates/collections-agent/packages/agent-runtime/README.md +12 -0
  326. package/templates/collections-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  327. package/templates/collections-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  328. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  329. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  330. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  331. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  332. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  333. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  334. package/templates/collections-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  335. package/templates/collections-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  336. package/templates/collections-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  337. package/templates/collections-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  338. package/templates/collections-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  339. package/templates/collections-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  340. package/templates/collections-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  341. package/templates/collections-agent/packages/agent-runtime/src/index.ts +1 -0
  342. package/templates/collections-agent/packages/agent-runtime/src/inspect.ts +5 -1
  343. package/templates/collections-agent/packages/agent-runtime/src/kit.ts +25 -2
  344. package/templates/collections-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  345. package/templates/collections-agent/packages/agent-runtime/src/setup.ts +13 -3
  346. package/templates/collections-agent/packages/agent-runtime/src/terminal.ts +127 -18
  347. package/templates/collections-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  348. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  349. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  350. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  351. package/templates/collections-agent/scripts/whatsapp-emulator.mjs +84 -0
  352. package/templates/hello-agent/README.md +1 -1
  353. package/templates/hello-agent/packages/agent-core/README.md +14 -1
  354. package/templates/hello-agent/packages/agent-core/package.json +1 -1
  355. package/templates/hello-agent/packages/agent-core/src/agent.ts +39 -1
  356. package/templates/hello-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  357. package/templates/hello-agent/packages/agent-core/src/api/client.ts +28 -5
  358. package/templates/hello-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  359. package/templates/hello-agent/packages/agent-core/src/api/rail.ts +62 -26
  360. package/templates/hello-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  361. package/templates/hello-agent/packages/agent-core/src/approval.ts +14 -2
  362. package/templates/hello-agent/packages/agent-core/src/bundle.ts +35 -3
  363. package/templates/hello-agent/packages/agent-core/src/channels.ts +49 -3
  364. package/templates/hello-agent/packages/agent-core/src/check.ts +25 -2
  365. package/templates/hello-agent/packages/agent-core/src/engine.ts +235 -46
  366. package/templates/hello-agent/packages/agent-core/src/hash.ts +57 -1
  367. package/templates/hello-agent/packages/agent-core/src/index.ts +2 -0
  368. package/templates/hello-agent/packages/agent-core/src/quote.ts +50 -0
  369. package/templates/hello-agent/packages/agent-core/src/rail.ts +89 -4
  370. package/templates/hello-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  371. package/templates/hello-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  372. package/templates/hello-agent/packages/agent-core/src/revocation.ts +1 -1
  373. package/templates/hello-agent/packages/agent-core/src/state-machine.ts +10 -1
  374. package/templates/hello-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  375. package/templates/hello-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  376. package/templates/hello-agent/packages/agent-core/src/tools.ts +17 -6
  377. package/templates/hello-agent/packages/agent-core/src/types.ts +54 -1
  378. package/templates/hello-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  379. package/templates/hello-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  380. package/templates/hello-agent/packages/agent-core/test/composition.test.ts +204 -0
  381. package/templates/hello-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  382. package/templates/hello-agent/packages/agent-core/test/helpers.ts +4 -1
  383. package/templates/hello-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  384. package/templates/hello-agent/packages/agent-core/test/quote.test.ts +198 -0
  385. package/templates/hello-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  386. package/templates/hello-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  387. package/templates/hello-agent/packages/agent-core/test/receivables.test.ts +103 -3
  388. package/templates/hello-agent/packages/agent-runtime/README.md +12 -0
  389. package/templates/hello-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  390. package/templates/hello-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  391. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  392. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  393. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  394. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  395. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  396. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  397. package/templates/hello-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  398. package/templates/hello-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  399. package/templates/hello-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  400. package/templates/hello-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  401. package/templates/hello-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  402. package/templates/hello-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  403. package/templates/hello-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  404. package/templates/hello-agent/packages/agent-runtime/src/index.ts +1 -0
  405. package/templates/hello-agent/packages/agent-runtime/src/inspect.ts +5 -1
  406. package/templates/hello-agent/packages/agent-runtime/src/kit.ts +25 -2
  407. package/templates/hello-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  408. package/templates/hello-agent/packages/agent-runtime/src/setup.ts +13 -3
  409. package/templates/hello-agent/packages/agent-runtime/src/terminal.ts +127 -18
  410. package/templates/hello-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  411. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  412. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  413. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  414. package/templates/kits.lock.json +29 -6
  415. package/templates/supplier-payments-agent/README.md +1 -1
  416. package/templates/supplier-payments-agent/agents/supplier-payments-agent/AGENTS.md +1 -1
  417. package/templates/supplier-payments-agent/agents/supplier-payments-agent/CLAUDE.md +1 -1
  418. package/templates/supplier-payments-agent/agents/supplier-payments-agent/README.md +11 -9
  419. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/kit.ts +1 -1
  420. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/modules/batch-payout.ts +67 -20
  421. package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/batch.test.ts +504 -5
  422. package/templates/supplier-payments-agent/packages/agent-core/README.md +14 -1
  423. package/templates/supplier-payments-agent/packages/agent-core/package.json +1 -1
  424. package/templates/supplier-payments-agent/packages/agent-core/src/agent.ts +39 -1
  425. package/templates/supplier-payments-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  426. package/templates/supplier-payments-agent/packages/agent-core/src/api/client.ts +28 -5
  427. package/templates/supplier-payments-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  428. package/templates/supplier-payments-agent/packages/agent-core/src/api/rail.ts +62 -26
  429. package/templates/supplier-payments-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  430. package/templates/supplier-payments-agent/packages/agent-core/src/approval.ts +14 -2
  431. package/templates/supplier-payments-agent/packages/agent-core/src/bundle.ts +35 -3
  432. package/templates/supplier-payments-agent/packages/agent-core/src/channels.ts +49 -3
  433. package/templates/supplier-payments-agent/packages/agent-core/src/check.ts +25 -2
  434. package/templates/supplier-payments-agent/packages/agent-core/src/engine.ts +235 -46
  435. package/templates/supplier-payments-agent/packages/agent-core/src/hash.ts +57 -1
  436. package/templates/supplier-payments-agent/packages/agent-core/src/index.ts +2 -0
  437. package/templates/supplier-payments-agent/packages/agent-core/src/quote.ts +50 -0
  438. package/templates/supplier-payments-agent/packages/agent-core/src/rail.ts +89 -4
  439. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  440. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  441. package/templates/supplier-payments-agent/packages/agent-core/src/revocation.ts +1 -1
  442. package/templates/supplier-payments-agent/packages/agent-core/src/state-machine.ts +10 -1
  443. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  444. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  445. package/templates/supplier-payments-agent/packages/agent-core/src/tools.ts +17 -6
  446. package/templates/supplier-payments-agent/packages/agent-core/src/types.ts +54 -1
  447. package/templates/supplier-payments-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  448. package/templates/supplier-payments-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  449. package/templates/supplier-payments-agent/packages/agent-core/test/composition.test.ts +204 -0
  450. package/templates/supplier-payments-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  451. package/templates/supplier-payments-agent/packages/agent-core/test/helpers.ts +4 -1
  452. package/templates/supplier-payments-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  453. package/templates/supplier-payments-agent/packages/agent-core/test/quote.test.ts +198 -0
  454. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  455. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  456. package/templates/supplier-payments-agent/packages/agent-core/test/receivables.test.ts +103 -3
  457. package/templates/supplier-payments-agent/packages/agent-runtime/README.md +12 -0
  458. package/templates/supplier-payments-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  459. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  460. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  461. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  462. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  463. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  464. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  465. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  466. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  467. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  468. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  469. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  470. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  471. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  472. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  473. package/templates/supplier-payments-agent/packages/agent-runtime/src/index.ts +1 -0
  474. package/templates/supplier-payments-agent/packages/agent-runtime/src/inspect.ts +5 -1
  475. package/templates/supplier-payments-agent/packages/agent-runtime/src/kit.ts +25 -2
  476. package/templates/supplier-payments-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  477. package/templates/supplier-payments-agent/packages/agent-runtime/src/setup.ts +13 -3
  478. package/templates/supplier-payments-agent/packages/agent-runtime/src/terminal.ts +127 -18
  479. package/templates/supplier-payments-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  480. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  481. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  482. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  483. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  484. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  485. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  486. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
@@ -0,0 +1,173 @@
1
+ /**
2
+ * One interface, several backends. What a channel is, from the runner's side:
3
+ * a place a person writes from, a place the agent writes to, and a record of
4
+ * what happened to each message.
5
+ *
6
+ * The shape is deliberately the shape of a MESSAGING channel and not of a
7
+ * terminal, because the two are not the same thing and pretending they are is
8
+ * how an approval question ends up in a debtor's chat. A terminal has one
9
+ * stream and one reader; a channel has a counterparty on one side and an
10
+ * operator somewhere else entirely, and the runner keeps them apart by
11
+ * construction: `send` reaches the counterparty, and the operator's console
12
+ * and questions never touch it.
13
+ *
14
+ * Every backend answers the same five questions, and the ADAPTER above them
15
+ * (`whatsapp/index.ts`) is where the rules live, so a rule cannot be skipped
16
+ * by choosing a different backend.
17
+ */
18
+ import type { ChannelName } from "@codespar/agent-core";
19
+
20
+ /**
21
+ * What a provider tells us about a message after it left. `queued` is ours
22
+ * (accepted, not yet handed over); the rest are the provider's own, and a
23
+ * backend that cannot observe one never invents it.
24
+ */
25
+ export type DeliveryState = "queued" | "sent" | "delivered" | "read" | "failed";
26
+
27
+ export interface InboundMessage {
28
+ /** The PROVIDER's message id (`wamid....` on WhatsApp). Never ours. */
29
+ id: string;
30
+ /** Who wrote it, E.164 on WhatsApp. */
31
+ from: string;
32
+ /**
33
+ * What the person wrote. For a TAP (`reply` set) the backend puts the
34
+ * button's title here, and the channel replaces it with the intent the
35
+ * template that offered the button declared — the model never reads a
36
+ * title as if the person had typed it.
37
+ */
38
+ text: string;
39
+ /** Unix seconds on the PROVIDER's clock, which is not this process's clock. */
40
+ timestamp: number;
41
+ /** Set when the person tapped a quick reply instead of typing. */
42
+ reply?: InboundReply;
43
+ }
44
+
45
+ /** A tapped quick reply, as the provider reports it: which button, and on which message when it says. */
46
+ export interface InboundReply {
47
+ /** `button_reply` / `list_reply` (an interactive message), `button` (a template's quick reply). */
48
+ type: "button_reply" | "list_reply" | "button";
49
+ id: string;
50
+ title: string;
51
+ /** The message the button was on (`context.id`), when the provider sends it. */
52
+ context_id?: string;
53
+ }
54
+
55
+ /**
56
+ * What the provider reports about a message the agent sent, after it left.
57
+ * `failed` carries Meta's errors (131026: not a WhatsApp number, or it blocked
58
+ * the business). Delivered by webhook, at any time, to whoever is listening.
59
+ */
60
+ export interface StatusUpdate {
61
+ message_id: string;
62
+ status: DeliveryState;
63
+ /** Unix seconds, the provider's clock. */
64
+ timestamp: number;
65
+ errors: Array<{ code?: number; title?: string; details?: string }>;
66
+ }
67
+
68
+ /** The execution an outbound message tells the outcome of: what a failed delivery of it means. */
69
+ export interface OutcomeTag {
70
+ execution_id: string;
71
+ state: string;
72
+ }
73
+
74
+ /**
75
+ * What the agent can put on a channel.
76
+ *
77
+ * `text` is the ordinary message. `media` is a QR, which on WhatsApp is an
78
+ * image and never ASCII art. `template` is the only thing a provider accepts
79
+ * outside the 24-hour session window, which is why it is a separate kind
80
+ * rather than a flag.
81
+ *
82
+ * `instrument` is the Pix copy-and-paste or the boleto line, and it is its own
83
+ * kind for a reason that is not cosmetic: those strings are minted by the rail
84
+ * and are long runs of digits, and the rule that keeps a CPF out of a message
85
+ * would refuse a boleto line (whose last field is fourteen digits) if it read
86
+ * one as prose. Separating them says which strings the conversation composed
87
+ * and which the rail did, and only the first are checked for a document.
88
+ */
89
+ export type OutboundBody =
90
+ | { kind: "text"; text: string; about?: OutcomeTag }
91
+ | { kind: "media"; media: "qr"; data: string; caption?: string }
92
+ | { kind: "instrument"; instrument: "pix_copy_paste" | "boleto_bank_line"; value: string }
93
+ /** `buttons` are the declaration's, attached by the channel; a caller never composes them. */
94
+ | { kind: "template"; template: string; language: string; variables: string[]; about?: OutcomeTag; buttons?: Array<{ id: string; title: string }> };
95
+
96
+ export interface SentMessage {
97
+ /** The provider's id when it accepted one, else the id the backend minted. */
98
+ id: string;
99
+ state: DeliveryState;
100
+ /**
101
+ * Set when something REFUSED the message. Nothing left the process: a
102
+ * refusal here is not a delivery failure, it is the message never having
103
+ * been sent. `rule` is the stable name a test and a log both key on.
104
+ */
105
+ refused?: { rule: string; detail: string };
106
+ }
107
+
108
+ /** What the person on the other side is called, and what the conversation may be about. */
109
+ export interface Conversation {
110
+ /** The contact the conversation is bound to. Every outbound goes here and nowhere else. */
111
+ contact: string;
112
+ /** What the conversation may name — an agreement alias. Absent means the channel enforces no subject rule. */
113
+ subject?: string | undefined;
114
+ }
115
+
116
+ export interface ChannelBackend {
117
+ /** `simulator`, `cloud-api`. Named in the console, in the bundle and in the gate's output. */
118
+ readonly name: string;
119
+ /** Whether this backend talks to a real provider. The gate and the tests refuse to run a live one. */
120
+ readonly live: boolean;
121
+ open(): Promise<void>;
122
+ /** The next inbound message, or `undefined` when the conversation is over. */
123
+ next(): Promise<InboundMessage | undefined>;
124
+ /** Hands the message to the provider. Rules have already run: a backend never re-decides policy. */
125
+ deliver(to: string, body: OutboundBody): Promise<SentMessage>;
126
+ /** Where the status webhooks go. A backend that cannot observe statuses never calls it. */
127
+ onStatus?(listener: (status: StatusUpdate) => void): void;
128
+ close(): Promise<void>;
129
+ }
130
+
131
+ export interface Channel {
132
+ readonly name: ChannelName;
133
+ readonly backend: string;
134
+ readonly conversation: Conversation;
135
+ open(): Promise<void>;
136
+ next(): Promise<InboundMessage | undefined>;
137
+ /** Applies the channel's rules, then delivers. A refused message is recorded and never sent. */
138
+ send(body: OutboundBody): Promise<SentMessage>;
139
+ /** Convenience for the common case. */
140
+ say(text: string): Promise<SentMessage>;
141
+ close(): Promise<void>;
142
+ /** Every message of this conversation, in order, as the bundle records it. */
143
+ log(): ReadonlyArray<ChannelLogLine>;
144
+ }
145
+
146
+ export interface ChannelLogLine {
147
+ at: string;
148
+ /** `status`: what the provider reported about an outbound message, after it left. */
149
+ direction: "in" | "out" | "status";
150
+ /** The contact, MASKED. The raw value never reaches a file. */
151
+ contact: string;
152
+ /** `reply`: an inbound tap. `status`: a delivery status. */
153
+ kind: OutboundBody["kind"] | "reply" | "status";
154
+ /** The provider's id, or ours when nothing was sent. */
155
+ message_id: string;
156
+ state: DeliveryState;
157
+ text?: string;
158
+ /**
159
+ * Inbound only: the PROVIDER's own timestamp, unix seconds. Recorded
160
+ * because the 24-hour session window is counted on WhatsApp's clock and
161
+ * `at` is this process's, and a later run has to read the window back from
162
+ * this log rather than from a clock it does not share.
163
+ */
164
+ provider_timestamp?: number;
165
+ refused?: { rule: string; detail: string };
166
+ /** Outbound: the execution whose outcome this message tells. Status: the same, looked up by message id. */
167
+ about?: OutcomeTag;
168
+ /** Outbound template: the reply ids it offered. Inbound tap: the reply tapped. */
169
+ offered?: string[];
170
+ reply?: { type: InboundReply["type"]; id: string; title: string };
171
+ /** Status `failed`: Meta's errors, as reported. */
172
+ errors?: StatusUpdate["errors"];
173
+ }
@@ -0,0 +1,459 @@
1
+ /**
2
+ * The official backend: Meta's WhatsApp Cloud API, behind the same interface
3
+ * the simulator implements. An ADAPTER and nothing more — the rules, the
4
+ * session window and the conversation record are the channel's, above this.
5
+ *
6
+ * Three things about it are deliberate.
7
+ *
8
+ * The credentials are ABSENT by default and this backend refuses to open
9
+ * without them, by name. No fallback, no "development mode" that quietly
10
+ * talks to nobody: a channel that pretends to send is worse than one that
11
+ * says it cannot.
12
+ *
13
+ * Nothing here is called from a test or from the CI, ever. What the tests
14
+ * exercise are the four pure functions — the signature check, the webhook
15
+ * parse, the verification handshake and the request builder — which is
16
+ * exactly the part where a mistake is silent. The send itself is one `fetch`
17
+ * over a request those functions built.
18
+ *
19
+ * And the shapes are written against Meta's published documentation, not
20
+ * against a call we made: nobody on this repo has a Business account, so the
21
+ * first live run belongs to the developer who has one. The README says that
22
+ * in those words, and `docs/OPEN_QUESTIONS.md` names what a live run would
23
+ * settle.
24
+ */
25
+ import { createHmac, timingSafeEqual } from "node:crypto";
26
+ import { createServer, type Server } from "node:http";
27
+ import type { ChannelBackend, DeliveryState, InboundMessage, InboundReply, OutboundBody, SentMessage, StatusUpdate } from "../types.js";
28
+
29
+ export interface CloudApiConfig {
30
+ /**
31
+ * Where the Graph API lives. Meta's own by default, and the local emulator's
32
+ * when the run is against the local emulator — which is the whole point of
33
+ * having it be a field: the two backends are THE SAME CODE with a different
34
+ * base URL, so the simulator exercises the adapter instead of standing in
35
+ * for it. A mock of our own would only ever agree with us.
36
+ */
37
+ baseUrl: string;
38
+ /** The number the business sends from, as Meta's dashboard names it. */
39
+ phoneNumberId: string;
40
+ accessToken: string;
41
+ /** Echoed back on the `GET` handshake when Meta registers the webhook. */
42
+ verifyToken: string;
43
+ /** The app secret that signs deliveries. Without it no delivery is trusted. */
44
+ appSecret: string;
45
+ apiVersion: string;
46
+ webhookPort: number;
47
+ }
48
+
49
+ /** Meta's own Graph host. Anything else is a stand-in, and the channel treats it as one. */
50
+ export const META_GRAPH_HOST = "graph.facebook.com";
51
+
52
+ /**
53
+ * Whether this base URL is Meta. It decides one thing that matters beyond
54
+ * logging: `live`, which is what the consent-evidence builder refuses on. An
55
+ * act observed by an emulator was observed by nobody.
56
+ */
57
+ export function isMetaBaseUrl(baseUrl: string): boolean {
58
+ try {
59
+ return new URL(baseUrl).hostname === META_GRAPH_HOST;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ export const CLOUD_API_ENV = {
66
+ phoneNumberId: "WHATSAPP_PHONE_NUMBER_ID",
67
+ accessToken: "WHATSAPP_ACCESS_TOKEN",
68
+ verifyToken: "WHATSAPP_VERIFY_TOKEN",
69
+ appSecret: "WHATSAPP_APP_SECRET",
70
+ apiVersion: "WHATSAPP_API_VERSION",
71
+ webhookPort: "WHATSAPP_WEBHOOK_PORT",
72
+ baseUrl: "WHATSAPP_GRAPH_URL",
73
+ } as const;
74
+
75
+ export interface ConfigResult {
76
+ config?: CloudApiConfig;
77
+ /** The variables that are not set. A refusal names them rather than saying "misconfigured". */
78
+ missing: string[];
79
+ }
80
+
81
+ export function loadCloudApiConfig(env: NodeJS.ProcessEnv): ConfigResult {
82
+ const read = (name: string) => {
83
+ const v = env[name]?.trim();
84
+ return v ? v : undefined;
85
+ };
86
+ const phoneNumberId = read(CLOUD_API_ENV.phoneNumberId);
87
+ const accessToken = read(CLOUD_API_ENV.accessToken);
88
+ const verifyToken = read(CLOUD_API_ENV.verifyToken);
89
+ const appSecret = read(CLOUD_API_ENV.appSecret);
90
+ const missing = [
91
+ ...(phoneNumberId ? [] : [CLOUD_API_ENV.phoneNumberId]),
92
+ ...(accessToken ? [] : [CLOUD_API_ENV.accessToken]),
93
+ ...(verifyToken ? [] : [CLOUD_API_ENV.verifyToken]),
94
+ ...(appSecret ? [] : [CLOUD_API_ENV.appSecret]),
95
+ ];
96
+ if (!phoneNumberId || !accessToken || !verifyToken || !appSecret) return { missing };
97
+ return {
98
+ missing,
99
+ config: {
100
+ baseUrl: read(CLOUD_API_ENV.baseUrl) ?? `https://${META_GRAPH_HOST}`,
101
+ phoneNumberId,
102
+ accessToken,
103
+ verifyToken,
104
+ appSecret,
105
+ apiVersion: read(CLOUD_API_ENV.apiVersion) ?? "v21.0",
106
+ webhookPort: Number(read(CLOUD_API_ENV.webhookPort) ?? 3111),
107
+ },
108
+ };
109
+ }
110
+
111
+ /* ── the pure half: what the tests drive ─────────────────────── */
112
+
113
+ /**
114
+ * `X-Hub-Signature-256: sha256=<hex>`, HMAC-SHA256 of the RAW body under the
115
+ * app secret. Raw and not re-serialized: a body that round-tripped through
116
+ * `JSON.parse` is a different string, and the signature is over bytes.
117
+ */
118
+ export function verifyWebhookSignature(appSecret: string, header: string | undefined, rawBody: string): { ok: true } | { ok: false; reason: string } {
119
+ if (!header) return { ok: false, reason: "missing X-Hub-Signature-256" };
120
+ const [algorithm, signature] = header.split("=");
121
+ if (algorithm !== "sha256" || !signature) return { ok: false, reason: "malformed signature header" };
122
+ const expected = createHmac("sha256", appSecret).update(rawBody, "utf8").digest("hex");
123
+ if (expected.length !== signature.length) return { ok: false, reason: "signature mismatch" };
124
+ if (!timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(signature, "hex"))) return { ok: false, reason: "signature mismatch" };
125
+ return { ok: true };
126
+ }
127
+
128
+ /** The `GET` handshake Meta makes once, when the webhook URL is registered. */
129
+ export function verifyChallenge(config: CloudApiConfig, query: Record<string, string | undefined>): { status: number; body: string } {
130
+ if (query["hub.mode"] !== "subscribe") return { status: 400, body: "bad mode" };
131
+ if (query["hub.verify_token"] !== config.verifyToken) return { status: 403, body: "bad verify token" };
132
+ return { status: 200, body: query["hub.challenge"] ?? "" };
133
+ }
134
+
135
+ interface CloudApiEnvelope {
136
+ object?: string;
137
+ entry?: Array<{
138
+ changes?: Array<{
139
+ field?: string;
140
+ value?: {
141
+ messaging_product?: string;
142
+ metadata?: { phone_number_id?: string };
143
+ messages?: Array<{
144
+ id?: string;
145
+ from?: string;
146
+ timestamp?: string;
147
+ type?: string;
148
+ text?: { body?: string };
149
+ interactive?: { type?: string; button_reply?: { id?: string; title?: string }; list_reply?: { id?: string; title?: string } };
150
+ button?: { payload?: string; text?: string };
151
+ context?: { id?: string };
152
+ }>;
153
+ statuses?: Array<{ id?: string; status?: string; timestamp?: string; errors?: Array<{ code?: number; title?: string; message?: string; error_data?: { details?: string } }> }>;
154
+ };
155
+ }>;
156
+ }>;
157
+ }
158
+
159
+ /**
160
+ * What one delivery carries. A delivery holds several entries, each with
161
+ * several changes, each with messages and with STATUS updates about messages
162
+ * we sent. A message is read when it is text, or a tapped quick reply (an
163
+ * interactive `button_reply` / `list_reply`, or a template's `button`), which
164
+ * keeps its id — the channel decides whether that id means anything. Anything
165
+ * else (an image, a location) is dropped with its id named: this channel
166
+ * reads words and taps, and an unreadable message must not become an empty
167
+ * turn.
168
+ */
169
+ export function parseInbound(payload: unknown): { messages: InboundMessage[]; statuses: StatusUpdate[]; ignored: Array<{ id: string; type: string }> } {
170
+ const envelope = payload as CloudApiEnvelope;
171
+ const messages: InboundMessage[] = [];
172
+ const statuses: StatusUpdate[] = [];
173
+ const ignored: Array<{ id: string; type: string }> = [];
174
+ for (const entry of envelope.entry ?? []) {
175
+ for (const change of entry.changes ?? []) {
176
+ for (const message of change.value?.messages ?? []) {
177
+ const id = message.id;
178
+ const from = message.from;
179
+ if (!id || !from) continue;
180
+ const base = { id, from: from.startsWith("+") ? from : `+${from}`, timestamp: Number(message.timestamp ?? 0) };
181
+ const reply = replyOf(message);
182
+ if (reply) {
183
+ messages.push({ ...base, text: reply.title, reply: { ...reply, ...(message.context?.id ? { context_id: message.context.id } : {}) } });
184
+ continue;
185
+ }
186
+ if (message.type !== "text" || !message.text?.body) {
187
+ ignored.push({ id, type: message.type === "interactive" ? `interactive:${message.interactive?.type ?? "unknown"}` : message.type ?? "unknown" });
188
+ continue;
189
+ }
190
+ messages.push({ ...base, text: message.text.body });
191
+ }
192
+ for (const status of change.value?.statuses ?? []) {
193
+ if (!status.id || !isDeliveryState(status.status)) continue;
194
+ statuses.push({
195
+ message_id: status.id,
196
+ status: status.status,
197
+ timestamp: Number(status.timestamp ?? 0),
198
+ errors: (status.errors ?? []).map((e) => ({
199
+ ...(typeof e.code === "number" ? { code: e.code } : {}),
200
+ ...(e.title ?? e.message ? { title: e.title ?? e.message } : {}),
201
+ ...(e.error_data?.details ? { details: e.error_data.details } : {}),
202
+ })),
203
+ });
204
+ }
205
+ }
206
+ }
207
+ return { messages, statuses, ignored };
208
+ }
209
+
210
+ type CloudMessage = NonNullable<NonNullable<NonNullable<NonNullable<CloudApiEnvelope["entry"]>[number]["changes"]>[number]["value"]>["messages"]>[number];
211
+
212
+ function replyOf(message: CloudMessage): Omit<InboundReply, "context_id"> | undefined {
213
+ if (message.type === "interactive") {
214
+ const kind = message.interactive?.type;
215
+ const picked = kind === "button_reply" ? message.interactive?.button_reply : kind === "list_reply" ? message.interactive?.list_reply : undefined;
216
+ if ((kind === "button_reply" || kind === "list_reply") && picked?.id) return { type: kind, id: picked.id, title: picked.title ?? picked.id };
217
+ return undefined;
218
+ }
219
+ if (message.type === "button" && message.button?.payload) return { type: "button", id: message.button.payload, title: message.button.text ?? message.button.payload };
220
+ return undefined;
221
+ }
222
+
223
+ function isDeliveryState(value: unknown): value is DeliveryState {
224
+ return value === "sent" || value === "delivered" || value === "read" || value === "failed";
225
+ }
226
+
227
+ export interface SendRequest {
228
+ url: string;
229
+ method: "POST";
230
+ headers: Record<string, string>;
231
+ body: string;
232
+ }
233
+
234
+ /**
235
+ * The `/messages` call, as Meta documents it. `to` loses its `+`: the Cloud
236
+ * API wants the number in international format without the plus.
237
+ *
238
+ * `media` has no branch here and that is the honest answer, not an omission.
239
+ * Sending an image means uploading it first (`POST /{version}/{phone-number-id}/media`,
240
+ * multipart) or handing Meta a public URL, and this repo hosts nothing and
241
+ * renders no PNG — `qrcode-terminal` draws characters. The channel therefore
242
+ * sends the copy-and-paste as its own message, which is the string that
243
+ * actually pays, and the QR is the simulator's. Named in the README and in
244
+ * `docs/OPEN_QUESTIONS.md`.
245
+ */
246
+ export function buildSendRequest(config: CloudApiConfig, to: string, body: OutboundBody): SendRequest | { unsupported: string } {
247
+ const base = {
248
+ url: `${config.baseUrl.replace(/\/$/, "")}/${config.apiVersion}/${config.phoneNumberId}/messages`,
249
+ method: "POST" as const,
250
+ headers: { authorization: `Bearer ${config.accessToken}`, "content-type": "application/json" },
251
+ };
252
+ const recipient = { messaging_product: "whatsapp", recipient_type: "individual", to: toGraphNumber(to) };
253
+ switch (body.kind) {
254
+ case "text":
255
+ return { ...base, body: JSON.stringify({ ...recipient, type: "text", text: { preview_url: false, body: body.text } }) };
256
+ case "instrument":
257
+ return { ...base, body: JSON.stringify({ ...recipient, type: "text", text: { preview_url: false, body: body.value } }) };
258
+ case "template":
259
+ return {
260
+ ...base,
261
+ body: JSON.stringify({
262
+ ...recipient,
263
+ type: "template",
264
+ template: {
265
+ name: body.template,
266
+ language: { code: body.language },
267
+ ...templateComponents(body),
268
+ },
269
+ }),
270
+ };
271
+ case "media":
272
+ return { unsupported: "media_upload_unimplemented" };
273
+ }
274
+ }
275
+
276
+ /**
277
+ * A template's parameters as Meta takes them: the body's variables, and one
278
+ * `quick_reply` button component per declared reply, whose `payload` is the
279
+ * reply id — what comes back as `button.payload` when the person taps it.
280
+ */
281
+ function templateComponents(body: Extract<OutboundBody, { kind: "template" }>): { components?: unknown[] } {
282
+ const components: unknown[] = [];
283
+ if (body.variables.length) components.push({ type: "body", parameters: body.variables.map((text) => ({ type: "text", text })) });
284
+ for (const [index, button] of (body.buttons ?? []).entries()) components.push({ type: "button", sub_type: "quick_reply", index: String(index), parameters: [{ type: "payload", payload: button.id }] });
285
+ return components.length ? { components } : {};
286
+ }
287
+
288
+ /** Meta's error for a free-form message outside the 24 hours after the person's last one. */
289
+ export const WINDOW_CLOSED_ERROR = 131047;
290
+
291
+ /**
292
+ * What a non-2xx from `/messages` means, read from Meta's error body.
293
+ *
294
+ * One code gets its own rule: 131047 is the provider saying the 24-hour
295
+ * window is shut, which is the SAME rule the channel enforces before a send
296
+ * and is answered the same way — with a template. It reaches here only when
297
+ * the two clocks disagree (ours read the window open, the provider's read it
298
+ * shut), which at the boundary is a race and not a bug, so a caller has to be
299
+ * able to tell it from a refusal it cannot do anything about.
300
+ */
301
+ export function providerRefusal(status: number, body: string): { rule: string; detail: string } {
302
+ let error: { code?: unknown; message?: unknown; error_data?: { details?: unknown } } | undefined;
303
+ try {
304
+ error = (JSON.parse(body) as { error?: typeof error }).error;
305
+ } catch {
306
+ /* an error without Meta's envelope: the status is all there is */
307
+ }
308
+ const details = typeof error?.error_data?.details === "string" ? error.error_data.details : undefined;
309
+ if (error?.code === WINDOW_CLOSED_ERROR) {
310
+ return {
311
+ rule: "session_window_closed",
312
+ detail: `the provider refused the free-form message (${status}/${WINDOW_CLOSED_ERROR}): its 24h window is shut${details ? ` — ${details}` : ""}`,
313
+ };
314
+ }
315
+ const code = typeof error?.code === "number" ? `/${error.code}` : "";
316
+ return { rule: "provider_refused", detail: `${status}${code} from the Cloud API${details ? ` — ${details}` : ""}` };
317
+ }
318
+
319
+ /* ── the impure half: one fetch over a request the above built ── */
320
+
321
+ /** `+5511987654321` -> `5511987654321`, the only form the Graph API takes. */
322
+ export function toGraphNumber(contact: string): string {
323
+ return contact.replace(/^\+/, "");
324
+ }
325
+
326
+ export interface CloudApiOptions {
327
+ config: CloudApiConfig;
328
+ conversation: { contact: string };
329
+ say: (line: string) => void;
330
+ /** Injected so a test can drive `deliver` without a network. The default is the platform's. */
331
+ fetchImpl?: typeof fetch;
332
+ }
333
+
334
+ export class WhatsAppCloudApi implements ChannelBackend {
335
+ readonly name: string;
336
+ /** True only against Meta. The emulator is a stand-in and says so. */
337
+ readonly live: boolean;
338
+ private server: Server | undefined;
339
+ private readonly queue: InboundMessage[] = [];
340
+ /** Message ids already handed on. Meta delivers at-least-once, so the same message can arrive twice. */
341
+ private readonly seen = new Set<string>();
342
+ private waiting: ((m: InboundMessage | undefined) => void) | undefined;
343
+ private statusListener: ((status: StatusUpdate) => void) | undefined;
344
+ private closed = false;
345
+
346
+ constructor(private readonly options: CloudApiOptions) {
347
+ this.live = isMetaBaseUrl(options.config.baseUrl);
348
+ this.name = this.live ? "cloud-api" : "emulator";
349
+ }
350
+
351
+ /** The port the receiver is listening on, which the emulator has to be told about. */
352
+ get webhookPort(): number {
353
+ return this.options.config.webhookPort;
354
+ }
355
+
356
+ async open(): Promise<void> {
357
+ const { config, say } = this.options;
358
+ this.server = createServer((req, res) => {
359
+ const url = new URL(req.url ?? "/", `http://127.0.0.1:${config.webhookPort}`);
360
+ if (req.method === "GET") {
361
+ const query = Object.fromEntries(url.searchParams.entries());
362
+ const answer = verifyChallenge(config, query);
363
+ res.writeHead(answer.status, { "content-type": "text/plain" }).end(answer.body);
364
+ return;
365
+ }
366
+ if (req.method !== "POST") {
367
+ res.writeHead(405).end();
368
+ return;
369
+ }
370
+ const chunks: Buffer[] = [];
371
+ req.on("data", (c: Buffer) => chunks.push(c));
372
+ req.on("end", () => {
373
+ const raw = Buffer.concat(chunks).toString("utf8");
374
+ const header = req.headers["x-hub-signature-256"];
375
+ const check = verifyWebhookSignature(config.appSecret, Array.isArray(header) ? header[0] : header, raw);
376
+ if (!check.ok) {
377
+ say(`[whatsapp] delivery refused: ${check.reason}`);
378
+ res.writeHead(401, { "content-type": "application/json" }).end(JSON.stringify({ error: "bad_signature" }));
379
+ return;
380
+ }
381
+ let parsed: ReturnType<typeof parseInbound>;
382
+ try {
383
+ parsed = parseInbound(JSON.parse(raw));
384
+ } catch {
385
+ res.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify({ error: "invalid_json" }));
386
+ return;
387
+ }
388
+ for (const ignored of parsed.ignored) say(`[whatsapp] ignored ${ignored.type} message ${ignored.id}: this channel reads text and taps`);
389
+ for (const message of parsed.messages) this.push(message);
390
+ for (const status of parsed.statuses) this.statusListener?.(status);
391
+ // Always 200 once read: Meta retries on anything else, and a duplicate is not an error.
392
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ received: parsed.messages.length }));
393
+ });
394
+ });
395
+ await new Promise<void>((resolve) => this.server!.listen(config.webhookPort, resolve));
396
+ say(
397
+ this.live
398
+ ? `[whatsapp] cloud API backend: webhook on http://127.0.0.1:${config.webhookPort}/ — expose it and register it with Meta; deliveries are verified against ${CLOUD_API_ENV.appSecret}`
399
+ : `[whatsapp] emulator backend: sending to ${config.baseUrl}/${config.apiVersion}, receiving signed deliveries on http://127.0.0.1:${config.webhookPort}/`,
400
+ );
401
+ }
402
+
403
+ private push(message: InboundMessage): void {
404
+ // A redelivery is the same message, not a second turn: answering it twice
405
+ // would put two replies in front of the person for one thing they said.
406
+ if (this.seen.has(message.id)) {
407
+ this.options.say(`[whatsapp] duplicate delivery of ${message.id} dropped`);
408
+ return;
409
+ }
410
+ this.seen.add(message.id);
411
+ if (this.waiting) {
412
+ const resolve = this.waiting;
413
+ this.waiting = undefined;
414
+ resolve(message);
415
+ return;
416
+ }
417
+ this.queue.push(message);
418
+ }
419
+
420
+ onStatus(listener: (status: StatusUpdate) => void): void {
421
+ this.statusListener = listener;
422
+ }
423
+
424
+ async next(): Promise<InboundMessage | undefined> {
425
+ if (this.closed) return undefined;
426
+ const queued = this.queue.shift();
427
+ if (queued) return queued;
428
+ return new Promise<InboundMessage | undefined>((resolve) => {
429
+ this.waiting = resolve;
430
+ });
431
+ }
432
+
433
+ async deliver(to: string, body: OutboundBody): Promise<SentMessage> {
434
+ const request = buildSendRequest(this.options.config, to, body);
435
+ if ("unsupported" in request) {
436
+ return { id: "", state: "failed", refused: { rule: request.unsupported, detail: "this backend cannot upload media: the copy-and-paste text is what pays, and the QR image is the simulator's" } };
437
+ }
438
+ const doFetch = this.options.fetchImpl ?? fetch;
439
+ const response = await doFetch(request.url, { method: request.method, headers: request.headers, body: request.body });
440
+ const text = await response.text();
441
+ if (!response.ok) return { id: "", state: "failed", refused: providerRefusal(response.status, text) };
442
+ let id = "";
443
+ try {
444
+ id = String((JSON.parse(text) as { messages?: Array<{ id?: string }> }).messages?.[0]?.id ?? "");
445
+ } catch {
446
+ /* a 2xx with an unreadable body: the message went, the id did not come back */
447
+ }
448
+ return { id, state: "sent" };
449
+ }
450
+
451
+ async close(): Promise<void> {
452
+ this.closed = true;
453
+ if (this.waiting) {
454
+ this.waiting(undefined);
455
+ this.waiting = undefined;
456
+ }
457
+ await new Promise<void>((resolve) => (this.server ? this.server.close(() => resolve()) : resolve()));
458
+ }
459
+ }