A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

Add public AppView actor lookup for landing handle input

+110
+59
src/lib/landing/actor-lookup.test.ts
··· 1 + import { describe, expect, it, vi } from 'vitest'; 2 + import { lookupActor } from './actor-lookup'; 3 + 4 + const ok = ( body: unknown ) => 5 + ( { ok: true, json: async () => body } ) as unknown as Response; 6 + const notOk = () => ( { ok: false, json: async () => ( {} ) } ) as unknown as Response; 7 + 8 + describe( 'lookupActor', () => { 9 + it( 'returns a preview for a resolvable handle', async () => { 10 + const fetchImpl = vi.fn().mockResolvedValue( 11 + ok( { 12 + did: 'did:plc:abc', 13 + handle: 'alice.bsky.social', 14 + displayName: 'Alice Rivers', 15 + avatar: 'https://cdn.example/a.jpg', 16 + } ) 17 + ); 18 + const preview = await lookupActor( '@Alice.bsky.social', fetchImpl as unknown as typeof fetch ); 19 + expect( preview ).toEqual( { 20 + did: 'did:plc:abc', 21 + handle: 'alice.bsky.social', 22 + displayName: 'Alice Rivers', 23 + avatar: 'https://cdn.example/a.jpg', 24 + } ); 25 + expect( fetchImpl ).toHaveBeenCalledWith( 26 + 'https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=alice.bsky.social' 27 + ); 28 + } ); 29 + 30 + it( 'passes a DID through untouched', async () => { 31 + const fetchImpl = vi.fn().mockResolvedValue( ok( { did: 'did:plc:xyz', handle: 'x.test' } ) ); 32 + await lookupActor( 'did:plc:xyz', fetchImpl as unknown as typeof fetch ); 33 + expect( fetchImpl ).toHaveBeenCalledWith( 34 + 'https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=did%3Aplc%3Axyz' 35 + ); 36 + } ); 37 + 38 + it( 'returns null without fetching for syntactically invalid input', async () => { 39 + const fetchImpl = vi.fn(); 40 + expect( await lookupActor( 'notahandle', fetchImpl as unknown as typeof fetch ) ).toBeNull(); 41 + expect( await lookupActor( ' ', fetchImpl as unknown as typeof fetch ) ).toBeNull(); 42 + expect( fetchImpl ).not.toHaveBeenCalled(); 43 + } ); 44 + 45 + it( 'returns null on a non-ok response', async () => { 46 + const fetchImpl = vi.fn().mockResolvedValue( notOk() ); 47 + expect( await lookupActor( 'ghost.bsky.social', fetchImpl as unknown as typeof fetch ) ).toBeNull(); 48 + } ); 49 + 50 + it( 'returns null when the network throws', async () => { 51 + const fetchImpl = vi.fn().mockRejectedValue( new Error( 'offline' ) ); 52 + expect( await lookupActor( 'alice.bsky.social', fetchImpl as unknown as typeof fetch ) ).toBeNull(); 53 + } ); 54 + 55 + it( 'returns null when the body has no did', async () => { 56 + const fetchImpl = vi.fn().mockResolvedValue( ok( { handle: 'x.test' } ) ); 57 + expect( await lookupActor( 'x.test', fetchImpl as unknown as typeof fetch ) ).toBeNull(); 58 + } ); 59 + } );
+51
src/lib/landing/actor-lookup.ts
··· 1 + /** 2 + * Live "find me on the network" lookup for the landing handle input. 3 + * 4 + * Calls the PUBLIC Bluesky AppView UNAUTHENTICATED (no OAuth, no secrets) purely to 5 + * show the writer their own avatar + name as confirmation before sign-in. This runs 6 + * client-side in a React island, the host is a fixed constant, and `actor` is the only 7 + * user input — validated by `isValidHandleOrDid` and URL-encoded — so it can't be 8 + * coerced into a different host or path. Never throws: any failure resolves to `null` 9 + * ("no preview"), because submission must never depend on this lookup succeeding. 10 + */ 11 + import { isValidHandleOrDid, normalizeHandle } from '../auth/config'; 12 + 13 + const APPVIEW = 'https://public.api.bsky.app'; 14 + 15 + export interface ActorPreview { 16 + did: string; 17 + handle: string; 18 + displayName: string | null; 19 + avatar: string | null; 20 + } 21 + 22 + export async function lookupActor( 23 + input: string, 24 + fetchImpl: typeof fetch = fetch 25 + ): Promise< ActorPreview | null > { 26 + const value = input.trim(); 27 + if ( ! isValidHandleOrDid( value ) ) { 28 + return null; 29 + } 30 + const actor = value.startsWith( 'did:' ) ? value : normalizeHandle( value ); 31 + try { 32 + const res = await fetchImpl( 33 + `${ APPVIEW }/xrpc/app.bsky.actor.getProfile?actor=${ encodeURIComponent( actor ) }` 34 + ); 35 + if ( ! res.ok ) { 36 + return null; 37 + } 38 + const data = ( await res.json() ) as Partial< ActorPreview >; 39 + if ( ! data?.did ) { 40 + return null; 41 + } 42 + return { 43 + did: data.did, 44 + handle: data.handle ?? actor, 45 + displayName: data.displayName ?? null, 46 + avatar: data.avatar ?? null, 47 + }; 48 + } catch { 49 + return null; 50 + } 51 + }