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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
103 lines · 3.7 KB Raw History
/**
 * A run the repository's owner asked for, rather than one a push caused.
 *
 * The runner listens on the firehose and has no inbound address, so a request
 * is a record in the owner's repo. Reaching the run needs one more read: the
 * request names a repository and a ref, and the check needs the repository
 * record's current version and the commit that ref points at.
 */

import { GIT_REPO_COLLECTION, parseRepoRecord, rkeyOf } from '@pdsjs/git';

/**
 * @typedef {Object} CheckRequestRecord
 * @property {string} repo - record key of the repository to check
 * @property {string} ref - full ref name
 * @property {string|null} sha - commit to check, or null for the ref's own
 */

/**
 * A request record's fields, or null for a record this runner cannot act on.
 * A malformed request is ignored rather than thrown: anyone may write any
 * record, and one bad value must not stop the events behind it.
 * @param {unknown} value
 * @returns {CheckRequestRecord|null}
 */
export function parseCheckRequest(value) {
  const record = /** @type {Record<string, unknown>} */ (value ?? {});
  if (typeof record.repo !== 'string' || !record.repo) return null;
  if (typeof record.ref !== 'string' || !record.ref) return null;
  const sha = typeof record.sha === 'string' && record.sha ? record.sha : null;
  return { repo: record.repo, ref: record.ref, sha };
}

/**
 * @typedef {Object} RepoRecordVersion
 * @property {string} uri
 * @property {string} cid
 * @property {string} name - what the repository is called
 * @property {string} rkey - its record key, which names it everywhere else
 * @property {Array<{name: string, sha: string}>} refs
 */

/**
 * The repository record as it stands now. A request carries no strongref, so
 * the version a requested run reads is the one this returns.
 *
 * The command line names a repository, and a request names its record key,
 * so this takes either: a key reads one record, and a name is matched
 * against the account's listing.
 * @param {Object} target
 * @param {string} target.service - PDS base URL
 * @param {string} target.did - repository owner
 * @param {string} [target.repoName]
 * @param {string} [target.rkey]
 * @returns {Promise<RepoRecordVersion>}
 */
export async function readRepoRecord(target) {
  const at = target.rkey ?? target.repoName ?? '';
  const params = new URLSearchParams({
    repo: target.did,
    collection: GIT_REPO_COLLECTION,
  });
  if (target.rkey) params.set('rkey', target.rkey);
  else params.set('limit', '100');
  const method = target.rkey ? 'getRecord' : 'listRecords';
  const res = await fetch(
    `${target.service}/xrpc/com.atproto.repo.${method}?${params}`,
  );
  if (!res.ok) {
    throw new Error(`could not read ${target.did}/${at} (${res.status})`);
  }
  const body =
    /** @type {{uri?: string, cid?: string, value?: unknown, records?: Array<{uri: string, cid: string, value: unknown}>}} */ (
      await res.json()
    );
  const row = target.rkey
    ? body
    : (body.records ?? []).find(
        (entry) => parseRepoRecord(entry.value).name === target.repoName,
      );
  if (!row?.uri || !row.cid) {
    throw new Error(`${target.did}/${at} has no record version`);
  }
  const record = parseRepoRecord(row.value);
  return {
    uri: row.uri,
    cid: row.cid,
    name: record.name,
    rkey: rkeyOf(row.uri),
    refs: record.refs,
  };
}

/**
 * The commit a request names: its own, or the one the ref points at.
 * @param {CheckRequestRecord} request
 * @param {Array<{name: string, sha: string}>} refs
 * @returns {string|null} null when the ref is not in the record
 */
export function requestedSha(request, refs) {
  if (request.sha) return request.sha;
  return refs.find((ref) => ref.name === request.ref)?.sha ?? null;
}