fix(content): owner never pays for their own files; purchased serves from cache

- serve_content takes owner_session: a validated operator session skips the
  availability/paid gates (Availability::Nobody stays delisted); the cookie
  is re-validated in the content handler, same discipline as the model proxy
- the Tor proxy serves already-purchased items from the local content_owned
  cache with Range slicing (206) instead of re-hitting the seller's 402 —
  the buyer-side store exists so an owned item is never bought twice, and
  its cards were rendering as permanent placeholders
- adapter: 'own'-scope items never render locked (a locked card suppresses
  the playable URL — the placeholder-only grid the operator reported)
- broker: normalize 'purchased' OwnedRpcItems per item with the seller's
  onion, and group 'peers' items per seller onion, so buildMediaUrl gets a
  peerOnion and card URLs stop coming out empty

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-07 08:40:49 -04:00
co-authored by Claude
parent f0d2cdb7b2
commit c25fd8b650
8 changed files with 230 additions and 26 deletions
@@ -47,6 +47,7 @@ impl ApiHandler {
}
pub(super) async fn handle_content_request(
&self,
path: &str,
headers: &hyper::HeaderMap,
config: &Config,
@@ -87,6 +88,16 @@ impl ApiHandler {
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
// The authenticated local operator never pays for their own node's
// content: validate the session cookie (same discipline as the model
// proxy — re-derived here, never trusted to the front door) and hand
// serve_content the owner bypass. No cookie / bad session is simply
// the buyer path, unchanged.
let owner_session = match crate::session::extract_session_cookie(headers) {
Some(token) => self.session_store.validate(&token).await,
None => false,
};
// Parse Range header for streaming support
let range = headers
.get("range")
@@ -100,6 +111,7 @@ impl ApiHandler {
invoice_hash.as_deref(),
peer_did.as_deref(),
range,
owner_session,
)
.await
{