A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1/**
2 * Read a writer's `app.bsky.actor.profile` straight from their PDS (SP10, step G).
3 *
4 * The public author index borrows the writer's Bluesky identity (name, bio, avatar, cover) to
5 * decorate the page. Per Decision 0010 this comes from the PDS record directly — NOT the
6 * Bluesky appview — so the page has no third-party-service dependency. `getRecord` already
7 * guards the (DID-doc-derived) PDS host against SSRF and degrades to null on failure.
8 *
9 * `displayName` is plain text. `description` is plain text in the record, but the author
10 * page linkifies it at render via `detectBioSegments` (rich-text.ts), mirroring Bluesky.
11 * Neither is ever injected as raw HTML. (Decision 0015.)
12 */
13import { getRecord } from './records';
14import { buildGetBlobUrl, type BlobRefJson } from '../media/blob';
15
16export interface ActorProfile {
17 displayName: string | null;
18 description: string | null;
19 avatar: string | null;
20 banner: string | null;
21}
22
23interface RawActorProfile {
24 displayName?: string;
25 description?: string;
26 avatar?: BlobRefJson;
27 banner?: BlobRefJson;
28}
29
30function blobUrl( pdsUrl: string, did: string, blob?: BlobRefJson ): string | null {
31 const cid = blob?.ref?.$link;
32 return cid ? buildGetBlobUrl( pdsUrl, did, cid ) : null;
33}
34
35export async function fetchActorProfile( pdsUrl: string, did: string ): Promise< ActorProfile > {
36 const record = await getRecord< RawActorProfile >(
37 pdsUrl,
38 did,
39 'app.bsky.actor.profile',
40 'self'
41 );
42 const value = record?.value ?? {};
43 return {
44 displayName: value.displayName?.trim() || null,
45 description: value.description?.trim() || null,
46 avatar: blobUrl( pdsUrl, did, value.avatar ),
47 banner: blobUrl( pdsUrl, did, value.banner ),
48 };
49}