@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
@@ -2,12 +2,16 @@
2
2
  * Section 4.7 against the real client: `ApiMandateStatusSource` reads
3
3
  * `GET /v1/mandates/{id}` on a mocked HTTP server, and the engine is driven
4
4
  * through it. Every answer but `active` must keep `rail.pay` from ever
5
- * being called, and a read that does not answer must refuse too.
5
+ * being called, and a read that does not answer must refuse too. The
6
+ * organization kill switch (ent#1648) rides on the same read as `org_paused`,
7
+ * and the spend route refuses 403 `org_paused` on its own when the switch is
8
+ * pressed after the read.
6
9
  */
7
10
  import { createServer, type Server } from "node:http";
8
11
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
9
12
  import { createCodeSparClient } from "../src/api/client.js";
10
13
  import { ApiMandateStatusSource } from "../src/api/mandate-status.js";
14
+ import { CodeSparRail } from "../src/api/rail.js";
11
15
  import type { MandateStatusSource } from "../src/revocation.js";
12
16
  import { harness, type Harness } from "./helpers.js";
13
17
 
@@ -15,7 +19,7 @@ const MANDATE_ID = "mdt_test_0001";
15
19
  const NOW = new Date("2026-09-23T18:00:00Z");
16
20
  const approver = { id: "usr_demo_titular", channel: "terminal" };
17
21
 
18
- /** The 14-field projection `GET /v1/mandates/{id}` answers (measured on staging, 2026-09-23). */
22
+ /** The 14-field projection `GET /v1/mandates/{id}` answers (measured on staging, 2026-09-23), plus the two kill-switch fields of ent#1648. */
19
23
  function mandateBody(over: Record<string, unknown> = {}): Record<string, unknown> {
20
24
  return {
21
25
  id: MANDATE_ID,
@@ -32,6 +36,8 @@ function mandateBody(over: Record<string, unknown> = {}): Record<string, unknown
32
36
  status: "active",
33
37
  expires_at: "2027-09-23T00:00:00.000Z",
34
38
  created_at: "2026-09-23T00:00:00.000Z",
39
+ org_paused: false,
40
+ org_paused_at: null,
35
41
  ...over,
36
42
  };
37
43
  }
@@ -41,14 +47,17 @@ type Answer = { status: number; body?: unknown; hang?: boolean };
41
47
  let server: Server;
42
48
  let baseUrl: string;
43
49
  let next: Answer = { status: 200, body: mandateBody() };
50
+ /** What a POST (the spend) answers, when a test sends one; the mandate read keeps answering `next`. */
51
+ let spendNext: Answer | undefined;
44
52
  const requests: Array<{ method: string; url: string; bearerIsTestKey: boolean }> = [];
45
53
 
46
54
  beforeAll(async () => {
47
55
  server = createServer((req, res) => {
48
56
  requests.push({ method: req.method ?? "", url: req.url ?? "", bearerIsTestKey: (req.headers.authorization ?? "").startsWith("Bearer csk_test_") });
49
- if (next.hang) return; // never answers: the client's timeout is the only way out
50
- res.writeHead(next.status, { "content-type": "application/json" });
51
- res.end(typeof next.body === "string" ? next.body : JSON.stringify(next.body));
57
+ const answer = req.method === "POST" && spendNext ? spendNext : next;
58
+ if (answer.hang) return; // never answers: the client's timeout is the only way out
59
+ res.writeHead(answer.status, { "content-type": "application/json" });
60
+ res.end(typeof answer.body === "string" ? answer.body : JSON.stringify(answer.body));
52
61
  });
53
62
  await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
54
63
  baseUrl = `http://127.0.0.1:${(server.address() as { port: number }).port}`;
@@ -61,12 +70,17 @@ afterAll(async () => {
61
70
 
62
71
  beforeEach(() => {
63
72
  next = { status: 200, body: mandateBody() };
73
+ spendNext = undefined;
64
74
  requests.length = 0;
65
75
  });
66
76
 
67
- function apiSource(timeoutMs = 5_000): ApiMandateStatusSource {
77
+ function apiClient(timeoutMs = 5_000) {
68
78
  // The placeholder passes the `csk_test_` guard and is the one test-key-shaped string the secret scan allows.
69
- return new ApiMandateStatusSource(createCodeSparClient({ apiKey: "csk_test_your_key_here", baseUrl, timeoutMs }), () => NOW);
79
+ return createCodeSparClient({ apiKey: "csk_test_your_key_here", baseUrl, timeoutMs });
80
+ }
81
+
82
+ function apiSource(timeoutMs = 5_000): ApiMandateStatusSource {
83
+ return new ApiMandateStatusSource(apiClient(timeoutMs), () => NOW);
70
84
  }
71
85
 
72
86
  function apiHarness(status: MandateStatusSource = apiSource()): Harness {
@@ -147,7 +161,69 @@ describe("section 4.7 through the engine, on the API source: executing only on a
147
161
  expect(h.rail.payCount).toBe(0);
148
162
  });
149
163
 
150
- it("the API source never reports org_paused: the API has no kill-switch read, and the source does not invent one", async () => {
151
- expect((await apiSource().check(MANDATE_ID)).org_paused).toBe(false);
164
+ });
165
+
166
+ describe("the organization kill switch (ent#1648): org_paused on the read, 403 org_paused on the spend", () => {
167
+ const PAUSED = { org_paused: true, org_paused_at: "2026-09-23T17:30:00.000Z" };
168
+
169
+ it("the source reports org_paused as the API says it, and keeps the mandate's own status next to it", async () => {
170
+ next = { status: 200, body: mandateBody(PAUSED) };
171
+ expect(await apiSource().check(MANDATE_ID)).toMatchObject({ status: "active", org_paused: true, source: "api" });
172
+ });
173
+
174
+ it("org_paused missing, or not a boolean, is unknown: never read as a running organization", async () => {
175
+ const { org_paused: _dropped, ...withoutFlag } = mandateBody();
176
+ next = { status: 200, body: withoutFlag };
177
+ expect(await apiSource().check(MANDATE_ID)).toMatchObject({ status: "unknown", org_paused: false, detail: expect.stringContaining("org_paused") });
178
+ next = { status: 200, body: mandateBody({ org_paused: "false" }) };
179
+ expect(await apiSource().check(MANDATE_ID)).toMatchObject({ status: "unknown", detail: expect.stringContaining("org_paused") });
180
+ next = { status: 200, body: mandateBody({ org_paused: null }) };
181
+ expect((await apiSource().check(MANDATE_ID)).status).toBe("unknown");
182
+ });
183
+
184
+ it("paused before execute: the gate denies (org_paused) whatever status says, no outbox row, rail.pay never called", async () => {
185
+ const h = apiHarness();
186
+ const id = await approvedThen(h, { status: 200, body: mandateBody(PAUSED) });
187
+ const out = await h.engine.execute(id);
188
+ expect(out).toMatchObject({ state: "denied", reason: "org_paused" });
189
+ expect(h.store.listOutbox()).toHaveLength(0);
190
+ expect(h.rail.payCount).toBe(0);
191
+ expect(await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] })).toMatchObject({ ok: false, refused_before_draft: true, reason: "org_paused" });
192
+ });
193
+
194
+ it("a read without org_paused after approval: denied (mandate_status_unavailable), rail.pay never called", async () => {
195
+ const h = apiHarness();
196
+ const { org_paused: _dropped, ...withoutFlag } = mandateBody();
197
+ const id = await approvedThen(h, { status: 200, body: withoutFlag });
198
+ const out = await h.engine.execute(id);
199
+ expect(out).toMatchObject({ state: "denied", reason: "mandate_status_unavailable" });
200
+ expect(out.detail).toContain("org_paused");
201
+ expect(h.rail.payCount).toBe(0);
152
202
  });
203
+
204
+ // Paused between the gate and the spend: the read said running, the API refuses the spend itself before any hold.
205
+ // The execution is already `executing`, and section 4.7 closes that state as `settled` or `failed` only: no new edge.
206
+ const spendRefusals: Array<[string, unknown]> = [
207
+ ["the documented envelope", { error: { code: "org_paused", message: "the organization paused all agent spend (kill switch)" }, request_id: null }],
208
+ ["the flat body the guardrail sends", { error: "org_paused", message: "the organization paused all agent spend (kill switch)" }],
209
+ ];
210
+ for (const [shape, body] of spendRefusals) {
211
+ it(`paused between the gate and the spend, 403 org_paused (${shape}): failed (org_paused), and nothing but the read and the one spend reached the API`, async () => {
212
+ const h = harness({ mode: "mandate", now: NOW, manifest: { escalate_above: {} }, guardrails: { escalate_above: {} }, status: apiSource(), wrapRail: () => new CodeSparRail(apiClient()) });
213
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 1000 }] });
214
+ if (!d.ok) throw new Error(`refused before draft: ${d.reason}`);
215
+ spendNext = { status: 403, body };
216
+ const out = await h.engine.execute(d.execution.id);
217
+ expect(out.state).toBe("failed");
218
+ expect(out.reason).toBe("org_paused");
219
+ expect(out.history.at(-1)).toMatchObject({ from: "executing", to: "failed", reason: "org_paused" });
220
+ expect(out.outcomes).toEqual([expect.objectContaining({ status: "failed", code: "org_paused" })]);
221
+ expect(out.outcomes.some((o) => "receipt_id" in o || "transaction_id" in o)).toBe(false);
222
+ expect(h.store.listOutbox({ execution_id: out.id })).toEqual([expect.objectContaining({ status: "failed" })]);
223
+ // No receipt read, no ledger or fund call, no second spend: the mandate read(s) and exactly one POST.
224
+ const spend = `/v1/consumers/mandates/${out.mandate.id}/spend`;
225
+ expect(requests.filter((r) => r.method === "POST").map((r) => r.url)).toEqual([spend]);
226
+ expect(requests.filter((r) => !(r.method === "GET" && r.url === `/v1/mandates/${out.mandate.id}`) && r.url !== spend)).toEqual([]);
227
+ });
228
+ }
153
229
  });
@@ -0,0 +1,198 @@
1
+ /**
2
+ * OPEN_QUESTIONS §18: the sealed receipt names the payee only when the spend
3
+ * carries a `SpendQuote`. Every spend carries one, built from the approval
4
+ * artifact's line; a spend without one, or with one that disagrees with the
5
+ * money about to move, never reaches the API; and a receipt whose sealed
6
+ * payee is not the payee that was paid is an error, not a line in a log.
7
+ */
8
+ import { readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import type { ApiClient } from "@codespar/sdk";
11
+ import { describe, expect, it, vi } from "vitest";
12
+ import { hmacSigner } from "../src/approval.js";
13
+ import { CodeSparRail } from "../src/api/rail.js";
14
+ import { maskPayee } from "../src/bundle.js";
15
+ import { ReceiptSealMismatchError } from "../src/engine.js";
16
+ import { canonicalJson } from "../src/hash.js";
17
+ import { checkQuote } from "../src/quote.js";
18
+ import type { PaymentRail, RailPayment } from "../src/rail.js";
19
+ import { ESCOLA, harness, MERCADO } from "./helpers.js";
20
+
21
+ const approver = { id: "usr_demo", channel: "terminal" };
22
+ const actor = { type: "agent" as const, agent: "bills-agent@0.1.0", on_behalf_of: "usr_demo" };
23
+
24
+ function payment(over: Partial<RailPayment> = {}): RailPayment {
25
+ return {
26
+ attempt_id: "att_quote_0",
27
+ mandate_id: "mdt_test_0001",
28
+ amount_minor: 185000,
29
+ currency: "BRL",
30
+ payee: ESCOLA,
31
+ purpose: "contas do mes",
32
+ agent_id: "bills-agent",
33
+ quote: { seller: "Escola Aurora", resource: "outubro", price_minor: 185000, payee: ESCOLA, at: "2026-09-23T18:00:00.000Z" },
34
+ approval: { items_hash: `sha256:${"1".repeat(64)}` },
35
+ actor,
36
+ ...over,
37
+ };
38
+ }
39
+
40
+ /** An ApiClient that records what it was asked and answers a settled spend. */
41
+ function fakeApi(receiptBody?: unknown) {
42
+ const post = vi.fn(async (_path: string, _init: { body: Record<string, unknown> }) => ({ payment: { transactionId: "tx_1", moneyMoved: false }, receipt: { id: "rcpt_1" } }));
43
+ const get = vi.fn(async () => receiptBody);
44
+ return { api: { post, get } as unknown as ApiClient, post, get };
45
+ }
46
+
47
+ /** The stub rail, but the receipt it hands back seals whatever payee the test says. */
48
+ function sealing(inner: PaymentRail, sealedPayee: string | null): PaymentRail {
49
+ return {
50
+ name: inner.name,
51
+ pay: (p) => inner.pay(p),
52
+ lookup: (id, p, tx) => inner.lookup(id, p, tx),
53
+ receipt: async (id, a) => {
54
+ const r = await inner.receipt(id, a);
55
+ return r && { ...r, payment: { ...r.payment, payee: sealedPayee } };
56
+ },
57
+ };
58
+ }
59
+
60
+ describe("§18: every spend presents the quote of what was approved", () => {
61
+ it("the quote is the approved line: seller, resource, price, payee and the moment of approval", async () => {
62
+ const h = harness({ mode: "human" });
63
+ const seen: RailPayment[] = [];
64
+ const inner = h.rail;
65
+ const pay = inner.pay.bind(inner);
66
+ inner.pay = async (p) => (seen.push(p), pay(p));
67
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 185000, description: "outubro" }, { payee: "mercado", amount: 32000 }] });
68
+ if (!d.ok) throw new Error("refused");
69
+ const approved = h.engine.approve(d.execution.id, approver);
70
+ const artifact = h.store.getApproval(approved.approval_id!)!;
71
+ expect((await h.engine.execute(approved.id)).state).toBe("settled");
72
+
73
+ expect(seen.map((p) => p.quote)).toEqual([
74
+ { seller: "Escola Aurora", resource: "outubro", price_minor: 185000, payee: ESCOLA, at: artifact.approved_at },
75
+ // No description on the line: the resource is the mandate's purpose, never something the model adds later.
76
+ { seller: "Mercado do Bairro", resource: "contas do mes", price_minor: 32000, payee: MERCADO, at: artifact.approved_at },
77
+ ]);
78
+ });
79
+
80
+ it("the local receipt copy carries the payee the receipt sealed, masked", async () => {
81
+ const h = harness({ mode: "human" });
82
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 185000 }] });
83
+ if (!d.ok) throw new Error("refused");
84
+ const settled = await h.engine.execute(h.engine.approve(d.execution.id, approver).id);
85
+ const copy = JSON.parse(readFileSync(join(h.bundle.dir, "receipts", `${settled.outcomes[0]!.receipt_id}.json`), "utf8"));
86
+ expect(copy.payment.payee).toBe(maskPayee(ESCOLA));
87
+ expect(JSON.stringify(copy)).not.toContain(ESCOLA);
88
+ });
89
+
90
+ it("an artifact whose line disagrees with the execution is refused before the call, even when its hash and signature hold", async () => {
91
+ const h = harness({ mode: "human" });
92
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 185000 }] });
93
+ if (!d.ok) throw new Error("refused");
94
+ const approved = h.engine.approve(d.execution.id, approver);
95
+ // A writer bug, not an attacker: the artifact's items no longer say what its items_hash says. Re-signed with the harness key.
96
+ const { signature: _old, ...unsigned } = h.store.getApproval(approved.approval_id!)!;
97
+ const drifted = { ...unsigned, items: unsigned.items.map((i) => ({ ...i, amount: 999 })) };
98
+ h.store.saveApproval({ ...drifted, signature: { alg: "HMAC-SHA256", key_id: "test", value: hmacSigner("test", Buffer.alloc(32, 1)).sign(canonicalJson(drifted)) } });
99
+
100
+ const denied = await h.engine.execute(approved.id);
101
+ expect(denied.state).toBe("denied");
102
+ expect(denied.reason).toBe("quote_mismatch");
103
+ expect(h.rail.payCount).toBe(0);
104
+ });
105
+ });
106
+
107
+ describe("§18: a spend that would not seal what was approved never goes out", () => {
108
+ it("checkQuote: missing, wrong price, wrong payee", () => {
109
+ expect(checkQuote(payment())).toMatchObject({ ok: true });
110
+ const { quote: _q, ...bare } = payment();
111
+ expect(checkQuote(bare)).toMatchObject({ ok: false, code: "quote_missing" });
112
+ expect(checkQuote(payment({ amount_minor: 185001 }))).toMatchObject({ ok: false, code: "quote_mismatch" });
113
+ expect(checkQuote(payment({ payee: MERCADO }))).toMatchObject({ ok: false, code: "quote_mismatch" });
114
+ });
115
+
116
+ it("the CodeSpar rail refuses a spend without a quote, and one that disagrees, without calling the API", async () => {
117
+ const { api, post } = fakeApi();
118
+ const rail = new CodeSparRail(api, { canonical: { any: "envelope" } as never, signature: "a".repeat(64) });
119
+ const { quote: _q, ...bare } = payment();
120
+ expect(await rail.pay(bare)).toMatchObject({ status: "failed", code: "quote_missing" });
121
+ expect(await rail.pay(payment({ amount_minor: 1 }))).toMatchObject({ status: "failed", code: "quote_mismatch" });
122
+ expect(await rail.pay(payment({ payee: MERCADO }))).toMatchObject({ status: "failed", code: "quote_mismatch" });
123
+ expect(post).not.toHaveBeenCalled();
124
+ });
125
+
126
+ it("the CodeSpar rail sends the quote on both spend routes", async () => {
127
+ const quote = payment().quote;
128
+ const byEnvelope = fakeApi();
129
+ await new CodeSparRail(byEnvelope.api, { canonical: { any: "envelope" } as never, signature: "a".repeat(64) }).pay(payment());
130
+ expect(byEnvelope.post.mock.calls[0]![0]).toBe("/v1/consumer-payments/execute");
131
+ expect(byEnvelope.post.mock.calls[0]![1].body["quote"]).toEqual(quote);
132
+ const byId = fakeApi();
133
+ await new CodeSparRail(byId.api).pay(payment());
134
+ expect(byId.post.mock.calls[0]![0]).toBe("/v1/consumers/mandates/{id}/spend");
135
+ expect(byId.post.mock.calls[0]![1].body["quote"]).toEqual(quote);
136
+ });
137
+
138
+ it("the stub rail makes the same refusal, so a scenario cannot pass without a quote", async () => {
139
+ const h = harness();
140
+ const { quote: _q, ...bare } = payment();
141
+ expect(await h.rail.pay(bare)).toMatchObject({ status: "failed", code: "quote_missing" });
142
+ expect(h.rail.payCount).toBe(0);
143
+ });
144
+ });
145
+
146
+ describe("§18: the receipt's payee is the sealed one, and a mismatch is an error", () => {
147
+ const sealed = (quote: unknown) => ({
148
+ receipt_id: "rcpt_1",
149
+ state: "paid",
150
+ mandate: { id: "cm_1", nonce: "n", scope: "s", currency: "BRL", sig: "x" },
151
+ quote,
152
+ payment: { rail: "pix-consent", provider: "pix", tx_id: "tx_1", amount_minor: 185000, amount_atomic: null, sandbox: true, attempt_id: "att_quote_0", money_moved: false, at: "2026-09-23T18:00:01.000Z" },
153
+ delivery: null,
154
+ chain: "c".repeat(64),
155
+ receipt_sig: "s",
156
+ exceptions: [],
157
+ });
158
+
159
+ it("the CodeSpar rail reads the payee from the sealed quote, and null when the receipt sealed none", async () => {
160
+ const withQuote = await new CodeSparRail(fakeApi(sealed({ seller: "Escola Aurora", resource: "outubro", price_minor: 185000, payee: ESCOLA, session_id: null, sig: "q", at: null })).api).receipt("rcpt_1", actor);
161
+ expect(withQuote?.payment.payee).toBe(ESCOLA);
162
+ const without = await new CodeSparRail(fakeApi(sealed(null)).api).receipt("rcpt_1", actor);
163
+ expect(without?.payment.payee).toBeNull();
164
+ });
165
+
166
+ it("a mixed-case key sealed as it was sent raises nothing; the same key sealed lower-cased is a mismatch", async () => {
167
+ const MIXED = "Financeiro@Escola-Aurora.example.com.br";
168
+ const mandate = { merchant_allowlist: [MIXED] as [string], beneficiaries: [{ alias: "escola", name: "Escola Aurora", payee: MIXED }] as [{ alias: string; name: string; payee: string }] };
169
+ const verbatim = harness({ mode: "human", mandate });
170
+ const d1 = await verbatim.engine.draft({ items: [{ payee: "escola", amount: 185000 }] });
171
+ if (!d1.ok) throw new Error("refused");
172
+ expect((await verbatim.engine.execute(verbatim.engine.approve(d1.execution.id, approver).id)).state).toBe("settled");
173
+ expect(verbatim.bundle.readEvents().some((e) => e["type"] === "receipt.seal_mismatch")).toBe(false);
174
+
175
+ // The API stores and returns quote.payee verbatim (OPEN_QUESTIONS §18); if that ever changes, this is what the kit says.
176
+ const lowered = harness({ mode: "human", mandate, wrapRail: (stub) => sealing(stub, MIXED.toLowerCase()) });
177
+ const d2 = await lowered.engine.draft({ items: [{ payee: "escola", amount: 185000 }] });
178
+ if (!d2.ok) throw new Error("refused");
179
+ await expect(lowered.engine.execute(lowered.engine.approve(d2.execution.id, approver).id)).rejects.toBeInstanceOf(ReceiptSealMismatchError);
180
+ });
181
+
182
+ for (const [label, sealedPayee] of [["a different payee", MERCADO], ["no payee at all (a spend the API sealed without a quote)", null]] as const) {
183
+ it(`a receipt sealing ${label} throws after the outcome is saved, and the bundle says why without the key in the clear`, async () => {
184
+ const h = harness({ mode: "human", wrapRail: (stub) => sealing(stub, sealedPayee) });
185
+ const d = await h.engine.draft({ items: [{ payee: "escola", amount: 185000 }] });
186
+ if (!d.ok) throw new Error("refused");
187
+ const approved = h.engine.approve(d.execution.id, approver);
188
+
189
+ await expect(h.engine.execute(approved.id)).rejects.toBeInstanceOf(ReceiptSealMismatchError);
190
+ // The money is recorded where the rail put it; the error is about the evidence.
191
+ expect(h.engine.get(approved.id)?.state).toBe("settled");
192
+ const mismatch = h.bundle.readEvents().find((e) => e["type"] === "receipt.seal_mismatch");
193
+ expect(mismatch?.["payload"]).toMatchObject({ sealed_payee: sealedPayee === null ? null : maskPayee(sealedPayee), paid_payee: maskPayee(ESCOLA) });
194
+ expect(JSON.stringify(mismatch)).not.toContain(ESCOLA);
195
+ if (sealedPayee) expect(JSON.stringify(mismatch)).not.toContain(sealedPayee);
196
+ });
197
+ }
198
+ });
@@ -0,0 +1,220 @@
1
+ /**
2
+ * The chain recomputed from a receipt read (ent#1670): the RFC 8785
3
+ * canonicalization, the published recipe as data, and the digest.
4
+ *
5
+ * Nothing here is checked against the module agreeing with itself. The
6
+ * canonicalization is held to the RFC's own examples; the recipe is the one
7
+ * production served on 2026-09-26 (`fixtures/production-receipt-keys.json`,
8
+ * fetched from `https://api.codespar.dev/.well-known/codespar-receipt-keys.json`;
9
+ * staging served the same recipe that day); and the chains are either a
10
+ * literal the enterprise computed with its own code before ent#1670 existed,
11
+ * or the digest of a link list written out by hand, field by field, the way
12
+ * the enterprise's own seal test writes it. The published recipe carries no
13
+ * worked example of its own, so those two are the external vectors.
14
+ */
15
+ import { createHash } from "node:crypto";
16
+ import { readFileSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { describe, expect, it } from "vitest";
19
+ import { canonicalize, isMaskedPayee, readChainRecipe, readSealedApproval, recomputeChain, sameApprovalHash, type ChainRecipe } from "../src/receipt-chain.js";
20
+
21
+ const PUBLISHED = JSON.parse(readFileSync(join(import.meta.dirname, "fixtures", "production-receipt-keys.json"), "utf8")) as Record<string, unknown>;
22
+
23
+ function recipe(document: unknown = PUBLISHED): ChainRecipe {
24
+ const read = readChainRecipe(document);
25
+ if (!read.ok) throw new Error(read.message);
26
+ return read.recipe;
27
+ }
28
+
29
+ const sha256 = (s: string) => createHash("sha256").update(s, "utf8").digest("hex");
30
+ const ITEMS = `sha256:${"1".repeat(64)}`;
31
+ const BATCH = `sha256:${"2".repeat(64)}`;
32
+ const MANDATE_SIG = "a".repeat(64);
33
+
34
+ /** A v4 read, shaped as `GET /v1/consumers/receipts/{id}` answers it: fields the recipe does not name are there too, and must be ignored. */
35
+ function v4Read(over: Record<string, unknown> = {}): Record<string, unknown> {
36
+ return {
37
+ receipt_id: "rcpt_V1StGXR8Z5jdHi6BmyT0sw",
38
+ state: "paid",
39
+ chain_version: 4,
40
+ mandate: { id: "cm_gWEZO68q", nonce: "nonce-123456789", scope: "food", currency: "BRL", sig: MANDATE_SIG, sig_sha256: sha256(MANDATE_SIG) },
41
+ quote: { seller: "Sushi Yassu", resource: "Combinado 20 pc", price_minor: 9980, payee: "yassu@pix.com.br", session_id: "chk_abc123", sig: "q".repeat(64), at: "2026-06-25T12:00:00.000Z" },
42
+ approval: { items_hash: ITEMS, batch_hash: BATCH },
43
+ payment: {
44
+ rail: "pix",
45
+ provider: "celcoin",
46
+ tx_id: "E1393589202606251200",
47
+ amount_minor: 9980,
48
+ amount_atomic: null,
49
+ amount_authorized: null,
50
+ amount_charged: null,
51
+ amount_refunded: null,
52
+ metering: null,
53
+ attempt_id: "cmexec_nonce-123456789",
54
+ money_moved: true,
55
+ at: "2026-06-25T12:00:05.000Z",
56
+ },
57
+ delivery: null,
58
+ chain: "0".repeat(64),
59
+ receipt_sig: "hmac-not-read-here",
60
+ exceptions: [],
61
+ ...over,
62
+ };
63
+ }
64
+
65
+ describe("RFC 8785", () => {
66
+ it("serializes the RFC's own example (section 3.2.4) byte for byte", () => {
67
+ // The input as JSON text, so the numbers go through a parser exactly as the RFC's do.
68
+ const input = JSON.parse(
69
+ '{"numbers": [333333333.33333329, 1E30, 4.50, 2e-3, 0.000000000000000000000000001], "string": "\\u20ac$\\u000F\\u000aA\'\\u0042\\u0022\\u005c\\\\\\"\\/", "literals": [null, true, false]}',
70
+ ) as unknown;
71
+ expect(canonicalize(input)).toBe('{"literals":[null,true,false],"numbers":[333333333.3333333,1e+30,4.5,0.002,1e-27],"string":"€$\\u000f\\nA\'B\\"\\\\\\\\\\"/"}');
72
+ });
73
+
74
+ it("sorts members by UTF-16 code units, the RFC's sorting example (section 3.2.3)", () => {
75
+ const input = JSON.parse('{"\\u20ac": "Euro Sign", "\\r": "Carriage Return", "\\ufb33": "Hebrew Letter Dalet With Dagesh", "1": "One", "\\ud83d\\ude00": "Emoji: Grinning Face", "\\u0080": "Control", "\\u00f6": "Latin Small Letter O With Diaeresis"}') as Record<string, string>;
76
+ const order = [...canonicalize(input).matchAll(/"([^"]+)":"([^"]+)"/g)].map((m) => m[2]);
77
+ expect(order).toEqual(["Carriage Return", "One", "Control", "Latin Small Letter O With Diaeresis", "Euro Sign", "Emoji: Grinning Face", "Hebrew Letter Dalet With Dagesh"]);
78
+ });
79
+
80
+ it("serializes numbers as the RFC's Appendix B lists them", () => {
81
+ const cases: Array<[string, string]> = [
82
+ ["0000000000000000", "0"],
83
+ ["8000000000000000", "0"],
84
+ ["0000000000000001", "5e-324"],
85
+ ["7fefffffffffffff", "1.7976931348623157e+308"],
86
+ ["4340000000000000", "9007199254740992"],
87
+ ["4430000000000000", "295147905179352830000"],
88
+ ["44b52d02c7e14af6", "1e+23"],
89
+ ["444b1ae4d6e2ef50", "1e+21"],
90
+ ["3eb0c6f7a0b5ed8d", "0.000001"],
91
+ ["41b3de4355555555", "333333333.3333333"],
92
+ ["becbf647612f3696", "-0.0000033333333333333333"],
93
+ ["43143ff3c1cb0959", "1424953923781206.2"],
94
+ ];
95
+ for (const [ieee, text] of cases) expect(canonicalize(Buffer.from(ieee, "hex").readDoubleBE(0))).toBe(text);
96
+ });
97
+
98
+ it("refuses what is not I-JSON rather than hashing something nobody can reproduce", () => {
99
+ expect(() => canonicalize(Number.NaN)).toThrow(/not a JSON number/);
100
+ expect(() => canonicalize(Number.POSITIVE_INFINITY)).toThrow(/not a JSON number/);
101
+ expect(() => canonicalize("\ud800")).toThrow(/lone surrogate/);
102
+ expect(() => canonicalize({ ["\udc00"]: 1 })).toThrow(/lone surrogate/);
103
+ expect(() => canonicalize(undefined)).toThrow(/not a JSON value/);
104
+ expect(() => canonicalize({ a: undefined })).toThrow(/not a JSON value/);
105
+ expect(canonicalize("😀")).toBe('"😀"');
106
+ });
107
+ });
108
+
109
+ describe("the published recipe is read as data", () => {
110
+ it("reads the document production serves", () => {
111
+ const r = recipe();
112
+ expect(r.canonicalization).toBe("RFC 8785 (JCS)");
113
+ expect(r.links.map((l) => l.link)).toEqual(["mandate", "quote", "approval", "payment", "delivery"]);
114
+ expect(r.links[0]!.fields_by_version?.["4"]).toContain("sig_sha256");
115
+ });
116
+
117
+ it("names a document with no recipe for what it is: a deployment older than ent#1670", () => {
118
+ const { chain_recipe: _r, ...older } = PUBLISHED;
119
+ expect(readChainRecipe(older)).toMatchObject({ ok: false, reason: "recipe_unpublished" });
120
+ });
121
+
122
+ it("refuses a recipe it would only half follow", () => {
123
+ const base = PUBLISHED["chain_recipe"] as Record<string, unknown>;
124
+ expect(readChainRecipe({ chain_recipe: { ...base, canonicalization: "sorted keys" } })).toMatchObject({ ok: false, reason: "recipe_unrecognized" });
125
+ expect(readChainRecipe({ chain_recipe: { ...base, digest: "SHA-512" } })).toMatchObject({ ok: false, reason: "recipe_unrecognized" });
126
+ expect(readChainRecipe({ chain_recipe: { ...base, links: [] } })).toMatchObject({ ok: false, reason: "recipe_unrecognized" });
127
+ });
128
+ });
129
+
130
+ describe("the chain, recomputed from the read", () => {
131
+ it("reproduces a chain the enterprise's own code computed before ent#1670 (v2, pinned in its receipt-approval-seal test)", () => {
132
+ const read = {
133
+ chain_version: 2,
134
+ mandate: { id: "cm_gWEZO68q", nonce: "nonce-123456789", scope: "food", currency: "BRL", sig: MANDATE_SIG },
135
+ quote: null,
136
+ payment: { rail: "usdc-onchain", provider: "cdp", tx_id: "0xabc", amount_minor: 1, amount_atomic: "10000", amount_authorized: null, amount_charged: null, amount_refunded: null, metering: null, sandbox: true, attempt_id: "cmexec_nonce-123456789", money_moved: true, at: "2026-06-25T12:00:05.000Z" },
137
+ delivery: null,
138
+ };
139
+ expect(recomputeChain(read, recipe())).toEqual({ ok: true, version: 2, chain: "3519728810c899d3b85de0edd0dad1f9991c1bf659ce98597d099b09400df26b" });
140
+ });
141
+
142
+ it("v4 is the digest of {v:4, links:[mandate, quote, approval, payment]} with sig_sha256 in place of the signature", () => {
143
+ const read = v4Read();
144
+ const byHand = sha256(
145
+ canonicalize({
146
+ v: 4,
147
+ links: [
148
+ { id: "cm_gWEZO68q", nonce: "nonce-123456789", scope: "food", currency: "BRL", sig_sha256: sha256(MANDATE_SIG) },
149
+ { seller: "Sushi Yassu", resource: "Combinado 20 pc", price_minor: 9980, payee: "yassu@pix.com.br", session_id: "chk_abc123", sig: "q".repeat(64), at: "2026-06-25T12:00:00.000Z" },
150
+ { items_hash: ITEMS, batch_hash: BATCH },
151
+ { rail: "pix", provider: "celcoin", tx_id: "E1393589202606251200", amount_minor: 9980, attempt_id: "cmexec_nonce-123456789", money_moved: true, at: "2026-06-25T12:00:05.000Z" },
152
+ ],
153
+ }),
154
+ );
155
+ expect(recomputeChain(read, recipe())).toEqual({ ok: true, version: 4, chain: byHand });
156
+ });
157
+
158
+ it("a v4 read recomputes without the mandate's signature, which a third party must never be handed", () => {
159
+ const read = v4Read();
160
+ const { sig: _sig, ...mandate } = read["mandate"] as Record<string, unknown>;
161
+ expect(recomputeChain({ ...read, mandate }, recipe())).toEqual(recomputeChain(read, recipe()));
162
+ });
163
+
164
+ it("every field it seals changes it: payee, amount, approval, batch, a timestamp", () => {
165
+ const original = recomputeChain(v4Read(), recipe());
166
+ const q = v4Read()["quote"] as Record<string, unknown>;
167
+ const p = v4Read()["payment"] as Record<string, unknown>;
168
+ for (const altered of [
169
+ v4Read({ quote: { ...q, payee: "outra@pix.com.br" } }),
170
+ v4Read({ payment: { ...p, amount_minor: 9981 } }),
171
+ v4Read({ approval: { items_hash: `sha256:${"3".repeat(64)}`, batch_hash: BATCH } }),
172
+ v4Read({ approval: { items_hash: ITEMS, batch_hash: null } }),
173
+ v4Read({ payment: { ...p, at: "2026-06-25T12:00:05Z" } }),
174
+ ]) {
175
+ const r = recomputeChain(altered, recipe());
176
+ expect(r.ok).toBe(true);
177
+ expect(r.ok && original.ok && r.chain !== original.chain).toBe(true);
178
+ }
179
+ });
180
+
181
+ it("follows the order the document publishes, not one of its own", () => {
182
+ const base = PUBLISHED["chain_recipe"] as { links: unknown[] };
183
+ const [mandate, quote, approval, payment, delivery] = base.links;
184
+ const reordered = recipe({ chain_recipe: { ...base, links: [mandate, approval, quote, payment, delivery] } });
185
+ const a = recomputeChain(v4Read(), recipe());
186
+ const b = recomputeChain(v4Read(), reordered);
187
+ expect(a.ok && b.ok && a.chain !== b.chain).toBe(true);
188
+ });
189
+
190
+ it("answers why it cannot, and never guesses", () => {
191
+ const { sig: _sig, ...v2Mandate } = v4Read()["mandate"] as Record<string, unknown>;
192
+ expect(recomputeChain(v4Read({ chain_version: 2, approval: undefined, mandate: v2Mandate }), recipe())).toMatchObject({ ok: false, reason: "mandate_sig_required", version: 2 });
193
+ expect(recomputeChain(v4Read({ quote: { ...(v4Read()["quote"] as object), payee: "yu***@pix.com.br" } }), recipe())).toMatchObject({ ok: false, reason: "payee_masked" });
194
+ expect(recomputeChain(v4Read({ chain_version: undefined }), recipe())).toMatchObject({ ok: false, reason: "chain_version_missing" });
195
+ expect(recomputeChain(v4Read({ approval: null }), recipe())).toMatchObject({ ok: false, reason: "field_missing" });
196
+ const base = PUBLISHED["chain_recipe"] as { links: Array<Record<string, unknown>> };
197
+ const strange = recipe({ chain_recipe: { ...base, links: base.links.map((l) => (l["link"] === "delivery" ? { ...l, when: "the moon is full" } : l)) } });
198
+ expect(recomputeChain(v4Read(), strange)).toMatchObject({ ok: false, reason: "recipe_unrecognized" });
199
+ });
200
+ });
201
+
202
+ describe("the approval link", () => {
203
+ it("is read off the receipt, batch_hash null when the spend sent none", () => {
204
+ expect(readSealedApproval(v4Read())).toEqual({ items_hash: ITEMS, batch_hash: BATCH });
205
+ expect(readSealedApproval(v4Read({ approval: { items_hash: ITEMS, batch_hash: null } }))).toEqual({ items_hash: ITEMS, batch_hash: null });
206
+ expect(readSealedApproval(v4Read({ approval: undefined }))).toBeNull();
207
+ });
208
+
209
+ it("compares the two spellings of one hash as one hash, and nothing else as equal", () => {
210
+ expect(sameApprovalHash(ITEMS, "1".repeat(64))).toBe(true);
211
+ expect(sameApprovalHash(null, null)).toBe(true);
212
+ expect(sameApprovalHash(ITEMS, BATCH)).toBe(false);
213
+ expect(sameApprovalHash(null, BATCH)).toBe(false);
214
+ });
215
+
216
+ it("knows a masked payee when it sees one", () => {
217
+ expect(isMaskedPayee("es***@exemplo.com.br")).toBe(true);
218
+ expect(isMaskedPayee("escola@exemplo.com.br")).toBe(false);
219
+ });
220
+ });