Handoff: ship 0.2.0:58 agent-payment-connect; document the connect gate

Current state rewritten to :58-shipped (both onboarding stages completed-clean,
validated separately); payments guide gains the scoped (agent) BTCPay connect
sandbox-gate section (two-gate fail-closed design, migration 0025, GET-callback
status gotcha, regtest validation facts); guide index flags it for the connect
gate + migrations 0024-0025.
This commit is contained in:
Keysat
2026-06-17 10:49:36 -05:00
parent 316d4c961c
commit 47db41a238
2 changed files with 59 additions and 51 deletions
+27
View File
@@ -108,6 +108,33 @@ response carries no parseable positive amount. Regression tests in `tests/api.rs
- Zaprite is optional, gated by the `zaprite_payments` entitlement; recurring
auto-charge works via `charge_order_with_profile`.
### Scoped (agent) BTCPay connect — the sandbox gate
Shipped `:58` (spec: `plans/agent-payment-connect-scope.md`). Provider connect is
master-only EXCEPT: a scoped key carrying the à-la-carte `payment_providers:write`
scope (in NO role, granted per-key via `extra_scopes`) may connect a BTCPay provider
**iff the daemon is in sandbox mode AND the target store is non-mainnet**. Disconnect,
mainnet, and any production daemon stay master-only — a key that can repoint settlement
is a fund-redirection key. Two gates, both fail-closed:
- **Outer (`api_keys.rs::require_provider_connect`)**: replaces `require_admin` on
`start_connect`. Master → any. Scoped+`payment_providers:write` → only if
`Config.sandbox_mode` (env `KEYSAT_SANDBOX_MODE`, boot-only, never API-settable).
- **Inner (`btcpay_authorize.rs::finish_connect`)**: the initiator (master vs scoped) is
carried across the OAuth round-trip in `btcpay_authorize_state` (migration 0025:
`scoped_initiator` + `initiator_actor_hash`). For a scoped initiator, the store network
is resolved from its on-chain receive address (`btcpay/network.rs::classify_address_network`
via `client::fetch_onchain_network`) and a mainnet/undetermined result is refused **before**
any webhook/persist. Fail-closed: no on-chain wallet, unreachable BTCPay, non-JSON, or
unknown prefix all → mainnet → deny. Scoped connects write an audit row.
Validated on a live regtest BTCPay (`onboarding-harness/stage2/`): the on-chain pmid is
`BTC-CHAIN` (BTCPay 2.x; legacy `BTC` is normalized to it — don't hardcode), the
`wallet/address` endpoint needs `btcpay.store.canmodifystoresettings` (which the daemon's
OAuth already requests), and a regtest address is `bcrt1…`. Gotcha: both callback forms
(GET + POST) must return the error's real HTTP status on a denied connect — the GET handler
uses `AppError::status_code()` so a refusal is a 4xx, not a misleading 200.
## Migrations & gotchas
- Migrations 00200022 are **one-way**. 0020 ports the singleton