///|
/// ══════════════════════════════════════════════════════════════════════════
/// BUG-466:challenge 的 **StoreBackend 分发**(内存真实现 + SQLite 落库)。
///
/// ## 与 BUG-439/441 的对照(这是本文件要避免的形态)
///
/// BUG-439(熔断器)与 BUG-441(saga)的病灶都是:**内存后端返回占位值**
/// (`None` / `Ok(())` / `[]`),而占位值是合法值 ⇒ 工具不报错、回执齐全、**功能从未生效**。
///
/// 这里**不重复那个错误**:内存后端**真实读写** `MemoryStore.challenges`。
/// 但内存后端的 nonce **跨不了进程**(它就在本进程内存里)——
/// **这一点由 `challenge_persisted()` 如实披露**,让调用面看得见。
///
/// ## 为什么 `persisted` 要单独一个函数
///
/// 「有没有真的落库」是**部署属性**,不是调用参数:同一个工具在内存后端跑出来的
/// `+challenge_verified` 与在 SQLite 后端跑出来的**可信度不同**(前者换进程就失效)。
/// 调用方拿到 `challenge_persisted=false` 就知道「这次验签只在本次进程生命周期内成立」。
/// ══════════════════════════════════════════════════════════════════════════
///
///|
/// 后端是否**真正持久化**了 challenge(`true` = SQLite 落库、跨进程可见;
/// `false` = 内存后端,只在本次进程生命周期内有效)。
pub fn StoreBackend::challenge_persisted(self : StoreBackend) -> Bool {
match self {
Mem(_) => false
Sql(_) => true
}
}
///|
/// 签发一张 challenge(两个后端**都真写**)。
pub fn StoreBackend::challenge_issue(
self : StoreBackend,
ns : String,
agent_fp : String,
nonce : String,
issued_at : String,
) -> Result[Unit, String] {
match self {
Mem(m) => {
// 真写内存 Map(**不是占位**)。幂等:已存在则不覆盖 issued_at。
let k = challenge_key(ns, agent_fp, nonce)
if m.challenges.contains(k) {
Ok(())
} else {
m.challenges.set(k, (issued_at, false))
Ok(())
}
}
Sql(s) => s.challenge_issue(ns, agent_fp, nonce, issued_at)
}
}
///|
/// **原子一次性消费**(两个后端同语义:返回 `true` ⇔ 本次调用把它从「未消费」变成「已消费」)。
pub fn StoreBackend::challenge_consume(
self : StoreBackend,
ns : String,
agent_fp : String,
nonce : String,
) -> Bool {
match self {
Mem(m) => {
let k = challenge_key(ns, agent_fp, nonce)
match m.challenges.get(k) {
None => false
Some((issued_at, consumed)) =>
if consumed {
false
} else {
m.challenges.set(k, (issued_at, true))
true
}
}
}
Sql(s) => s.challenge_consume(ns, agent_fp, nonce)
}
}
///|
/// 清理过期/已消费的 challenge,返回删除行数。
pub fn StoreBackend::challenge_cleanup(
self : StoreBackend,
cutoff : String,
) -> Int32 {
match self {
Mem(m) => {
let keys : Array[String] = []
m.challenges.each(fn(k, v) {
let (issued_at, consumed) = v
if consumed || issued_at < cutoff {
keys.push(k)
}
})
let mut n : Int32 = 0
for k in keys {
m.challenges.remove(k)
n = n + 1
}
n
}
Sql(s) => s.challenge_cleanup(cutoff)
}
}
///|
/// BUG-467:**保守档清理(默认推荐)** —— 只删「签发早于 cutoff」的行,**不管消费与否**。
///
/// 与激进档 `challenge_cleanup` 的差别:这一档**保留 cutoff 之内的审计证据**
/// (`consumed=1` 是「这张票被用过」的唯一记录)。表仍受「签发时间上界」约束不会无限长。
pub fn StoreBackend::challenge_cleanup_conservative(
self : StoreBackend,
cutoff : String,
) -> Int32 {
match self {
Mem(m) => {
let keys : Array[String] = []
m.challenges.each(fn(k, v) {
let (issued_at, _consumed) = v
if issued_at < cutoff {
keys.push(k)
}
})
let mut n : Int32 = 0
for k in keys {
m.challenges.remove(k)
n = n + 1
}
n
}
Sql(s) => s.challenge_cleanup_conservative(cutoff)
}
}
///|
/// BUG-467:**激进档清理** —— 额外删已消费的行(**审计证据一并丢失**)。
pub fn StoreBackend::challenge_cleanup_aggressive(
self : StoreBackend,
cutoff : String,
) -> Int32 {
match self {
Mem(m) => {
let keys : Array[String] = []
m.challenges.each(fn(k, v) {
let (issued_at, consumed) = v
if consumed || issued_at < cutoff {
keys.push(k)
}
})
let mut n : Int32 = 0
for k in keys {
m.challenges.remove(k)
n = n + 1
}
n
}
Sql(s) => s.challenge_cleanup_aggressive(cutoff)
}
}
///|
/// BUG-467:表**总行数**(让「表在长」可观测)。
pub fn StoreBackend::challenge_total_count(self : StoreBackend) -> Int32 {
match self {
Mem(m) => {
let mut n : Int32 = 0
m.challenges.each(fn(_k, _v) { n = n + 1 })
n
}
Sql(s) => s.challenge_total_count()
}
}
///|
/// BUG-467:**已消费**的票数。
pub fn StoreBackend::challenge_consumed_count(self : StoreBackend) -> Int32 {
match self {
Mem(m) => {
let mut n : Int32 = 0
m.challenges.each(fn(_k, v) {
let (_issued, consumed) = v
if consumed {
n = n + 1
}
})
n
}
Sql(s) => s.challenge_consumed_count()
}
}
///|
/// 只读诊断:某 agent 在 ns 下**未消费**的票数。
pub fn StoreBackend::challenge_pending_count(
self : StoreBackend,
ns : String,
agent_fp : String,
) -> Int32 {
match self {
Mem(m) => {
let mut n : Int32 = 0
m.challenges.each(fn(k, v) {
let (_issued, consumed) = v
let prefix = challenge_prefix(ns, agent_fp)
if !consumed && k.has_prefix(prefix) {
n = n + 1
}
})
n
}
Sql(s) => s.challenge_pending_count(ns, agent_fp)
}
}
///|
/// `challenge_key` 的前缀(用于内存后端按 (ns, agent_fp) 过滤)。
fn challenge_prefix(ns : String, agent_fp : String) -> String {
ns + "\u{0}" + agent_fp + "\u{0}"
}