Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
1import type { HandleResolver, ResolveHandleOptions, ResolvedHandle } from '@atproto-labs/handle-resolver'; 2import type { AtprotoDid } from '@atproto/did'; 3import { logger } from './logger'; 4 5/** 6 * Custom HandleResolver that uses Slingshot's identity resolver service 7 * to work around bugs in atproto-oauth-node when handles have redirects 8 * in their well-known configuration. 9 * 10 * Uses: https://slingshot.microcosm.blue/xrpc/com.atproto.identity.resolveHandle 11 */ 12export class SlingshotHandleResolver implements HandleResolver { 13 private readonly endpoint = 'https://slingshot.microcosm.blue/xrpc/com.atproto.identity.resolveHandle'; 14 15 async resolve(handle: string, options?: ResolveHandleOptions): Promise<ResolvedHandle> { 16 try { 17 logger.debug('[SlingshotHandleResolver] Resolving handle', { handle }); 18 19 const url = new URL(this.endpoint); 20 url.searchParams.set('handle', handle); 21 22 const controller = new AbortController(); 23 const timeoutId = setTimeout(() => controller.abort(), 5000); // 5s timeout 24 25 try { 26 const response = await fetch(url.toString(), { 27 signal: options?.signal || controller.signal, 28 headers: { 29 'Accept': 'application/json', 30 }, 31 }); 32 33 clearTimeout(timeoutId); 34 35 if (!response.ok) { 36 logger.error('[SlingshotHandleResolver] Failed to resolve handle', { 37 handle, 38 status: response.status, 39 statusText: response.statusText, 40 }); 41 return null; 42 } 43 44 const data = await response.json() as { did: string }; 45 46 if (!data.did) { 47 logger.warn('[SlingshotHandleResolver] No DID in response', { handle }); 48 return null; 49 } 50 51 // Validate that it's a proper DID format 52 if (!data.did.startsWith('did:')) { 53 logger.error('[SlingshotHandleResolver] Invalid DID format', { handle, did: data.did }); 54 return null; 55 } 56 57 logger.debug('[SlingshotHandleResolver] Successfully resolved handle', { handle, did: data.did }); 58 return data.did as AtprotoDid; 59 } catch (fetchError) { 60 clearTimeout(timeoutId); 61 62 if (fetchError instanceof Error && fetchError.name === 'AbortError') { 63 logger.error('[SlingshotHandleResolver] Request aborted', { handle }); 64 throw fetchError; // Re-throw abort errors 65 } 66 67 throw fetchError; 68 } 69 } catch (error) { 70 logger.error('[SlingshotHandleResolver] Error resolving handle', error, { handle }); 71 72 // If it's an abort error, propagate it 73 if (error instanceof Error && error.name === 'AbortError') { 74 throw error; 75 } 76 77 // For other unexpected errors, return null (handle not found) 78 return null; 79 } 80 } 81}