@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.
- package/README.md +1 -0
- package/package.json +1 -1
- package/templates/bills-agent/README.md +1 -1
- package/templates/bills-agent/agents/bills-agent/AGENTS.md +1 -1
- package/templates/bills-agent/agents/bills-agent/CLAUDE.md +1 -1
- package/templates/bills-agent/agents/bills-agent/README.md +6 -5
- package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +5 -4
- package/templates/bills-agent/packages/agent-core/README.md +14 -1
- package/templates/bills-agent/packages/agent-core/package.json +1 -1
- package/templates/bills-agent/packages/agent-core/src/agent.ts +39 -1
- package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
- package/templates/bills-agent/packages/agent-core/src/api/client.ts +28 -5
- package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
- package/templates/bills-agent/packages/agent-core/src/api/rail.ts +62 -26
- package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
- package/templates/bills-agent/packages/agent-core/src/approval.ts +14 -2
- package/templates/bills-agent/packages/agent-core/src/bundle.ts +35 -3
- package/templates/bills-agent/packages/agent-core/src/channels.ts +49 -3
- package/templates/bills-agent/packages/agent-core/src/check.ts +25 -2
- package/templates/bills-agent/packages/agent-core/src/engine.ts +235 -46
- package/templates/bills-agent/packages/agent-core/src/hash.ts +57 -1
- package/templates/bills-agent/packages/agent-core/src/index.ts +2 -0
- package/templates/bills-agent/packages/agent-core/src/quote.ts +50 -0
- package/templates/bills-agent/packages/agent-core/src/rail.ts +89 -4
- package/templates/bills-agent/packages/agent-core/src/receipt-chain.ts +270 -0
- package/templates/bills-agent/packages/agent-core/src/receipt-verification.ts +194 -18
- package/templates/bills-agent/packages/agent-core/src/revocation.ts +1 -1
- package/templates/bills-agent/packages/agent-core/src/state-machine.ts +10 -1
- package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
- package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +100 -32
- package/templates/bills-agent/packages/agent-core/src/tools.ts +17 -6
- package/templates/bills-agent/packages/agent-core/src/types.ts +54 -1
- package/templates/bills-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
- package/templates/bills-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
- package/templates/bills-agent/packages/agent-core/test/composition.test.ts +204 -0
- package/templates/bills-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
- package/templates/bills-agent/packages/agent-core/test/helpers.ts +4 -1
- package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
- package/templates/bills-agent/packages/agent-core/test/quote.test.ts +198 -0
- package/templates/bills-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
- package/templates/bills-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
- package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +103 -3
- package/templates/bills-agent/packages/agent-runtime/README.md +12 -0
- package/templates/bills-agent/packages/agent-runtime/src/adversarial.ts +32 -2
- package/templates/bills-agent/packages/agent-runtime/src/channels/types.ts +53 -4
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
- package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
- package/templates/bills-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
- package/templates/bills-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
- package/templates/bills-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
- package/templates/bills-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
- package/templates/bills-agent/packages/agent-runtime/src/commands/start.ts +6 -3
- package/templates/bills-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
- package/templates/bills-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
- package/templates/bills-agent/packages/agent-runtime/src/index.ts +1 -0
- package/templates/bills-agent/packages/agent-runtime/src/inspect.ts +5 -1
- package/templates/bills-agent/packages/agent-runtime/src/kit.ts +25 -2
- package/templates/bills-agent/packages/agent-runtime/src/scenarios.ts +8 -2
- package/templates/bills-agent/packages/agent-runtime/src/setup.ts +13 -3
- package/templates/bills-agent/packages/agent-runtime/src/terminal.ts +127 -18
- package/templates/bills-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
- package/templates/bills-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
- package/templates/bills-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
- package/templates/bills-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
- package/templates/checkout-agent/LICENSE +21 -0
- package/templates/checkout-agent/README.md +28 -0
- package/templates/checkout-agent/_gitignore +10 -0
- package/templates/checkout-agent/agents/checkout-agent/.env.example +5 -0
- package/templates/checkout-agent/agents/checkout-agent/AGENTS.md +20 -0
- package/templates/checkout-agent/agents/checkout-agent/CLAUDE.md +20 -0
- package/templates/checkout-agent/agents/checkout-agent/README.md +94 -0
- package/templates/checkout-agent/agents/checkout-agent/SYSTEM_PROMPT.md +36 -0
- package/templates/checkout-agent/agents/checkout-agent/agent.yaml +30 -0
- package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/README.md +28 -0
- package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-beatriz.json +11 -0
- package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina-retomada.json +10 -0
- package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina.json +11 -0
- package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/templates.json +49 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap-human.json +12 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.json +11 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +5 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.json +13 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.json +26 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.json +11 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.transcript.jsonl +5 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.json +12 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.transcript.jsonl +5 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.json +13 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.transcript.jsonl +5 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.json +15 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.transcript.jsonl +7 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.json +12 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.transcript.jsonl +5 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.json +13 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.json +16 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.transcript.jsonl +9 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.json +15 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.transcript.jsonl +8 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/webhook-replay.json +11 -0
- package/templates/checkout-agent/agents/checkout-agent/evals/eval.yaml +11 -0
- package/templates/checkout-agent/agents/checkout-agent/guardrails.json +25 -0
- package/templates/checkout-agent/agents/checkout-agent/mandate.example.json +28 -0
- package/templates/checkout-agent/agents/checkout-agent/package.json +33 -0
- package/templates/checkout-agent/agents/checkout-agent/runbook.md +19 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.json +42 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.json +32 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.json +61 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.transcript.jsonl +9 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.json +61 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.transcript.jsonl +9 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.json +36 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.transcript.jsonl +6 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.json +23 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.json +72 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.transcript.jsonl +9 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.json +15 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.transcript.jsonl +6 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.json +23 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.transcript.jsonl +3 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.json +42 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.transcript.jsonl +12 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.json +14 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.transcript.jsonl +9 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.json +34 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.transcript.jsonl +7 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.json +33 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.json +32 -0
- package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.transcript.jsonl +4 -0
- package/templates/checkout-agent/agents/checkout-agent/src/catalog.ts +92 -0
- package/templates/checkout-agent/agents/checkout-agent/src/kit.ts +338 -0
- package/templates/checkout-agent/agents/checkout-agent/src/modules/bolepix-receivables.ts +179 -0
- package/templates/checkout-agent/agents/checkout-agent/src/modules/nfse-invoice.ts +374 -0
- package/templates/checkout-agent/agents/checkout-agent/src/modules/storefront-cart.ts +220 -0
- package/templates/checkout-agent/agents/checkout-agent/src/pricing.ts +297 -0
- package/templates/checkout-agent/agents/checkout-agent/test/adversarial.test.ts +117 -0
- package/templates/checkout-agent/agents/checkout-agent/test/check.test.ts +67 -0
- package/templates/checkout-agent/agents/checkout-agent/test/cli.test.ts +169 -0
- package/templates/checkout-agent/agents/checkout-agent/test/fixtures/order-marina.transcript.jsonl +5 -0
- package/templates/checkout-agent/agents/checkout-agent/test/fixtures/ordered-marina-awaiting-payer.transcript.jsonl +6 -0
- package/templates/checkout-agent/agents/checkout-agent/test/fixtures/tap-falar-agora-marina.transcript.jsonl +2 -0
- package/templates/checkout-agent/agents/checkout-agent/test/nfse.test.ts +170 -0
- package/templates/checkout-agent/agents/checkout-agent/test/pricing.test.ts +138 -0
- package/templates/checkout-agent/agents/checkout-agent/test/scenarios.test.ts +113 -0
- package/templates/checkout-agent/agents/checkout-agent/test/whatsapp.test.ts +171 -0
- package/templates/checkout-agent/agents/checkout-agent/tools.json +68 -0
- package/templates/checkout-agent/agents/checkout-agent/tsconfig.json +10 -0
- package/templates/checkout-agent/package.json +38 -0
- package/templates/checkout-agent/packages/agent-core/README.md +34 -0
- package/templates/checkout-agent/packages/agent-core/package.json +24 -0
- package/templates/checkout-agent/packages/agent-core/src/agent.ts +163 -0
- package/templates/checkout-agent/packages/agent-core/src/api/charge-rail.ts +195 -0
- package/templates/checkout-agent/packages/agent-core/src/api/client.ts +76 -0
- package/templates/checkout-agent/packages/agent-core/src/api/mandate-status.ts +74 -0
- package/templates/checkout-agent/packages/agent-core/src/api/rail.ts +153 -0
- package/templates/checkout-agent/packages/agent-core/src/api/sandbox-payer.ts +43 -0
- package/templates/checkout-agent/packages/agent-core/src/approval.ts +135 -0
- package/templates/checkout-agent/packages/agent-core/src/bundle.ts +191 -0
- package/templates/checkout-agent/packages/agent-core/src/channels.ts +186 -0
- package/templates/checkout-agent/packages/agent-core/src/check.ts +276 -0
- package/templates/checkout-agent/packages/agent-core/src/clock.ts +42 -0
- package/templates/checkout-agent/packages/agent-core/src/engine.ts +1074 -0
- package/templates/checkout-agent/packages/agent-core/src/escalate.ts +85 -0
- package/templates/checkout-agent/packages/agent-core/src/events.ts +40 -0
- package/templates/checkout-agent/packages/agent-core/src/guardrails.ts +45 -0
- package/templates/checkout-agent/packages/agent-core/src/hash.ts +118 -0
- package/templates/checkout-agent/packages/agent-core/src/ids.ts +10 -0
- package/templates/checkout-agent/packages/agent-core/src/index.ts +34 -0
- package/templates/checkout-agent/packages/agent-core/src/mandate.ts +100 -0
- package/templates/checkout-agent/packages/agent-core/src/manifest.ts +76 -0
- package/templates/checkout-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
- package/templates/checkout-agent/packages/agent-core/src/providers/replay.ts +63 -0
- package/templates/checkout-agent/packages/agent-core/src/providers/types.ts +39 -0
- package/templates/checkout-agent/packages/agent-core/src/quote.ts +50 -0
- package/templates/checkout-agent/packages/agent-core/src/rail.ts +186 -0
- package/templates/checkout-agent/packages/agent-core/src/receipt-chain.ts +270 -0
- package/templates/checkout-agent/packages/agent-core/src/receipt-verification.ts +577 -0
- package/templates/checkout-agent/packages/agent-core/src/revocation.ts +30 -0
- package/templates/checkout-agent/packages/agent-core/src/secrets.ts +30 -0
- package/templates/checkout-agent/packages/agent-core/src/state/store.ts +307 -0
- package/templates/checkout-agent/packages/agent-core/src/state-machine.ts +157 -0
- package/templates/checkout-agent/packages/agent-core/src/stubs/charge-rail.ts +195 -0
- package/templates/checkout-agent/packages/agent-core/src/stubs/mandate-status.ts +58 -0
- package/templates/checkout-agent/packages/agent-core/src/stubs/rail.ts +209 -0
- package/templates/checkout-agent/packages/agent-core/src/tools.ts +66 -0
- package/templates/checkout-agent/packages/agent-core/src/types.ts +187 -0
- package/templates/checkout-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
- package/templates/checkout-agent/packages/agent-core/test/approval.test.ts +117 -0
- package/templates/checkout-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
- package/templates/checkout-agent/packages/agent-core/test/bundle.test.ts +85 -0
- package/templates/checkout-agent/packages/agent-core/test/clock.test.ts +25 -0
- package/templates/checkout-agent/packages/agent-core/test/composition.test.ts +204 -0
- package/templates/checkout-agent/packages/agent-core/test/engine.test.ts +596 -0
- package/templates/checkout-agent/packages/agent-core/test/escalate.test.ts +42 -0
- package/templates/checkout-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
- package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
- package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
- package/templates/checkout-agent/packages/agent-core/test/helpers.ts +142 -0
- package/templates/checkout-agent/packages/agent-core/test/mandate-status.test.ts +229 -0
- package/templates/checkout-agent/packages/agent-core/test/manifest.test.ts +45 -0
- package/templates/checkout-agent/packages/agent-core/test/quote.test.ts +198 -0
- package/templates/checkout-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
- package/templates/checkout-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
- package/templates/checkout-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
- package/templates/checkout-agent/packages/agent-core/test/receivables.test.ts +416 -0
- package/templates/checkout-agent/packages/agent-core/test/secrets.test.ts +28 -0
- package/templates/checkout-agent/packages/agent-core/test/state-machine.test.ts +67 -0
- package/templates/checkout-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
- package/templates/checkout-agent/packages/agent-core/tsconfig.json +10 -0
- package/templates/checkout-agent/packages/agent-runtime/README.md +80 -0
- package/templates/checkout-agent/packages/agent-runtime/bin.mjs +32 -0
- package/templates/checkout-agent/packages/agent-runtime/package.json +23 -0
- package/templates/checkout-agent/packages/agent-runtime/src/adversarial.ts +199 -0
- package/templates/checkout-agent/packages/agent-runtime/src/agent.ts +68 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/index.ts +72 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/types.ts +173 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +459 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +216 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +350 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +180 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +120 -0
- package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +127 -0
- package/templates/checkout-agent/packages/agent-runtime/src/cli.ts +98 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/check.ts +15 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +367 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/poll.ts +145 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/resume.ts +49 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +134 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/start.ts +248 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/verify.ts +330 -0
- package/templates/checkout-agent/packages/agent-runtime/src/commands/webhook.ts +40 -0
- package/templates/checkout-agent/packages/agent-runtime/src/default-kit.ts +83 -0
- package/templates/checkout-agent/packages/agent-runtime/src/index.ts +13 -0
- package/templates/checkout-agent/packages/agent-runtime/src/inspect.ts +801 -0
- package/templates/checkout-agent/packages/agent-runtime/src/kit.ts +229 -0
- package/templates/checkout-agent/packages/agent-runtime/src/poll.ts +105 -0
- package/templates/checkout-agent/packages/agent-runtime/src/scenarios.ts +274 -0
- package/templates/checkout-agent/packages/agent-runtime/src/setup.ts +239 -0
- package/templates/checkout-agent/packages/agent-runtime/src/terminal.ts +263 -0
- package/templates/checkout-agent/packages/agent-runtime/src/webhook.ts +131 -0
- package/templates/checkout-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
- package/templates/checkout-agent/packages/agent-runtime/test/verify-cli.test.ts +329 -0
- package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +277 -0
- package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
- package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
- package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-session.test.ts +167 -0
- package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
- package/templates/checkout-agent/packages/agent-runtime/tsconfig.json +9 -0
- package/templates/checkout-agent/scripts/whatsapp-emulator.mjs +84 -0
- package/templates/checkout-agent/tsconfig.base.json +16 -0
- package/templates/checkout-agent/vitest.config.ts +12 -0
- package/templates/collections-agent/README.md +6 -1
- package/templates/collections-agent/agents/collections-agent/README.md +12 -8
- package/templates/collections-agent/agents/collections-agent/channels/whatsapp/README.md +8 -0
- package/templates/collections-agent/agents/collections-agent/channels/whatsapp/acordo-1042-retomada.json +10 -0
- package/templates/collections-agent/agents/collections-agent/channels/whatsapp/templates.json +27 -1
- package/templates/collections-agent/agents/collections-agent/src/kit.ts +2 -0
- package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +12 -2
- package/templates/collections-agent/agents/collections-agent/test/ambiguous-charge.test.ts +55 -0
- package/templates/collections-agent/agents/collections-agent/test/fixtures/tap-emitir-nova-1042.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/test/whatsapp-poll.test.ts +12 -6
- package/templates/collections-agent/agents/collections-agent/test/whatsapp-status-replies.test.ts +148 -0
- package/templates/collections-agent/agents/collections-agent/test/whatsapp.test.ts +5 -0
- package/templates/collections-agent/package.json +1 -0
- package/templates/collections-agent/packages/agent-core/README.md +14 -1
- package/templates/collections-agent/packages/agent-core/package.json +1 -1
- package/templates/collections-agent/packages/agent-core/src/agent.ts +39 -1
- package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
- package/templates/collections-agent/packages/agent-core/src/api/client.ts +28 -5
- package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
- package/templates/collections-agent/packages/agent-core/src/api/rail.ts +62 -26
- package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
- package/templates/collections-agent/packages/agent-core/src/approval.ts +14 -2
- package/templates/collections-agent/packages/agent-core/src/bundle.ts +35 -3
- package/templates/collections-agent/packages/agent-core/src/channels.ts +49 -3
- package/templates/collections-agent/packages/agent-core/src/check.ts +25 -2
- package/templates/collections-agent/packages/agent-core/src/engine.ts +235 -46
- package/templates/collections-agent/packages/agent-core/src/hash.ts +57 -1
- package/templates/collections-agent/packages/agent-core/src/index.ts +2 -0
- package/templates/collections-agent/packages/agent-core/src/quote.ts +50 -0
- package/templates/collections-agent/packages/agent-core/src/rail.ts +89 -4
- package/templates/collections-agent/packages/agent-core/src/receipt-chain.ts +270 -0
- package/templates/collections-agent/packages/agent-core/src/receipt-verification.ts +194 -18
- package/templates/collections-agent/packages/agent-core/src/revocation.ts +1 -1
- package/templates/collections-agent/packages/agent-core/src/state-machine.ts +10 -1
- package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
- package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +100 -32
- package/templates/collections-agent/packages/agent-core/src/tools.ts +17 -6
- package/templates/collections-agent/packages/agent-core/src/types.ts +54 -1
- package/templates/collections-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
- package/templates/collections-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
- package/templates/collections-agent/packages/agent-core/test/composition.test.ts +204 -0
- package/templates/collections-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
- package/templates/collections-agent/packages/agent-core/test/helpers.ts +4 -1
- package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
- package/templates/collections-agent/packages/agent-core/test/quote.test.ts +198 -0
- package/templates/collections-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
- package/templates/collections-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
- package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +103 -3
- package/templates/collections-agent/packages/agent-runtime/README.md +12 -0
- package/templates/collections-agent/packages/agent-runtime/src/adversarial.ts +32 -2
- package/templates/collections-agent/packages/agent-runtime/src/channels/types.ts +53 -4
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
- package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
- package/templates/collections-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
- package/templates/collections-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
- package/templates/collections-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
- package/templates/collections-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
- package/templates/collections-agent/packages/agent-runtime/src/commands/start.ts +6 -3
- package/templates/collections-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
- package/templates/collections-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
- package/templates/collections-agent/packages/agent-runtime/src/index.ts +1 -0
- package/templates/collections-agent/packages/agent-runtime/src/inspect.ts +5 -1
- package/templates/collections-agent/packages/agent-runtime/src/kit.ts +25 -2
- package/templates/collections-agent/packages/agent-runtime/src/scenarios.ts +8 -2
- package/templates/collections-agent/packages/agent-runtime/src/setup.ts +13 -3
- package/templates/collections-agent/packages/agent-runtime/src/terminal.ts +127 -18
- package/templates/collections-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
- package/templates/collections-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
- package/templates/collections-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
- package/templates/collections-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
- package/templates/collections-agent/scripts/whatsapp-emulator.mjs +84 -0
- package/templates/hello-agent/README.md +1 -1
- package/templates/hello-agent/packages/agent-core/README.md +14 -1
- package/templates/hello-agent/packages/agent-core/package.json +1 -1
- package/templates/hello-agent/packages/agent-core/src/agent.ts +39 -1
- package/templates/hello-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
- package/templates/hello-agent/packages/agent-core/src/api/client.ts +28 -5
- package/templates/hello-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
- package/templates/hello-agent/packages/agent-core/src/api/rail.ts +62 -26
- package/templates/hello-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
- package/templates/hello-agent/packages/agent-core/src/approval.ts +14 -2
- package/templates/hello-agent/packages/agent-core/src/bundle.ts +35 -3
- package/templates/hello-agent/packages/agent-core/src/channels.ts +49 -3
- package/templates/hello-agent/packages/agent-core/src/check.ts +25 -2
- package/templates/hello-agent/packages/agent-core/src/engine.ts +235 -46
- package/templates/hello-agent/packages/agent-core/src/hash.ts +57 -1
- package/templates/hello-agent/packages/agent-core/src/index.ts +2 -0
- package/templates/hello-agent/packages/agent-core/src/quote.ts +50 -0
- package/templates/hello-agent/packages/agent-core/src/rail.ts +89 -4
- package/templates/hello-agent/packages/agent-core/src/receipt-chain.ts +270 -0
- package/templates/hello-agent/packages/agent-core/src/receipt-verification.ts +194 -18
- package/templates/hello-agent/packages/agent-core/src/revocation.ts +1 -1
- package/templates/hello-agent/packages/agent-core/src/state-machine.ts +10 -1
- package/templates/hello-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
- package/templates/hello-agent/packages/agent-core/src/stubs/rail.ts +100 -32
- package/templates/hello-agent/packages/agent-core/src/tools.ts +17 -6
- package/templates/hello-agent/packages/agent-core/src/types.ts +54 -1
- package/templates/hello-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
- package/templates/hello-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
- package/templates/hello-agent/packages/agent-core/test/composition.test.ts +204 -0
- package/templates/hello-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
- package/templates/hello-agent/packages/agent-core/test/helpers.ts +4 -1
- package/templates/hello-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
- package/templates/hello-agent/packages/agent-core/test/quote.test.ts +198 -0
- package/templates/hello-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
- package/templates/hello-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
- package/templates/hello-agent/packages/agent-core/test/receivables.test.ts +103 -3
- package/templates/hello-agent/packages/agent-runtime/README.md +12 -0
- package/templates/hello-agent/packages/agent-runtime/src/adversarial.ts +32 -2
- package/templates/hello-agent/packages/agent-runtime/src/channels/types.ts +53 -4
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
- package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
- package/templates/hello-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
- package/templates/hello-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
- package/templates/hello-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
- package/templates/hello-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
- package/templates/hello-agent/packages/agent-runtime/src/commands/start.ts +6 -3
- package/templates/hello-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
- package/templates/hello-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
- package/templates/hello-agent/packages/agent-runtime/src/index.ts +1 -0
- package/templates/hello-agent/packages/agent-runtime/src/inspect.ts +5 -1
- package/templates/hello-agent/packages/agent-runtime/src/kit.ts +25 -2
- package/templates/hello-agent/packages/agent-runtime/src/scenarios.ts +8 -2
- package/templates/hello-agent/packages/agent-runtime/src/setup.ts +13 -3
- package/templates/hello-agent/packages/agent-runtime/src/terminal.ts +127 -18
- package/templates/hello-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
- package/templates/hello-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
- package/templates/hello-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
- package/templates/hello-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
- package/templates/kits.lock.json +29 -6
- package/templates/supplier-payments-agent/README.md +1 -1
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/AGENTS.md +1 -1
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/CLAUDE.md +1 -1
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/README.md +11 -9
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/kit.ts +1 -1
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/modules/batch-payout.ts +67 -20
- package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/batch.test.ts +504 -5
- package/templates/supplier-payments-agent/packages/agent-core/README.md +14 -1
- package/templates/supplier-payments-agent/packages/agent-core/package.json +1 -1
- package/templates/supplier-payments-agent/packages/agent-core/src/agent.ts +39 -1
- package/templates/supplier-payments-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
- package/templates/supplier-payments-agent/packages/agent-core/src/api/client.ts +28 -5
- package/templates/supplier-payments-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
- package/templates/supplier-payments-agent/packages/agent-core/src/api/rail.ts +62 -26
- package/templates/supplier-payments-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
- package/templates/supplier-payments-agent/packages/agent-core/src/approval.ts +14 -2
- package/templates/supplier-payments-agent/packages/agent-core/src/bundle.ts +35 -3
- package/templates/supplier-payments-agent/packages/agent-core/src/channels.ts +49 -3
- package/templates/supplier-payments-agent/packages/agent-core/src/check.ts +25 -2
- package/templates/supplier-payments-agent/packages/agent-core/src/engine.ts +235 -46
- package/templates/supplier-payments-agent/packages/agent-core/src/hash.ts +57 -1
- package/templates/supplier-payments-agent/packages/agent-core/src/index.ts +2 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/quote.ts +50 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/rail.ts +89 -4
- package/templates/supplier-payments-agent/packages/agent-core/src/receipt-chain.ts +270 -0
- package/templates/supplier-payments-agent/packages/agent-core/src/receipt-verification.ts +194 -18
- package/templates/supplier-payments-agent/packages/agent-core/src/revocation.ts +1 -1
- package/templates/supplier-payments-agent/packages/agent-core/src/state-machine.ts +10 -1
- package/templates/supplier-payments-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
- package/templates/supplier-payments-agent/packages/agent-core/src/stubs/rail.ts +100 -32
- package/templates/supplier-payments-agent/packages/agent-core/src/tools.ts +17 -6
- package/templates/supplier-payments-agent/packages/agent-core/src/types.ts +54 -1
- package/templates/supplier-payments-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/composition.test.ts +204 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/helpers.ts +4 -1
- package/templates/supplier-payments-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
- package/templates/supplier-payments-agent/packages/agent-core/test/quote.test.ts +198 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
- package/templates/supplier-payments-agent/packages/agent-core/test/receivables.test.ts +103 -3
- package/templates/supplier-payments-agent/packages/agent-runtime/README.md +12 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/adversarial.ts +32 -2
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/types.ts +53 -4
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
- package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start.ts +6 -3
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
- package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
- package/templates/supplier-payments-agent/packages/agent-runtime/src/index.ts +1 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/src/inspect.ts +5 -1
- package/templates/supplier-payments-agent/packages/agent-runtime/src/kit.ts +25 -2
- package/templates/supplier-payments-agent/packages/agent-runtime/src/scenarios.ts +8 -2
- package/templates/supplier-payments-agent/packages/agent-runtime/src/setup.ts +13 -3
- package/templates/supplier-payments-agent/packages/agent-runtime/src/terminal.ts +127 -18
- package/templates/supplier-payments-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
- package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
- package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
- package/templates/bills-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
- package/templates/collections-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
- package/templates/hello-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
- package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandate status from the API: `GET /v1/mandates/{id}`, the one registered
|
|
3
|
+
* spelling of the consumer-allowance point read (the SDK's OpenAPI says the
|
|
4
|
+
* path has no alias; `/v1/consumers/mandates/{id}` answers 404 on staging,
|
|
5
|
+
* measured 2026-09-23). Consulted before `executing` (section 4.7), through
|
|
6
|
+
* the same `csk_test_` client every other call of the kit uses.
|
|
7
|
+
*
|
|
8
|
+
* Fail-closed: this source never throws and never assumes. A transport
|
|
9
|
+
* failure, a timeout, a 404, a 5xx, an unreadable body, a status outside
|
|
10
|
+
* `active | paused | revoked | expired` or an `org_paused` that is not a
|
|
11
|
+
* boolean is reported as `unknown`, and the engine executes nothing on
|
|
12
|
+
* `unknown`.
|
|
13
|
+
*
|
|
14
|
+
* The organization kill switch (ent#1648) answers on the same read:
|
|
15
|
+
* `org_paused` next to `status`, which stays the mandate's own. A missing
|
|
16
|
+
* `org_paused` is `unknown`, never "not paused": a deployment that cannot
|
|
17
|
+
* say whether the organization stopped every spend has not said it did not.
|
|
18
|
+
*/
|
|
19
|
+
import type { ApiOperation, ApiSuccess, ApiClient } from "@codespar/sdk";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import type { MandateStatusReport, MandateStatusSource } from "../revocation.js";
|
|
22
|
+
import { describeApiError } from "./client.js";
|
|
23
|
+
|
|
24
|
+
/** `GET /v1/mandates/{id}` as the SDK's generated OpenAPI types it. */
|
|
25
|
+
type MandateRead = ApiSuccess<ApiOperation<"/v1/mandates/{id}", "get">>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The three fields the gate reads; the rest is passed through untouched.
|
|
29
|
+
* Bound to the SDK's type, so a field the API renames or retypes breaks the
|
|
30
|
+
* compile here instead of reading as absent at run time.
|
|
31
|
+
*/
|
|
32
|
+
const MandateReadSchema = z
|
|
33
|
+
.object({
|
|
34
|
+
status: z.enum(["active", "paused", "revoked", "expired"]),
|
|
35
|
+
expires_at: z.string(),
|
|
36
|
+
org_paused: z.boolean(),
|
|
37
|
+
})
|
|
38
|
+
.passthrough() satisfies z.ZodType<Pick<MandateRead, "status" | "expires_at" | "org_paused">>;
|
|
39
|
+
|
|
40
|
+
export class ApiMandateStatusSource implements MandateStatusSource {
|
|
41
|
+
constructor(
|
|
42
|
+
private readonly api: ApiClient,
|
|
43
|
+
private readonly clock: () => Date = () => new Date(),
|
|
44
|
+
) {}
|
|
45
|
+
|
|
46
|
+
async check(mandateId: string): Promise<MandateStatusReport> {
|
|
47
|
+
const base = { mandate_id: mandateId, org_paused: false, checked_at: this.clock().toISOString(), source: "api" as const };
|
|
48
|
+
let body: MandateRead;
|
|
49
|
+
try {
|
|
50
|
+
body = await this.api.get("/v1/mandates/{id}", { path: { id: mandateId } });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const failure = describeApiError(err);
|
|
53
|
+
const detail =
|
|
54
|
+
failure.status === 404
|
|
55
|
+
? `the API has no mandate ${mandateId} for this key (${failure.code})`
|
|
56
|
+
: `GET /v1/mandates/${mandateId} did not answer (${failure.code}): ${failure.message}`;
|
|
57
|
+
return { ...base, status: "unknown", detail };
|
|
58
|
+
}
|
|
59
|
+
// The type says what the API promises; the parse checks that this answer kept the promise.
|
|
60
|
+
const parsed = MandateReadSchema.safeParse(body);
|
|
61
|
+
if (!parsed.success) {
|
|
62
|
+
const unread = parsed.error.issues.map((issue) => issue.path.join(".") || "body");
|
|
63
|
+
const status = body && typeof body === "object" && "status" in body ? String(body.status) : "missing";
|
|
64
|
+
const detail = unread.includes("org_paused")
|
|
65
|
+
? `GET /v1/mandates/${mandateId} did not say whether the organization is paused (org_paused unreadable)`
|
|
66
|
+
: `GET /v1/mandates/${mandateId} answered a status this kit does not read (${status})`;
|
|
67
|
+
return { ...base, status: "unknown", detail };
|
|
68
|
+
}
|
|
69
|
+
if (parsed.data.org_paused) return { ...base, org_paused: true, status: parsed.data.status };
|
|
70
|
+
const expiresAt = new Date(parsed.data.expires_at).getTime();
|
|
71
|
+
const expired = Number.isFinite(expiresAt) && expiresAt <= this.clock().getTime();
|
|
72
|
+
return { ...base, status: expired && parsed.data.status === "active" ? "expired" : parsed.data.status };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CodeSpar sandbox rail. With the signed envelope the consent returned
|
|
3
|
+
* (`mandate.canonical` + `mandate.signature`) it spends through
|
|
4
|
+
* `POST /v1/consumer-payments/execute`; without it, by mandate id through
|
|
5
|
+
* `POST /v1/consumers/mandates/{id}/spend`. Idempotence on both is OPT-IN
|
|
6
|
+
* and keyed on an explicit `attempt_id`, which every payment here carries: a
|
|
7
|
+
* spend without one is a fresh payment on every request (ent#1671, #1683).
|
|
8
|
+
* What a repeat of an attempt answers is in `lookup` below. The by-id route answered `bad_signature` on staging for a
|
|
9
|
+
* mandate carrying `periodic_cap` (OPEN_QUESTIONS §14), which is why the
|
|
10
|
+
* envelope is preferred when it exists. Receipts come back from
|
|
11
|
+
* `GET /v1/consumers/receipts/{id}`.
|
|
12
|
+
*
|
|
13
|
+
* The API has no `actor` field on the wire today (OPEN_QUESTIONS §2). The
|
|
14
|
+
* spend carries `agent_id`, which the mandate binds; the full actor is
|
|
15
|
+
* stamped on the local receipt copy and on every event of the bundle.
|
|
16
|
+
*/
|
|
17
|
+
import type { ApiClient } from "@codespar/sdk";
|
|
18
|
+
import type { Mandate } from "../mandate.js";
|
|
19
|
+
import { checkQuote } from "../quote.js";
|
|
20
|
+
import { checkSpendApproval, type PaymentRail, type RailLookup, type RailOutcome, type RailPayment, type RailReceipt } from "../rail.js";
|
|
21
|
+
import type { Actor } from "../types.js";
|
|
22
|
+
import { describeApiError, isUncertain, type SpendErrorCode } from "./client.js";
|
|
23
|
+
|
|
24
|
+
/** Another presentation of this attempt is claimed and has no recorded outcome yet: it may be moving money right now. */
|
|
25
|
+
const ATTEMPT_IN_FLIGHT: SpendErrorCode = "psp_attempt_in_flight";
|
|
26
|
+
/** This attempt failed, moved no money and was compensated; the API will never run this id again. */
|
|
27
|
+
const ATTEMPT_SPENT: SpendErrorCode = "psp_attempt_conflict";
|
|
28
|
+
/** This attempt id was already used for a DIFFERENT payment (checked before its state); nothing was held or sent. */
|
|
29
|
+
const ATTEMPT_ID_CONFLICT: SpendErrorCode = "attempt_id_conflict";
|
|
30
|
+
/** Another project of this organization holds this attempt id; opaque by design, nothing was held or sent. */
|
|
31
|
+
const ATTEMPT_ID_UNAVAILABLE: SpendErrorCode = "attempt_id_unavailable";
|
|
32
|
+
|
|
33
|
+
export class CodeSparRail implements PaymentRail {
|
|
34
|
+
readonly name = "codespar" as const;
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
private readonly api: ApiClient,
|
|
38
|
+
private readonly envelope?: Pick<Mandate, "canonical" | "signature"> | undefined,
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
async pay(payment: RailPayment): Promise<RailOutcome> {
|
|
42
|
+
// Without the quote the sealed receipt names no payee (OPEN_QUESTIONS §18), and the API pays a quote that disagrees. Neither goes out.
|
|
43
|
+
const quoted = checkQuote(payment);
|
|
44
|
+
if (!quoted.ok) return { status: "failed", code: quoted.code, message: `${quoted.detail}; nothing was sent` };
|
|
45
|
+
// Without it the receipt seals no link to what was approved (OPEN_QUESTIONS §3), and the API refuses a malformed one with 400 anyway.
|
|
46
|
+
const approval = checkSpendApproval(payment);
|
|
47
|
+
if (!approval.ok) return { status: "failed", code: approval.code, message: `${approval.detail}; nothing was sent` };
|
|
48
|
+
try {
|
|
49
|
+
const outcome =
|
|
50
|
+
this.envelope?.canonical && this.envelope.signature
|
|
51
|
+
? await this.api.post("/v1/consumer-payments/execute", {
|
|
52
|
+
body: {
|
|
53
|
+
mandate: this.envelope.canonical,
|
|
54
|
+
signature: this.envelope.signature,
|
|
55
|
+
amount_minor: payment.amount_minor,
|
|
56
|
+
purpose: payment.purpose,
|
|
57
|
+
agent_id: payment.agent_id,
|
|
58
|
+
payee: payment.payee,
|
|
59
|
+
attempt_id: payment.attempt_id,
|
|
60
|
+
quote: quoted.quote,
|
|
61
|
+
approval: approval.approval,
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
: await this.api.post("/v1/consumers/mandates/{id}/spend", {
|
|
65
|
+
path: { id: payment.mandate_id },
|
|
66
|
+
body: { amount_minor: payment.amount_minor, payee: payment.payee, agent_id: payment.agent_id, attempt_id: payment.attempt_id, quote: quoted.quote, approval: approval.approval },
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
status: "settled",
|
|
70
|
+
transaction_id: outcome.payment.transactionId,
|
|
71
|
+
receipt_id: outcome.receipt?.id ?? null,
|
|
72
|
+
money_moved: outcome.payment.moneyMoved,
|
|
73
|
+
sandbox: !outcome.payment.moneyMoved,
|
|
74
|
+
...(outcome.idempotent_replay ? { replayed: true as const } : {}),
|
|
75
|
+
raw: outcome,
|
|
76
|
+
};
|
|
77
|
+
} catch (err) {
|
|
78
|
+
const failure = describeApiError(err);
|
|
79
|
+
// Not a refusal: the other presentation may already have reached the provider, so "nothing moved" would be a claim nobody can make.
|
|
80
|
+
if (failure.code === ATTEMPT_IN_FLIGHT) return { status: "uncertain", code: failure.code, message: failure.message };
|
|
81
|
+
if (isUncertain(failure)) return { status: "uncertain", code: failure.code, message: failure.message };
|
|
82
|
+
if (failure.code === ATTEMPT_SPENT) return { status: "failed", code: failure.code, message: failure.message, spent: true };
|
|
83
|
+
// Held, never spent: the id belongs to another payment or another project, so no generation may be derived past it.
|
|
84
|
+
if (failure.code === ATTEMPT_ID_CONFLICT) return { status: "failed", code: failure.code, message: failure.message, held: "conflict" };
|
|
85
|
+
if (failure.code === ATTEMPT_ID_UNAVAILABLE) return { status: "failed", code: failure.code, message: failure.message, held: "unavailable" };
|
|
86
|
+
return { status: "failed", code: failure.code, message: failure.message };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* There is no read route for an attempt: the way to learn what became of
|
|
92
|
+
* one is to present the same `attempt_id`, with the same payment, again.
|
|
93
|
+
* Against the deployed API (ent#1671, #1683) that call never moves money
|
|
94
|
+
* for an attempt it already holds, and each answer maps to one reading:
|
|
95
|
+
*
|
|
96
|
+
* - settled: `200` with the ORIGINAL body verbatim (same `transactionId`,
|
|
97
|
+
* same stored `receipt.id`, which the receipt route resolves) plus
|
|
98
|
+
* `idempotent_replay: true`. Read as `settled`, exactly as the first
|
|
99
|
+
* answer, and marked `replayed`.
|
|
100
|
+
* - `psp_attempt_in_flight` (409): claimed, no outcome yet, on every rail.
|
|
101
|
+
* Read as `in_flight`; the next reconcile asks again with the SAME id.
|
|
102
|
+
* - `psp_attempt_uncertain` (409): pinned, dispatch outcome unknown. Read as
|
|
103
|
+
* `uncertain`, which leaves the execution open for a person.
|
|
104
|
+
* - `psp_attempt_conflict` (409): failed, compensated, no money. Read as
|
|
105
|
+
* `failed` and `spent`.
|
|
106
|
+
* - `attempt_id_conflict` / `attempt_id_unavailable` (409): the id is held for
|
|
107
|
+
* a different payment, or by another project. Read as `failed` and `held`,
|
|
108
|
+
* never `spent`: nothing was held or sent by this call. Neither can come from re-presenting the
|
|
109
|
+
* payment this execution sent, whose tuple is the one the id was claimed
|
|
110
|
+
* with, so seeing one here is a defect worth its readable failure.
|
|
111
|
+
* - an attempt the API never took: there is no record to answer from, so
|
|
112
|
+
* this call IS the payment. Reconcile runs only after the outbox row
|
|
113
|
+
* flipped to `sent`, so an attempt it looks up was presented once already.
|
|
114
|
+
*/
|
|
115
|
+
async lookup(_attemptId: string, payment: RailPayment): Promise<RailLookup> {
|
|
116
|
+
const outcome = await this.pay(payment);
|
|
117
|
+
if (outcome.status === "uncertain" && outcome.code === ATTEMPT_IN_FLIGHT) return { status: "in_flight" };
|
|
118
|
+
return outcome;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async receipt(receiptId: string, actor: Actor): Promise<RailReceipt | undefined> {
|
|
122
|
+
try {
|
|
123
|
+
const r = await this.api.get("/v1/consumers/receipts/{id}", { path: { id: receiptId } });
|
|
124
|
+
return {
|
|
125
|
+
receipt_id: r.receipt_id,
|
|
126
|
+
state: r.state,
|
|
127
|
+
mandate: { id: r.mandate.id, sig_sha256: r.mandate.sig_sha256 },
|
|
128
|
+
chain_version: r.chain_version,
|
|
129
|
+
approval: r.approval ?? null,
|
|
130
|
+
payment: {
|
|
131
|
+
amount_minor: r.payment.amount_minor,
|
|
132
|
+
payee: r.quote?.payee ?? null,
|
|
133
|
+
attempt_id: r.payment.attempt_id,
|
|
134
|
+
money_moved: r.payment.money_moved,
|
|
135
|
+
sandbox: r.payment.sandbox === true,
|
|
136
|
+
at: r.payment.at,
|
|
137
|
+
},
|
|
138
|
+
chain: r.chain,
|
|
139
|
+
receipt_sig: r.receipt_sig,
|
|
140
|
+
// `||`, not `??`: a deployment older than ent#1633 omits both fields,
|
|
141
|
+
// and an empty string is no signature either. Both read as `unsigned`.
|
|
142
|
+
receipt_sig_ed25519: r.receipt_sig_ed25519 || null,
|
|
143
|
+
receipt_sig_kid: r.receipt_sig_kid || null,
|
|
144
|
+
actor,
|
|
145
|
+
raw: r,
|
|
146
|
+
};
|
|
147
|
+
} catch (err) {
|
|
148
|
+
const failure = describeApiError(err);
|
|
149
|
+
if (failure.status === 404) return undefined;
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sandbox PAYER: `POST /v1/test/charges/{chargeId}/pay` (alias
|
|
3
|
+
* `POST /v1/charges/{chargeId}/sandbox/pay`), test environment only. It
|
|
4
|
+
* plays the debtor: the charge goes through the same settlement path a
|
|
5
|
+
* provider `charge-in` webhook takes, `commerce.charge.paid` fans out to the
|
|
6
|
+
* project's triggers, and every record it leaves carries `simulated: true`
|
|
7
|
+
* plus `settled_against: "sandbox_fixture"`. No money moves anywhere. A
|
|
8
|
+
* live-environment key is refused with `sandbox_pay_not_permitted` before
|
|
9
|
+
* anything is read. Since `@codespar/sdk@0.16.5` the route is in the SDK's
|
|
10
|
+
* OpenAPI document, so this is the client's own typed call (same base URL,
|
|
11
|
+
* key and project header as every other call); until 0.16.4 it was a plain
|
|
12
|
+
* `fetch`, see docs/OPEN_QUESTIONS.md section 31c. The key is checked for
|
|
13
|
+
* the `csk_test_` prefix before a client exists, like every other call.
|
|
14
|
+
*/
|
|
15
|
+
import { ApiClient, type ApiOperation, type ApiSuccess } from "@codespar/sdk";
|
|
16
|
+
import type { ApiFailure } from "./client.js";
|
|
17
|
+
import { createCodeSparClient, describeApiError } from "./client.js";
|
|
18
|
+
|
|
19
|
+
/** What the payer route answers, as the SDK's generated OpenAPI types it. */
|
|
20
|
+
export type SandboxPaidState = ApiSuccess<ApiOperation<"/v1/test/charges/{chargeId}/pay", "post">>;
|
|
21
|
+
|
|
22
|
+
export type SandboxPayResult = { ok: true; state: SandboxPaidState } | { ok: false; failure: ApiFailure };
|
|
23
|
+
|
|
24
|
+
/** Where the payer route lives when no client exists yet: the same three things `createCodeSparClient` takes. */
|
|
25
|
+
export interface SandboxPayerTarget {
|
|
26
|
+
apiKey: string | undefined;
|
|
27
|
+
baseUrl?: string | undefined;
|
|
28
|
+
projectId?: string | undefined;
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function paySandboxCharge(target: ApiClient | SandboxPayerTarget, chargeRef: string, amountMinor?: number): Promise<SandboxPayResult> {
|
|
33
|
+
const api = target instanceof ApiClient ? target : createCodeSparClient(target);
|
|
34
|
+
try {
|
|
35
|
+
const state = await api.post("/v1/test/charges/{chargeId}/pay", {
|
|
36
|
+
path: { chargeId: chargeRef },
|
|
37
|
+
body: amountMinor !== undefined ? { amount_minor: amountMinor } : {},
|
|
38
|
+
});
|
|
39
|
+
return { ok: true, state };
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return { ok: false, failure: describeApiError(err) };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section 4.2: the approval artifact.
|
|
3
|
+
*
|
|
4
|
+
* STUB, on purpose: the artifact is signed with a LOCAL development key
|
|
5
|
+
* (`.codespar/approval.key`, generated on first use, never committed). The
|
|
6
|
+
* CodeSpar API does not sign approval lists; its HMAC is the mandate proof
|
|
7
|
+
* and the receipt seal. Until an API-side signature exists the artifact
|
|
8
|
+
* proves what was approved to whoever runs the agent, and to nobody else.
|
|
9
|
+
* See docs/OPEN_QUESTIONS.md.
|
|
10
|
+
*/
|
|
11
|
+
import { randomBytes } from "node:crypto";
|
|
12
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { canonicalJson, hexEqual, hmacSha256Hex, itemsHash } from "./hash.js";
|
|
15
|
+
import type { Execution } from "./state-machine.js";
|
|
16
|
+
import type { Actor, ApprovalArtifact, EscalationTrigger, ExecutionBatch, ExecutionComposition } from "./types.js";
|
|
17
|
+
import { newId } from "./ids.js";
|
|
18
|
+
|
|
19
|
+
export const APPROVAL_KEY_ID = "local-dev-stub";
|
|
20
|
+
export const DEFAULT_APPROVAL_TTL_MS = 15 * 60 * 1000;
|
|
21
|
+
|
|
22
|
+
export interface ApprovalSigner {
|
|
23
|
+
keyId: string;
|
|
24
|
+
sign(payload: string): string;
|
|
25
|
+
verify(payload: string, signature: string): boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function loadOrCreateLocalApprovalKey(stateDir: string): ApprovalSigner {
|
|
29
|
+
const path = join(stateDir, "approval.key");
|
|
30
|
+
let key: Buffer;
|
|
31
|
+
if (existsSync(path)) {
|
|
32
|
+
key = Buffer.from(readFileSync(path, "utf8").trim(), "hex");
|
|
33
|
+
} else {
|
|
34
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
35
|
+
key = randomBytes(32);
|
|
36
|
+
writeFileSync(path, key.toString("hex") + "\n", { mode: 0o600 });
|
|
37
|
+
chmodSync(path, 0o600);
|
|
38
|
+
}
|
|
39
|
+
return hmacSigner(APPROVAL_KEY_ID, key);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function hmacSigner(keyId: string, key: Buffer): ApprovalSigner {
|
|
43
|
+
return {
|
|
44
|
+
keyId,
|
|
45
|
+
sign: (payload) => hmacSha256Hex(key, payload),
|
|
46
|
+
verify: (payload, signature) => hexEqual(hmacSha256Hex(key, payload), signature),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function unsignedPayload(artifact: Omit<ApprovalArtifact, "signature">): string {
|
|
51
|
+
return canonicalJson(artifact);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface CreateApprovalInput {
|
|
55
|
+
execution: Execution;
|
|
56
|
+
approver: ApprovalArtifact["approver"];
|
|
57
|
+
actor: Actor;
|
|
58
|
+
now: Date;
|
|
59
|
+
ttlMs?: number;
|
|
60
|
+
escalation?: { trigger: EscalationTrigger; detail: string };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createApprovalArtifact(signer: ApprovalSigner, input: CreateApprovalInput): ApprovalArtifact {
|
|
64
|
+
const { execution } = input;
|
|
65
|
+
const approvedAt = input.now.toISOString();
|
|
66
|
+
const expiresAt = new Date(input.now.getTime() + (input.ttlMs ?? DEFAULT_APPROVAL_TTL_MS)).toISOString();
|
|
67
|
+
const unsigned: Omit<ApprovalArtifact, "signature"> = {
|
|
68
|
+
approval_id: newId("apr"),
|
|
69
|
+
execution_id: execution.id,
|
|
70
|
+
mode: execution.mode,
|
|
71
|
+
approver: input.approver,
|
|
72
|
+
approved_at: approvedAt,
|
|
73
|
+
expires_at: expiresAt,
|
|
74
|
+
mandate: execution.mandate,
|
|
75
|
+
items: execution.items,
|
|
76
|
+
items_hash: itemsHash(execution.items),
|
|
77
|
+
// The SET this line belongs to, when it belongs to one. Omitted — never
|
|
78
|
+
// null — on an execution that is not part of a batch, so the payload a
|
|
79
|
+
// bills-agent artifact signs is unchanged by this field existing.
|
|
80
|
+
...(execution.batch ? { batch: execution.batch } : {}),
|
|
81
|
+
// What the single amount was composed of, when it was composed of lines. Same rule as `batch`: omitted, never null.
|
|
82
|
+
...(execution.composition ? { composition: execution.composition } : {}),
|
|
83
|
+
...(input.escalation ? { escalation: input.escalation } : {}),
|
|
84
|
+
actor: input.actor,
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
...unsigned,
|
|
88
|
+
signature: { alg: "HMAC-SHA256", key_id: signer.keyId, value: signer.sign(unsignedPayload(unsigned)) },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type ApprovalCheck =
|
|
93
|
+
| { ok: true }
|
|
94
|
+
| { ok: false; problem: "signature_invalid" | "items_hash_mismatch" | "expired" | "wrong_execution" | "wrong_mandate" | "batch_mismatch" | "composition_mismatch" };
|
|
95
|
+
|
|
96
|
+
/** Two batch bindings are the same binding, or one is absent and so is the other. */
|
|
97
|
+
function sameBatch(a: ExecutionBatch | undefined, b: ExecutionBatch | undefined): boolean {
|
|
98
|
+
if (a === undefined || b === undefined) return a === b;
|
|
99
|
+
return a.ref === b.ref && a.batch_hash === b.batch_hash && a.index === b.index && a.count === b.count;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Two compositions are the same binding, or one is absent and so is the other. */
|
|
103
|
+
function sameComposition(a: ExecutionComposition | undefined, b: ExecutionComposition | undefined): boolean {
|
|
104
|
+
if (a === undefined || b === undefined) return a === b;
|
|
105
|
+
return a.ref === b.ref && a.composition_hash === b.composition_hash && a.line_count === b.line_count;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The rule that makes the artifact proof: before `executing`, recompute the
|
|
110
|
+
* hash of what is about to be executed and compare it with the artifact.
|
|
111
|
+
*/
|
|
112
|
+
export function checkApprovalArtifact(
|
|
113
|
+
signer: ApprovalSigner,
|
|
114
|
+
artifact: ApprovalArtifact,
|
|
115
|
+
execution: Execution,
|
|
116
|
+
now: Date,
|
|
117
|
+
): ApprovalCheck {
|
|
118
|
+
const { signature, ...unsigned } = artifact;
|
|
119
|
+
if (!signer.verify(unsignedPayload(unsigned), signature.value)) return { ok: false, problem: "signature_invalid" };
|
|
120
|
+
if (artifact.execution_id !== execution.id) return { ok: false, problem: "wrong_execution" };
|
|
121
|
+
if (artifact.mandate.id !== execution.mandate.id || artifact.mandate.version !== execution.mandate.version) {
|
|
122
|
+
return { ok: false, problem: "wrong_mandate" };
|
|
123
|
+
}
|
|
124
|
+
if (itemsHash(execution.items) !== artifact.items_hash) return { ok: false, problem: "items_hash_mismatch" };
|
|
125
|
+
// The line's own list is attested by the hash above; this is the list the
|
|
126
|
+
// line was one OF. An execution that gained, lost or moved its place in a
|
|
127
|
+
// batch after approval is not the one that was approved.
|
|
128
|
+
if (!sameBatch(artifact.batch, execution.batch)) return { ok: false, problem: "batch_mismatch" };
|
|
129
|
+
// The item's amount is attested above; this is what that amount was made of.
|
|
130
|
+
// A cart recomposed at a constant total leaves `items_hash` unchanged and
|
|
131
|
+
// this hash changed, and that is the only thing that sees it.
|
|
132
|
+
if (!sameComposition(artifact.composition, execution.composition)) return { ok: false, problem: "composition_mismatch" };
|
|
133
|
+
if (new Date(artifact.expires_at).getTime() <= now.getTime()) return { ok: false, problem: "expired" };
|
|
134
|
+
return { ok: true };
|
|
135
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section 11: the proof bundle, one folder per run under `runs/<run-id>/`.
|
|
3
|
+
* It answers "why could the agent pay this". It holds no key and no secret;
|
|
4
|
+
* payee keys are masked the way a bank statement masks them.
|
|
5
|
+
*/
|
|
6
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { basename, join } from "node:path";
|
|
8
|
+
import { canonicalJson } from "./hash.js";
|
|
9
|
+
import type { Mandate } from "./mandate.js";
|
|
10
|
+
import type { RailReceipt } from "./rail.js";
|
|
11
|
+
import type { ApprovalArtifact } from "./types.js";
|
|
12
|
+
|
|
13
|
+
export interface TranscriptLine {
|
|
14
|
+
at: string;
|
|
15
|
+
kind: "user" | "assistant_step" | "tool_call" | "tool_result" | "assistant" | "system";
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class ProofBundle {
|
|
20
|
+
readonly dir: string;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
runsDir: string,
|
|
24
|
+
readonly runId: string,
|
|
25
|
+
) {
|
|
26
|
+
this.dir = join(runsDir, runId);
|
|
27
|
+
mkdirSync(join(this.dir, "receipts"), { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static open(runsDir: string, runId: string): ProofBundle | undefined {
|
|
31
|
+
return existsSync(join(runsDir, runId)) ? new ProofBundle(runsDir, runId) : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
transcript(line: TranscriptLine): void {
|
|
35
|
+
appendFileSync(join(this.dir, "transcript.jsonl"), JSON.stringify(line) + "\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
readTranscript(): TranscriptLine[] {
|
|
39
|
+
const path = join(this.dir, "transcript.jsonl");
|
|
40
|
+
if (!existsSync(path)) return [];
|
|
41
|
+
return readFileSync(path, "utf8")
|
|
42
|
+
.split("\n")
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.map((l) => JSON.parse(l) as TranscriptLine);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
event(event: Record<string, unknown>): void {
|
|
48
|
+
appendFileSync(join(this.dir, "events.jsonl"), JSON.stringify(event) + "\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The conversation, when the run had one: every message in and out of a
|
|
53
|
+
* channel, in order, with what the channel did with it. Separate from the
|
|
54
|
+
* transcript, which is the MODEL's side, because on a channel the two are
|
|
55
|
+
* not the same thing — a message the channel refused never reached the
|
|
56
|
+
* person, and the QR and the copy-and-paste were sent by the runner.
|
|
57
|
+
*
|
|
58
|
+
* It carries no contact in the clear: the channel masks it before it gets
|
|
59
|
+
* here, because the bundle travels.
|
|
60
|
+
*/
|
|
61
|
+
channel(line: Record<string, unknown>): void {
|
|
62
|
+
appendFileSync(join(this.dir, "channel.jsonl"), JSON.stringify(line) + "\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
readChannel(): Record<string, unknown>[] {
|
|
66
|
+
const path = join(this.dir, "channel.jsonl");
|
|
67
|
+
if (!existsSync(path)) return [];
|
|
68
|
+
return readFileSync(path, "utf8")
|
|
69
|
+
.split("\n")
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.map((l) => JSON.parse(l) as Record<string, unknown>);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
readEvents(): Record<string, unknown>[] {
|
|
75
|
+
const path = join(this.dir, "events.jsonl");
|
|
76
|
+
if (!existsSync(path)) return [];
|
|
77
|
+
return readFileSync(path, "utf8")
|
|
78
|
+
.split("\n")
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.map((l) => JSON.parse(l) as Record<string, unknown>);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** `approval.json` is the list of artifacts of this run, in approval order. */
|
|
84
|
+
approval(artifact: ApprovalArtifact): void {
|
|
85
|
+
const path = join(this.dir, "approval.json");
|
|
86
|
+
const current: ApprovalArtifact[] = existsSync(path) ? (JSON.parse(readFileSync(path, "utf8")) as ApprovalArtifact[]) : [];
|
|
87
|
+
if (!current.some((a) => a.approval_id === artifact.approval_id)) current.push(artifact);
|
|
88
|
+
writeFileSync(path, JSON.stringify(current, null, 2) + "\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
readApprovals(): ApprovalArtifact[] {
|
|
92
|
+
const path = join(this.dir, "approval.json");
|
|
93
|
+
return existsSync(path) ? (JSON.parse(readFileSync(path, "utf8")) as ApprovalArtifact[]) : [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
mandateSnapshot(mandate: Mandate): void {
|
|
97
|
+
const { signature: _signature, canonical: _canonical, ...safe } = mandate;
|
|
98
|
+
writeFileSync(join(this.dir, "mandate.snapshot.json"), JSON.stringify({ ...safe, beneficiaries: safe.beneficiaries.map((b) => ({ ...b, payee: maskPayee(b.payee) })), merchant_allowlist: safe.merchant_allowlist.map(maskPayee) }, null, 2) + "\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Returns the path INSIDE the bundle (`receipts/<id>.json`): a bundle travels, and an absolute path names the machine that wrote it.
|
|
103
|
+
*
|
|
104
|
+
* `approval_id` names the artifact in `approval.json` whose hashes the spend carried, so `npm run verify` holds the receipt's sealed
|
|
105
|
+
* approval link against THAT artifact, found by identity and never by looking for one whose hash happens to match. The copy stays
|
|
106
|
+
* masked: binding its body to the chain needs the unmasked read, which `verify --from-api` takes from the API and writes nowhere.
|
|
107
|
+
*/
|
|
108
|
+
receipt(receipt: RailReceipt, context: { approval_id?: string | undefined } = {}): string {
|
|
109
|
+
const relative = join("receipts", `${receipt.receipt_id}.json`);
|
|
110
|
+
const { raw: _raw, ...safe } = receipt;
|
|
111
|
+
const copy = { ...safe, payment: { ...safe.payment, payee: safe.payment.payee ? maskPayee(safe.payment.payee) : null }, ...(context.approval_id ? { approval_id: context.approval_id } : {}) };
|
|
112
|
+
writeFileSync(join(this.dir, relative), JSON.stringify(copy, null, 2) + "\n");
|
|
113
|
+
return relative;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The masked mandate as it stood when the run started. Absent on a bundle whose run never got that far. */
|
|
117
|
+
readMandateSnapshot(): Record<string, unknown> | undefined {
|
|
118
|
+
const path = join(this.dir, "mandate.snapshot.json");
|
|
119
|
+
return existsSync(path) ? (JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>) : undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** One receipt by its file name, as `listReceipts()` returns it. */
|
|
123
|
+
readReceipt(file: string): Record<string, unknown> | undefined {
|
|
124
|
+
const path = join(this.dir, "receipts", basename(file));
|
|
125
|
+
return existsSync(path) ? (JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>) : undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Section 11: `verify.json` is the output of `codespar audit replay`, which
|
|
130
|
+
* is not a registered command of the CLI, so nothing here writes it and
|
|
131
|
+
* nothing here reimplements the hash-chain check. The reader exists so a
|
|
132
|
+
* bundle that gains one later is noticed rather than ignored.
|
|
133
|
+
*/
|
|
134
|
+
hasVerify(): boolean {
|
|
135
|
+
return existsSync(join(this.dir, "verify.json"));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
listReceipts(): string[] {
|
|
139
|
+
const dir = join(this.dir, "receipts");
|
|
140
|
+
if (!existsSync(dir)) return [];
|
|
141
|
+
return readdirSorted(dir);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
meta(meta: Record<string, unknown>): void {
|
|
145
|
+
writeFileSync(join(this.dir, "run.json"), JSON.stringify(meta, null, 2) + "\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
readMeta(): Record<string, unknown> | undefined {
|
|
149
|
+
const path = join(this.dir, "run.json");
|
|
150
|
+
return existsSync(path) ? (JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>) : undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readdirSorted(dir: string): string[] {
|
|
155
|
+
return readdirSync(dir)
|
|
156
|
+
.filter((f) => f.endsWith(".json"))
|
|
157
|
+
.sort();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Where the bundle's masked copy of a receipt says something the API's read of the same receipt does not. The copy is not what the
|
|
162
|
+
* signature covers, so a copy edited after the fact still verifies at the signature level; this is what catches it once the read is
|
|
163
|
+
* in hand. The payee is compared masked, the only form the copy holds. An empty list means the copy is a faithful mask of the read.
|
|
164
|
+
*/
|
|
165
|
+
export function copyDisagreesWithRead(copy: Record<string, unknown>, read: Record<string, unknown>): string[] {
|
|
166
|
+
const field = (o: unknown, k: string): unknown => (o && typeof o === "object" ? (o as Record<string, unknown>)[k] : undefined);
|
|
167
|
+
const readPayee = field(read["quote"], "payee");
|
|
168
|
+
const expected: Array<[string, unknown, unknown]> = [
|
|
169
|
+
["receipt_id", copy["receipt_id"], read["receipt_id"]],
|
|
170
|
+
["chain", copy["chain"], read["chain"]],
|
|
171
|
+
["mandate.id", field(copy["mandate"], "id"), field(read["mandate"], "id")],
|
|
172
|
+
["payment.amount_minor", field(copy["payment"], "amount_minor"), field(read["payment"], "amount_minor")],
|
|
173
|
+
["payment.attempt_id", field(copy["payment"], "attempt_id"), field(read["payment"], "attempt_id")],
|
|
174
|
+
["payment.money_moved", field(copy["payment"], "money_moved"), field(read["payment"], "money_moved")],
|
|
175
|
+
["payment.at", field(copy["payment"], "at"), field(read["payment"], "at")],
|
|
176
|
+
["payment.payee", field(copy["payment"], "payee"), typeof readPayee === "string" ? maskPayee(readPayee) : null],
|
|
177
|
+
];
|
|
178
|
+
// Written onto the copy since the rail reads them through the SDK's types (0.16.10); a copy from before carries neither, and is not faulted for it.
|
|
179
|
+
if ("chain_version" in copy) expected.push(["chain_version", copy["chain_version"], read["chain_version"]]);
|
|
180
|
+
if ("approval" in copy) expected.push(["approval", canonicalJson(copy["approval"] ?? null), canonicalJson(read["approval"] ?? null)]);
|
|
181
|
+
return expected.filter(([, a, b]) => a !== b).map(([name]) => name);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** `escola@exemplo.com.br` -> `es***@exemplo.com.br`; `+5511999998888` -> `+55***8888`; a UUID keeps its first and last 4. */
|
|
185
|
+
export function maskPayee(payee: string): string {
|
|
186
|
+
if (payee === "*") return payee;
|
|
187
|
+
const at = payee.indexOf("@");
|
|
188
|
+
if (at > 0) return `${payee.slice(0, Math.min(2, at))}***${payee.slice(at)}`;
|
|
189
|
+
if (payee.length <= 8) return `${payee.slice(0, 2)}***`;
|
|
190
|
+
return `${payee.slice(0, 4)}***${payee.slice(-4)}`;
|
|
191
|
+
}
|