forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {type Did, isDid} from '@atproto/api'
2import {useQuery} from '@tanstack/react-query'
3
4import {getDidDocumentUrl} from '#/lib/atproto/did'
5import {readPlcDirectory} from '#/state/preferences/plc-directory'
6import {createPublicAgent} from '#/state/session/agent'
7import {STALE} from '.'
8import {LRU} from './direct-fetch-record'
9const RQKEY_ROOT = 'resolve-identity'
10export const RQKEY = (did: string) => [RQKEY_ROOT, did]
11
12// this isn't trusted...
13export type DidDocument = {
14 '@context'?: string[]
15 id?: string
16 alsoKnownAs?: string[]
17 verificationMethod?: VerificationMethod[]
18 service?: Service[]
19}
20
21export type VerificationMethod = {
22 id?: string
23 type?: string
24 controller?: string
25 publicKeyMultibase?: string
26}
27
28export type Service = {
29 id?: string
30 type?: string
31 serviceEndpoint?: string
32}
33
34const serviceCache = new LRU<string, DidDocument>()
35
36async function resolveDidDocumentUsingAppView(did: Did) {
37 const agent = createPublicAgent()
38 try {
39 const res = await agent.com.atproto.identity.resolveDid({did})
40 return res.data.didDoc as DidDocument
41 } finally {
42 agent.dispose()
43 }
44}
45
46export async function resolveDidDocument(did: Did) {
47 const plcDirectory = readPlcDirectory()
48 const cacheKey = did.startsWith('did:plc:') ? `${plcDirectory}|${did}` : did
49
50 return await serviceCache.getOrTryInsertWith(cacheKey, async () => {
51 const docUrl = getDidDocumentUrl(did, plcDirectory)
52 if (!docUrl) {
53 throw new Error(`Unsupported DID method for ${did}`)
54 }
55
56 try {
57 const res = await fetch(docUrl, {
58 headers: {
59 accept: 'application/did+ld+json, application/json',
60 },
61 })
62 if (!res.ok) {
63 throw new Error(`Failed to resolve DID document for ${did}`)
64 }
65
66 return (await res.json()) as DidDocument
67 } catch (err) {
68 if (!did.startsWith('did:web:')) {
69 throw err
70 }
71 return await resolveDidDocumentUsingAppView(did)
72 }
73 })
74}
75
76export function findService(doc: DidDocument, id: string, type?: string) {
77 // probably not defensive enough, but we don't have atproto/did as a dep...
78 if (!Array.isArray(doc?.service)) return
79 return doc.service.find(
80 s => s?.serviceEndpoint && s?.id === id && (!type || s?.type === type),
81 )
82}
83
84export async function resolvePdsServiceUrl(did: Did) {
85 const doc = await resolveDidDocument(did)
86 return findService(doc, '#atproto_pds', 'AtprotoPersonalDataServer')
87 ?.serviceEndpoint
88}
89
90export function useDidDocument({did}: {did: string}) {
91 return useQuery<DidDocument | undefined>({
92 staleTime: STALE.HOURS.ONE,
93 queryKey: RQKEY(did || ''),
94 async queryFn() {
95 if (!isDid(did)) return undefined
96 return await resolveDidDocument(did)
97 },
98 enabled: isDid(did) && !(did.includes('#') || did.includes('?')),
99 })
100}