fix(content): never charge for a file the seller can't serve or replay a spent token
After the keyset-id fix, a Minibits paid download still failed and the buyer lost the sats. What happened, 2026-09-29, amishparadise: 1. The seller redeemed the token, then failed to read the file. It was a FileBrowser upload owned by the container subuid (100999) with mode 0640. The handler mapped that Err to 404. 2. The buyer's FIPS dial treats 404 as "fall back to Tor" and resent the request with the same, now spent, token. The seller answered 402, and the buyer showed "seller doesn't accept your Cashu mint". Fixes: - serve_content checks the file is readable before the paid gate. If it isn't, it grants read with `podman unshare chmod a+r`, which matches the other shared files. If that also fails it returns Unavailable (503) without taking payment. - The content handler returns 500 on internal errors and logs them, instead of a silent 404. - New PeerRequest::single_delivery(), used for the paid download: the FIPS answer is final, FIPS retries only when it never connected, and there's no Tor replay once the request may have been delivered. - The buyer shows the seller's error text for non-402 failures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -555,6 +555,9 @@ impl RpcHandler {
|
||||
.service(crate::settings::transport::PeerService::PeerFiles)
|
||||
.header("X-Federation-DID", local_did)
|
||||
.header("X-Payment-Token", token_str.clone())
|
||||
// The token is a bearer instrument the seller redeems on first sight:
|
||||
// a Tor replay after FIPS delivered it can only arrive spent.
|
||||
.single_delivery()
|
||||
.timeout(std::time::Duration::from_secs(900))
|
||||
.send_get()
|
||||
.await
|
||||
@@ -610,8 +613,12 @@ impl RpcHandler {
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
tracing::warn!("paid download: seller {onion} returned {status}: {body}");
|
||||
reclaim_spent_ecash(&self.config.data_dir, &token_str, used_backend).await;
|
||||
let reason = serde_json::from_str::<serde_json::Value>(&body)
|
||||
.ok()
|
||||
.and_then(|v| v.get("error").and_then(|e| e.as_str()).map(str::to_string))
|
||||
.unwrap_or_else(|| format!("Peer returned an error ({status})."));
|
||||
return Ok(serde_json::json!({
|
||||
"error": format!("Peer returned an error ({status}). Your ecash was refunded to your wallet.")
|
||||
"error": format!("{reason} Your ecash was refunded to your wallet.")
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user