chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
/**
* Resolve an atproto authority (handle or DID) to the DID plus the PDS
* service URL that hosts it. ATPROTO_GIT_SERVICE short-circuits discovery,
* which is how tests and single-PDS setups avoid network resolution.
*/
import { XrpcClient } from './xrpc.js';
const PLC_DIRECTORY = 'https://plc.directory';
/**
* @typedef {Object} RepoLocation
* @property {string} did
* @property {string} service - PDS base URL
*/
/**
* @param {string} handle
* @returns {Promise<string>} the DID
*/
async function resolveHandlePublic(handle) {
try {
const res = await fetch(`https://${handle}/.well-known/atproto-did`);
if (res.ok) {
const text = (await res.text()).trim();
if (text.startsWith('did:')) return text;
}
} catch {
// Fall through to DNS
}
const res = await fetch(
`https://cloudflare-dns.com/dns-query?name=_atproto.${handle}&type=TXT`,
{ headers: { accept: 'application/dns-json' } },
);
if (res.ok) {
const body = /** @type {{Answer?: Array<{data: string}>}} */ (
await res.json()
);
for (const answer of body.Answer ?? []) {
const text = answer.data.replace(/^"|"$/g, '');
if (text.startsWith('did=did:')) return text.slice(4);
}
}
throw new Error(`could not resolve handle "${handle}" to a DID`);
}
/**
* @typedef {Object} DidInfo
* @property {string} did
* @property {string} service - PDS base URL
* @property {string|null} handle - from alsoKnownAs, when the document has one
* @property {string|null} kemKey - the `#atproto_kem` did:key, which reads
* with a space credential ask for a MAC against
*/
/**
* Resolve a DID document to its PDS endpoint and handle. Unlike
* resolveRepoLocation this never consults ATPROTO_GIT_SERVICE: that override
* names the remote authority's PDS, and the DIDs resolved here are space
* members hosted anywhere.
* @param {string} did
* @param {Record<string, string|undefined>} env
* @returns {Promise<DidInfo>}
*/
export async function resolveDidInfo(did, env) {
const plcUrl = env.ATPROTO_GIT_PLC_URL ?? PLC_DIRECTORY;
/** @type {string} */
let docUrl;
if (did.startsWith('did:plc:')) {
docUrl = `${plcUrl}/${did}`;
} else if (did.startsWith('did:web:')) {
const domain = did.slice('did:web:'.length);
if (domain.includes(':')) {
throw new Error(`unsupported did:web with path segments: ${did}`);
}
docUrl = `https://${domain}/.well-known/did.json`;
} else {
throw new Error(`unsupported DID method: ${did}`);
}
const res = await fetch(docUrl);
if (!res.ok) {
throw new Error(`could not fetch DID document for ${did}`);
}
const doc =
/** @type {{service?: Array<{id: string, type: string, serviceEndpoint: string}>, alsoKnownAs?: string[], verificationMethod?: Array<{id: string, publicKeyMultibase?: string}>}} */ (
await res.json()
);
const service = (doc.service ?? []).find(
(s) => s.id === '#atproto_pds' || s.id.endsWith('#atproto_pds'),
);
if (!service || typeof service.serviceEndpoint !== 'string') {
throw new Error(`no PDS service endpoint in DID document for ${did}`);
}
const aka = (doc.alsoKnownAs ?? []).find((a) => a.startsWith('at://'));
const kem = (doc.verificationMethod ?? []).find(
(m) => m.id === '#atproto_kem' || m.id === `${did}#atproto_kem`,
);
return {
did,
service: service.serviceEndpoint,
handle: aka ? aka.slice('at://'.length) : null,
kemKey: kem?.publicKeyMultibase
? `did:key:${kem.publicKeyMultibase}`
: null,
};
}
/**
* @param {string} authority - handle or DID from the remote URL
* @param {Record<string, string|undefined>} env
* @returns {Promise<RepoLocation>}
*/
export async function resolveRepoLocation(authority, env) {
const serviceOverride = env.ATPROTO_GIT_SERVICE;
if (serviceOverride) {
if (authority.startsWith('did:')) {
return { did: authority, service: serviceOverride };
}
const client = new XrpcClient(serviceOverride);
return {
did: await client.resolveHandle(authority),
service: serviceOverride,
};
}
const did = authority.startsWith('did:')
? authority
: await resolveHandlePublic(authority);
const { service } = await resolveDidInfo(did, env);
return { did, service };
}