Something went wrong. Try again.
A Social Deduction / Deception Game
Something went wrong. Try again.
5.1 kB · 124 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125// Unauthenticated public reads from any repo, by DID. Used to resolve a player's sprite and a// game's map regardless of whether that DID ever logged in through us (rosters and map authors// aren't necessarily our OAuth users). Resolve the DID's PDS endpoint, then hit its public// com.atproto.repo.getRecord. No tokens involved — these records are world-readable.
const PLC_URL = (process.env.PLC_URL ?? 'https://plc.directory').replace(/\/$/, '')
export interface AtUriParts { did: string collection: string rkey: string}
/** Parse `at://<did>/<collection>/<rkey>`; null if it isn't a well-formed record URI. */export function parseAtUri(uri: string): AtUriParts | null { const m = /^at:\/\/([^/]+)\/([^/]+)\/([^/]+)$/.exec(uri) if (!m) return null return { did: m[1]!, collection: m[2]!, rkey: m[3]! }}
interface DidDoc { service?: Array<{ id?: string; type?: string; serviceEndpoint?: unknown }>}
const pdsCache = new Map<string, string | null>()
/** Resolve a DID's atproto PDS endpoint (did:plc via the PLC directory, did:web via well-known). */export async function resolvePds(did: string): Promise<string | null> { if (pdsCache.has(did)) return pdsCache.get(did)! let endpoint: string | null = null try { let doc: DidDoc | null = null if (did.startsWith('did:plc:')) { const res = await fetch(`${PLC_URL}/${encodeURIComponent(did)}`) if (res.ok) doc = (await res.json()) as DidDoc } else if (did.startsWith('did:web:')) { const host = decodeURIComponent(did.slice('did:web:'.length)).replace(/:/g, '/') const res = await fetch(`https://${host}/.well-known/did.json`) if (res.ok) doc = (await res.json()) as DidDoc } const svc = doc?.service?.find( (s) => s.id === '#atproto_pds' || s.type === 'AtprotoPersonalDataServer', ) if (typeof svc?.serviceEndpoint === 'string') endpoint = svc.serviceEndpoint } catch { /* fall through to null */ } pdsCache.set(did, endpoint) return endpoint}
/** Fetch a record's value via the repo's PDS, or null if absent/unreachable. */export async function getRecord( did: string, collection: string, rkey: string,): Promise<Record<string, unknown> | null> { const pds = await resolvePds(did) if (!pds) return null const url = `${pds.replace(/\/$/, '')}/xrpc/com.atproto.repo.getRecord` + `?repo=${encodeURIComponent(did)}&collection=${encodeURIComponent(collection)}&rkey=${encodeURIComponent(rkey)}` try { const res = await fetch(url) if (!res.ok) return null const data = (await res.json()) as { value?: Record<string, unknown> } return data.value ?? null } catch { return null }}
// --- sprite resolution ------------------------------------------------------
const SPRITE_COLLECTION = 'actor.rpg.sprite'const spriteCache = new Map<string, string | null>()
/** * Resolve a player's self-keyed sprite. Returns the sprite record's AT-URI when present (the client * fetches the sprite data itself from that URI), or null. Server-authoritative: the spriteRef is no * longer a client-supplied hint. Cached per DID for the process lifetime. */export async function resolveSprite(did: string): Promise<string | null> { if (spriteCache.has(did)) return spriteCache.get(did)! let ref: string | null = null const rec = await getRecord(did, SPRITE_COLLECTION, 'self') if (rec) ref = `at://${did}/${SPRITE_COLLECTION}/self` spriteCache.set(did, ref) return ref}
/** * Uncached check for an `actor.rpg.sprite/self` record. The login flow uses this (not resolveSprite) * to decide whether to request the sprite-write scope — it must reflect the repo's current state, not * a value the process cached on an earlier visit. */export async function spriteRecordExists(did: string): Promise<boolean> { return (await getRecord(did, SPRITE_COLLECTION, 'self')) != null}
/** Drop any cached sprite resolution for a DID — call after writing/replacing its sprite record so the * next resolveSprite (e.g. the lobby join path) re-reads the repo rather than the stale null. */export function invalidateSprite(did: string): void { spriteCache.delete(did)}
/** The source DID's self-keyed sprite record value, or null if absent. Used to clone the default actor. */export function getSpriteRecord(did: string): Promise<Record<string, unknown> | null> { return getRecord(did, SPRITE_COLLECTION, 'self')}
/** Fetch a repo's blob bytes (com.atproto.sync.getBlob) as a Uint8Array, resolving its PDS first. */export async function fetchBlob(did: string, cid: string): Promise<Uint8Array> { const pds = await resolvePds(did) if (!pds) throw new Error(`could not resolve PDS for ${did}`) const url = `${pds.replace(/\/$/, '')}/xrpc/com.atproto.sync.getBlob` + `?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cid)}` const res = await fetch(url) if (!res.ok) throw new Error(`getBlob failed (${res.status}) for ${cid}`) return new Uint8Array(await res.arrayBuffer())}
export const SPRITE_NSID = SPRITE_COLLECTION