@codespar/cli 0.17.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 (480) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/templates/bills-agent/README.md +1 -1
  4. package/templates/bills-agent/agents/bills-agent/AGENTS.md +1 -1
  5. package/templates/bills-agent/agents/bills-agent/CLAUDE.md +1 -1
  6. package/templates/bills-agent/agents/bills-agent/README.md +6 -5
  7. package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +5 -4
  8. package/templates/bills-agent/packages/agent-core/README.md +14 -1
  9. package/templates/bills-agent/packages/agent-core/package.json +1 -1
  10. package/templates/bills-agent/packages/agent-core/src/agent.ts +39 -1
  11. package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  12. package/templates/bills-agent/packages/agent-core/src/api/client.ts +28 -5
  13. package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  14. package/templates/bills-agent/packages/agent-core/src/api/rail.ts +62 -26
  15. package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  16. package/templates/bills-agent/packages/agent-core/src/approval.ts +14 -2
  17. package/templates/bills-agent/packages/agent-core/src/bundle.ts +35 -3
  18. package/templates/bills-agent/packages/agent-core/src/channels.ts +49 -3
  19. package/templates/bills-agent/packages/agent-core/src/check.ts +25 -2
  20. package/templates/bills-agent/packages/agent-core/src/engine.ts +235 -46
  21. package/templates/bills-agent/packages/agent-core/src/hash.ts +57 -1
  22. package/templates/bills-agent/packages/agent-core/src/index.ts +2 -0
  23. package/templates/bills-agent/packages/agent-core/src/quote.ts +50 -0
  24. package/templates/bills-agent/packages/agent-core/src/rail.ts +89 -4
  25. package/templates/bills-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  26. package/templates/bills-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  27. package/templates/bills-agent/packages/agent-core/src/revocation.ts +1 -1
  28. package/templates/bills-agent/packages/agent-core/src/state-machine.ts +10 -1
  29. package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  30. package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  31. package/templates/bills-agent/packages/agent-core/src/tools.ts +17 -6
  32. package/templates/bills-agent/packages/agent-core/src/types.ts +54 -1
  33. package/templates/bills-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  34. package/templates/bills-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  35. package/templates/bills-agent/packages/agent-core/test/composition.test.ts +204 -0
  36. package/templates/bills-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  37. package/templates/bills-agent/packages/agent-core/test/helpers.ts +4 -1
  38. package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  39. package/templates/bills-agent/packages/agent-core/test/quote.test.ts +198 -0
  40. package/templates/bills-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  41. package/templates/bills-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  42. package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +103 -3
  43. package/templates/bills-agent/packages/agent-runtime/README.md +12 -0
  44. package/templates/bills-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  45. package/templates/bills-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  46. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  47. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  48. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  49. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  50. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  51. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  52. package/templates/bills-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  53. package/templates/bills-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  54. package/templates/bills-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  55. package/templates/bills-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  56. package/templates/bills-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  57. package/templates/bills-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  58. package/templates/bills-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  59. package/templates/bills-agent/packages/agent-runtime/src/index.ts +1 -0
  60. package/templates/bills-agent/packages/agent-runtime/src/inspect.ts +5 -1
  61. package/templates/bills-agent/packages/agent-runtime/src/kit.ts +25 -2
  62. package/templates/bills-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  63. package/templates/bills-agent/packages/agent-runtime/src/setup.ts +13 -3
  64. package/templates/bills-agent/packages/agent-runtime/src/terminal.ts +127 -18
  65. package/templates/bills-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  66. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  67. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  68. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  69. package/templates/checkout-agent/LICENSE +21 -0
  70. package/templates/checkout-agent/README.md +28 -0
  71. package/templates/checkout-agent/_gitignore +10 -0
  72. package/templates/checkout-agent/agents/checkout-agent/.env.example +5 -0
  73. package/templates/checkout-agent/agents/checkout-agent/AGENTS.md +20 -0
  74. package/templates/checkout-agent/agents/checkout-agent/CLAUDE.md +20 -0
  75. package/templates/checkout-agent/agents/checkout-agent/README.md +94 -0
  76. package/templates/checkout-agent/agents/checkout-agent/SYSTEM_PROMPT.md +36 -0
  77. package/templates/checkout-agent/agents/checkout-agent/agent.yaml +30 -0
  78. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/README.md +28 -0
  79. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-beatriz.json +11 -0
  80. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina-retomada.json +10 -0
  81. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina.json +11 -0
  82. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/templates.json +49 -0
  83. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap-human.json +12 -0
  84. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.json +11 -0
  85. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +5 -0
  86. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.json +13 -0
  87. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.transcript.jsonl +4 -0
  88. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.json +26 -0
  89. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
  90. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.json +11 -0
  91. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.transcript.jsonl +5 -0
  92. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.json +12 -0
  93. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.transcript.jsonl +5 -0
  94. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.json +13 -0
  95. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.transcript.jsonl +5 -0
  96. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.json +15 -0
  97. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.transcript.jsonl +7 -0
  98. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.json +12 -0
  99. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.transcript.jsonl +5 -0
  100. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.json +13 -0
  101. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.transcript.jsonl +4 -0
  102. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.json +16 -0
  103. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.transcript.jsonl +9 -0
  104. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.json +15 -0
  105. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.transcript.jsonl +8 -0
  106. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/webhook-replay.json +11 -0
  107. package/templates/checkout-agent/agents/checkout-agent/evals/eval.yaml +11 -0
  108. package/templates/checkout-agent/agents/checkout-agent/guardrails.json +25 -0
  109. package/templates/checkout-agent/agents/checkout-agent/mandate.example.json +28 -0
  110. package/templates/checkout-agent/agents/checkout-agent/package.json +33 -0
  111. package/templates/checkout-agent/agents/checkout-agent/runbook.md +19 -0
  112. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.json +42 -0
  113. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +4 -0
  114. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.json +32 -0
  115. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.transcript.jsonl +4 -0
  116. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.json +61 -0
  117. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.transcript.jsonl +9 -0
  118. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.json +61 -0
  119. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.transcript.jsonl +9 -0
  120. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.json +36 -0
  121. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.transcript.jsonl +6 -0
  122. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.json +23 -0
  123. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.transcript.jsonl +4 -0
  124. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.json +72 -0
  125. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.transcript.jsonl +9 -0
  126. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.json +15 -0
  127. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.transcript.jsonl +6 -0
  128. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.json +23 -0
  129. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.transcript.jsonl +3 -0
  130. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.json +42 -0
  131. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.transcript.jsonl +12 -0
  132. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.json +14 -0
  133. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.transcript.jsonl +9 -0
  134. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.json +34 -0
  135. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.transcript.jsonl +7 -0
  136. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.json +33 -0
  137. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.transcript.jsonl +4 -0
  138. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.json +32 -0
  139. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.transcript.jsonl +4 -0
  140. package/templates/checkout-agent/agents/checkout-agent/src/catalog.ts +92 -0
  141. package/templates/checkout-agent/agents/checkout-agent/src/kit.ts +338 -0
  142. package/templates/checkout-agent/agents/checkout-agent/src/modules/bolepix-receivables.ts +179 -0
  143. package/templates/checkout-agent/agents/checkout-agent/src/modules/nfse-invoice.ts +374 -0
  144. package/templates/checkout-agent/agents/checkout-agent/src/modules/storefront-cart.ts +220 -0
  145. package/templates/checkout-agent/agents/checkout-agent/src/pricing.ts +297 -0
  146. package/templates/checkout-agent/agents/checkout-agent/test/adversarial.test.ts +117 -0
  147. package/templates/checkout-agent/agents/checkout-agent/test/check.test.ts +67 -0
  148. package/templates/checkout-agent/agents/checkout-agent/test/cli.test.ts +169 -0
  149. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/order-marina.transcript.jsonl +5 -0
  150. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/ordered-marina-awaiting-payer.transcript.jsonl +6 -0
  151. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/tap-falar-agora-marina.transcript.jsonl +2 -0
  152. package/templates/checkout-agent/agents/checkout-agent/test/nfse.test.ts +170 -0
  153. package/templates/checkout-agent/agents/checkout-agent/test/pricing.test.ts +138 -0
  154. package/templates/checkout-agent/agents/checkout-agent/test/scenarios.test.ts +113 -0
  155. package/templates/checkout-agent/agents/checkout-agent/test/whatsapp.test.ts +171 -0
  156. package/templates/checkout-agent/agents/checkout-agent/tools.json +68 -0
  157. package/templates/checkout-agent/agents/checkout-agent/tsconfig.json +10 -0
  158. package/templates/checkout-agent/package.json +38 -0
  159. package/templates/checkout-agent/packages/agent-core/README.md +34 -0
  160. package/templates/checkout-agent/packages/agent-core/package.json +24 -0
  161. package/templates/checkout-agent/packages/agent-core/src/agent.ts +163 -0
  162. package/templates/checkout-agent/packages/agent-core/src/api/charge-rail.ts +195 -0
  163. package/templates/checkout-agent/packages/agent-core/src/api/client.ts +76 -0
  164. package/templates/checkout-agent/packages/agent-core/src/api/mandate-status.ts +74 -0
  165. package/templates/checkout-agent/packages/agent-core/src/api/rail.ts +153 -0
  166. package/templates/checkout-agent/packages/agent-core/src/api/sandbox-payer.ts +43 -0
  167. package/templates/checkout-agent/packages/agent-core/src/approval.ts +135 -0
  168. package/templates/checkout-agent/packages/agent-core/src/bundle.ts +191 -0
  169. package/templates/checkout-agent/packages/agent-core/src/channels.ts +186 -0
  170. package/templates/checkout-agent/packages/agent-core/src/check.ts +276 -0
  171. package/templates/checkout-agent/packages/agent-core/src/clock.ts +42 -0
  172. package/templates/checkout-agent/packages/agent-core/src/engine.ts +1074 -0
  173. package/templates/checkout-agent/packages/agent-core/src/escalate.ts +85 -0
  174. package/templates/checkout-agent/packages/agent-core/src/events.ts +40 -0
  175. package/templates/checkout-agent/packages/agent-core/src/guardrails.ts +45 -0
  176. package/templates/checkout-agent/packages/agent-core/src/hash.ts +118 -0
  177. package/templates/checkout-agent/packages/agent-core/src/ids.ts +10 -0
  178. package/templates/checkout-agent/packages/agent-core/src/index.ts +34 -0
  179. package/templates/checkout-agent/packages/agent-core/src/mandate.ts +100 -0
  180. package/templates/checkout-agent/packages/agent-core/src/manifest.ts +76 -0
  181. package/templates/checkout-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
  182. package/templates/checkout-agent/packages/agent-core/src/providers/replay.ts +63 -0
  183. package/templates/checkout-agent/packages/agent-core/src/providers/types.ts +39 -0
  184. package/templates/checkout-agent/packages/agent-core/src/quote.ts +50 -0
  185. package/templates/checkout-agent/packages/agent-core/src/rail.ts +186 -0
  186. package/templates/checkout-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  187. package/templates/checkout-agent/packages/agent-core/src/receipt-verification.ts +577 -0
  188. package/templates/checkout-agent/packages/agent-core/src/revocation.ts +30 -0
  189. package/templates/checkout-agent/packages/agent-core/src/secrets.ts +30 -0
  190. package/templates/checkout-agent/packages/agent-core/src/state/store.ts +307 -0
  191. package/templates/checkout-agent/packages/agent-core/src/state-machine.ts +157 -0
  192. package/templates/checkout-agent/packages/agent-core/src/stubs/charge-rail.ts +195 -0
  193. package/templates/checkout-agent/packages/agent-core/src/stubs/mandate-status.ts +58 -0
  194. package/templates/checkout-agent/packages/agent-core/src/stubs/rail.ts +209 -0
  195. package/templates/checkout-agent/packages/agent-core/src/tools.ts +66 -0
  196. package/templates/checkout-agent/packages/agent-core/src/types.ts +187 -0
  197. package/templates/checkout-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  198. package/templates/checkout-agent/packages/agent-core/test/approval.test.ts +117 -0
  199. package/templates/checkout-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  200. package/templates/checkout-agent/packages/agent-core/test/bundle.test.ts +85 -0
  201. package/templates/checkout-agent/packages/agent-core/test/clock.test.ts +25 -0
  202. package/templates/checkout-agent/packages/agent-core/test/composition.test.ts +204 -0
  203. package/templates/checkout-agent/packages/agent-core/test/engine.test.ts +596 -0
  204. package/templates/checkout-agent/packages/agent-core/test/escalate.test.ts +42 -0
  205. package/templates/checkout-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  206. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
  207. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
  208. package/templates/checkout-agent/packages/agent-core/test/helpers.ts +142 -0
  209. package/templates/checkout-agent/packages/agent-core/test/mandate-status.test.ts +229 -0
  210. package/templates/checkout-agent/packages/agent-core/test/manifest.test.ts +45 -0
  211. package/templates/checkout-agent/packages/agent-core/test/quote.test.ts +198 -0
  212. package/templates/checkout-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  213. package/templates/checkout-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  214. package/templates/checkout-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
  215. package/templates/checkout-agent/packages/agent-core/test/receivables.test.ts +416 -0
  216. package/templates/checkout-agent/packages/agent-core/test/secrets.test.ts +28 -0
  217. package/templates/checkout-agent/packages/agent-core/test/state-machine.test.ts +67 -0
  218. package/templates/checkout-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
  219. package/templates/checkout-agent/packages/agent-core/tsconfig.json +10 -0
  220. package/templates/checkout-agent/packages/agent-runtime/README.md +80 -0
  221. package/templates/checkout-agent/packages/agent-runtime/bin.mjs +32 -0
  222. package/templates/checkout-agent/packages/agent-runtime/package.json +23 -0
  223. package/templates/checkout-agent/packages/agent-runtime/src/adversarial.ts +199 -0
  224. package/templates/checkout-agent/packages/agent-runtime/src/agent.ts +68 -0
  225. package/templates/checkout-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
  226. package/templates/checkout-agent/packages/agent-runtime/src/channels/index.ts +72 -0
  227. package/templates/checkout-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
  228. package/templates/checkout-agent/packages/agent-runtime/src/channels/types.ts +173 -0
  229. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +459 -0
  230. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +216 -0
  231. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
  232. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +350 -0
  233. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +180 -0
  234. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
  235. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +120 -0
  236. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +127 -0
  237. package/templates/checkout-agent/packages/agent-runtime/src/cli.ts +98 -0
  238. package/templates/checkout-agent/packages/agent-runtime/src/commands/check.ts +15 -0
  239. package/templates/checkout-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
  240. package/templates/checkout-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
  241. package/templates/checkout-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
  242. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +367 -0
  243. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll.ts +145 -0
  244. package/templates/checkout-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
  245. package/templates/checkout-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
  246. package/templates/checkout-agent/packages/agent-runtime/src/commands/resume.ts +49 -0
  247. package/templates/checkout-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +134 -0
  248. package/templates/checkout-agent/packages/agent-runtime/src/commands/start.ts +248 -0
  249. package/templates/checkout-agent/packages/agent-runtime/src/commands/verify.ts +330 -0
  250. package/templates/checkout-agent/packages/agent-runtime/src/commands/webhook.ts +40 -0
  251. package/templates/checkout-agent/packages/agent-runtime/src/default-kit.ts +83 -0
  252. package/templates/checkout-agent/packages/agent-runtime/src/index.ts +13 -0
  253. package/templates/checkout-agent/packages/agent-runtime/src/inspect.ts +801 -0
  254. package/templates/checkout-agent/packages/agent-runtime/src/kit.ts +229 -0
  255. package/templates/checkout-agent/packages/agent-runtime/src/poll.ts +105 -0
  256. package/templates/checkout-agent/packages/agent-runtime/src/scenarios.ts +274 -0
  257. package/templates/checkout-agent/packages/agent-runtime/src/setup.ts +239 -0
  258. package/templates/checkout-agent/packages/agent-runtime/src/terminal.ts +263 -0
  259. package/templates/checkout-agent/packages/agent-runtime/src/webhook.ts +131 -0
  260. package/templates/checkout-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
  261. package/templates/checkout-agent/packages/agent-runtime/test/verify-cli.test.ts +329 -0
  262. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +277 -0
  263. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
  264. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
  265. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-session.test.ts +167 -0
  266. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  267. package/templates/checkout-agent/packages/agent-runtime/tsconfig.json +9 -0
  268. package/templates/checkout-agent/scripts/whatsapp-emulator.mjs +84 -0
  269. package/templates/checkout-agent/tsconfig.base.json +16 -0
  270. package/templates/checkout-agent/vitest.config.ts +12 -0
  271. package/templates/collections-agent/README.md +6 -1
  272. package/templates/collections-agent/agents/collections-agent/README.md +12 -8
  273. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/README.md +8 -0
  274. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/acordo-1042-retomada.json +10 -0
  275. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/templates.json +27 -1
  276. package/templates/collections-agent/agents/collections-agent/src/kit.ts +2 -0
  277. package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +12 -2
  278. package/templates/collections-agent/agents/collections-agent/test/ambiguous-charge.test.ts +55 -0
  279. package/templates/collections-agent/agents/collections-agent/test/fixtures/tap-emitir-nova-1042.transcript.jsonl +2 -0
  280. package/templates/collections-agent/agents/collections-agent/test/whatsapp-poll.test.ts +12 -6
  281. package/templates/collections-agent/agents/collections-agent/test/whatsapp-status-replies.test.ts +148 -0
  282. package/templates/collections-agent/agents/collections-agent/test/whatsapp.test.ts +5 -0
  283. package/templates/collections-agent/package.json +1 -0
  284. package/templates/collections-agent/packages/agent-core/README.md +14 -1
  285. package/templates/collections-agent/packages/agent-core/package.json +1 -1
  286. package/templates/collections-agent/packages/agent-core/src/agent.ts +39 -1
  287. package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  288. package/templates/collections-agent/packages/agent-core/src/api/client.ts +28 -5
  289. package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  290. package/templates/collections-agent/packages/agent-core/src/api/rail.ts +62 -26
  291. package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  292. package/templates/collections-agent/packages/agent-core/src/approval.ts +14 -2
  293. package/templates/collections-agent/packages/agent-core/src/bundle.ts +35 -3
  294. package/templates/collections-agent/packages/agent-core/src/channels.ts +49 -3
  295. package/templates/collections-agent/packages/agent-core/src/check.ts +25 -2
  296. package/templates/collections-agent/packages/agent-core/src/engine.ts +235 -46
  297. package/templates/collections-agent/packages/agent-core/src/hash.ts +57 -1
  298. package/templates/collections-agent/packages/agent-core/src/index.ts +2 -0
  299. package/templates/collections-agent/packages/agent-core/src/quote.ts +50 -0
  300. package/templates/collections-agent/packages/agent-core/src/rail.ts +89 -4
  301. package/templates/collections-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  302. package/templates/collections-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  303. package/templates/collections-agent/packages/agent-core/src/revocation.ts +1 -1
  304. package/templates/collections-agent/packages/agent-core/src/state-machine.ts +10 -1
  305. package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  306. package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  307. package/templates/collections-agent/packages/agent-core/src/tools.ts +17 -6
  308. package/templates/collections-agent/packages/agent-core/src/types.ts +54 -1
  309. package/templates/collections-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  310. package/templates/collections-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  311. package/templates/collections-agent/packages/agent-core/test/composition.test.ts +204 -0
  312. package/templates/collections-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  313. package/templates/collections-agent/packages/agent-core/test/helpers.ts +4 -1
  314. package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  315. package/templates/collections-agent/packages/agent-core/test/quote.test.ts +198 -0
  316. package/templates/collections-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  317. package/templates/collections-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  318. package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +103 -3
  319. package/templates/collections-agent/packages/agent-runtime/README.md +12 -0
  320. package/templates/collections-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  321. package/templates/collections-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  322. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  323. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  324. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  325. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  326. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  327. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  328. package/templates/collections-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  329. package/templates/collections-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  330. package/templates/collections-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  331. package/templates/collections-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  332. package/templates/collections-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  333. package/templates/collections-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  334. package/templates/collections-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  335. package/templates/collections-agent/packages/agent-runtime/src/index.ts +1 -0
  336. package/templates/collections-agent/packages/agent-runtime/src/inspect.ts +5 -1
  337. package/templates/collections-agent/packages/agent-runtime/src/kit.ts +25 -2
  338. package/templates/collections-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  339. package/templates/collections-agent/packages/agent-runtime/src/setup.ts +13 -3
  340. package/templates/collections-agent/packages/agent-runtime/src/terminal.ts +127 -18
  341. package/templates/collections-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  342. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  343. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  344. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  345. package/templates/collections-agent/scripts/whatsapp-emulator.mjs +84 -0
  346. package/templates/hello-agent/README.md +1 -1
  347. package/templates/hello-agent/packages/agent-core/README.md +14 -1
  348. package/templates/hello-agent/packages/agent-core/package.json +1 -1
  349. package/templates/hello-agent/packages/agent-core/src/agent.ts +39 -1
  350. package/templates/hello-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  351. package/templates/hello-agent/packages/agent-core/src/api/client.ts +28 -5
  352. package/templates/hello-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  353. package/templates/hello-agent/packages/agent-core/src/api/rail.ts +62 -26
  354. package/templates/hello-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  355. package/templates/hello-agent/packages/agent-core/src/approval.ts +14 -2
  356. package/templates/hello-agent/packages/agent-core/src/bundle.ts +35 -3
  357. package/templates/hello-agent/packages/agent-core/src/channels.ts +49 -3
  358. package/templates/hello-agent/packages/agent-core/src/check.ts +25 -2
  359. package/templates/hello-agent/packages/agent-core/src/engine.ts +235 -46
  360. package/templates/hello-agent/packages/agent-core/src/hash.ts +57 -1
  361. package/templates/hello-agent/packages/agent-core/src/index.ts +2 -0
  362. package/templates/hello-agent/packages/agent-core/src/quote.ts +50 -0
  363. package/templates/hello-agent/packages/agent-core/src/rail.ts +89 -4
  364. package/templates/hello-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  365. package/templates/hello-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  366. package/templates/hello-agent/packages/agent-core/src/revocation.ts +1 -1
  367. package/templates/hello-agent/packages/agent-core/src/state-machine.ts +10 -1
  368. package/templates/hello-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  369. package/templates/hello-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  370. package/templates/hello-agent/packages/agent-core/src/tools.ts +17 -6
  371. package/templates/hello-agent/packages/agent-core/src/types.ts +54 -1
  372. package/templates/hello-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  373. package/templates/hello-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  374. package/templates/hello-agent/packages/agent-core/test/composition.test.ts +204 -0
  375. package/templates/hello-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  376. package/templates/hello-agent/packages/agent-core/test/helpers.ts +4 -1
  377. package/templates/hello-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  378. package/templates/hello-agent/packages/agent-core/test/quote.test.ts +198 -0
  379. package/templates/hello-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  380. package/templates/hello-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  381. package/templates/hello-agent/packages/agent-core/test/receivables.test.ts +103 -3
  382. package/templates/hello-agent/packages/agent-runtime/README.md +12 -0
  383. package/templates/hello-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  384. package/templates/hello-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  385. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  386. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  387. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  388. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  389. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  390. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  391. package/templates/hello-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  392. package/templates/hello-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  393. package/templates/hello-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  394. package/templates/hello-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  395. package/templates/hello-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  396. package/templates/hello-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  397. package/templates/hello-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  398. package/templates/hello-agent/packages/agent-runtime/src/index.ts +1 -0
  399. package/templates/hello-agent/packages/agent-runtime/src/inspect.ts +5 -1
  400. package/templates/hello-agent/packages/agent-runtime/src/kit.ts +25 -2
  401. package/templates/hello-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  402. package/templates/hello-agent/packages/agent-runtime/src/setup.ts +13 -3
  403. package/templates/hello-agent/packages/agent-runtime/src/terminal.ts +127 -18
  404. package/templates/hello-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  405. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  406. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  407. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  408. package/templates/kits.lock.json +29 -6
  409. package/templates/supplier-payments-agent/README.md +1 -1
  410. package/templates/supplier-payments-agent/agents/supplier-payments-agent/AGENTS.md +1 -1
  411. package/templates/supplier-payments-agent/agents/supplier-payments-agent/CLAUDE.md +1 -1
  412. package/templates/supplier-payments-agent/agents/supplier-payments-agent/README.md +11 -9
  413. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/kit.ts +1 -1
  414. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/modules/batch-payout.ts +67 -20
  415. package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/batch.test.ts +504 -5
  416. package/templates/supplier-payments-agent/packages/agent-core/README.md +14 -1
  417. package/templates/supplier-payments-agent/packages/agent-core/package.json +1 -1
  418. package/templates/supplier-payments-agent/packages/agent-core/src/agent.ts +39 -1
  419. package/templates/supplier-payments-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  420. package/templates/supplier-payments-agent/packages/agent-core/src/api/client.ts +28 -5
  421. package/templates/supplier-payments-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  422. package/templates/supplier-payments-agent/packages/agent-core/src/api/rail.ts +62 -26
  423. package/templates/supplier-payments-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  424. package/templates/supplier-payments-agent/packages/agent-core/src/approval.ts +14 -2
  425. package/templates/supplier-payments-agent/packages/agent-core/src/bundle.ts +35 -3
  426. package/templates/supplier-payments-agent/packages/agent-core/src/channels.ts +49 -3
  427. package/templates/supplier-payments-agent/packages/agent-core/src/check.ts +25 -2
  428. package/templates/supplier-payments-agent/packages/agent-core/src/engine.ts +235 -46
  429. package/templates/supplier-payments-agent/packages/agent-core/src/hash.ts +57 -1
  430. package/templates/supplier-payments-agent/packages/agent-core/src/index.ts +2 -0
  431. package/templates/supplier-payments-agent/packages/agent-core/src/quote.ts +50 -0
  432. package/templates/supplier-payments-agent/packages/agent-core/src/rail.ts +89 -4
  433. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  434. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  435. package/templates/supplier-payments-agent/packages/agent-core/src/revocation.ts +1 -1
  436. package/templates/supplier-payments-agent/packages/agent-core/src/state-machine.ts +10 -1
  437. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  438. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  439. package/templates/supplier-payments-agent/packages/agent-core/src/tools.ts +17 -6
  440. package/templates/supplier-payments-agent/packages/agent-core/src/types.ts +54 -1
  441. package/templates/supplier-payments-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  442. package/templates/supplier-payments-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  443. package/templates/supplier-payments-agent/packages/agent-core/test/composition.test.ts +204 -0
  444. package/templates/supplier-payments-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  445. package/templates/supplier-payments-agent/packages/agent-core/test/helpers.ts +4 -1
  446. package/templates/supplier-payments-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  447. package/templates/supplier-payments-agent/packages/agent-core/test/quote.test.ts +198 -0
  448. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  449. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  450. package/templates/supplier-payments-agent/packages/agent-core/test/receivables.test.ts +103 -3
  451. package/templates/supplier-payments-agent/packages/agent-runtime/README.md +12 -0
  452. package/templates/supplier-payments-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  453. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  454. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  455. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  456. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  457. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  458. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  459. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  460. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  461. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  462. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  463. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  464. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  465. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  466. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  467. package/templates/supplier-payments-agent/packages/agent-runtime/src/index.ts +1 -0
  468. package/templates/supplier-payments-agent/packages/agent-runtime/src/inspect.ts +5 -1
  469. package/templates/supplier-payments-agent/packages/agent-runtime/src/kit.ts +25 -2
  470. package/templates/supplier-payments-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  471. package/templates/supplier-payments-agent/packages/agent-runtime/src/setup.ts +13 -3
  472. package/templates/supplier-payments-agent/packages/agent-runtime/src/terminal.ts +127 -18
  473. package/templates/supplier-payments-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  474. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  475. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  476. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  477. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  478. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  479. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  480. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
@@ -1,5 +1,5 @@
1
1
  import { createHash, createHmac, timingSafeEqual } from "node:crypto";
2
- import type { ExecutionItem } from "./types.js";
2
+ import type { CompositionLine, ExecutionBatch, ExecutionItem } from "./types.js";
3
3
 
4
4
  /** JSON with keys sorted at every level, so the same value always hashes the same. */
5
5
  export function canonicalJson(value: unknown): string {
@@ -52,6 +52,62 @@ export function batchHash(lines: readonly ExecutionItem[]): string {
52
52
  return itemsHash(lines);
53
53
  }
54
54
 
55
+ /**
56
+ * The hash of what an execution's amount is composed of (a cart's resolved
57
+ * lines, in order). The same canonicalisation as `itemsHash` — sorted-key
58
+ * JSON, SHA-256, the `sha256:` prefix — over the fields that decide a
59
+ * composition, and deliberately not `itemsHash` itself: its fields are the
60
+ * ones that decide where money goes (payee, amount, currency, due date), and
61
+ * none of them sees a quantity or a unit price. Ten units at a 10% discount
62
+ * and nine at list price are the same line amount; they are not the same
63
+ * sale. Order is part of it, like a batch.
64
+ */
65
+ export function compositionHash(lines: readonly CompositionLine[]): string {
66
+ const canonical = lines.map((line) => ({
67
+ ref: line.ref,
68
+ quantity: line.quantity,
69
+ unit_amount: line.unit_amount,
70
+ amount: line.amount,
71
+ currency: line.currency,
72
+ }));
73
+ return `sha256:${sha256Hex(canonicalJson(canonical))}`;
74
+ }
75
+
76
+ /**
77
+ * The `attempt_id` of an item of a batch line, derived from WHAT is being
78
+ * paid rather than from the execution that happens to pay it:
79
+ *
80
+ * ska_ + hex(sha256(mandate_id | batch_hash | line index | item index))
81
+ *
82
+ * An execution's own attempt ids come from its `idempotency_key`, which comes
83
+ * from a random execution id, so a second run of the same batch on a machine
84
+ * that has lost `.codespar/state.db` (or never had it) would present fresh ids
85
+ * and the API would take each line as a new payment (OPEN_QUESTIONS §39c).
86
+ * Derived this way, the same line of the same list under the same mandate is
87
+ * the same attempt on any machine, and the API answers a repeat of it from its
88
+ * record of that attempt (ent#1671): the original body when it settled, a 409
89
+ * while it is in flight or pinned, never a second dispatch.
90
+ *
91
+ * Every input decides the payment: the mandate is whose money, the hash is the
92
+ * exact ordered list (payee, amount, currency of every line), the index is
93
+ * which line of it. A list that changes in any line is a different hash and so
94
+ * different ids. `ska_` names the namespace (starter-kit attempt) and 68
95
+ * characters sit inside the API's 128.
96
+ *
97
+ * `generation` exists because the API never runs an attempt id twice: once an
98
+ * attempt FAILED and moved no money, a repeat of it answers
99
+ * `psp_attempt_conflict` for good. A line whose payment the provider refused
100
+ * must still be payable by a later run of the same list, so it moves to the
101
+ * next generation, and only on that answer: the server saying the id is
102
+ * spent is the one fact every machine reads the same way, so every machine
103
+ * walks the same sequence and none of them finds a "free" id past one that
104
+ * paid. Generation 0 hashes exactly as above.
105
+ */
106
+ export function batchAttemptId(mandateId: string, batch: Pick<ExecutionBatch, "batch_hash" | "index">, item: number, generation = 0): string {
107
+ const base = `${mandateId}|${batch.batch_hash}|${batch.index}|${item}`;
108
+ return `ska_${sha256Hex(generation === 0 ? base : `${base}|${generation}`)}`;
109
+ }
110
+
55
111
  export function hmacSha256Hex(key: Buffer, payload: string): string {
56
112
  return createHmac("sha256", key).update(payload).digest("hex");
57
113
  }
@@ -12,7 +12,9 @@ export * from "./clock.js";
12
12
  export * from "./secrets.js";
13
13
  export * from "./revocation.js";
14
14
  export * from "./rail.js";
15
+ export * from "./quote.js";
15
16
  export * from "./receipt-verification.js";
17
+ export * from "./receipt-chain.js";
16
18
  export * from "./tools.js";
17
19
  export * from "./bundle.js";
18
20
  export * from "./engine.js";
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The offer a spend presents to the API (`SpendQuote`): seller, resource,
3
+ * price and payee. When a spend carries one, the API binds it into the
4
+ * receipt's chain between the mandate and the payment, so the sealed receipt
5
+ * names the payee; without one the chain carries no payee at all, and
6
+ * `GET /v1/consumers/receipts/{id}` answers `quote: null` (OPEN_QUESTIONS §18).
7
+ *
8
+ * The quote is built from the approval artifact's items, the list its
9
+ * `items_hash` covers, and from nothing the model says after approval. The
10
+ * API compares the quote to what settled in OBSERVE mode only: a divergence is
11
+ * recorded on the receipt and the money still moves. So the kit refuses a
12
+ * divergence itself, before the call.
13
+ */
14
+ import type { ApprovalArtifact } from "./types.js";
15
+
16
+ export interface SpendQuote {
17
+ seller: string;
18
+ resource: string;
19
+ price_minor: number;
20
+ payee: string;
21
+ /**
22
+ * When the offer was approved: the artifact's `approved_at`. Absent on a
23
+ * batch line, whose quote must be the same on every run of the list: the
24
+ * API compares the quote's digest on a repeat of an `attempt_id`, so an
25
+ * approval time in it would make the same line a different payment on
26
+ * every machine (OPEN_QUESTIONS §39c). The time is still in the artifact.
27
+ */
28
+ at?: string;
29
+ }
30
+
31
+ /**
32
+ * The quote for line `index` of what was approved, or undefined when the
33
+ * artifact has no such line. `stable` leaves the approval time out, for a
34
+ * batch line (see `SpendQuote.at`).
35
+ */
36
+ export function quoteFromApproval(artifact: ApprovalArtifact, index: number, purpose: string, options: { stable?: boolean } = {}): SpendQuote | undefined {
37
+ const item = artifact.items[index];
38
+ if (!item) return undefined;
39
+ const quote = { seller: item.beneficiary, resource: item.description ?? purpose, price_minor: item.amount, payee: item.payee };
40
+ return options.stable ? quote : { ...quote, at: artifact.approved_at };
41
+ }
42
+
43
+ /** The quote a spend presents, or why it may not go out: none, or one that does not name exactly the amount and the payee it pays. */
44
+ export function checkQuote(payment: { amount_minor: number; payee: string; quote?: SpendQuote | undefined }): { ok: true; quote: SpendQuote } | { ok: false; code: "quote_missing" | "quote_mismatch"; detail: string } {
45
+ const { quote } = payment;
46
+ if (!quote) return { ok: false, code: "quote_missing", detail: "a spend carries the quote of what was approved, or it does not go out" };
47
+ if (quote.price_minor !== payment.amount_minor) return { ok: false, code: "quote_mismatch", detail: `the approved price ${quote.price_minor} is not the amount ${payment.amount_minor} about to be spent` };
48
+ if (quote.payee !== payment.payee) return { ok: false, code: "quote_mismatch", detail: "the approved payee is not the payee about to be paid" };
49
+ return { ok: true, quote };
50
+ }
@@ -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
+ }