mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 07:21:32 +02:00
feat(db): unified single-use challenge store [operator §0.2]
The shared server-nonce store every signed action consumes (admin login,
first-run setup, self-service erasure). DB-backed so consumption is atomic and
correct under concurrent requests and a future load-balanced fleet:
DELETE FROM challenges WHERE nonce=$1 AND purpose=$2 AND expires_at > NOW()
RETURNING subject
Winning the race returns one row; zero rows = replay/expired/wrong-purpose =>
reject (no TOCTOU). issue_challenge mints a 256-bit nonce bound to a purpose and
optional subject; consume_challenge is the atomic single-use step that pairs with
verify_signed_action (already built, §0.1). Periodic expiry sweep bounds the
table; the table holds no secret and is documented as backup-excluded
(ephemeral, links pubkey->time).
migration 20260625000001; ConsumedChallenge exported; 3 L1 tests (consume-once /
replay-rejected, wrong-purpose rejected, expired rejected).
This commit is contained in:
+15
@@ -80,6 +80,21 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
});
|
||||
}
|
||||
|
||||
// Periodic expiry sweep for the unified challenge store. The consume query
|
||||
// already ignores expired rows; this just bounds the table.
|
||||
{
|
||||
let db = state.db.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut tick = tokio::time::interval(std::time::Duration::from_secs(600));
|
||||
loop {
|
||||
tick.tick().await;
|
||||
if let Err(e) = db.delete_expired_challenges().await {
|
||||
tracing::warn!(error = %e, "challenge expiry sweep failed");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Background price refresh (only when the feed is enabled). On each tick we
|
||||
// fetch the configured feeds and replace the snapshot; a failure logs and
|
||||
// keeps the last good values rather than blanking them. The first interval
|
||||
|
||||
Reference in New Issue
Block a user