Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place

use slingshot for handle resolution

+3 -1
src/lib/oauth-client.ts
··· 2 2 import { JoseKey } from "@atproto/jwk-jose"; 3 3 import { db } from "./db"; 4 4 import { logger } from "./logger"; 5 + import { SlingshotHandleResolver } from "./slingshot-handle-resolver"; 5 6 6 7 // Session timeout configuration (30 days in seconds) 7 8 const SESSION_TIMEOUT = 30 * 24 * 60 * 60; // 2592000 seconds ··· 244 245 clientMetadata: createClientMetadata(config), 245 246 keyset: keys, 246 247 stateStore, 247 - sessionStore 248 + sessionStore, 249 + handleResolver: new SlingshotHandleResolver() 248 250 }); 249 251 };
+81
src/lib/slingshot-handle-resolver.ts
··· 1 + import type { HandleResolver, ResolveHandleOptions, ResolvedHandle } from '@atproto-labs/handle-resolver'; 2 + import type { AtprotoDid } from '@atproto/did'; 3 + import { 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 + */ 12 + export 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 + }