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
+12 -2
View File
@@ -83,9 +83,9 @@ async fn tick(state: &AppState) -> anyhow::Result<()> {
},
};
match provider.get_invoice_status(&inv.btcpay_invoice_id).await {
Ok(status) => {
Ok(snapshot) => {
use crate::payment::ProviderInvoiceStatus::*;
let new_status = match status {
let new_status = match snapshot.status {
Settled => "settled",
Expired => "expired",
Invalid => "invalid",
@@ -124,6 +124,16 @@ async fn tick(state: &AppState) -> anyhow::Result<()> {
}
if new_status == "settled" {
// Same advisory amount tripwire the webhook path applies
// (see crate::api::webhook::audit_settle_amount). Never
// blocks issuance — logs + audits any amount/currency
// drift from what we charged.
crate::api::webhook::audit_settle_amount(
state,
&inv,
snapshot.amount.as_ref(),
)
.await;
if let Err(e) = ensure_license(state, &inv).await {
tracing::warn!(
error = %e,