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