Wire scoped API keys and add advisory settle-amount tripwire

Scoped API keys (P1): migrate 58 admin endpoints from require_admin to
require_scope so ks_ keys with Read-only/License-issuer/Support/Full-admin roles
work as intended. 12 sensitive endpoints stay master-key-only (issuer key,
provider connect/disconnect, web password, api-key CRUD, db-info, operator-name,
per-license tier change). require_scope is re-exported from api::admin so both
auth gates import from one place. Adds role-boundary tests.

Settle-amount tripwire (P1): get_invoice_status now returns
ProviderInvoiceSnapshot { status, amount }. On a confirmed settle,
audit_settle_amount (shared by the webhook and reconcile issue paths) compares
the provider-reported sat amount against the invoice's amount_sats and, on drift,
logs a warning + writes an invoice.amount_mismatch audit row, then issues anyway.
Advisory by design: a hard gate would fight an operator's BTCPay payment
tolerance, and Settled already implies paid-in-full. SAT-only — skips non-SAT
settles (fiat subscription renewals) and unparseable amounts.
This commit is contained in:
Keysat
2026-06-13 00:10:45 -05:00
committed by Grant
parent 495fbbf351
commit 0508690d5a
23 changed files with 652 additions and 115 deletions
+20 -2
View File
@@ -18,7 +18,8 @@
//!
//! - `kind()` — provider identity, for logs / audit / admin UI
//! - `create_invoice` — make a hosted-checkout session, return a URL
//! - `get_invoice_status` — for the reconcile loop (webhook misses)
//! - `get_invoice_status` — authoritative status + amount, for the reconcile
//! loop (webhook misses) and the webhook settle-confirmation gate
//! - `validate_webhook` — provider-specific signature scheme + parse
//! - `pay_lightning_invoice` — for the tip-recipient flow; default impl
//! returns a "not supported" error so providers without a Lightning
@@ -280,6 +281,23 @@ pub enum ProviderInvoiceStatus {
Invalid,
}
/// The provider's current view of an invoice: its `status` plus the amount
/// the provider has the invoice denominated for. Returned by
/// `PaymentProvider::get_invoice_status`.
///
/// `amount` is the price the provider has on record for the invoice (what we
/// asked it to charge), normalized to `SAT` when the provider used a Bitcoin
/// unit. It is `None` when the response carried no parseable amount/currency.
/// `status` is the load-bearing settle gate; `amount` feeds only the
/// **advisory** settle-amount tripwire in `api::webhook` / `reconcile` —
/// callers treat `None` as "no opinion" and MUST NOT gate issuance on it.
/// See docs/guides/payments.md.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderInvoiceSnapshot {
pub status: ProviderInvoiceStatus,
pub amount: Option<Money>,
}
/// Parsed webhook event. Only the kinds Keysat actually acts on are
/// modeled; everything else falls into `Other` and is ignored.
#[derive(Debug, Clone)]
@@ -355,7 +373,7 @@ pub trait PaymentProvider: Send + Sync + Any {
async fn get_invoice_status(
&self,
provider_invoice_id: &str,
) -> Result<ProviderInvoiceStatus>;
) -> Result<ProviderInvoiceSnapshot>;
/// Verify and parse a webhook delivery. Implementations are
/// responsible for reading whatever signature header their provider