@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
@@ -2,10 +2,52 @@
2
2
  * The payment rail the core dispatches to once an execution is `executing`.
3
3
  * Two implementations: the CodeSpar sandbox through the API (needs a
4
4
  * `csk_test_` key) and a local stub persisted in state.db for CI and
5
- * scenarios. Both are idempotent on `attempt_id`: presenting the same
6
- * attempt again answers the earlier outcome instead of paying twice.
5
+ * scenarios. Idempotence is OPT-IN on the API and the core always opts in:
6
+ * every payment carries an explicit `attempt_id`, and presenting the same
7
+ * attempt again answers from the rail's record of it instead of paying twice
8
+ * (ent#1671): the original outcome when it settled, "still running" while it
9
+ * is in flight, "unknown, reconcile" while it is pinned, and a refusal when
10
+ * the same id arrives with a different payment. A payment sent WITHOUT an id
11
+ * is a new payment every time; nothing here sends one.
7
12
  */
8
- import type { Actor, ChargeInstrument } from "./types.js";
13
+ import type { ApiOperation, ApiRequestBody } from "@codespar/sdk";
14
+ import type { SpendQuote } from "./quote.js";
15
+ import type { Actor, ApprovalArtifact, ChargeInstrument } from "./types.js";
16
+
17
+ /**
18
+ * The `approval` a spend presents (`SpendApproval` in the API's document,
19
+ * typed since @codespar/sdk 0.16.10): `sha256:<64 lowercase hex>`, as
20
+ * `itemsHash` and `batchHash` write it. Taken from the SDK so a change on
21
+ * the API's side fails `tsc` here.
22
+ */
23
+ export type SpendApproval = NonNullable<ApiRequestBody<ApiOperation<"/v1/consumer-payments/execute", "post">>["approval"]>;
24
+
25
+ /** What a receipt says it SEALED as the approval link: `batch_hash` is null when the spend sent none. */
26
+ export interface SealedSpendApproval {
27
+ items_hash: string;
28
+ batch_hash: string | null;
29
+ }
30
+
31
+ /** The hashes of what was approved, for a spend of any line of this artifact: the ones the artifact's own HMAC covers. */
32
+ export function spendApprovalOf(artifact: Pick<ApprovalArtifact, "items_hash" | "batch">): SpendApproval {
33
+ return { items_hash: artifact.items_hash, ...(artifact.batch ? { batch_hash: artifact.batch.batch_hash } : {}) };
34
+ }
35
+
36
+ const APPROVAL_HASH = /^(sha256:)?[0-9a-f]{64}$/;
37
+
38
+ /**
39
+ * The approval a spend presents, or why it may not go out: none, or a hash
40
+ * the API would refuse (`invalid_approval_hash`: lowercase hex SHA-256, the
41
+ * `sha256:` prefix optional). The shape and never the content, like the API:
42
+ * WHICH list is the artifact's business, checked before `executing`.
43
+ */
44
+ export function checkSpendApproval(payment: { approval?: SpendApproval | undefined }): { ok: true; approval: SpendApproval } | { ok: false; code: "approval_missing" | "approval_malformed"; detail: string } {
45
+ const { approval } = payment;
46
+ if (!approval) return { ok: false, code: "approval_missing", detail: "a spend carries the hashes of the approval artifact, or it does not go out" };
47
+ if (!APPROVAL_HASH.test(approval.items_hash)) return { ok: false, code: "approval_malformed", detail: "the approval's items_hash is not a lowercase hex SHA-256" };
48
+ if (approval.batch_hash !== undefined && !APPROVAL_HASH.test(approval.batch_hash)) return { ok: false, code: "approval_malformed", detail: "the approval's batch_hash is not a lowercase hex SHA-256" };
49
+ return { ok: true, approval };
50
+ }
9
51
 
10
52
  export interface RailPayment {
11
53
  attempt_id: string;
@@ -22,6 +64,17 @@ export interface RailPayment {
22
64
  beneficiary?: string;
23
65
  /** Due date of a receivable, `YYYY-MM-DD`. */
24
66
  due_date?: string;
67
+ /** What was approved for this line, presented with a spend so the sealed receipt names the payee. A charge rail ignores it. */
68
+ quote?: SpendQuote;
69
+ /**
70
+ * The approval artifact's hashes, exactly as the artifact carries them: its
71
+ * `items_hash`, and for a batch line its `batch.batch_hash`. The API seals
72
+ * them into the receipt's chain as a link of their own (chain v4,
73
+ * ent#1670), so whoever holds the receipt read can show THIS payment was
74
+ * sealed against THAT list. It is the caller's claim, sealed: the API checks
75
+ * its shape and never its content. A charge rail ignores it.
76
+ */
77
+ approval?: SpendApproval;
25
78
  /** Section 4.5: carried on every call. The API has no wire field for it yet; see OPEN_QUESTIONS. */
26
79
  actor: Actor;
27
80
  }
@@ -45,6 +98,13 @@ export type RailOutcome =
45
98
  receipt_id: string | null;
46
99
  money_moved: boolean;
47
100
  sandbox: boolean;
101
+ /**
102
+ * The rail answered from its record of an attempt that had ALREADY
103
+ * settled (`idempotent_replay`, ent#1671): every other field is the
104
+ * original answer, and nothing moved, was held or was sealed by this
105
+ * call. Absent on the call that settled it.
106
+ */
107
+ replayed?: true;
48
108
  raw: unknown;
49
109
  }
50
110
  | {
@@ -52,6 +112,21 @@ export type RailOutcome =
52
112
  status: "failed";
53
113
  code: string;
54
114
  message: string;
115
+ /**
116
+ * The rail holds this `attempt_id` as an attempt that failed and moved
117
+ * no money, and will refuse it for good (`psp_attempt_conflict`). Set
118
+ * only on that answer: the id is spent, and a batch line pays under the
119
+ * next generation of its id (`batchAttemptId`).
120
+ */
121
+ spent?: true;
122
+ /**
123
+ * The rail holds this `attempt_id` for something else: a DIFFERENT
124
+ * payment (`attempt_id_conflict`, checked before whatever became of it)
125
+ * or another project of the organization (`attempt_id_unavailable`).
126
+ * Nothing was held or sent. Never `spent`: the id is not free and not
127
+ * burned, it is someone else's, and presenting it again answers the same.
128
+ */
129
+ held?: "conflict" | "unavailable";
55
130
  raw?: unknown;
56
131
  }
57
132
  | {
@@ -67,7 +142,17 @@ export interface RailReceipt {
67
142
  /** `payment` (the API's sealed receipt) or `charge` (the paid receivable as the API reports it; no seal exists for it today). */
68
143
  kind?: "payment" | "charge";
69
144
  state: string;
70
- mandate: { id: string };
145
+ /** `sig_sha256` is SHA-256 of the mandate's own signature, which a v4 chain seals in place of it; never the signature itself, which is a bearer proof. */
146
+ mandate: { id: string; sig_sha256?: string };
147
+ /**
148
+ * The chain format the receipt was sealed under (4 once it carries the
149
+ * approval link), and the approval it sealed, as the API read them back:
150
+ * `null` when it sealed none. Absent when the rail cannot say — a charge,
151
+ * which seals nothing.
152
+ */
153
+ chain_version?: number;
154
+ approval?: SealedSpendApproval | null;
155
+ /** `payee` is the one the receipt SEALED — the quote's, on a payment — and null when it sealed none. Never copied from the execution. */
71
156
  payment: { amount_minor: number; payee: string | null; attempt_id: string; money_moved: boolean; sandbox: boolean; at: string };
72
157
  /** Null when the API seals nothing for this kind of record. */
73
158
  chain: string | null;
@@ -0,0 +1,270 @@
1
+ /**
2
+ * `receipt-chain`: the receipt's `chain` recomputed from its body, the way a
3
+ * party outside CodeSpar does it.
4
+ *
5
+ * The Ed25519 signature covers `codespar-receipt:v1:<id>:<chain>`, so on its
6
+ * own it proves CodeSpar sealed SOME body with that digest. Recomputing the
7
+ * digest from the receipt read is what binds THIS body — the payee, the
8
+ * amount, and since chain v4 the approval hashes — to the signature. CodeSpar
9
+ * publishes how, as data: `chain_recipe` on the unauthenticated
10
+ * `/.well-known/codespar-receipt-keys.json` (ent#1670). This module reads that
11
+ * recipe and applies it. It does not carry its own copy of the link shapes: a
12
+ * recipe it does not understand is an answer ("cannot recompute"), never a
13
+ * guess, because a verifier that silently skipped a clause would compute a
14
+ * different digest and blame the receipt.
15
+ *
16
+ * chain = hex(SHA-256(UTF-8(JCS({ "v": chain_version, "links": [...] }))))
17
+ *
18
+ * JCS is RFC 8785, implemented below in full rather than imported: it is
19
+ * thirty lines, the enterprise's own canonicalizer is exactly the code a
20
+ * verifier must not depend on, and a dependency would be one more thing to
21
+ * audit for a third party who wants to reimplement this. Like
22
+ * `receipt-verification`, it imports `node:crypto` and nothing else.
23
+ */
24
+ import { createHash } from "node:crypto";
25
+
26
+ /* ── RFC 8785 ────────────────────────────────────────────────── */
27
+
28
+ /** A high surrogate not followed by a low one, or a low one not preceded by a high one. */
29
+ const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
30
+
31
+ /**
32
+ * The JSON Canonicalization Scheme (RFC 8785) of a JSON value.
33
+ *
34
+ * - Objects: members sorted by their names as arrays of UTF-16 code units,
35
+ * which is what the default `Array.prototype.sort` compares (section 3.2.3).
36
+ * - Numbers: the ECMAScript `Number.prototype.toString` serialization, which
37
+ * is what `JSON.stringify` emits for a finite number (section 3.2.2.3);
38
+ * NaN and the infinities are not JSON and are refused.
39
+ * - Strings: `JSON.stringify`'s escaping, which is the RFC's (section
40
+ * 3.2.2.2) — except that JCS works on I-JSON, where a lone surrogate is not
41
+ * a string at all, so one is refused instead of escaped.
42
+ * - No whitespace anywhere.
43
+ *
44
+ * `undefined`, functions and other non-JSON values are refused: canonicalizing
45
+ * something that is not JSON would hash a value nobody can reproduce.
46
+ */
47
+ export function canonicalize(value: unknown): string {
48
+ if (value === null) return "null";
49
+ switch (typeof value) {
50
+ case "boolean":
51
+ return value ? "true" : "false";
52
+ case "number":
53
+ if (!Number.isFinite(value)) throw new TypeError(`RFC 8785: ${value} is not a JSON number`);
54
+ return JSON.stringify(value);
55
+ case "string":
56
+ if (LONE_SURROGATE.test(value)) throw new TypeError("RFC 8785: a string with a lone surrogate is not I-JSON");
57
+ return JSON.stringify(value);
58
+ case "object": {
59
+ if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
60
+ const members = Object.keys(value as Record<string, unknown>).sort();
61
+ return `{${members
62
+ .map((name) => {
63
+ if (LONE_SURROGATE.test(name)) throw new TypeError("RFC 8785: a member name with a lone surrogate is not I-JSON");
64
+ return `${JSON.stringify(name)}:${canonicalize((value as Record<string, unknown>)[name])}`;
65
+ })
66
+ .join(",")}}`;
67
+ }
68
+ default:
69
+ throw new TypeError(`RFC 8785: a ${typeof value} is not a JSON value`);
70
+ }
71
+ }
72
+
73
+ /* ── The published recipe ────────────────────────────────────── */
74
+
75
+ /** One link of the recipe, as `chain_recipe.links[]` publishes it. */
76
+ export interface ChainRecipeLink {
77
+ link: string;
78
+ /** The member of the receipt read the link's fields are taken from. */
79
+ from: string;
80
+ /** When the link is part of the chain: a clause from a small published vocabulary. */
81
+ when: string;
82
+ fields?: string[];
83
+ fields_by_version?: Record<string, string[]>;
84
+ }
85
+
86
+ export interface ChainRecipe {
87
+ canonicalization: string;
88
+ digest: string;
89
+ version: { field: string; rule?: string };
90
+ /** The suffix that marks a field included only when the read has it and it is not null. */
91
+ optional_marker: string;
92
+ links: ChainRecipeLink[];
93
+ }
94
+
95
+ /** The canonicalization this module implements, spelled as the recipe spells it. */
96
+ export const RECIPE_CANONICALIZATION = "RFC 8785 (JCS)";
97
+
98
+ function isRecord(value: unknown): value is Record<string, unknown> {
99
+ return typeof value === "object" && value !== null && !Array.isArray(value);
100
+ }
101
+
102
+ function isStringArray(value: unknown): value is string[] {
103
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
104
+ }
105
+
106
+ export type RecipeRead = { ok: true; recipe: ChainRecipe } | { ok: false; reason: "recipe_unpublished" | "recipe_unrecognized"; message: string };
107
+
108
+ /**
109
+ * The `chain_recipe` of a key document, or why there is none this module can
110
+ * follow. A document without one comes from a deployment older than ent#1670:
111
+ * nothing is wrong with the receipt, there is just no published way to bind
112
+ * its body. A recipe whose canonicalization or digest is not the one
113
+ * implemented here is refused whole rather than followed halfway.
114
+ */
115
+ export function readChainRecipe(keyDocument: unknown): RecipeRead {
116
+ // OUTSIDE THE SDK'S TYPES, and the one place it is read: the well-known
117
+ // document's schema does not declare `chain_recipe` yet (ent#1759), so no
118
+ // SDK type exists to read it through. It is validated field by field below
119
+ // instead. When ent#1759 lands and the SDK types it, this read moves onto
120
+ // that type and the validation stays: a verifier checks what it is served.
121
+ const raw = isRecord(keyDocument) ? keyDocument["chain_recipe"] : undefined;
122
+ if (raw === undefined || raw === null) {
123
+ return { ok: false, reason: "recipe_unpublished", message: "the key set publishes no `chain_recipe`, so the receipt's body cannot be bound to its chain (a deployment older than ent#1670)" };
124
+ }
125
+ const unrecognized = (why: string): RecipeRead => ({ ok: false, reason: "recipe_unrecognized", message: `the published \`chain_recipe\` ${why}, so this verifier does not follow it` });
126
+ if (!isRecord(raw)) return unrecognized("is not an object");
127
+ if (raw["canonicalization"] !== RECIPE_CANONICALIZATION) return unrecognized(`names the canonicalization ${JSON.stringify(raw["canonicalization"])}, not ${RECIPE_CANONICALIZATION}`);
128
+ const digest = raw["digest"];
129
+ if (typeof digest !== "string" || !/\bSHA-256\b/.test(digest) || !/\blowercase hex\b/.test(digest)) return unrecognized(`names the digest ${JSON.stringify(digest)}, not lowercase-hex SHA-256`);
130
+ const version = raw["version"];
131
+ if (!isRecord(version) || typeof version["field"] !== "string") return unrecognized("does not name the field that carries the chain version");
132
+ const marker = raw["optional_marker"];
133
+ if (typeof marker !== "string" || marker.length === 0) return unrecognized("does not name its optional-field marker");
134
+ const links = raw["links"];
135
+ if (!Array.isArray(links) || links.length === 0) return unrecognized("lists no links");
136
+ const parsed: ChainRecipeLink[] = [];
137
+ for (const entry of links) {
138
+ if (!isRecord(entry) || typeof entry["link"] !== "string" || typeof entry["from"] !== "string" || typeof entry["when"] !== "string") return unrecognized("carries a link without `link`, `from` and `when`");
139
+ const fields = entry["fields"];
140
+ const byVersion = entry["fields_by_version"];
141
+ if (fields !== undefined && !isStringArray(fields)) return unrecognized(`gives the link ${entry["link"]} fields that are not a list of names`);
142
+ if (byVersion !== undefined && !(isRecord(byVersion) && Object.values(byVersion).every(isStringArray))) return unrecognized(`gives the link ${entry["link"]} per-version fields that are not lists of names`);
143
+ if (fields === undefined && byVersion === undefined) return unrecognized(`gives the link ${entry["link"]} no fields`);
144
+ parsed.push({
145
+ link: entry["link"],
146
+ from: entry["from"],
147
+ when: entry["when"],
148
+ ...(fields !== undefined ? { fields } : {}),
149
+ ...(byVersion !== undefined ? { fields_by_version: byVersion as Record<string, string[]> } : {}),
150
+ });
151
+ }
152
+ return {
153
+ ok: true,
154
+ recipe: {
155
+ canonicalization: RECIPE_CANONICALIZATION,
156
+ digest,
157
+ version: { field: version["field"], ...(typeof version["rule"] === "string" ? { rule: version["rule"] } : {}) },
158
+ optional_marker: marker,
159
+ links: parsed,
160
+ },
161
+ };
162
+ }
163
+
164
+ /* ── Recomputing ─────────────────────────────────────────────── */
165
+
166
+ export type ChainRecomputation =
167
+ | { ok: true; chain: string; version: number }
168
+ | {
169
+ ok: false;
170
+ /**
171
+ * `chain_version_missing` the read does not say which version sealed it.
172
+ * `recipe_unrecognized` a clause, or a version, the recipe does not
173
+ * define in a way this module can follow.
174
+ * `mandate_sig_required` a v1–v3 chain seals the mandate's raw
175
+ * signature, which the read given here does
176
+ * not carry.
177
+ * `payee_masked` the quote's payee is a masked copy; the chain
178
+ * sealed the key itself.
179
+ * `field_missing` any other field the recipe needs is absent.
180
+ */
181
+ reason: "chain_version_missing" | "recipe_unrecognized" | "mandate_sig_required" | "payee_masked" | "field_missing";
182
+ message: string;
183
+ version: number | null;
184
+ };
185
+
186
+ /** A payee the proof bundle masked (`es***@exemplo.com.br`). No Pix key contains `***`. */
187
+ export function isMaskedPayee(payee: unknown): boolean {
188
+ return typeof payee === "string" && payee.includes("***");
189
+ }
190
+
191
+ /** Whether a link applies. `undefined` for a clause the recipe did not publish before this module was written. */
192
+ function applies(when: string, from: string, read: Record<string, unknown>, version: number): boolean | undefined {
193
+ if (when === "always") return true;
194
+ if (when === `${from} is not null`) return read[from] !== null && read[from] !== undefined;
195
+ const byVersion = /^chain_version is (\d+)$/.exec(when);
196
+ if (byVersion) return version === Number(byVersion[1]);
197
+ return undefined;
198
+ }
199
+
200
+ /**
201
+ * Recompute `chain` from a receipt read (the JSON of
202
+ * `GET /v1/consumers/receipts/{id}`), following the recipe exactly: links in
203
+ * the published order, a link whose clause does not hold left out rather than
204
+ * nulled, values copied as read (`null` included), a field marked optional
205
+ * included only when present and not null. Timestamps are taken as read: the
206
+ * API seals them in the form it reads them back (RFC 3339 UTC, milliseconds).
207
+ *
208
+ * This is the pure recomputation, for any version. Whether a verifier SHOULD
209
+ * recompute a given version is policy, and lives in `verifyReceiptRead`.
210
+ */
211
+ export function recomputeChain(read: Record<string, unknown>, recipe: ChainRecipe): ChainRecomputation {
212
+ const versionRaw = read[recipe.version.field];
213
+ if (typeof versionRaw !== "number" || !Number.isSafeInteger(versionRaw) || versionRaw < 1) {
214
+ return { ok: false, reason: "chain_version_missing", version: null, message: `the read carries no \`${recipe.version.field}\`, so which links were sealed cannot be known` };
215
+ }
216
+ const version = versionRaw;
217
+ const links: Record<string, unknown>[] = [];
218
+ for (const spec of recipe.links) {
219
+ const include = applies(spec.when, spec.from, read, version);
220
+ if (include === undefined) return { ok: false, reason: "recipe_unrecognized", version, message: `the recipe includes the link ${spec.link} "when ${spec.when}", a clause this verifier does not know` };
221
+ if (!include) continue;
222
+ const fields = spec.fields ?? spec.fields_by_version?.[String(version)];
223
+ if (!fields) return { ok: false, reason: "recipe_unrecognized", version, message: `the recipe gives the link ${spec.link} no fields for chain v${version}` };
224
+ const source = read[spec.from];
225
+ if (!isRecord(source)) return { ok: false, reason: "field_missing", version, message: `the read carries no \`${spec.from}\` object, which the ${spec.link} link of a v${version} chain is taken from` };
226
+ const link: Record<string, unknown> = {};
227
+ for (const field of fields) {
228
+ const optional = field.endsWith(recipe.optional_marker);
229
+ const name = optional ? field.slice(0, -recipe.optional_marker.length) : field;
230
+ const value = source[name];
231
+ if (optional) {
232
+ if (value !== undefined && value !== null) link[name] = value;
233
+ continue;
234
+ }
235
+ if (value === undefined) {
236
+ if (spec.from === "mandate" && name === "sig") {
237
+ return { ok: false, reason: "mandate_sig_required", version, message: `chain v${version} seals the mandate's own signature, and the read carries none: it is a bearer proof only the tenant holds` };
238
+ }
239
+ return { ok: false, reason: "field_missing", version, message: `the read carries no \`${spec.from}.${name}\`, which the ${spec.link} link of a v${version} chain seals` };
240
+ }
241
+ if (spec.from === "quote" && name === "payee" && isMaskedPayee(value)) {
242
+ return { ok: false, reason: "payee_masked", version, message: "the quote's payee is a masked copy; the chain sealed the key itself, so this body cannot be recomputed from it" };
243
+ }
244
+ link[name] = value;
245
+ }
246
+ links.push(link);
247
+ }
248
+ return { ok: true, version, chain: createHash("sha256").update(canonicalize({ v: version, links }), "utf8").digest("hex") };
249
+ }
250
+
251
+ /* ── The approval link (chain v4) ────────────────────────────── */
252
+
253
+ export interface SealedApproval {
254
+ items_hash: string;
255
+ batch_hash: string | null;
256
+ }
257
+
258
+ /** The approval a read says was sealed, or `null` when it carries none (every chain before v4). */
259
+ export function readSealedApproval(read: Record<string, unknown>): SealedApproval | null {
260
+ const approval = read["approval"];
261
+ if (!isRecord(approval) || typeof approval["items_hash"] !== "string") return null;
262
+ const batch = approval["batch_hash"];
263
+ return { items_hash: approval["items_hash"], batch_hash: typeof batch === "string" ? batch : null };
264
+ }
265
+
266
+ /** Two spellings of one SHA-256: the API seals the value as sent, and the `sha256:` prefix is optional on the wire. */
267
+ export function sameApprovalHash(a: string | null | undefined, b: string | null | undefined): boolean {
268
+ const bare = (h: string | null | undefined) => (typeof h === "string" ? h.replace(/^sha256:/, "") : null);
269
+ return bare(a) === bare(b);
270
+ }
@@ -17,28 +17,34 @@
17
17
  * reconciling a payout, or a court — parties that
18
18
  * have no relationship with CodeSpar at all.
19
19
  *
20
- * This module checks the second. It imports `node:crypto` and nothing else: no
20
+ * This module checks the second. It imports `node:crypto` and two modules of
21
+ * this package that import nothing else (`receipt-chain`, `hash`): no
21
22
  * CodeSpar SDK, no key material, no token, no network of its own (the key
22
23
  * document is passed in, or fetched by a function the caller supplies). A
23
24
  * third party can read the forty lines that matter and reimplement them.
24
25
  *
25
- * WHAT A `verified` VERDICT PROVES, exactly: CodeSpar sealed a receipt with
26
- * THIS id and THIS chain. The chain is a SHA-256 over the four links of the
27
- * Control Record, so the seller, the amount and the payee are inside it — but
28
- * recomputing that digest from the receipt body needs the canonical link
29
- * shapes, which CodeSpar does not publish today. Binding the body to the chain
30
- * is therefore not something this module can promise, and it does not pretend
31
- * to: see `docs/OPEN_QUESTIONS.md` §47. It is also why the signature survives
32
- * the proof bundle's masking — the bundle masks the payee, and the signature
33
- * covers the id and the digest, not the copy in front of you.
26
+ * TWO LAYERS, and a verdict says which one it reached. The signature proves
27
+ * CodeSpar sealed a receipt with THIS id and THIS chain; `verifyReceiptWithKeys`
28
+ * checks that and nothing else, which is why it survives the proof bundle's
29
+ * masking — the bundle masks the payee, and the signature covers the id and
30
+ * the digest, not the copy in front of you. The chain is a SHA-256 over the
31
+ * links of the Control Record, and since ent#1670 CodeSpar publishes how to
32
+ * recompute it (`chain_recipe`, beside the keys). `verifyReceiptRead` does:
33
+ * from the API's receipt READ, under the recipe of the SAME document whose
34
+ * key verified the signature, it recomputes the chain and holds it against
35
+ * the signed one — and for a v4 chain, holds the sealed approval link against
36
+ * the local approval artifact. `verified` from it means all of that held;
37
+ * `signature_only` means the signature held and the body could not be bound
38
+ * (a v1–v3 chain, a masked payee, no recipe), which is not a failure and is
39
+ * not a verification of the body either. See `docs/OPEN_QUESTIONS.md` §47.
34
40
  *
35
- * WHICH KEY SET. `kid` is `<did>#<n>` and the DID is the platform's, so it is
36
- * the same string on every deployment while the KEY behind it is not: a
37
- * sandbox receipt checked against the production key set finds a key of that
38
- * name, fails to verify, and reads `tampered`. The verifier therefore points
39
- * at ONE deployment's `/.well-known/codespar-receipt-keys.json` — the default
40
- * is production, and `--url` names another. Nothing in the two documents tells
41
- * them apart today; `docs/OPEN_QUESTIONS.md` §47 says so and names the ask.
41
+ * WHICH KEY SET. The verifier points at ONE deployment's
42
+ * `/.well-known/codespar-receipt-keys.json` — the default is production, and
43
+ * `--url` names another. Since ent#1641 a deployment names its keys by
44
+ * namespace (`#production-2`, `#staging-2`), so a receipt checked against the
45
+ * wrong set finds no key of its name (`unknown_key`); the retired `#1` is
46
+ * still published by both with different keys, and a receipt sealed under it
47
+ * checked against the wrong set reads `tampered`. `docs/OPEN_QUESTIONS.md` §47.
42
48
  *
43
49
  * The signed string is the enterprise's `receiptSigningString`
44
50
  * (`packages/api/src/receipt-signature.ts`, ent#1633), reproduced here rather
@@ -46,6 +52,9 @@
46
52
  * proves nothing about the signer.
47
53
  */
48
54
  import { createPublicKey, verify as verifyDetached } from "node:crypto";
55
+ import { itemsHash } from "./hash.js";
56
+ import { readChainRecipe, readSealedApproval, recomputeChain, sameApprovalHash, type SealedApproval } from "./receipt-chain.js";
57
+ import type { ExecutionItem } from "./types.js";
49
58
 
50
59
  /** The domain tag. A receipt names the version it was sealed under through
51
60
  * this literal, so a future `v2` does not invalidate a `v1` signature. */
@@ -86,8 +95,51 @@ export function receiptSigningString(receiptId: string, chain: string): string {
86
95
  * arbitrary JSON file would state that it is a receipt from
87
96
  * before the capability existed, which is a claim about a
88
97
  * file nobody checked.
98
+ *
99
+ * And three that only the body check (`verifyReceiptRead`) reaches, each
100
+ * after the signature verified:
101
+ *
102
+ * `signature_only` the body could not be bound to the chain: a v1–v3
103
+ * chain, a masked payee, no read, no recipe. NOT a
104
+ * failure, and NOT `verified`: the signature is
105
+ * CodeSpar's, and nothing here says the body beside it
106
+ * is the body it sealed.
107
+ * `chain_mismatch` the body, recomputed under the published recipe, is
108
+ * NOT the signed chain. The signature is genuine and
109
+ * the body in front of you is not the one it covers.
110
+ * `approval_mismatch` the body is the signed one, and the approval link it
111
+ * seals is NOT the local artifact's: this payment was
112
+ * not sealed against the list that artifact approved.
89
113
  */
90
- export type ReceiptVerdict = "verified" | "tampered" | "unsigned" | "unknown_key" | "unreachable" | "malformed";
114
+ export type ReceiptVerdict = "verified" | "tampered" | "unsigned" | "unknown_key" | "unreachable" | "malformed" | "signature_only" | "chain_mismatch" | "approval_mismatch";
115
+
116
+ /** What recomputing the chain from the body found. */
117
+ export interface ChainCheck {
118
+ /** `recomputed`: equal to the signed chain. `mismatch`: not equal. `not_recomputed`: see `reason`. */
119
+ status: "recomputed" | "mismatch" | "not_recomputed";
120
+ version: number | null;
121
+ /** The digest recomputed from the body, when it could be. */
122
+ recomputed: string | null;
123
+ reason: string;
124
+ }
125
+
126
+ /** What the sealed approval link was held against. Present on a v4 chain that recomputed. */
127
+ export interface ApprovalLinkCheck {
128
+ /** `matched`: the artifact's hashes are the sealed ones. `mismatch`: they are not. `not_compared`: no artifact was given. */
129
+ status: "matched" | "mismatch" | "not_compared";
130
+ sealed: SealedApproval;
131
+ artifact: { approval_id: string; items_hash: string; batch_hash: string | null } | null;
132
+ reason: string;
133
+ }
134
+
135
+ /** The part of an approval artifact the approval link is held against: what was approved, never who. */
136
+ export interface ApprovalClaim {
137
+ approval_id: string;
138
+ items_hash: string;
139
+ /** When present, `items_hash` is recomputed from them first: a hash that does not describe its own list proves nothing about the list. */
140
+ items?: readonly ExecutionItem[];
141
+ batch?: { batch_hash: string };
142
+ }
91
143
 
92
144
  export interface ReceiptVerification {
93
145
  verdict: ReceiptVerdict;
@@ -106,6 +158,9 @@ export interface ReceiptVerification {
106
158
  reason: string;
107
159
  /** One sentence for a person. Never a stack trace. */
108
160
  message: string;
161
+ /** The body check, when one was attempted: only `verifyReceiptRead` attempts it, and only on a signature that verified. */
162
+ chain_check?: ChainCheck;
163
+ approval_check?: ApprovalLinkCheck;
109
164
  }
110
165
 
111
166
  /** One published key, RFC 8037 shape, as the JWKS serves it. */
@@ -398,4 +453,125 @@ export const VERDICT_EXIT_CODES: Record<ReceiptVerdict, number> = {
398
453
  unknown_key: 4,
399
454
  unreachable: 5,
400
455
  malformed: 6,
456
+ signature_only: 7,
457
+ chain_mismatch: 8,
458
+ approval_mismatch: 9,
401
459
  };
460
+
461
+ /* ── The body: the chain recomputed from the read ────────────── */
462
+
463
+ /** The first chain version that seals `sha256(mandate.sig)` instead of the signature, and the approval link. */
464
+ export const FIRST_THIRD_PARTY_CHAIN_VERSION = 4;
465
+
466
+ /** Whether a JSON is the API's receipt read, rather than the proof bundle's masked copy of one: only the read carries the links. */
467
+ export function isReceiptRead(input: unknown): input is Record<string, unknown> {
468
+ if (!isRecord(input)) return false;
469
+ const mandate = input["mandate"];
470
+ const payment = input["payment"];
471
+ return isRecord(mandate) && "nonce" in mandate && isRecord(payment) && "rail" in payment;
472
+ }
473
+
474
+ /** A verified signature whose body could not be bound: `signature_only`, with the reason. */
475
+ export function signatureOnly(signature: ReceiptVerification, reason: string, why: string, version: number | null = null): ReceiptVerification {
476
+ return {
477
+ ...signature,
478
+ verdict: "signature_only",
479
+ reason,
480
+ message: `receipt ${signature.receipt_id ?? "(no id)"} carries CodeSpar's signature over its id and chain, and its body is NOT bound to that chain: ${why}`,
481
+ chain_check: { status: "not_recomputed", version, recomputed: null, reason },
482
+ };
483
+ }
484
+
485
+ /**
486
+ * Verify a receipt READ — the JSON of `GET /v1/consumers/receipts/{id}` —
487
+ * against a key set already in hand: the signature, then the body, then the
488
+ * approval. Synchronous and offline, like `verifyReceiptWithKeys`.
489
+ *
490
+ * The recipe is taken from the SAME document whose key verified the
491
+ * signature, never from another: each deployment names its keys by
492
+ * namespace (§47), so a document that verified the signature is the sealing
493
+ * deployment's, and its recipe is the one that sealed.
494
+ *
495
+ * A v1–v3 chain is never recomputed here, even from a tenant's read that
496
+ * carries `mandate.sig`: that signature is a bearer proof (it authorizes
497
+ * spends), a verifier has no business handling it, and the API is retiring
498
+ * it from the read. Those chains also predate the API's timestamp fix
499
+ * (ent#1670), so some of them no longer recompute from their read at all —
500
+ * and a mismatch there would accuse a genuine receipt. They answer
501
+ * `signature_only`, never `verified` and never `chain_mismatch`.
502
+ */
503
+ export function verifyReceiptRead(read: unknown, keyDocument: unknown, options: { label?: string; approval?: ApprovalClaim } = {}): ReceiptVerification {
504
+ const signature = verifyReceiptWithKeys(read, keyDocument, options.label ?? "file");
505
+ if (signature.verdict !== "verified" || !isRecord(read)) return signature;
506
+ if (!isReceiptRead(read)) return signatureOnly(signature, "read_required", "this is not the API's receipt read (the proof bundle's copy masks the payee and carries none of the links); recompute from `GET /v1/consumers/receipts/{id}`");
507
+
508
+ const recipe = readChainRecipe(keyDocument);
509
+ if (!recipe.ok) return signatureOnly(signature, recipe.reason, recipe.message);
510
+
511
+ const versionRaw = read[recipe.recipe.version.field];
512
+ if (typeof versionRaw === "number" && versionRaw < FIRST_THIRD_PARTY_CHAIN_VERSION) {
513
+ return signatureOnly(
514
+ signature,
515
+ "mandate_sig_required",
516
+ `the chain is not recomputable without the mandate signature. Chain v${versionRaw} seals the mandate's raw signature, a bearer proof this verifier does not take; and a receipt sealed before ent#1670 may not recompute from its read at all. It carries no approval link either: that starts at v${FIRST_THIRD_PARTY_CHAIN_VERSION}`,
517
+ versionRaw,
518
+ );
519
+ }
520
+
521
+ const recomputed = recomputeChain(read, recipe.recipe);
522
+ if (!recomputed.ok) return signatureOnly(signature, recomputed.reason, recomputed.message, recomputed.version);
523
+ const sealedChain = read["chain"];
524
+ if (recomputed.chain !== sealedChain) {
525
+ return {
526
+ ...signature,
527
+ verdict: "chain_mismatch",
528
+ reason: "body_does_not_recompute",
529
+ message: `receipt ${signature.receipt_id} carries a genuine CodeSpar signature over chain ${sealedChain}, and the body beside it recomputes to ${recomputed.chain} under the published recipe (v${recomputed.version}): this body is not the one that was sealed. A payee, an amount, a timestamp or the approval was changed after sealing`,
530
+ chain_check: { status: "mismatch", version: recomputed.version, recomputed: recomputed.chain, reason: "body_does_not_recompute" },
531
+ };
532
+ }
533
+ const chainCheck: ChainCheck = { status: "recomputed", version: recomputed.version, recomputed: recomputed.chain, reason: "body_recomputes" };
534
+ const bound = `the body recomputes to the signed chain under the published recipe (v${recomputed.version})`;
535
+
536
+ const sealed = readSealedApproval(read);
537
+ if (!sealed) {
538
+ // Unreachable through the published recipe (v4 seals the link, and a read without it fails the recomputation above), kept so a future version is not misread.
539
+ return { ...signature, reason: "body_recomputes", message: `receipt ${signature.receipt_id} was sealed by CodeSpar and ${bound}; it seals no approval link`, chain_check: chainCheck };
540
+ }
541
+ const approval = options.approval;
542
+ if (!approval) {
543
+ return {
544
+ ...signature,
545
+ reason: "body_recomputes",
546
+ message: `receipt ${signature.receipt_id} was sealed by CodeSpar and ${bound}. It seals the approval ${sealed.items_hash}${sealed.batch_hash ? ` (batch ${sealed.batch_hash})` : ""}; no approval artifact was given to hold it against`,
547
+ chain_check: chainCheck,
548
+ approval_check: { status: "not_compared", sealed, artifact: null, reason: "no_artifact" },
549
+ };
550
+ }
551
+
552
+ const artifact = { approval_id: approval.approval_id, items_hash: approval.items_hash, batch_hash: approval.batch?.batch_hash ?? null };
553
+ const mismatch = (reason: string, why: string): ReceiptVerification => ({
554
+ ...signature,
555
+ verdict: "approval_mismatch",
556
+ reason,
557
+ message: `receipt ${signature.receipt_id} was sealed by CodeSpar and ${bound}, and ${why}: this payment was not sealed against the list artifact ${approval.approval_id} approved`,
558
+ chain_check: chainCheck,
559
+ approval_check: { status: "mismatch", sealed, artifact, reason },
560
+ });
561
+ if (approval.items && itemsHash(approval.items) !== approval.items_hash) {
562
+ return mismatch("artifact_items_hash_inconsistent", `the artifact's items_hash ${approval.items_hash} is not the hash of the items it lists (${itemsHash(approval.items)})`);
563
+ }
564
+ if (!sameApprovalHash(sealed.items_hash, artifact.items_hash)) {
565
+ return mismatch("items_hash_differs", `it seals items_hash ${sealed.items_hash} where the artifact carries ${artifact.items_hash}`);
566
+ }
567
+ if (!sameApprovalHash(sealed.batch_hash, artifact.batch_hash)) {
568
+ return mismatch("batch_hash_differs", `it seals batch_hash ${sealed.batch_hash ?? "none"} where the artifact carries ${artifact.batch_hash ?? "none"}`);
569
+ }
570
+ return {
571
+ ...signature,
572
+ reason: "approval_matches",
573
+ message: `receipt ${signature.receipt_id} was sealed by CodeSpar, ${bound}, and it was paid against the list artifact ${approval.approval_id} approved (${sealed.items_hash}${sealed.batch_hash ? `, batch ${sealed.batch_hash}` : ""}). WHO approved it is in the artifact, under a local key, and nowhere in the seal`,
574
+ chain_check: chainCheck,
575
+ approval_check: { status: "matched", sealed, artifact, reason: "approval_matches" },
576
+ };
577
+ }