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:
@@ -98,11 +98,21 @@ describe('adaptContentItems', () => {
|
||||
expect(bundle.images[0]!.url).toBe('/content/img-1')
|
||||
})
|
||||
|
||||
it('locks a paid image: price carried, no URL to fetch bytes the user has not bought', () => {
|
||||
it('never locks an OWN paid image: the node serves the authenticated owner (owner-bypass), price stays as a badge', () => {
|
||||
const bundle = adaptContentItems(
|
||||
[item({ id: 'p-1', filename: 'photo.jpg', mime_type: 'image/jpeg', access: { paid: { price_sats: 100 } } })],
|
||||
{ source: 'own' },
|
||||
)
|
||||
expect(bundle.images[0]!.locked).toBe(false)
|
||||
expect(bundle.images[0]!.priceSats).toBe(100)
|
||||
expect(bundle.images[0]!.url).toBe('/content/p-1')
|
||||
})
|
||||
|
||||
it('locks a PEER paid image: price carried, no URL to fetch bytes the user has not bought', () => {
|
||||
const bundle = adaptContentItems(
|
||||
[item({ id: 'p-1', filename: 'photo.jpg', mime_type: 'image/jpeg', access: { paid: { price_sats: 100 } } })],
|
||||
{ source: 'peer', peerOnion: 'seller.onion' },
|
||||
)
|
||||
expect(bundle.images[0]!.locked).toBe(true)
|
||||
expect(bundle.images[0]!.priceSats).toBe(100)
|
||||
expect(bundle.images[0]!.url).toBe('')
|
||||
|
||||
@@ -314,7 +314,12 @@ function buildMediaUrl(item: ArchyContentItem, opts: AdaptContentOptions): strin
|
||||
|
||||
export function adaptToFilm(item: ArchyContentItem, opts: AdaptContentOptions): Film {
|
||||
const priceSats = paidPriceSats(item.access)
|
||||
const locked = priceSats !== null
|
||||
// 'own' items are served to the authenticated owner by the node's
|
||||
// owner-bypass (`serve_content`) even when they're listed paid for
|
||||
// buyers — the operator never pays for their own files, so never lock
|
||||
// them (a locked card suppresses the playable URL, which is exactly the
|
||||
// placeholder-only grid the operator reported).
|
||||
const locked = opts.source !== 'own' && priceSats !== null
|
||||
const sourceType = FILM_SOURCE_TYPE[opts.source]
|
||||
return {
|
||||
id: item.id,
|
||||
@@ -348,7 +353,12 @@ export function adaptToFilm(item: ArchyContentItem, opts: AdaptContentOptions):
|
||||
*/
|
||||
export function adaptToImage(item: ArchyContentItem, opts: AdaptContentOptions): ImageItem {
|
||||
const priceSats = paidPriceSats(item.access)
|
||||
const locked = priceSats !== null
|
||||
// 'own' items are served to the authenticated owner by the node's
|
||||
// owner-bypass (`serve_content`) even when they're listed paid for
|
||||
// buyers — the operator never pays for their own files, so never lock
|
||||
// them (a locked card suppresses the playable URL, which is exactly the
|
||||
// placeholder-only grid the operator reported).
|
||||
const locked = opts.source !== 'own' && priceSats !== null
|
||||
const title = stripExtension(item.filename || '')
|
||||
return {
|
||||
id: item.id,
|
||||
@@ -366,7 +376,12 @@ export function adaptToImage(item: ArchyContentItem, opts: AdaptContentOptions):
|
||||
|
||||
export function adaptToSong(item: ArchyContentItem, opts: AdaptContentOptions): Song {
|
||||
const priceSats = paidPriceSats(item.access)
|
||||
const locked = priceSats !== null
|
||||
// 'own' items are served to the authenticated owner by the node's
|
||||
// owner-bypass (`serve_content`) even when they're listed paid for
|
||||
// buyers — the operator never pays for their own files, so never lock
|
||||
// them (a locked card suppresses the playable URL, which is exactly the
|
||||
// placeholder-only grid the operator reported).
|
||||
const locked = opts.source !== 'own' && priceSats !== null
|
||||
const sourceType = SONG_SOURCE_TYPE[opts.source]
|
||||
return {
|
||||
id: item.id,
|
||||
@@ -391,7 +406,12 @@ export function adaptToSong(item: ArchyContentItem, opts: AdaptContentOptions):
|
||||
* rather than a song. */
|
||||
export function adaptToPodcast(item: ArchyContentItem, opts: AdaptContentOptions): Podcast {
|
||||
const priceSats = paidPriceSats(item.access)
|
||||
const locked = priceSats !== null
|
||||
// 'own' items are served to the authenticated owner by the node's
|
||||
// owner-bypass (`serve_content`) even when they're listed paid for
|
||||
// buyers — the operator never pays for their own files, so never lock
|
||||
// them (a locked card suppresses the playable URL, which is exactly the
|
||||
// placeholder-only grid the operator reported).
|
||||
const locked = opts.source !== 'own' && priceSats !== null
|
||||
const sourceType = PODCAST_SOURCE_TYPE[opts.source]
|
||||
return {
|
||||
id: item.id,
|
||||
|
||||
Reference in New Issue
Block a user