Every paid download logged "filing into filebrowser/Music/... failed
(non-fatal): Permission denied". The purchase played in-app but never
appeared in Files. FileBrowser's folders belong to its rootless container
range (host uid 100000, mode 755). This service is host uid 1000, outside
that range, so it can read them but not create files in them.
New container::filebrowser::save_new_file:
- Writes directly when the folder allows it.
- Otherwise writes through `podman unshare`, where that uid range is
ours: to a temp file, then chowned to the folder's owner, set to 0644,
and hard-linked into place. FileBrowser never sees a partial file and an
existing file is never replaced. A missing folder is created and given
its parent's owner. No sudo.
- Keeps the "name (2).ext" de-duplication the RPC did inline.
Checked the unshare script on amishparadise in a scratch folder owned
like FileBrowser's: new folder + file OK, owner/mode right, no clobber,
no temp file left, and the service can read the result.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- mint_client: a stub mint shows swap() sends the full v2 keyset id when
given a cashuB short id, and leaves complete v1/v2 ids unchanged.
- fips::dial: the single-delivery decisions are now small functions
(fips_answer_is_final, fips_retryable). Tests cover them and, against a
silent local peer, check that a single-delivery request isn't resent
after a timeout while an ordinary one still is.
- content_server: an unreadable paid file returns Unavailable before the
payment gate runs, and a readable one still returns 402. Also covers
ensure_readable's grant/reopen behaviour. The podman grant is replaced
by a refusal under cfg(test) so results don't depend on the host.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Paid cloud downloads paid with Minibits ecash were always rejected. The
buyer sends a cashuB token, which carries NUT-02 v2 keyset ids in their
8-byte short form. Minibits rotated its active keyset to a v2 id, and the
seller's verify_and_receive_payment called MintClient::swap directly,
skipping the short->full id repair that only receive_token applied. The
mint answered 422 ("ID length invalid"). The buyer then showed the
misleading "seller doesn't accept your Cashu mint" hint.
Move the repair into swap() so every caller is covered: payment verify,
streaming gate, send change, and cross-mint swaps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>