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
+7 -4
View File
@@ -734,8 +734,8 @@ async fn apply_tier_change_mutates_license_and_subscription() {
#[tokio::test]
async fn renewal_worker_applies_pending_tier_change_before_billing() {
use keysat::payment::{
CreateInvoiceParams, CreatedInvoiceHandle, PaymentProvider, ProviderInvoiceStatus,
ProviderKind, ProviderWebhookEvent,
CreateInvoiceParams, CreatedInvoiceHandle, PaymentProvider, ProviderInvoiceSnapshot,
ProviderInvoiceStatus, ProviderKind, ProviderWebhookEvent,
};
use std::any::Any;
use std::sync::atomic::{AtomicU64, Ordering};
@@ -765,8 +765,11 @@ async fn renewal_worker_applies_pending_tier_change_before_billing() {
checkout_url: format!("http://cap/{n}"),
})
}
async fn get_invoice_status(&self, _id: &str) -> anyhow::Result<ProviderInvoiceStatus> {
Ok(ProviderInvoiceStatus::Pending)
async fn get_invoice_status(&self, _id: &str) -> anyhow::Result<ProviderInvoiceSnapshot> {
Ok(ProviderInvoiceSnapshot {
status: ProviderInvoiceStatus::Pending,
amount: None,
})
}
fn validate_webhook(
&self,