+3
-1
src/lib/oauth-client.ts
+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
+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
+
}