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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
78 lines · 2.6 KB Raw History
/**
 * Per-repository secrets, read from the file `PDSJS_CI_SECRETS` names.
 *
 * A step sees a secret only when both sides name it: the file grants it to
 * the repository, and the workflow declares it in `secrets`. The names are
 * public in the workflow file; the values exist only on the runner host.
 */

/** Where the file format is described, for error messages. */
const FILE_LABEL = 'secrets file';

/**
 * A grant maps `<handle-or-did>/<repo>` to the secrets that repository may
 * request. The authority resolves to a DID the same way a watch spec does,
 * and the name to the record key a run names the repository by.
 * @typedef {Record<string, Record<string, string>>} SecretsGrants
 */

/**
 * What one run receives: the secrets this repository is granted and its
 * workflow declares.
 * @typedef {(did: string, repoRkey: string, names: string[]) => Record<string, string>} SecretsResolver
 */

/**
 * @param {string} source - file contents
 * @returns {SecretsGrants}
 */
export function parseSecretsFile(source) {
  /** @type {unknown} */
  let parsed;
  try {
    parsed = JSON.parse(source);
  } catch (err) {
    throw new Error(
      `${FILE_LABEL}: invalid JSON (${err instanceof Error ? err.message : err})`,
    );
  }
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
    throw new Error(`${FILE_LABEL}: must be a JSON object keyed by repository`);
  }

  /** @type {SecretsGrants} */
  const grants = {};
  for (const [repo, entry] of Object.entries(parsed)) {
    if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
      throw new Error(`${FILE_LABEL}: ${repo} must be an object of secrets`);
    }
    /** @type {Record<string, string>} */
    const values = {};
    for (const [name, value] of Object.entries(entry)) {
      if (typeof value !== 'string') {
        throw new Error(`${FILE_LABEL}: ${repo}.${name} must be a string`);
      }
      values[name] = value;
    }
    grants[repo] = values;
  }
  return grants;
}

/**
 * The intersection of what the repository is granted and what the workflow
 * declares. A declared name with no grant is absent, and the workflow's own
 * guard reports it; the run does not fail here.
 * @param {Record<string, string>|undefined} granted - this repository's grants
 * @param {string[]} names - what the workflow declares
 * @returns {Record<string, string>}
 */
export function grantedSecrets(granted, names) {
  /** @type {Record<string, string>} */
  const env = {};
  if (!granted) return env;
  for (const name of names) {
    if (name in granted) env[name] = granted[name];
  }
  return env;
}