~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
156 lines · 4.4 KB Raw History
// @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);
  };
}