// @pdsjs/spaces/authority - resolving the accounts a space involves from their // DID documents. // // A space authority resolves like any other account: `#atproto_pds` is its // space host, and `#atproto` signs its space tokens. Every account that writes // space records also publishes `#atproto_kem`, the P-256 key a syncer derives // a per-request MAC secret against (see kem.js). export const ATPROTO_KEY_ID = '#atproto'; export const ATPROTO_PDS_ID = '#atproto_pds'; export const KEM_KEY_ID = '#atproto_kem'; export class SpaceAuthorityError extends Error { /** @param {string} message @param {string} [code] */ constructor(message, code = 'SpaceAuthorityNotFound') { super(message); this.name = 'SpaceAuthorityError'; this.code = code; } } /** * @param {any} didDoc * @param {string} did * @param {string} id - fragment, e.g. '#atproto' * @returns {string|null} did:key, or null when absent */ function verificationMethod(didDoc, did, id) { const methods = didDoc?.verificationMethod ?? []; for (const method of methods) { // A DID document may write the id either fully-qualified or as a bare // fragment; accept both. if (method?.id === id || method?.id === `${did}${id}`) { return method.publicKeyMultibase ? `did:key:${method.publicKeyMultibase}` : null; } } return null; } /** * @param {any} didDoc * @param {string} did * @param {string} id * @returns {string|null} */ function serviceEndpoint(didDoc, did, id) { const services = didDoc?.service ?? []; for (const service of services) { if (service?.id === id || service?.id === `${did}${id}`) { return typeof service.serviceEndpoint === 'string' ? service.serviceEndpoint : null; } } return null; } /** * The key that verifies an account's space tokens: delegation tokens and, for * an authority, credentials. Always the account's `#atproto` key. * * @param {any} didDoc * @param {string} did * @returns {string} did:key */ export function atprotoSigningKey(didDoc, did) { const key = verificationMethod(didDoc, did, ATPROTO_KEY_ID); if (!key) { throw new SpaceAuthorityError( `No atproto signing key for ${did}`, 'SigningKeyNotFound', ); } return key; } /** * The account's `#atproto_kem` key, which a syncer encapsulates a MAC secret * against when it reads the account's space repos. * * @param {any} didDoc * @param {string} did * @returns {string} did:key */ export function kemPublicKey(didDoc, did) { const key = verificationMethod(didDoc, did, KEM_KEY_ID); if (!key) { throw new SpaceAuthorityError(`No KEM key for ${did}`, 'KemKeyNotFound'); } return key; } /** * Where an account's PDS serves: for an authority, its space host endpoints; * for a member, its repos. * * @param {any} didDoc * @param {string} did * @returns {string} */ export function spaceHostEndpoint(didDoc, did) { const endpoint = serviceEndpoint(didDoc, did, ATPROTO_PDS_ID); if (!endpoint) { throw new SpaceAuthorityError( `No space host for ${did}`, 'SpaceHostNotFound', ); } return endpoint; } /** * Where a service identifier points. A service identifier is a DID with an * optional fragment naming the entry in its DID document. Without a fragment * the account's ordinary PDS entry answers. * * @param {any} didDoc * @param {string} service - e.g. 'did:web:syncer.example#atproto_space_syncer' * @returns {string|null} */ export function namedServiceEndpoint(didDoc, service) { const [did, fragment] = service.split('#'); return serviceEndpoint( didDoc, did, fragment ? `#${fragment}` : ATPROTO_PDS_ID, ); } /** * How a space authority is addressed as the space host: the `aud` of a * delegation token, a client attestation and a notifyWrite. * @param {string} spaceDid * @returns {string} */ export function spaceHostAudience(spaceDid) { return `${spaceDid}${ATPROTO_PDS_ID}`; } /** * Build the `getSigningKey` callback verifySpaceToken expects. * * @param {import('@pdsjs/core/ports').DidResolverPort} resolveDid * @returns {(iss: string, forceRefresh?: boolean) => Promise} */ export function didKeyResolver(resolveDid) { return async (iss, forceRefresh = false) => { const didDoc = await resolveDid(iss, { forceRefresh }); if (!didDoc) { throw new SpaceAuthorityError(`Could not resolve ${iss}`, 'DidNotFound'); } return atprotoSigningKey(didDoc, iss); }; }