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.wisp.place/xrpc/com.atproto.identity.resolveHandle
11 */
12export class SlingshotHandleResolver implements HandleResolver {
13 private readonly endpoint = 'https://slingshot.wisp.place/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}