chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// @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<string>}
*/
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);
};
}