@codespar/cli 0.17.0 → 0.18.1
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/surface.js +53 -1
- package/dist/surface.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
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `codespar-agent verify` at the process boundary: the contracts a person and
|
|
3
|
+
* a script depend on.
|
|
4
|
+
*
|
|
5
|
+
* It runs from a temporary directory with no `agent.yaml` anywhere above it
|
|
6
|
+
* and no key in the environment, which is the whole claim of wave 5 — the
|
|
7
|
+
* verifier is someone who has the receipt file and nothing else. The key set
|
|
8
|
+
* is a local copy, so no test opens a socket.
|
|
9
|
+
*/
|
|
10
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
11
|
+
import { createHash, generateKeyPairSync, sign as signDetached } from "node:crypto";
|
|
12
|
+
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { createServer, type Server } from "node:http";
|
|
14
|
+
import type { AddressInfo } from "node:net";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join, resolve } from "node:path";
|
|
17
|
+
import { canonicalize, itemsHash, maskPayee, type ExecutionItem } from "@codespar/agent-core";
|
|
18
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
19
|
+
|
|
20
|
+
const BIN = resolve(import.meta.dirname, "../bin.mjs");
|
|
21
|
+
const KID = "did:web:id.codespar.dev#1";
|
|
22
|
+
const CHAIN = "9f2c4a1b6d0e8f3a5c7b9d1e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f01";
|
|
23
|
+
const RECEIPT_ID = "rcpt_V1StGXR8Z5jdHi6BmyT0sw";
|
|
24
|
+
|
|
25
|
+
function fixture(over: Record<string, unknown> = {}) {
|
|
26
|
+
const dir = mkdtempSync(join(tmpdir(), "verify-cli-"));
|
|
27
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
28
|
+
const x = Buffer.from(publicKey.export({ format: "der", type: "spki" }).subarray(-32)).toString("base64url");
|
|
29
|
+
const signature = signDetached(null, Buffer.from(`codespar-receipt:v1:${RECEIPT_ID}:${CHAIN}`, "utf8"), privateKey).toString("base64url");
|
|
30
|
+
const receipt = {
|
|
31
|
+
receipt_id: RECEIPT_ID,
|
|
32
|
+
state: "paid",
|
|
33
|
+
mandate: { id: "mnd_1" },
|
|
34
|
+
payment: { amount_minor: 12345, payee: "es***@exemplo.com.br", attempt_id: "att_1", money_moved: false, sandbox: true, at: "2026-09-24T12:00:00.000Z" },
|
|
35
|
+
chain: CHAIN,
|
|
36
|
+
receipt_sig: "hmac-half",
|
|
37
|
+
receipt_sig_ed25519: signature,
|
|
38
|
+
receipt_sig_kid: KID,
|
|
39
|
+
...over,
|
|
40
|
+
};
|
|
41
|
+
const keys = { issuer: "did:web:id.codespar.dev", algorithm: "Ed25519", signing_string: "codespar-receipt:v1:<receipt_id>:<chain>", keys: [{ kid: KID, kty: "OKP", crv: "Ed25519", x, alg: "EdDSA", use: "sig", status: "active", created_at: "2026-09-01T00:00:00.000Z", retired_at: null }] };
|
|
42
|
+
const receiptPath = join(dir, "receipt.json");
|
|
43
|
+
const keysPath = join(dir, "codespar-receipt-keys.json");
|
|
44
|
+
writeFileSync(receiptPath, JSON.stringify(receipt, null, 2) + "\n");
|
|
45
|
+
writeFileSync(keysPath, JSON.stringify(keys, null, 2) + "\n");
|
|
46
|
+
return { dir, receiptPath, keysPath };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function run(args: string[], cwd: string) {
|
|
50
|
+
const result = spawnSync(process.execPath, [BIN, "verify", ...args], {
|
|
51
|
+
cwd,
|
|
52
|
+
// No CodeSpar key, no model key: a verifier holds nothing.
|
|
53
|
+
env: { ...process.env, CODESPAR_API_KEY: "", ANTHROPIC_API_KEY: "" },
|
|
54
|
+
encoding: "utf8",
|
|
55
|
+
timeout: 60_000,
|
|
56
|
+
});
|
|
57
|
+
return { code: result.status, stdout: result.stdout, stderr: result.stderr };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("codespar-agent verify", () => {
|
|
61
|
+
it("checks a run's copy from a directory with no agent and no key, and says it proves the signature only (exit 7)", () => {
|
|
62
|
+
const { dir, receiptPath, keysPath } = fixture();
|
|
63
|
+
const out = run([receiptPath, "--keys", keysPath], dir);
|
|
64
|
+
expect(out.code).toBe(7);
|
|
65
|
+
expect(out.stdout).toContain("SIGNATURE_ONLY");
|
|
66
|
+
expect(out.stdout).toContain(RECEIPT_ID);
|
|
67
|
+
expect(out.stdout).toContain("read_required");
|
|
68
|
+
expect(out.stderr).toContain("carries CodeSpar's signature");
|
|
69
|
+
expect(out.stderr).toContain("--from-api");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("puts valid JSON and nothing else on stdout with --json; the sentence goes to stderr", () => {
|
|
73
|
+
const { dir, receiptPath, keysPath } = fixture();
|
|
74
|
+
const out = run([receiptPath, "--keys", keysPath, "--json"], dir);
|
|
75
|
+
const lines = out.stdout.split("\n").filter(Boolean);
|
|
76
|
+
expect(lines).toHaveLength(1);
|
|
77
|
+
const report = JSON.parse(lines[0]!) as { verdict: string; kid: string; signing_string: string; message: string };
|
|
78
|
+
expect(report.verdict).toBe("signature_only");
|
|
79
|
+
expect(report.kid).toBe(KID);
|
|
80
|
+
expect(report.signing_string).toBe(`codespar-receipt:v1:${RECEIPT_ID}:${CHAIN}`);
|
|
81
|
+
expect(out.stderr).toContain(report.message);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("exits 1 and says so on a receipt that does not match its signature", () => {
|
|
85
|
+
const { dir, receiptPath, keysPath } = fixture({ chain: CHAIN.replace(/^9/, "8") });
|
|
86
|
+
const out = run([receiptPath, "--keys", keysPath], dir);
|
|
87
|
+
expect(out.code).toBe(1);
|
|
88
|
+
expect(out.stdout).toContain("TAMPERED");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("exits 3 on a receipt sealed before Ed25519, and calls it what it is", () => {
|
|
92
|
+
const { dir, receiptPath, keysPath } = fixture({ receipt_sig_ed25519: null, receipt_sig_kid: null });
|
|
93
|
+
const out = run([receiptPath, "--keys", keysPath], dir);
|
|
94
|
+
expect(out.code).toBe(3);
|
|
95
|
+
expect(out.stdout).toContain("UNSIGNED");
|
|
96
|
+
expect(out.stderr).toContain("never will");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("exits 4 when the key set does not publish the kid the receipt names", () => {
|
|
100
|
+
const { dir, receiptPath, keysPath } = fixture({ receipt_sig_kid: "did:web:id.codespar.dev#9" });
|
|
101
|
+
const out = run([receiptPath, "--keys", keysPath], dir);
|
|
102
|
+
expect(out.code).toBe(4);
|
|
103
|
+
expect(out.stdout).toContain("UNKNOWN_KEY");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("exits 5 when the key set cannot be read: unknown, never invalid", () => {
|
|
107
|
+
const { dir, receiptPath } = fixture();
|
|
108
|
+
const empty = join(dir, "not-a-key-set.json");
|
|
109
|
+
writeFileSync(empty, '{"error":"not found"}\n');
|
|
110
|
+
const out = run([receiptPath, "--keys", empty], dir);
|
|
111
|
+
expect(out.code).toBe(5);
|
|
112
|
+
expect(out.stdout).toContain("UNREACHABLE");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("exits 6 rather than calling an arbitrary JSON file an unsigned receipt", () => {
|
|
116
|
+
const { dir, keysPath } = fixture();
|
|
117
|
+
const other = join(dir, "something-else.json");
|
|
118
|
+
writeFileSync(other, '{"hello":"world"}\n');
|
|
119
|
+
const out = run([other, "--keys", keysPath], dir);
|
|
120
|
+
expect(out.code).toBe(6);
|
|
121
|
+
expect(out.stdout).toContain("MALFORMED");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("refuses its arguments with 2, the way the other commands do", () => {
|
|
125
|
+
const { dir, receiptPath, keysPath } = fixture();
|
|
126
|
+
expect(run([], dir).code).toBe(2);
|
|
127
|
+
expect(run([receiptPath, "--keys", keysPath, "--url", "https://example.invalid/k.json"], dir).code).toBe(2);
|
|
128
|
+
expect(run([receiptPath, "--keys"], dir).code).toBe(2);
|
|
129
|
+
expect(run([join(dir, "missing.json"), "--keys", keysPath], dir).code).toBe(2);
|
|
130
|
+
expect(run(["--help"], dir).code).toBe(0);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/*
|
|
135
|
+
* The body and the approval (OPEN_QUESTIONS §3, §47). A v4 receipt read, its
|
|
136
|
+
* chain written out link by link and signed with a key generated here, under
|
|
137
|
+
* the recipe production published on 2026-09-26.
|
|
138
|
+
*/
|
|
139
|
+
const PUBLISHED = JSON.parse(readFileSync(resolve(import.meta.dirname, "../../agent-core/test/fixtures/production-receipt-keys.json"), "utf8")) as Record<string, unknown>;
|
|
140
|
+
const V4_KID = "did:web:id.codespar.dev#production-2";
|
|
141
|
+
const PAYEE = "escola@exemplo.com.br";
|
|
142
|
+
const ITEMS: ExecutionItem[] = [{ alias: "escola", beneficiary: "Escola Aurora", payee: PAYEE, amount: 185000, currency: "BRL", description: "outubro" }];
|
|
143
|
+
const sha256 = (v: string) => createHash("sha256").update(v, "utf8").digest("hex");
|
|
144
|
+
|
|
145
|
+
function v4Fixture(sealedItemsHash = itemsHash(ITEMS)) {
|
|
146
|
+
const dir = mkdtempSync(join(tmpdir(), "verify-cli-v4-"));
|
|
147
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
148
|
+
const x = Buffer.from(publicKey.export({ format: "der", type: "spki" }).subarray(-32)).toString("base64url");
|
|
149
|
+
const mandate = { id: "cm_test_1", nonce: "nonce-1", scope: "contas do mes", currency: "BRL", sig_sha256: sha256("s".repeat(64)) };
|
|
150
|
+
const quote = { seller: "Escola Aurora", resource: "outubro", price_minor: 185000, payee: PAYEE, session_id: null, sig: "q".repeat(64), at: "2026-09-26T18:00:00.000Z" };
|
|
151
|
+
const approval = { items_hash: sealedItemsHash, batch_hash: null };
|
|
152
|
+
const payment = { rail: "pix-consent", provider: "pix", tx_id: "tx_1", amount_minor: 185000, amount_atomic: null, sandbox: true, attempt_id: "att_1_0", money_moved: false, at: "2026-09-26T18:00:01.000Z" };
|
|
153
|
+
const chain = sha256(
|
|
154
|
+
canonicalize({
|
|
155
|
+
v: 4,
|
|
156
|
+
links: [
|
|
157
|
+
mandate,
|
|
158
|
+
quote,
|
|
159
|
+
approval,
|
|
160
|
+
{ rail: payment.rail, provider: payment.provider, tx_id: payment.tx_id, amount_minor: payment.amount_minor, sandbox: true, attempt_id: payment.attempt_id, money_moved: false, at: payment.at },
|
|
161
|
+
],
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
const signature = signDetached(null, Buffer.from(`codespar-receipt:v1:${RECEIPT_ID}:${chain}`, "utf8"), privateKey).toString("base64url");
|
|
165
|
+
const read = { receipt_id: RECEIPT_ID, state: "paid", chain_version: 4, mandate, quote, approval, payment, delivery: null, chain, receipt_sig: "hmac-not-read-here", receipt_sig_ed25519: signature, receipt_sig_kid: V4_KID, exceptions: [] };
|
|
166
|
+
const keys = { ...PUBLISHED, keys: [{ kid: V4_KID, kty: "OKP", crv: "Ed25519", x, alg: "EdDSA", use: "sig", status: "active" }] };
|
|
167
|
+
const artifact = { approval_id: "apr_1", execution_id: "exe_1", items: ITEMS, items_hash: itemsHash(ITEMS), approved_at: "2026-09-26T18:00:00.000Z" };
|
|
168
|
+
|
|
169
|
+
// A run's bundle, as the engine writes it: the masked copy naming its artifact, and approval.json beside receipts/.
|
|
170
|
+
const run = join(dir, "runs", "run_1");
|
|
171
|
+
mkdirSync(join(run, "receipts"), { recursive: true });
|
|
172
|
+
const copy = {
|
|
173
|
+
receipt_id: RECEIPT_ID,
|
|
174
|
+
state: "paid",
|
|
175
|
+
mandate: { id: mandate.id },
|
|
176
|
+
payment: { amount_minor: 185000, payee: maskPayee(PAYEE), attempt_id: payment.attempt_id, money_moved: false, sandbox: true, at: payment.at },
|
|
177
|
+
chain,
|
|
178
|
+
receipt_sig: "hmac-not-read-here",
|
|
179
|
+
receipt_sig_ed25519: signature,
|
|
180
|
+
receipt_sig_kid: V4_KID,
|
|
181
|
+
actor: { type: "agent", agent: "bills-agent@0.1.0", on_behalf_of: "usr_demo" },
|
|
182
|
+
approval_id: artifact.approval_id,
|
|
183
|
+
};
|
|
184
|
+
const paths = {
|
|
185
|
+
read: join(dir, "receipt-read.json"),
|
|
186
|
+
keys: join(dir, "codespar-receipt-keys.json"),
|
|
187
|
+
artifact: join(dir, "approval.json"),
|
|
188
|
+
copy: join(run, "receipts", `${RECEIPT_ID}.json`),
|
|
189
|
+
runApprovals: join(run, "approval.json"),
|
|
190
|
+
};
|
|
191
|
+
writeFileSync(paths.read, JSON.stringify(read, null, 2) + "\n");
|
|
192
|
+
writeFileSync(paths.keys, JSON.stringify(keys, null, 2) + "\n");
|
|
193
|
+
writeFileSync(paths.artifact, JSON.stringify([artifact], null, 2) + "\n");
|
|
194
|
+
writeFileSync(paths.copy, JSON.stringify(copy, null, 2) + "\n");
|
|
195
|
+
writeFileSync(paths.runApprovals, JSON.stringify([artifact], null, 2) + "\n");
|
|
196
|
+
return { dir, read, keys, copy, artifact, paths };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
describe("codespar-agent verify, the body and the approval of a v4 receipt read", () => {
|
|
200
|
+
it("exits 0 when the chain recomputes from the read and the sealed approval is the artifact's", () => {
|
|
201
|
+
const f = v4Fixture();
|
|
202
|
+
const out = run([f.paths.read, "--keys", f.paths.keys, "--approval", f.paths.artifact], f.dir);
|
|
203
|
+
expect(out.code).toBe(0);
|
|
204
|
+
expect(out.stdout).toContain("VERIFIED");
|
|
205
|
+
expect(out.stdout).toContain("body recomputed (chain v4)");
|
|
206
|
+
expect(out.stdout).toContain("approval matched");
|
|
207
|
+
expect(out.stderr).toContain("paid against the list artifact apr_1 approved");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("exits 9, approval_mismatch, when the receipt sealed another list than the artifact's", () => {
|
|
211
|
+
const f = v4Fixture(`sha256:${"9".repeat(64)}`);
|
|
212
|
+
const out = run([f.paths.read, "--keys", f.paths.keys, "--approval", f.paths.artifact, "--json"], f.dir);
|
|
213
|
+
expect(out.code).toBe(9);
|
|
214
|
+
const report = JSON.parse(out.stdout) as { verdict: string; reason: string; approval_check: { status: string } };
|
|
215
|
+
expect(report).toMatchObject({ verdict: "approval_mismatch", reason: "items_hash_differs", approval_check: { status: "mismatch" } });
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("exits 8, chain_mismatch, when the payee beside a genuine signature was changed", () => {
|
|
219
|
+
const f = v4Fixture();
|
|
220
|
+
writeFileSync(f.paths.read, JSON.stringify({ ...f.read, quote: { ...f.read.quote, payee: "outra@exemplo.com.br" } }));
|
|
221
|
+
const out = run([f.paths.read, "--keys", f.paths.keys, "--approval", f.paths.artifact], f.dir);
|
|
222
|
+
expect(out.code).toBe(8);
|
|
223
|
+
expect(out.stdout).toContain("CHAIN_MISMATCH");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("exits 7 for a v2 read, and says the chain is not recomputable without the mandate signature", () => {
|
|
227
|
+
const f = v4Fixture();
|
|
228
|
+
writeFileSync(f.paths.read, JSON.stringify({ ...f.read, chain_version: 2 }));
|
|
229
|
+
const out = run([f.paths.read, "--keys", f.paths.keys], f.dir);
|
|
230
|
+
expect(out.code).toBe(7);
|
|
231
|
+
expect(out.stderr).toContain("not recomputable without the mandate signature");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("refuses an --approval file with more than one artifact unless --approval-id names one", () => {
|
|
235
|
+
const f = v4Fixture();
|
|
236
|
+
writeFileSync(f.paths.artifact, JSON.stringify([f.artifact, { ...f.artifact, approval_id: "apr_2" }]));
|
|
237
|
+
expect(run([f.paths.read, "--keys", f.paths.keys, "--approval", f.paths.artifact], f.dir).code).toBe(2);
|
|
238
|
+
expect(run([f.paths.read, "--keys", f.paths.keys, "--approval", f.paths.artifact, "--approval-id", "apr_1"], f.dir).code).toBe(0);
|
|
239
|
+
expect(run([f.paths.read, "--keys", f.paths.keys, "--approval-id", "apr_1"], f.dir).code).toBe(2);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
/*
|
|
244
|
+
* `--from-api`: a run's masked copy, bound through the API's read. A local
|
|
245
|
+
* server plays the deployment — the receipt route and its well-known key set —
|
|
246
|
+
* so the read and the key set come from ONE origin, as the command requires.
|
|
247
|
+
*/
|
|
248
|
+
describe("codespar-agent verify --from-api", () => {
|
|
249
|
+
let server: Server;
|
|
250
|
+
let baseUrl: string;
|
|
251
|
+
let served: { read: unknown; keys: unknown } = { read: {}, keys: {} };
|
|
252
|
+
const asked: string[] = [];
|
|
253
|
+
|
|
254
|
+
beforeAll(async () => {
|
|
255
|
+
server = createServer((req, res) => {
|
|
256
|
+
asked.push(`${req.method} ${req.url} ${req.headers["authorization"] ? "auth" : "anon"}`);
|
|
257
|
+
const body = req.url === "/.well-known/codespar-receipt-keys.json" ? served.keys : req.url === `/v1/consumers/receipts/${RECEIPT_ID}` ? served.read : undefined;
|
|
258
|
+
res.writeHead(body === undefined ? 404 : 200, { "content-type": "application/json" });
|
|
259
|
+
res.end(JSON.stringify(body ?? { error: { code: "not_found", message: "no" } }));
|
|
260
|
+
});
|
|
261
|
+
await new Promise<void>((ok) => server.listen(0, "127.0.0.1", ok));
|
|
262
|
+
baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
afterAll(async () => {
|
|
266
|
+
server.closeAllConnections();
|
|
267
|
+
await new Promise<void>((ok) => server.close(() => ok()));
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
/** The server answers while the command runs, so the process is awaited, never spawned synchronously. */
|
|
271
|
+
function runAsync(args: string[], cwd: string): Promise<{ code: number | null; stdout: string; stderr: string }> {
|
|
272
|
+
return new Promise((done) => {
|
|
273
|
+
const child = spawn(process.execPath, [BIN, "verify", ...args], {
|
|
274
|
+
cwd,
|
|
275
|
+
// The placeholder passes the csk_test_ guard and is the one test-key-shaped string the secret scan allows.
|
|
276
|
+
env: { ...process.env, CODESPAR_API_KEY: "csk_test_your_key_here", CODESPAR_API_URL: baseUrl, ANTHROPIC_API_KEY: "" },
|
|
277
|
+
});
|
|
278
|
+
let stdout = "";
|
|
279
|
+
let stderr = "";
|
|
280
|
+
child.stdout.on("data", (d: Buffer) => (stdout += d.toString()));
|
|
281
|
+
child.stderr.on("data", (d: Buffer) => (stderr += d.toString()));
|
|
282
|
+
child.on("close", (code) => done({ code, stdout, stderr }));
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
it("binds the copy's body through the read and checks the run's own artifact: exit 0, and nothing unmasked is written", async () => {
|
|
287
|
+
const f = v4Fixture();
|
|
288
|
+
served = { read: f.read, keys: f.keys };
|
|
289
|
+
asked.length = 0;
|
|
290
|
+
const out = await runAsync([f.paths.copy, "--from-api"], f.dir);
|
|
291
|
+
expect(out.code).toBe(0);
|
|
292
|
+
expect(out.stdout).toContain("approval matched");
|
|
293
|
+
expect(asked).toEqual([`GET /.well-known/codespar-receipt-keys.json anon`, `GET /v1/consumers/receipts/${RECEIPT_ID} auth`]);
|
|
294
|
+
expect(readFileSync(f.paths.copy, "utf8")).not.toContain(PAYEE);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("exits 9 when the run's artifact is not the list the receipt sealed", async () => {
|
|
298
|
+
const f = v4Fixture();
|
|
299
|
+
served = { read: f.read, keys: f.keys };
|
|
300
|
+
writeFileSync(f.paths.runApprovals, JSON.stringify([{ ...f.artifact, items: [{ ...ITEMS[0]!, amount: 1 }], items_hash: itemsHash([{ ...ITEMS[0]!, amount: 1 }]) }]));
|
|
301
|
+
const out = await runAsync([f.paths.copy, "--from-api", "--json"], f.dir);
|
|
302
|
+
expect(out.code).toBe(9);
|
|
303
|
+
expect(JSON.parse(out.stdout)).toMatchObject({ verdict: "approval_mismatch", reason: "items_hash_differs" });
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("exits 8 when the copy was edited after it was written: the signature does not cover its body, the read does", async () => {
|
|
307
|
+
const f = v4Fixture();
|
|
308
|
+
served = { read: f.read, keys: f.keys };
|
|
309
|
+
writeFileSync(f.paths.copy, JSON.stringify({ ...f.copy, payment: { ...f.copy.payment, amount_minor: 1 } }));
|
|
310
|
+
const out = await runAsync([f.paths.copy, "--from-api", "--json"], f.dir);
|
|
311
|
+
expect(out.code).toBe(8);
|
|
312
|
+
expect(JSON.parse(out.stdout)).toMatchObject({ verdict: "chain_mismatch", reason: "copy_differs_from_read" });
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("answers signature_only, never a failure, when the read cannot be fetched", async () => {
|
|
316
|
+
const f = v4Fixture();
|
|
317
|
+
served = { read: undefined as unknown, keys: f.keys };
|
|
318
|
+
const out = await runAsync([f.paths.copy, "--from-api", "--json"], f.dir);
|
|
319
|
+
expect(out.code).toBe(7);
|
|
320
|
+
expect(JSON.parse(out.stdout)).toMatchObject({ verdict: "signature_only", reason: "read_unavailable" });
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("refuses a key set from another deployment than the read", async () => {
|
|
324
|
+
const f = v4Fixture();
|
|
325
|
+
const out = await runAsync([f.paths.copy, "--from-api", "--url", "https://api.codespar.dev/.well-known/codespar-receipt-keys.json"], f.dir);
|
|
326
|
+
expect(out.code).toBe(2);
|
|
327
|
+
expect(out.stderr).toContain("same deployment");
|
|
328
|
+
});
|
|
329
|
+
});
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The official backend, without Meta.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here reaches the network. What is driven is the half where a
|
|
5
|
+
* mistake is silent: the signature check (a wrong one that passes is an open
|
|
6
|
+
* webhook), the webhook parse (a shape misread is a message never answered),
|
|
7
|
+
* the verification handshake, and the request builder. The send itself is one
|
|
8
|
+
* `fetch` over a request these functions built, and the one test that touches
|
|
9
|
+
* `deliver` injects a fetch that asserts what it was handed — so a regression
|
|
10
|
+
* that started calling Meta would fail here instead of dialling out.
|
|
11
|
+
*
|
|
12
|
+
* The shapes come from Meta's published documentation. Nobody on this repo
|
|
13
|
+
* has a Business account, so none of this has been run against Meta, and the
|
|
14
|
+
* README says so in those words rather than implying otherwise.
|
|
15
|
+
*/
|
|
16
|
+
import { createHmac } from "node:crypto";
|
|
17
|
+
import { describe, expect, it } from "vitest";
|
|
18
|
+
import {
|
|
19
|
+
buildSendRequest,
|
|
20
|
+
isMetaBaseUrl,
|
|
21
|
+
loadCloudApiConfig,
|
|
22
|
+
parseInbound,
|
|
23
|
+
providerRefusal,
|
|
24
|
+
verifyChallenge,
|
|
25
|
+
verifyWebhookSignature,
|
|
26
|
+
WhatsAppCloudApi,
|
|
27
|
+
type CloudApiConfig,
|
|
28
|
+
} from "../src/channels/whatsapp/cloud-api.js";
|
|
29
|
+
|
|
30
|
+
const CONFIG: CloudApiConfig = {
|
|
31
|
+
baseUrl: "https://graph.facebook.com",
|
|
32
|
+
phoneNumberId: "111222333",
|
|
33
|
+
accessToken: "test-token-not-a-credential",
|
|
34
|
+
verifyToken: "verify-me",
|
|
35
|
+
appSecret: "app-secret-for-the-test",
|
|
36
|
+
apiVersion: "v21.0",
|
|
37
|
+
webhookPort: 3111,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const sign = (body: string, secret = CONFIG.appSecret) => `sha256=${createHmac("sha256", secret).update(body, "utf8").digest("hex")}`;
|
|
41
|
+
|
|
42
|
+
describe("credentials are absent by default", () => {
|
|
43
|
+
it("names every variable that is missing rather than saying 'misconfigured'", () => {
|
|
44
|
+
const { config, missing } = loadCloudApiConfig({});
|
|
45
|
+
expect(config).toBeUndefined();
|
|
46
|
+
expect(missing).toEqual(["WHATSAPP_PHONE_NUMBER_ID", "WHATSAPP_ACCESS_TOKEN", "WHATSAPP_VERIFY_TOKEN", "WHATSAPP_APP_SECRET"]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("treats an empty value as absent: a commented-out .env.example must not half-configure a channel", () => {
|
|
50
|
+
const { config, missing } = loadCloudApiConfig({ WHATSAPP_PHONE_NUMBER_ID: " ", WHATSAPP_ACCESS_TOKEN: "", WHATSAPP_VERIFY_TOKEN: "v", WHATSAPP_APP_SECRET: "s" });
|
|
51
|
+
expect(config).toBeUndefined();
|
|
52
|
+
expect(missing).toEqual(["WHATSAPP_PHONE_NUMBER_ID", "WHATSAPP_ACCESS_TOKEN"]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("defaults the version, the port and Meta's own host, and nothing else", () => {
|
|
56
|
+
const { config } = loadCloudApiConfig({ WHATSAPP_PHONE_NUMBER_ID: "1", WHATSAPP_ACCESS_TOKEN: "2", WHATSAPP_VERIFY_TOKEN: "3", WHATSAPP_APP_SECRET: "4" });
|
|
57
|
+
expect(config?.apiVersion).toBe("v21.0");
|
|
58
|
+
expect(config?.webhookPort).toBe(3111);
|
|
59
|
+
expect(config?.baseUrl).toBe("https://graph.facebook.com");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("live is decided by the base URL, and only by it", () => {
|
|
64
|
+
it("is Meta's host and nothing that merely looks like it", () => {
|
|
65
|
+
expect(isMetaBaseUrl("https://graph.facebook.com")).toBe(true);
|
|
66
|
+
expect(isMetaBaseUrl("https://graph.facebook.com/")).toBe(true);
|
|
67
|
+
expect(isMetaBaseUrl("http://127.0.0.1:4290")).toBe(false);
|
|
68
|
+
expect(isMetaBaseUrl("https://graph.facebook.com.evil.test")).toBe(false);
|
|
69
|
+
expect(isMetaBaseUrl("not a url")).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("names itself `emulator` and reports `live: false` off Meta, which is what the evidence builder refuses on", () => {
|
|
73
|
+
const options = { conversation: { contact: "+5511987654321" }, say: () => undefined, fetchImpl: async () => new Response("{}") };
|
|
74
|
+
const emulator = new WhatsAppCloudApi({ ...options, config: { ...CONFIG, baseUrl: "http://127.0.0.1:4290" } });
|
|
75
|
+
expect(emulator.live).toBe(false);
|
|
76
|
+
expect(emulator.name).toBe("emulator");
|
|
77
|
+
const meta = new WhatsAppCloudApi({ ...options, config: CONFIG });
|
|
78
|
+
expect(meta.live).toBe(true);
|
|
79
|
+
expect(meta.name).toBe("cloud-api");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("builds the send against whichever base URL it was given, with the path unchanged", () => {
|
|
83
|
+
const request = buildSendRequest({ ...CONFIG, baseUrl: "http://127.0.0.1:4290", apiVersion: "v22.0" }, "+5511987654321", { kind: "text", text: "oi" });
|
|
84
|
+
if ("unsupported" in request) throw new Error("expected a request");
|
|
85
|
+
expect(request.url).toBe("http://127.0.0.1:4290/v22.0/111222333/messages");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("a delivery is trusted only when it is signed", () => {
|
|
90
|
+
const body = JSON.stringify({ object: "whatsapp_business_account" });
|
|
91
|
+
|
|
92
|
+
it("accepts the app secret's own signature over the RAW body", () => {
|
|
93
|
+
expect(verifyWebhookSignature(CONFIG.appSecret, sign(body), body)).toEqual({ ok: true });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("refuses a body that was re-serialized: the signature is over bytes", () => {
|
|
97
|
+
const reserialized = JSON.stringify(JSON.parse(body), null, 2);
|
|
98
|
+
expect(verifyWebhookSignature(CONFIG.appSecret, sign(body), reserialized).ok).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("refuses another secret, a missing header and a malformed one", () => {
|
|
102
|
+
expect(verifyWebhookSignature(CONFIG.appSecret, sign(body, "someone-else"), body).ok).toBe(false);
|
|
103
|
+
expect(verifyWebhookSignature(CONFIG.appSecret, undefined, body)).toEqual({ ok: false, reason: "missing X-Hub-Signature-256" });
|
|
104
|
+
expect(verifyWebhookSignature(CONFIG.appSecret, "sha1=abc", body).ok).toBe(false);
|
|
105
|
+
expect(verifyWebhookSignature(CONFIG.appSecret, "nonsense", body).ok).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("the registration handshake", () => {
|
|
110
|
+
it("echoes the challenge for the right verify token", () => {
|
|
111
|
+
expect(verifyChallenge(CONFIG, { "hub.mode": "subscribe", "hub.verify_token": "verify-me", "hub.challenge": "12345" })).toEqual({ status: 200, body: "12345" });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("refuses a wrong token and a wrong mode", () => {
|
|
115
|
+
expect(verifyChallenge(CONFIG, { "hub.mode": "subscribe", "hub.verify_token": "nope", "hub.challenge": "1" }).status).toBe(403);
|
|
116
|
+
expect(verifyChallenge(CONFIG, { "hub.mode": "unsubscribe", "hub.verify_token": "verify-me" }).status).toBe(400);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("reading what Meta delivers", () => {
|
|
121
|
+
const delivery = {
|
|
122
|
+
object: "whatsapp_business_account",
|
|
123
|
+
entry: [
|
|
124
|
+
{
|
|
125
|
+
changes: [
|
|
126
|
+
{
|
|
127
|
+
field: "messages",
|
|
128
|
+
value: {
|
|
129
|
+
messaging_product: "whatsapp",
|
|
130
|
+
metadata: { phone_number_id: "111222333" },
|
|
131
|
+
messages: [
|
|
132
|
+
{ id: "wamid.HBgNNTUxMTk4NzY1NDMyMRUCABIYEjND", from: "5511987654321", timestamp: "1758643200", type: "text", text: { body: "fechado, pago à vista" } },
|
|
133
|
+
{ id: "wamid.SECOND", from: "5511987654321", timestamp: "1758643260", type: "image", image: { id: "media_1" } },
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
it("reads the text messages and puts the plus back on the number", () => {
|
|
143
|
+
const { messages } = parseInbound(delivery);
|
|
144
|
+
expect(messages).toEqual([{ id: "wamid.HBgNNTUxMTk4NzY1NDMyMRUCABIYEjND", from: "+5511987654321", text: "fechado, pago à vista", timestamp: 1758643200 }]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("names what it dropped instead of turning it into an empty message", () => {
|
|
148
|
+
expect(parseInbound(delivery).ignored).toEqual([{ id: "wamid.SECOND", type: "image" }]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("reads a delivery that carries only status updates as no messages", () => {
|
|
152
|
+
expect(parseInbound({ entry: [{ changes: [{ value: { statuses: [{ id: "wamid.X", status: "delivered" }] } }] }] }).messages).toEqual([]);
|
|
153
|
+
expect(parseInbound({}).messages).toEqual([]);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("the request that would go to Meta", () => {
|
|
158
|
+
it("builds the documented /messages call for a text", () => {
|
|
159
|
+
const request = buildSendRequest(CONFIG, "+5511987654321", { kind: "text", text: "Oi, Joana!" });
|
|
160
|
+
expect("unsupported" in request).toBe(false);
|
|
161
|
+
if ("unsupported" in request) return;
|
|
162
|
+
expect(request.url).toBe("https://graph.facebook.com/v21.0/111222333/messages");
|
|
163
|
+
expect(request.headers["authorization"]).toBe("Bearer test-token-not-a-credential");
|
|
164
|
+
expect(JSON.parse(request.body)).toEqual({ messaging_product: "whatsapp", recipient_type: "individual", to: "5511987654321", type: "text", text: { preview_url: false, body: "Oi, Joana!" } });
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("sends a copy-and-paste as its own plain text message: a code inside a paragraph cannot be tapped", () => {
|
|
168
|
+
const request = buildSendRequest(CONFIG, "+5511987654321", { kind: "instrument", instrument: "pix_copy_paste", value: "00020126580014br.gov.bcb.pix" });
|
|
169
|
+
if ("unsupported" in request) throw new Error("expected a request");
|
|
170
|
+
expect(JSON.parse(request.body).text.body).toBe("00020126580014br.gov.bcb.pix");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("builds a template with its body parameters", () => {
|
|
174
|
+
const request = buildSendRequest(CONFIG, "+5511987654321", { kind: "template", template: "cobranca_lembrete", language: "pt_BR", variables: ["Joana", "1042"] });
|
|
175
|
+
if ("unsupported" in request) throw new Error("expected a request");
|
|
176
|
+
expect(JSON.parse(request.body).template).toEqual({
|
|
177
|
+
name: "cobranca_lembrete",
|
|
178
|
+
language: { code: "pt_BR" },
|
|
179
|
+
components: [{ type: "body", parameters: [{ type: "text", text: "Joana" }, { type: "text", text: "1042" }] }],
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("says outright that it cannot send an image, instead of pretending", () => {
|
|
184
|
+
expect(buildSendRequest(CONFIG, "+5511987654321", { kind: "media", media: "qr", data: "00020126" })).toEqual({ unsupported: "media_upload_unimplemented" });
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("deliver, with the network replaced by an assertion", () => {
|
|
189
|
+
it("posts the request the builder produced and returns the provider's message id", async () => {
|
|
190
|
+
const calls: Array<{ url: string; init: RequestInit | undefined }> = [];
|
|
191
|
+
const backend = new WhatsAppCloudApi({
|
|
192
|
+
config: CONFIG,
|
|
193
|
+
conversation: { contact: "+5511987654321" },
|
|
194
|
+
say: () => undefined,
|
|
195
|
+
fetchImpl: async (url, init) => {
|
|
196
|
+
calls.push({ url: String(url), init });
|
|
197
|
+
return new Response(JSON.stringify({ messages: [{ id: "wamid.SENT" }] }), { status: 200 });
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
const sent = await backend.deliver("+5511987654321", { kind: "text", text: "Oi!" });
|
|
201
|
+
expect(sent).toEqual({ id: "wamid.SENT", state: "sent" });
|
|
202
|
+
expect(calls).toHaveLength(1);
|
|
203
|
+
expect(calls[0]!.url).toBe("https://graph.facebook.com/v21.0/111222333/messages");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("refuses an image without reaching for the network at all", async () => {
|
|
207
|
+
let called = false;
|
|
208
|
+
const backend = new WhatsAppCloudApi({
|
|
209
|
+
config: CONFIG,
|
|
210
|
+
conversation: { contact: "+5511987654321" },
|
|
211
|
+
say: () => undefined,
|
|
212
|
+
fetchImpl: async () => {
|
|
213
|
+
called = true;
|
|
214
|
+
return new Response("{}", { status: 200 });
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
const sent = await backend.deliver("+5511987654321", { kind: "media", media: "qr", data: "00020126" });
|
|
218
|
+
expect(sent.refused?.rule).toBe("media_upload_unimplemented");
|
|
219
|
+
expect(called).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("reports a provider refusal as a failure and never as a send", async () => {
|
|
223
|
+
const backend = new WhatsAppCloudApi({
|
|
224
|
+
config: CONFIG,
|
|
225
|
+
conversation: { contact: "+5511987654321" },
|
|
226
|
+
say: () => undefined,
|
|
227
|
+
fetchImpl: async () => new Response(JSON.stringify({ error: { message: "Invalid parameter" } }), { status: 400 }),
|
|
228
|
+
});
|
|
229
|
+
const sent = await backend.deliver("+5511987654321", { kind: "text", text: "Oi!" });
|
|
230
|
+
expect(sent.state).toBe("failed");
|
|
231
|
+
expect(sent.refused?.rule).toBe("provider_refused");
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("what Meta's error body says, read rather than collapsed into a status", () => {
|
|
236
|
+
const body = (error: unknown) => JSON.stringify({ error });
|
|
237
|
+
|
|
238
|
+
it("reads 131047 as the window rule it is, so a caller can answer it with a template", () => {
|
|
239
|
+
const refusal = providerRefusal(400, body({ code: 131047, message: "(#131047) Message failed to send", error_data: { details: "the 24h customer service window closed" } }));
|
|
240
|
+
expect(refusal.rule).toBe("session_window_closed");
|
|
241
|
+
expect(refusal.detail).toContain("400/131047");
|
|
242
|
+
expect(refusal.detail).toContain("the 24h customer service window closed");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("keeps any other code a provider refusal, with the code named", () => {
|
|
246
|
+
expect(providerRefusal(400, body({ code: 131026, message: "Message undeliverable" }))).toEqual({ rule: "provider_refused", detail: "400/131026 from the Cloud API" });
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("does not mistake a body without Meta's envelope for anything but the status", () => {
|
|
250
|
+
expect(providerRefusal(502, "<html>bad gateway</html>")).toEqual({ rule: "provider_refused", detail: "502 from the Cloud API" });
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe("a redelivered message is the same message", () => {
|
|
255
|
+
it("hands a message on once however many times it arrives, and says it dropped the rest", async () => {
|
|
256
|
+
const lines: string[] = [];
|
|
257
|
+
const port = 40000 + Math.floor(Math.random() * 20000);
|
|
258
|
+
const backend = new WhatsAppCloudApi({ config: { ...CONFIG, webhookPort: port }, conversation: { contact: "+5511987654321" }, say: (l) => lines.push(l) });
|
|
259
|
+
await backend.open();
|
|
260
|
+
try {
|
|
261
|
+
const raw = JSON.stringify({
|
|
262
|
+
entry: [{ changes: [{ value: { messages: [{ id: "wamid.ONCE", from: "5511987654321", timestamp: "1758643200", type: "text", text: { body: "fechado" } }] } }] }],
|
|
263
|
+
});
|
|
264
|
+
for (let i = 0; i < 2; i++) {
|
|
265
|
+
const response = await fetch(`http://127.0.0.1:${port}/`, { method: "POST", headers: { "x-hub-signature-256": sign(raw) }, body: raw });
|
|
266
|
+
// Still 200: Meta retries anything else, and a duplicate is not an error.
|
|
267
|
+
expect(response.status).toBe(200);
|
|
268
|
+
}
|
|
269
|
+
expect((await backend.next())?.id).toBe("wamid.ONCE");
|
|
270
|
+
const second = await Promise.race([backend.next(), new Promise((resolve) => setTimeout(() => resolve("nothing"), 200))]);
|
|
271
|
+
expect(second).toBe("nothing");
|
|
272
|
+
expect(lines).toContain("[whatsapp] duplicate delivery of wamid.ONCE dropped");
|
|
273
|
+
} finally {
|
|
274
|
+
await backend.close();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
});
|