iso-did
Version:
Isomorphic did core and did key tooling
247 lines (216 loc) • 5.67 kB
JavaScript
import { resolve as doh } from 'iso-web/doh'
import { request } from 'iso-web/http'
import pRace from 'p-race'
import { parse } from '../src/index.js'
import { DIRECTORY, isPlc, resolve as resolvePlc } from './plc.js'
import { didWebResolver } from './web.js'
const SUBDOMAIN = '_atproto'
/**
* @param {Object} options
* @param {import('did-resolver').ParsedDID} options.parsed
* @param {AbortSignal} [options.signal]
*/
export async function resolveDNS(options) {
const records = await doh(`${SUBDOMAIN}.${options.parsed.id}`, 'TXT', {
signal: options.signal,
})
if (records.error) {
throw records.error
}
const dids = new Set()
for (const record of records.result) {
if (!record.startsWith('did=')) {
continue
}
const did = record.slice(4)
if (isAtpDid(did)) {
dids.add(did)
}
}
if (dids.size === 0) {
return undefined
}
if (dids.size > 1) {
throw new Error('Multiple ATP DIDs found')
}
return Array.from(dids)[0]
}
/**
* @param {Object} options
* @param {import('did-resolver').ParsedDID} options.parsed
* @param {AbortSignal} [options.signal]
*/
export async function resolveHttp(options) {
const url = `https://${options.parsed.id}/.well-known/atproto-did`
const rsp = await request.get(url, {
signal: options.signal,
})
if (rsp.error) {
throw rsp.error
}
const did = (await rsp.result.text()).trim()
if (isAtpDid(did)) {
return did
}
return undefined
}
/**
* @param {Object} options
* @param {import('did-resolver').ParsedDID} options.parsed
*/
export async function resolvePlcDid(options) {
/** @type {string | undefined} */
const r = await pRace((signal) => [
resolveDNS({
...options,
signal,
}),
resolveHttp({
...options,
signal,
}),
])
return r
}
/**
* Resolves a did:atp DID, attempting resolution via DNS, well-known, and then falling back to did:plc or did:web.
*
* @param {object} options - Options for resolving the DID.
* @param {string} options.directory - The directory to use for resolving did:plc DIDs.
* @param {import('did-resolver').ParsedDID} options.parsed - The parsed DID object.
* @param {import('did-resolver').DIDResolutionOptions} options.options - Options for the DID resolver.
* @returns {Promise<import('did-resolver').DIDResolutionResult>} - A promise that resolves to the DID resolution result.
*
* @example
* ```ts twoslash
* // @filename: index.ts
* import { resolve } from 'iso-did/atp'
* import { parse } from 'iso-did'
*
* async function example() {
* const didResolutionResult = await resolve({
* directory: 'https://plc.directory',
* parsed: parse('did:atp:retr0.id'),
* options: {}
* })
*
* console.log(didResolutionResult.didDocument?.id)
* // => did:plc:vwzwgnygau7ed7b7wt5ux7y2
* }
* ```
*/
export async function resolve(options) {
try {
// resolve did in dns or well-known
const did = await resolvePlcDid({
parsed: options.parsed,
})
if (did == null) {
return {
didDocumentMetadata: {},
didResolutionMetadata: {
error: 'notFound',
message: 'DID not found',
},
didDocument: null,
}
}
// resolve did:plc
const plc = await resolvePlc({
directory: options.directory,
parsed: parse(did),
options: options.options,
})
// if did:plc fails, try did:web
if (plc.didResolutionMetadata.error) {
return await didWebResolver(
`did:web:${options.parsed.id}`,
parse(`did:web:${options.parsed.id}`),
// @ts-expect-error - testing
{},
options.options
)
}
return plc
} catch (error) {
return {
didDocumentMetadata: {},
didResolutionMetadata: {
error: 'notFound',
// @ts-expect-error - error.message is not typed
message: error.message,
},
didDocument: null,
}
}
}
/**
* Check if a DID is a valid ATP DID
*
* @param {string} did
* @returns {boolean}
*
* @example
* ```ts twoslash
* import { isAtpDid } from 'iso-did/atp'
*
* // when the did is a plc
* isAtpDid('did:plc:ewvi7nxzyoun6zhxrhs64oiz')
* //=> true
*
* // when the did is a web
* isAtpDid('did:web:robin.berjon.com')
* //=> true
*
* // when the did is invalid
* isAtpDid('did:key:z6MkmTBzK2JeYogW35ws63V9E9F66ayWTzoP62i7EL7e7VsZ')
* //=> false
* ```
*/
function isAtpDid(did) {
const parsed = parse(did)
if (parsed == null) {
return false
}
if (parsed.method === 'web') {
return true
}
if (parsed.method === 'plc' && isPlc(parsed.did)) {
return true
}
return false
}
/**
* Creates a DID resolver for `did:atp` method.
*
* This resolver uses either DNS or well-known endpoints to resolve the DID,
* eventually resolving to a `did:plc` DID.
*
* @param {string} [directory=DIRECTORY] - The directory to use for resolving `did:plc` DIDs.
* @returns {import('did-resolver').DIDResolver} A DID resolver for `did:atp` method.
*
* @example
* ```ts twoslash
* import { createDidAtpResolver } from 'iso-did/atp'
* import { resolve } from 'iso-did'
*
* const didDocument = await resolve('did:atp:retr0.id', {
* resolvers: {
* atp: createDidAtpResolver('https://plc.directory')
* },
* cache: true
* })
* console.log(didDocument)
* ```
*/
export function createAtpResolver(directory = DIRECTORY) {
/** @type {import('did-resolver').DIDResolver} */
function didAtpResolver(_did, parsedDid, _resolver, options) {
return resolve({
directory,
parsed: parsedDid,
options,
})
}
return didAtpResolver
}