Something went wrong. Try again.
playlist generator
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135import { PLAYLIST_COLLECTION, isPlaylistRecord, toPlaylist } from './playlistSync';import type { Playlist } from './playlists';
const DEFAULT_PLC_DIRECTORY = 'https://plc.directory';const PLC_PREFIX = 'did:plc:';const WEB_PREFIX = 'did:web:';
/** * Maps a DID to the URL of its DID document — the did:plc/did:web rules from * `packages/gateway/src/did-resolver.ts` `didDocumentUrl()` in * `/home/dev/workspace/szzt` (read for reference, not imported: that package * is a server verifying signatures on someone else's behalf and SSRF-guards * the URL before fetching it; this runs in the visitor's own browser fetching * on its own behalf, and needs none of the signing-key or PLC-audit-log * machinery that package also carries). */function didDocumentUrl(did: string, plcDirectory: string = DEFAULT_PLC_DIRECTORY): string { if (did.startsWith(PLC_PREFIX)) { if (did.length <= PLC_PREFIX.length) throw new Error('empty did:plc id'); return `${plcDirectory.replace(/\/$/, '')}/${did}`; } if (did.startsWith(WEB_PREFIX)) { const msi = did.slice(WEB_PREFIX.length); if (msi === '') throw new Error('empty did:web id'); const hasPath = msi.includes(':'); if (msi.startsWith(':') || msi.endsWith(':') || msi.includes('::')) { throw new Error('empty did:web segment'); } const decoded = decodeURIComponent(msi.replace(/:/g, '/')); return `https://${decoded}${hasPath ? '/did.json' : '/.well-known/did.json'}`; } throw new Error(`unsupported DID method: ${did}`);}
/** * Extracts the atproto PDS base URL from a DID document — mirrors * `packages/gateway/src/pds-resolver.ts` `extractAtprotoPds()` in the same * reference project. */function extractPds(doc: unknown, did: string): string { if (typeof doc !== 'object' || doc === null || Array.isArray(doc)) { throw new Error('DID document is not an object'); } const document = doc as { id?: unknown; service?: unknown }; if (typeof document.id !== 'string' || document.id !== did) { throw new Error("DID document id does not match the requested DID"); } const services = document.service; if (!Array.isArray(services)) throw new Error('DID document has no service entries'); for (const entry of services) { if (typeof entry !== 'object' || entry === null) continue; const service = entry as { id?: unknown; type?: unknown; serviceEndpoint?: unknown }; const isPds = service.type === 'AtprotoPersonalDataServer' || (typeof service.id === 'string' && service.id.endsWith('#atproto_pds')); if (!isPds) continue; if (typeof service.serviceEndpoint !== 'string' || service.serviceEndpoint === '') { throw new Error('atproto_pds service entry has no serviceEndpoint'); } return service.serviceEndpoint; } throw new Error('no #atproto_pds service entry');}
async function fetchJson(url: string): Promise<unknown> { const res = await fetch(url); if (!res.ok) throw new Error(`fetch failed: ${res.status} ${url}`); return res.json();}
/** * What opening a share link found, distinguishing the four ways it can fail * to show a playlist — an identifier that doesn't resolve, a record that * isn't there, a record that is a tombstone, and a record whose shape * doesn't match — from the one way it can succeed. */export type ShareResolution = | { status: 'ok'; playlist: Playlist } | { status: 'not-found' } | { status: 'deleted' } | { status: 'invalid' } | { status: 'unresolvable'; message: string };
/** * Resolves a share link's (author DID, playlist id) to the playlist record it * points at. Needs no sign-in and no atproto package: `com.atproto.repo. * getRecord` against a public repo takes no auth, so identity resolution plus * one record fetch is plain `fetch()` calls, the same way `src/atproto.ts` * `loadOAuthClient()` keeps `@atproto/api` and the OAuth client out of a * visitor's bundle until something actually needs them — opening a share * link and never signing in is not one of those somethings. */export async function resolveSharedPlaylist(did: string, id: string): Promise<ShareResolution> { let pds: string; try { const doc = await fetchJson(didDocumentUrl(did)); pds = extractPds(doc, did); } catch (e) { return { status: 'unresolvable', message: e instanceof Error ? e.message : String(e) }; }
const recordUrl = `${pds.replace(/\/$/, '')}/xrpc/com.atproto.repo.getRecord?${new URLSearchParams({ repo: did, collection: PLAYLIST_COLLECTION, rkey: id, })}`;
let res: Response; try { res = await fetch(recordUrl); } catch (e) { return { status: 'unresolvable', message: e instanceof Error ? e.message : String(e) }; }
let body: unknown; try { body = await res.json(); } catch { body = undefined; }
if (!res.ok) { // XRPC reports "no such record" as an application error rather than a // fixed HTTP status, so the error code is what distinguishes "not there" // from an actual resolution failure worth a different message. if ((body as { error?: unknown })?.error === 'RecordNotFound') return { status: 'not-found' }; return { status: 'unresolvable', message: `record fetch failed: ${res.status}` }; }
const value = (body as { value?: unknown } | undefined)?.value; if (!isPlaylistRecord(value)) return { status: 'invalid' }; if (value.deletedAt !== undefined) return { status: 'deleted' }; return { status: 'ok', playlist: toPlaylist(id, value) };}