@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
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One interface, several backends. What a channel is, from the runner's side:
|
|
3
|
+
* a place a person writes from, a place the agent writes to, and a record of
|
|
4
|
+
* what happened to each message.
|
|
5
|
+
*
|
|
6
|
+
* The shape is deliberately the shape of a MESSAGING channel and not of a
|
|
7
|
+
* terminal, because the two are not the same thing and pretending they are is
|
|
8
|
+
* how an approval question ends up in a debtor's chat. A terminal has one
|
|
9
|
+
* stream and one reader; a channel has a counterparty on one side and an
|
|
10
|
+
* operator somewhere else entirely, and the runner keeps them apart by
|
|
11
|
+
* construction: `send` reaches the counterparty, and the operator's console
|
|
12
|
+
* and questions never touch it.
|
|
13
|
+
*
|
|
14
|
+
* Every backend answers the same five questions, and the ADAPTER above them
|
|
15
|
+
* (`whatsapp/index.ts`) is where the rules live, so a rule cannot be skipped
|
|
16
|
+
* by choosing a different backend.
|
|
17
|
+
*/
|
|
18
|
+
import type { ChannelName } from "@codespar/agent-core";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* What a provider tells us about a message after it left. `queued` is ours
|
|
22
|
+
* (accepted, not yet handed over); the rest are the provider's own, and a
|
|
23
|
+
* backend that cannot observe one never invents it.
|
|
24
|
+
*/
|
|
25
|
+
export type DeliveryState = "queued" | "sent" | "delivered" | "read" | "failed";
|
|
26
|
+
|
|
27
|
+
export interface InboundMessage {
|
|
28
|
+
/** The PROVIDER's message id (`wamid....` on WhatsApp). Never ours. */
|
|
29
|
+
id: string;
|
|
30
|
+
/** Who wrote it, E.164 on WhatsApp. */
|
|
31
|
+
from: string;
|
|
32
|
+
/**
|
|
33
|
+
* What the person wrote. For a TAP (`reply` set) the backend puts the
|
|
34
|
+
* button's title here, and the channel replaces it with the intent the
|
|
35
|
+
* template that offered the button declared — the model never reads a
|
|
36
|
+
* title as if the person had typed it.
|
|
37
|
+
*/
|
|
38
|
+
text: string;
|
|
39
|
+
/** Unix seconds on the PROVIDER's clock, which is not this process's clock. */
|
|
40
|
+
timestamp: number;
|
|
41
|
+
/** Set when the person tapped a quick reply instead of typing. */
|
|
42
|
+
reply?: InboundReply;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A tapped quick reply, as the provider reports it: which button, and on which message when it says. */
|
|
46
|
+
export interface InboundReply {
|
|
47
|
+
/** `button_reply` / `list_reply` (an interactive message), `button` (a template's quick reply). */
|
|
48
|
+
type: "button_reply" | "list_reply" | "button";
|
|
49
|
+
id: string;
|
|
50
|
+
title: string;
|
|
51
|
+
/** The message the button was on (`context.id`), when the provider sends it. */
|
|
52
|
+
context_id?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* What the provider reports about a message the agent sent, after it left.
|
|
57
|
+
* `failed` carries Meta's errors (131026: not a WhatsApp number, or it blocked
|
|
58
|
+
* the business). Delivered by webhook, at any time, to whoever is listening.
|
|
59
|
+
*/
|
|
60
|
+
export interface StatusUpdate {
|
|
61
|
+
message_id: string;
|
|
62
|
+
status: DeliveryState;
|
|
63
|
+
/** Unix seconds, the provider's clock. */
|
|
64
|
+
timestamp: number;
|
|
65
|
+
errors: Array<{ code?: number; title?: string; details?: string }>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The execution an outbound message tells the outcome of: what a failed delivery of it means. */
|
|
69
|
+
export interface OutcomeTag {
|
|
70
|
+
execution_id: string;
|
|
71
|
+
state: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* What the agent can put on a channel.
|
|
76
|
+
*
|
|
77
|
+
* `text` is the ordinary message. `media` is a QR, which on WhatsApp is an
|
|
78
|
+
* image and never ASCII art. `template` is the only thing a provider accepts
|
|
79
|
+
* outside the 24-hour session window, which is why it is a separate kind
|
|
80
|
+
* rather than a flag.
|
|
81
|
+
*
|
|
82
|
+
* `instrument` is the Pix copy-and-paste or the boleto line, and it is its own
|
|
83
|
+
* kind for a reason that is not cosmetic: those strings are minted by the rail
|
|
84
|
+
* and are long runs of digits, and the rule that keeps a CPF out of a message
|
|
85
|
+
* would refuse a boleto line (whose last field is fourteen digits) if it read
|
|
86
|
+
* one as prose. Separating them says which strings the conversation composed
|
|
87
|
+
* and which the rail did, and only the first are checked for a document.
|
|
88
|
+
*/
|
|
89
|
+
export type OutboundBody =
|
|
90
|
+
| { kind: "text"; text: string; about?: OutcomeTag }
|
|
91
|
+
| { kind: "media"; media: "qr"; data: string; caption?: string }
|
|
92
|
+
| { kind: "instrument"; instrument: "pix_copy_paste" | "boleto_bank_line"; value: string }
|
|
93
|
+
/** `buttons` are the declaration's, attached by the channel; a caller never composes them. */
|
|
94
|
+
| { kind: "template"; template: string; language: string; variables: string[]; about?: OutcomeTag; buttons?: Array<{ id: string; title: string }> };
|
|
95
|
+
|
|
96
|
+
export interface SentMessage {
|
|
97
|
+
/** The provider's id when it accepted one, else the id the backend minted. */
|
|
98
|
+
id: string;
|
|
99
|
+
state: DeliveryState;
|
|
100
|
+
/**
|
|
101
|
+
* Set when something REFUSED the message. Nothing left the process: a
|
|
102
|
+
* refusal here is not a delivery failure, it is the message never having
|
|
103
|
+
* been sent. `rule` is the stable name a test and a log both key on.
|
|
104
|
+
*/
|
|
105
|
+
refused?: { rule: string; detail: string };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** What the person on the other side is called, and what the conversation may be about. */
|
|
109
|
+
export interface Conversation {
|
|
110
|
+
/** The contact the conversation is bound to. Every outbound goes here and nowhere else. */
|
|
111
|
+
contact: string;
|
|
112
|
+
/** What the conversation may name — an agreement alias. Absent means the channel enforces no subject rule. */
|
|
113
|
+
subject?: string | undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface ChannelBackend {
|
|
117
|
+
/** `simulator`, `cloud-api`. Named in the console, in the bundle and in the gate's output. */
|
|
118
|
+
readonly name: string;
|
|
119
|
+
/** Whether this backend talks to a real provider. The gate and the tests refuse to run a live one. */
|
|
120
|
+
readonly live: boolean;
|
|
121
|
+
open(): Promise<void>;
|
|
122
|
+
/** The next inbound message, or `undefined` when the conversation is over. */
|
|
123
|
+
next(): Promise<InboundMessage | undefined>;
|
|
124
|
+
/** Hands the message to the provider. Rules have already run: a backend never re-decides policy. */
|
|
125
|
+
deliver(to: string, body: OutboundBody): Promise<SentMessage>;
|
|
126
|
+
/** Where the status webhooks go. A backend that cannot observe statuses never calls it. */
|
|
127
|
+
onStatus?(listener: (status: StatusUpdate) => void): void;
|
|
128
|
+
close(): Promise<void>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface Channel {
|
|
132
|
+
readonly name: ChannelName;
|
|
133
|
+
readonly backend: string;
|
|
134
|
+
readonly conversation: Conversation;
|
|
135
|
+
open(): Promise<void>;
|
|
136
|
+
next(): Promise<InboundMessage | undefined>;
|
|
137
|
+
/** Applies the channel's rules, then delivers. A refused message is recorded and never sent. */
|
|
138
|
+
send(body: OutboundBody): Promise<SentMessage>;
|
|
139
|
+
/** Convenience for the common case. */
|
|
140
|
+
say(text: string): Promise<SentMessage>;
|
|
141
|
+
close(): Promise<void>;
|
|
142
|
+
/** Every message of this conversation, in order, as the bundle records it. */
|
|
143
|
+
log(): ReadonlyArray<ChannelLogLine>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface ChannelLogLine {
|
|
147
|
+
at: string;
|
|
148
|
+
/** `status`: what the provider reported about an outbound message, after it left. */
|
|
149
|
+
direction: "in" | "out" | "status";
|
|
150
|
+
/** The contact, MASKED. The raw value never reaches a file. */
|
|
151
|
+
contact: string;
|
|
152
|
+
/** `reply`: an inbound tap. `status`: a delivery status. */
|
|
153
|
+
kind: OutboundBody["kind"] | "reply" | "status";
|
|
154
|
+
/** The provider's id, or ours when nothing was sent. */
|
|
155
|
+
message_id: string;
|
|
156
|
+
state: DeliveryState;
|
|
157
|
+
text?: string;
|
|
158
|
+
/**
|
|
159
|
+
* Inbound only: the PROVIDER's own timestamp, unix seconds. Recorded
|
|
160
|
+
* because the 24-hour session window is counted on WhatsApp's clock and
|
|
161
|
+
* `at` is this process's, and a later run has to read the window back from
|
|
162
|
+
* this log rather than from a clock it does not share.
|
|
163
|
+
*/
|
|
164
|
+
provider_timestamp?: number;
|
|
165
|
+
refused?: { rule: string; detail: string };
|
|
166
|
+
/** Outbound: the execution whose outcome this message tells. Status: the same, looked up by message id. */
|
|
167
|
+
about?: OutcomeTag;
|
|
168
|
+
/** Outbound template: the reply ids it offered. Inbound tap: the reply tapped. */
|
|
169
|
+
offered?: string[];
|
|
170
|
+
reply?: { type: InboundReply["type"]; id: string; title: string };
|
|
171
|
+
/** Status `failed`: Meta's errors, as reported. */
|
|
172
|
+
errors?: StatusUpdate["errors"];
|
|
173
|
+
}
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The official backend: Meta's WhatsApp Cloud API, behind the same interface
|
|
3
|
+
* the simulator implements. An ADAPTER and nothing more — the rules, the
|
|
4
|
+
* session window and the conversation record are the channel's, above this.
|
|
5
|
+
*
|
|
6
|
+
* Three things about it are deliberate.
|
|
7
|
+
*
|
|
8
|
+
* The credentials are ABSENT by default and this backend refuses to open
|
|
9
|
+
* without them, by name. No fallback, no "development mode" that quietly
|
|
10
|
+
* talks to nobody: a channel that pretends to send is worse than one that
|
|
11
|
+
* says it cannot.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here is called from a test or from the CI, ever. What the tests
|
|
14
|
+
* exercise are the four pure functions — the signature check, the webhook
|
|
15
|
+
* parse, the verification handshake and the request builder — which is
|
|
16
|
+
* exactly the part where a mistake is silent. The send itself is one `fetch`
|
|
17
|
+
* over a request those functions built.
|
|
18
|
+
*
|
|
19
|
+
* And the shapes are written against Meta's published documentation, not
|
|
20
|
+
* against a call we made: nobody on this repo has a Business account, so the
|
|
21
|
+
* first live run belongs to the developer who has one. The README says that
|
|
22
|
+
* in those words, and `docs/OPEN_QUESTIONS.md` names what a live run would
|
|
23
|
+
* settle.
|
|
24
|
+
*/
|
|
25
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
26
|
+
import { createServer, type Server } from "node:http";
|
|
27
|
+
import type { ChannelBackend, DeliveryState, InboundMessage, InboundReply, OutboundBody, SentMessage, StatusUpdate } from "../types.js";
|
|
28
|
+
|
|
29
|
+
export interface CloudApiConfig {
|
|
30
|
+
/**
|
|
31
|
+
* Where the Graph API lives. Meta's own by default, and the local emulator's
|
|
32
|
+
* when the run is against the local emulator — which is the whole point of
|
|
33
|
+
* having it be a field: the two backends are THE SAME CODE with a different
|
|
34
|
+
* base URL, so the simulator exercises the adapter instead of standing in
|
|
35
|
+
* for it. A mock of our own would only ever agree with us.
|
|
36
|
+
*/
|
|
37
|
+
baseUrl: string;
|
|
38
|
+
/** The number the business sends from, as Meta's dashboard names it. */
|
|
39
|
+
phoneNumberId: string;
|
|
40
|
+
accessToken: string;
|
|
41
|
+
/** Echoed back on the `GET` handshake when Meta registers the webhook. */
|
|
42
|
+
verifyToken: string;
|
|
43
|
+
/** The app secret that signs deliveries. Without it no delivery is trusted. */
|
|
44
|
+
appSecret: string;
|
|
45
|
+
apiVersion: string;
|
|
46
|
+
webhookPort: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Meta's own Graph host. Anything else is a stand-in, and the channel treats it as one. */
|
|
50
|
+
export const META_GRAPH_HOST = "graph.facebook.com";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether this base URL is Meta. It decides one thing that matters beyond
|
|
54
|
+
* logging: `live`, which is what the consent-evidence builder refuses on. An
|
|
55
|
+
* act observed by an emulator was observed by nobody.
|
|
56
|
+
*/
|
|
57
|
+
export function isMetaBaseUrl(baseUrl: string): boolean {
|
|
58
|
+
try {
|
|
59
|
+
return new URL(baseUrl).hostname === META_GRAPH_HOST;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const CLOUD_API_ENV = {
|
|
66
|
+
phoneNumberId: "WHATSAPP_PHONE_NUMBER_ID",
|
|
67
|
+
accessToken: "WHATSAPP_ACCESS_TOKEN",
|
|
68
|
+
verifyToken: "WHATSAPP_VERIFY_TOKEN",
|
|
69
|
+
appSecret: "WHATSAPP_APP_SECRET",
|
|
70
|
+
apiVersion: "WHATSAPP_API_VERSION",
|
|
71
|
+
webhookPort: "WHATSAPP_WEBHOOK_PORT",
|
|
72
|
+
baseUrl: "WHATSAPP_GRAPH_URL",
|
|
73
|
+
} as const;
|
|
74
|
+
|
|
75
|
+
export interface ConfigResult {
|
|
76
|
+
config?: CloudApiConfig;
|
|
77
|
+
/** The variables that are not set. A refusal names them rather than saying "misconfigured". */
|
|
78
|
+
missing: string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function loadCloudApiConfig(env: NodeJS.ProcessEnv): ConfigResult {
|
|
82
|
+
const read = (name: string) => {
|
|
83
|
+
const v = env[name]?.trim();
|
|
84
|
+
return v ? v : undefined;
|
|
85
|
+
};
|
|
86
|
+
const phoneNumberId = read(CLOUD_API_ENV.phoneNumberId);
|
|
87
|
+
const accessToken = read(CLOUD_API_ENV.accessToken);
|
|
88
|
+
const verifyToken = read(CLOUD_API_ENV.verifyToken);
|
|
89
|
+
const appSecret = read(CLOUD_API_ENV.appSecret);
|
|
90
|
+
const missing = [
|
|
91
|
+
...(phoneNumberId ? [] : [CLOUD_API_ENV.phoneNumberId]),
|
|
92
|
+
...(accessToken ? [] : [CLOUD_API_ENV.accessToken]),
|
|
93
|
+
...(verifyToken ? [] : [CLOUD_API_ENV.verifyToken]),
|
|
94
|
+
...(appSecret ? [] : [CLOUD_API_ENV.appSecret]),
|
|
95
|
+
];
|
|
96
|
+
if (!phoneNumberId || !accessToken || !verifyToken || !appSecret) return { missing };
|
|
97
|
+
return {
|
|
98
|
+
missing,
|
|
99
|
+
config: {
|
|
100
|
+
baseUrl: read(CLOUD_API_ENV.baseUrl) ?? `https://${META_GRAPH_HOST}`,
|
|
101
|
+
phoneNumberId,
|
|
102
|
+
accessToken,
|
|
103
|
+
verifyToken,
|
|
104
|
+
appSecret,
|
|
105
|
+
apiVersion: read(CLOUD_API_ENV.apiVersion) ?? "v21.0",
|
|
106
|
+
webhookPort: Number(read(CLOUD_API_ENV.webhookPort) ?? 3111),
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* ── the pure half: what the tests drive ─────────────────────── */
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* `X-Hub-Signature-256: sha256=<hex>`, HMAC-SHA256 of the RAW body under the
|
|
115
|
+
* app secret. Raw and not re-serialized: a body that round-tripped through
|
|
116
|
+
* `JSON.parse` is a different string, and the signature is over bytes.
|
|
117
|
+
*/
|
|
118
|
+
export function verifyWebhookSignature(appSecret: string, header: string | undefined, rawBody: string): { ok: true } | { ok: false; reason: string } {
|
|
119
|
+
if (!header) return { ok: false, reason: "missing X-Hub-Signature-256" };
|
|
120
|
+
const [algorithm, signature] = header.split("=");
|
|
121
|
+
if (algorithm !== "sha256" || !signature) return { ok: false, reason: "malformed signature header" };
|
|
122
|
+
const expected = createHmac("sha256", appSecret).update(rawBody, "utf8").digest("hex");
|
|
123
|
+
if (expected.length !== signature.length) return { ok: false, reason: "signature mismatch" };
|
|
124
|
+
if (!timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(signature, "hex"))) return { ok: false, reason: "signature mismatch" };
|
|
125
|
+
return { ok: true };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The `GET` handshake Meta makes once, when the webhook URL is registered. */
|
|
129
|
+
export function verifyChallenge(config: CloudApiConfig, query: Record<string, string | undefined>): { status: number; body: string } {
|
|
130
|
+
if (query["hub.mode"] !== "subscribe") return { status: 400, body: "bad mode" };
|
|
131
|
+
if (query["hub.verify_token"] !== config.verifyToken) return { status: 403, body: "bad verify token" };
|
|
132
|
+
return { status: 200, body: query["hub.challenge"] ?? "" };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface CloudApiEnvelope {
|
|
136
|
+
object?: string;
|
|
137
|
+
entry?: Array<{
|
|
138
|
+
changes?: Array<{
|
|
139
|
+
field?: string;
|
|
140
|
+
value?: {
|
|
141
|
+
messaging_product?: string;
|
|
142
|
+
metadata?: { phone_number_id?: string };
|
|
143
|
+
messages?: Array<{
|
|
144
|
+
id?: string;
|
|
145
|
+
from?: string;
|
|
146
|
+
timestamp?: string;
|
|
147
|
+
type?: string;
|
|
148
|
+
text?: { body?: string };
|
|
149
|
+
interactive?: { type?: string; button_reply?: { id?: string; title?: string }; list_reply?: { id?: string; title?: string } };
|
|
150
|
+
button?: { payload?: string; text?: string };
|
|
151
|
+
context?: { id?: string };
|
|
152
|
+
}>;
|
|
153
|
+
statuses?: Array<{ id?: string; status?: string; timestamp?: string; errors?: Array<{ code?: number; title?: string; message?: string; error_data?: { details?: string } }> }>;
|
|
154
|
+
};
|
|
155
|
+
}>;
|
|
156
|
+
}>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* What one delivery carries. A delivery holds several entries, each with
|
|
161
|
+
* several changes, each with messages and with STATUS updates about messages
|
|
162
|
+
* we sent. A message is read when it is text, or a tapped quick reply (an
|
|
163
|
+
* interactive `button_reply` / `list_reply`, or a template's `button`), which
|
|
164
|
+
* keeps its id — the channel decides whether that id means anything. Anything
|
|
165
|
+
* else (an image, a location) is dropped with its id named: this channel
|
|
166
|
+
* reads words and taps, and an unreadable message must not become an empty
|
|
167
|
+
* turn.
|
|
168
|
+
*/
|
|
169
|
+
export function parseInbound(payload: unknown): { messages: InboundMessage[]; statuses: StatusUpdate[]; ignored: Array<{ id: string; type: string }> } {
|
|
170
|
+
const envelope = payload as CloudApiEnvelope;
|
|
171
|
+
const messages: InboundMessage[] = [];
|
|
172
|
+
const statuses: StatusUpdate[] = [];
|
|
173
|
+
const ignored: Array<{ id: string; type: string }> = [];
|
|
174
|
+
for (const entry of envelope.entry ?? []) {
|
|
175
|
+
for (const change of entry.changes ?? []) {
|
|
176
|
+
for (const message of change.value?.messages ?? []) {
|
|
177
|
+
const id = message.id;
|
|
178
|
+
const from = message.from;
|
|
179
|
+
if (!id || !from) continue;
|
|
180
|
+
const base = { id, from: from.startsWith("+") ? from : `+${from}`, timestamp: Number(message.timestamp ?? 0) };
|
|
181
|
+
const reply = replyOf(message);
|
|
182
|
+
if (reply) {
|
|
183
|
+
messages.push({ ...base, text: reply.title, reply: { ...reply, ...(message.context?.id ? { context_id: message.context.id } : {}) } });
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (message.type !== "text" || !message.text?.body) {
|
|
187
|
+
ignored.push({ id, type: message.type === "interactive" ? `interactive:${message.interactive?.type ?? "unknown"}` : message.type ?? "unknown" });
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
messages.push({ ...base, text: message.text.body });
|
|
191
|
+
}
|
|
192
|
+
for (const status of change.value?.statuses ?? []) {
|
|
193
|
+
if (!status.id || !isDeliveryState(status.status)) continue;
|
|
194
|
+
statuses.push({
|
|
195
|
+
message_id: status.id,
|
|
196
|
+
status: status.status,
|
|
197
|
+
timestamp: Number(status.timestamp ?? 0),
|
|
198
|
+
errors: (status.errors ?? []).map((e) => ({
|
|
199
|
+
...(typeof e.code === "number" ? { code: e.code } : {}),
|
|
200
|
+
...(e.title ?? e.message ? { title: e.title ?? e.message } : {}),
|
|
201
|
+
...(e.error_data?.details ? { details: e.error_data.details } : {}),
|
|
202
|
+
})),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { messages, statuses, ignored };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
type CloudMessage = NonNullable<NonNullable<NonNullable<NonNullable<CloudApiEnvelope["entry"]>[number]["changes"]>[number]["value"]>["messages"]>[number];
|
|
211
|
+
|
|
212
|
+
function replyOf(message: CloudMessage): Omit<InboundReply, "context_id"> | undefined {
|
|
213
|
+
if (message.type === "interactive") {
|
|
214
|
+
const kind = message.interactive?.type;
|
|
215
|
+
const picked = kind === "button_reply" ? message.interactive?.button_reply : kind === "list_reply" ? message.interactive?.list_reply : undefined;
|
|
216
|
+
if ((kind === "button_reply" || kind === "list_reply") && picked?.id) return { type: kind, id: picked.id, title: picked.title ?? picked.id };
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
if (message.type === "button" && message.button?.payload) return { type: "button", id: message.button.payload, title: message.button.text ?? message.button.payload };
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isDeliveryState(value: unknown): value is DeliveryState {
|
|
224
|
+
return value === "sent" || value === "delivered" || value === "read" || value === "failed";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface SendRequest {
|
|
228
|
+
url: string;
|
|
229
|
+
method: "POST";
|
|
230
|
+
headers: Record<string, string>;
|
|
231
|
+
body: string;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The `/messages` call, as Meta documents it. `to` loses its `+`: the Cloud
|
|
236
|
+
* API wants the number in international format without the plus.
|
|
237
|
+
*
|
|
238
|
+
* `media` has no branch here and that is the honest answer, not an omission.
|
|
239
|
+
* Sending an image means uploading it first (`POST /{version}/{phone-number-id}/media`,
|
|
240
|
+
* multipart) or handing Meta a public URL, and this repo hosts nothing and
|
|
241
|
+
* renders no PNG — `qrcode-terminal` draws characters. The channel therefore
|
|
242
|
+
* sends the copy-and-paste as its own message, which is the string that
|
|
243
|
+
* actually pays, and the QR is the simulator's. Named in the README and in
|
|
244
|
+
* `docs/OPEN_QUESTIONS.md`.
|
|
245
|
+
*/
|
|
246
|
+
export function buildSendRequest(config: CloudApiConfig, to: string, body: OutboundBody): SendRequest | { unsupported: string } {
|
|
247
|
+
const base = {
|
|
248
|
+
url: `${config.baseUrl.replace(/\/$/, "")}/${config.apiVersion}/${config.phoneNumberId}/messages`,
|
|
249
|
+
method: "POST" as const,
|
|
250
|
+
headers: { authorization: `Bearer ${config.accessToken}`, "content-type": "application/json" },
|
|
251
|
+
};
|
|
252
|
+
const recipient = { messaging_product: "whatsapp", recipient_type: "individual", to: toGraphNumber(to) };
|
|
253
|
+
switch (body.kind) {
|
|
254
|
+
case "text":
|
|
255
|
+
return { ...base, body: JSON.stringify({ ...recipient, type: "text", text: { preview_url: false, body: body.text } }) };
|
|
256
|
+
case "instrument":
|
|
257
|
+
return { ...base, body: JSON.stringify({ ...recipient, type: "text", text: { preview_url: false, body: body.value } }) };
|
|
258
|
+
case "template":
|
|
259
|
+
return {
|
|
260
|
+
...base,
|
|
261
|
+
body: JSON.stringify({
|
|
262
|
+
...recipient,
|
|
263
|
+
type: "template",
|
|
264
|
+
template: {
|
|
265
|
+
name: body.template,
|
|
266
|
+
language: { code: body.language },
|
|
267
|
+
...templateComponents(body),
|
|
268
|
+
},
|
|
269
|
+
}),
|
|
270
|
+
};
|
|
271
|
+
case "media":
|
|
272
|
+
return { unsupported: "media_upload_unimplemented" };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* A template's parameters as Meta takes them: the body's variables, and one
|
|
278
|
+
* `quick_reply` button component per declared reply, whose `payload` is the
|
|
279
|
+
* reply id — what comes back as `button.payload` when the person taps it.
|
|
280
|
+
*/
|
|
281
|
+
function templateComponents(body: Extract<OutboundBody, { kind: "template" }>): { components?: unknown[] } {
|
|
282
|
+
const components: unknown[] = [];
|
|
283
|
+
if (body.variables.length) components.push({ type: "body", parameters: body.variables.map((text) => ({ type: "text", text })) });
|
|
284
|
+
for (const [index, button] of (body.buttons ?? []).entries()) components.push({ type: "button", sub_type: "quick_reply", index: String(index), parameters: [{ type: "payload", payload: button.id }] });
|
|
285
|
+
return components.length ? { components } : {};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Meta's error for a free-form message outside the 24 hours after the person's last one. */
|
|
289
|
+
export const WINDOW_CLOSED_ERROR = 131047;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* What a non-2xx from `/messages` means, read from Meta's error body.
|
|
293
|
+
*
|
|
294
|
+
* One code gets its own rule: 131047 is the provider saying the 24-hour
|
|
295
|
+
* window is shut, which is the SAME rule the channel enforces before a send
|
|
296
|
+
* and is answered the same way — with a template. It reaches here only when
|
|
297
|
+
* the two clocks disagree (ours read the window open, the provider's read it
|
|
298
|
+
* shut), which at the boundary is a race and not a bug, so a caller has to be
|
|
299
|
+
* able to tell it from a refusal it cannot do anything about.
|
|
300
|
+
*/
|
|
301
|
+
export function providerRefusal(status: number, body: string): { rule: string; detail: string } {
|
|
302
|
+
let error: { code?: unknown; message?: unknown; error_data?: { details?: unknown } } | undefined;
|
|
303
|
+
try {
|
|
304
|
+
error = (JSON.parse(body) as { error?: typeof error }).error;
|
|
305
|
+
} catch {
|
|
306
|
+
/* an error without Meta's envelope: the status is all there is */
|
|
307
|
+
}
|
|
308
|
+
const details = typeof error?.error_data?.details === "string" ? error.error_data.details : undefined;
|
|
309
|
+
if (error?.code === WINDOW_CLOSED_ERROR) {
|
|
310
|
+
return {
|
|
311
|
+
rule: "session_window_closed",
|
|
312
|
+
detail: `the provider refused the free-form message (${status}/${WINDOW_CLOSED_ERROR}): its 24h window is shut${details ? ` — ${details}` : ""}`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const code = typeof error?.code === "number" ? `/${error.code}` : "";
|
|
316
|
+
return { rule: "provider_refused", detail: `${status}${code} from the Cloud API${details ? ` — ${details}` : ""}` };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* ── the impure half: one fetch over a request the above built ── */
|
|
320
|
+
|
|
321
|
+
/** `+5511987654321` -> `5511987654321`, the only form the Graph API takes. */
|
|
322
|
+
export function toGraphNumber(contact: string): string {
|
|
323
|
+
return contact.replace(/^\+/, "");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export interface CloudApiOptions {
|
|
327
|
+
config: CloudApiConfig;
|
|
328
|
+
conversation: { contact: string };
|
|
329
|
+
say: (line: string) => void;
|
|
330
|
+
/** Injected so a test can drive `deliver` without a network. The default is the platform's. */
|
|
331
|
+
fetchImpl?: typeof fetch;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export class WhatsAppCloudApi implements ChannelBackend {
|
|
335
|
+
readonly name: string;
|
|
336
|
+
/** True only against Meta. The emulator is a stand-in and says so. */
|
|
337
|
+
readonly live: boolean;
|
|
338
|
+
private server: Server | undefined;
|
|
339
|
+
private readonly queue: InboundMessage[] = [];
|
|
340
|
+
/** Message ids already handed on. Meta delivers at-least-once, so the same message can arrive twice. */
|
|
341
|
+
private readonly seen = new Set<string>();
|
|
342
|
+
private waiting: ((m: InboundMessage | undefined) => void) | undefined;
|
|
343
|
+
private statusListener: ((status: StatusUpdate) => void) | undefined;
|
|
344
|
+
private closed = false;
|
|
345
|
+
|
|
346
|
+
constructor(private readonly options: CloudApiOptions) {
|
|
347
|
+
this.live = isMetaBaseUrl(options.config.baseUrl);
|
|
348
|
+
this.name = this.live ? "cloud-api" : "emulator";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** The port the receiver is listening on, which the emulator has to be told about. */
|
|
352
|
+
get webhookPort(): number {
|
|
353
|
+
return this.options.config.webhookPort;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async open(): Promise<void> {
|
|
357
|
+
const { config, say } = this.options;
|
|
358
|
+
this.server = createServer((req, res) => {
|
|
359
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${config.webhookPort}`);
|
|
360
|
+
if (req.method === "GET") {
|
|
361
|
+
const query = Object.fromEntries(url.searchParams.entries());
|
|
362
|
+
const answer = verifyChallenge(config, query);
|
|
363
|
+
res.writeHead(answer.status, { "content-type": "text/plain" }).end(answer.body);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (req.method !== "POST") {
|
|
367
|
+
res.writeHead(405).end();
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const chunks: Buffer[] = [];
|
|
371
|
+
req.on("data", (c: Buffer) => chunks.push(c));
|
|
372
|
+
req.on("end", () => {
|
|
373
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
374
|
+
const header = req.headers["x-hub-signature-256"];
|
|
375
|
+
const check = verifyWebhookSignature(config.appSecret, Array.isArray(header) ? header[0] : header, raw);
|
|
376
|
+
if (!check.ok) {
|
|
377
|
+
say(`[whatsapp] delivery refused: ${check.reason}`);
|
|
378
|
+
res.writeHead(401, { "content-type": "application/json" }).end(JSON.stringify({ error: "bad_signature" }));
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
let parsed: ReturnType<typeof parseInbound>;
|
|
382
|
+
try {
|
|
383
|
+
parsed = parseInbound(JSON.parse(raw));
|
|
384
|
+
} catch {
|
|
385
|
+
res.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify({ error: "invalid_json" }));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
for (const ignored of parsed.ignored) say(`[whatsapp] ignored ${ignored.type} message ${ignored.id}: this channel reads text and taps`);
|
|
389
|
+
for (const message of parsed.messages) this.push(message);
|
|
390
|
+
for (const status of parsed.statuses) this.statusListener?.(status);
|
|
391
|
+
// Always 200 once read: Meta retries on anything else, and a duplicate is not an error.
|
|
392
|
+
res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ received: parsed.messages.length }));
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
await new Promise<void>((resolve) => this.server!.listen(config.webhookPort, resolve));
|
|
396
|
+
say(
|
|
397
|
+
this.live
|
|
398
|
+
? `[whatsapp] cloud API backend: webhook on http://127.0.0.1:${config.webhookPort}/ — expose it and register it with Meta; deliveries are verified against ${CLOUD_API_ENV.appSecret}`
|
|
399
|
+
: `[whatsapp] emulator backend: sending to ${config.baseUrl}/${config.apiVersion}, receiving signed deliveries on http://127.0.0.1:${config.webhookPort}/`,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private push(message: InboundMessage): void {
|
|
404
|
+
// A redelivery is the same message, not a second turn: answering it twice
|
|
405
|
+
// would put two replies in front of the person for one thing they said.
|
|
406
|
+
if (this.seen.has(message.id)) {
|
|
407
|
+
this.options.say(`[whatsapp] duplicate delivery of ${message.id} dropped`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
this.seen.add(message.id);
|
|
411
|
+
if (this.waiting) {
|
|
412
|
+
const resolve = this.waiting;
|
|
413
|
+
this.waiting = undefined;
|
|
414
|
+
resolve(message);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
this.queue.push(message);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
onStatus(listener: (status: StatusUpdate) => void): void {
|
|
421
|
+
this.statusListener = listener;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async next(): Promise<InboundMessage | undefined> {
|
|
425
|
+
if (this.closed) return undefined;
|
|
426
|
+
const queued = this.queue.shift();
|
|
427
|
+
if (queued) return queued;
|
|
428
|
+
return new Promise<InboundMessage | undefined>((resolve) => {
|
|
429
|
+
this.waiting = resolve;
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async deliver(to: string, body: OutboundBody): Promise<SentMessage> {
|
|
434
|
+
const request = buildSendRequest(this.options.config, to, body);
|
|
435
|
+
if ("unsupported" in request) {
|
|
436
|
+
return { id: "", state: "failed", refused: { rule: request.unsupported, detail: "this backend cannot upload media: the copy-and-paste text is what pays, and the QR image is the simulator's" } };
|
|
437
|
+
}
|
|
438
|
+
const doFetch = this.options.fetchImpl ?? fetch;
|
|
439
|
+
const response = await doFetch(request.url, { method: request.method, headers: request.headers, body: request.body });
|
|
440
|
+
const text = await response.text();
|
|
441
|
+
if (!response.ok) return { id: "", state: "failed", refused: providerRefusal(response.status, text) };
|
|
442
|
+
let id = "";
|
|
443
|
+
try {
|
|
444
|
+
id = String((JSON.parse(text) as { messages?: Array<{ id?: string }> }).messages?.[0]?.id ?? "");
|
|
445
|
+
} catch {
|
|
446
|
+
/* a 2xx with an unreadable body: the message went, the id did not come back */
|
|
447
|
+
}
|
|
448
|
+
return { id, state: "sent" };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async close(): Promise<void> {
|
|
452
|
+
this.closed = true;
|
|
453
|
+
if (this.waiting) {
|
|
454
|
+
this.waiting(undefined);
|
|
455
|
+
this.waiting = undefined;
|
|
456
|
+
}
|
|
457
|
+
await new Promise<void>((resolve) => (this.server ? this.server.close(() => resolve()) : resolve()));
|
|
458
|
+
}
|
|
459
|
+
}
|