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:
@@ -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 0020–0022 are **one-way**. 0020 ports the singleton
|
||||
|
||||
Reference in New Issue
Block a user