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