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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
192 lines · 5.0 KB Raw History
// @pdsjs/core/loader - Repository loader from CAR files
// Loads AT Protocol repositories from CAR archives into storage

import { getCarHeader, parseCarFile } from './car.js';
import { walkMst } from './mst.js';
import { cborDecode, cidToString } from './repo.js';

/**
 * @typedef {import('./ports.js').ActorStoragePort} ActorStoragePort
 */

/**
 * @typedef {Object} CommitData
 * @property {string} did - Repository DID
 * @property {number} version - Commit version (usually 3)
 * @property {string} rev - Revision string (TID)
 * @property {string|null} prev - Previous commit CID
 * @property {string|null} data - MST root CID
 * @property {Uint8Array} [sig] - Signature bytes
 */

/**
 * @typedef {Object} LoadResult
 * @property {string} did - Repository DID
 * @property {string} commitCid - Root commit CID
 * @property {string} rev - Revision string
 * @property {number} recordCount - Number of records loaded
 * @property {number} blockCount - Number of blocks loaded
 */

/**
 * Load a repository from CAR bytes into storage
 * @param {Uint8Array} carBytes - CAR file bytes
 * @param {ActorStoragePort} actorStorage - Storage to populate
 * @returns {Promise<LoadResult>}
 */
export async function loadRepositoryFromCar(carBytes, actorStorage) {
  // Parse the CAR file
  const { roots, blocks } = parseCarFile(carBytes);

  if (roots.length === 0) {
    throw new Error('CAR file has no roots');
  }

  const commitCid = roots[0];
  const commitBytes = blocks.get(commitCid);

  if (!commitBytes) {
    throw new Error(`Commit block not found: ${commitCid}`);
  }

  // Decode commit
  const commit = cborDecode(commitBytes);
  const did = commit.did;
  const rev = commit.rev;
  const version = commit.version;

  if (!did || typeof did !== 'string') {
    throw new Error('Invalid commit: missing DID');
  }

  if (version !== 3 && version !== 2) {
    throw new Error(`Unsupported commit version: ${version}`);
  }

  // Get MST root CID
  const mstRootCid = commit.data ? cidToString(commit.data) : null;

  // Store all blocks first
  for (const [cid, data] of blocks) {
    await actorStorage.putBlock(cid, data);
  }
  // A CAR may carry blocks its commit does not reach: the next getRepo
  // sweeps them before reading the table as the repo.
  await actorStorage.setBlocksCollected?.(false);

  // Set metadata
  await actorStorage.setDid(did);

  // Walk MST and extract records
  let recordCount = 0;
  if (mstRootCid) {
    /**
     * @param {string} cid
     * @returns {Promise<Uint8Array|null>}
     */
    const getBlock = async (cid) => blocks.get(cid) || null;

    for await (const { key, cid } of walkMst(mstRootCid, getBlock)) {
      // key format: "collection/rkey"
      const slashIndex = key.indexOf('/');
      if (slashIndex === -1) {
        console.warn(`Invalid record key format: ${key}`);
        continue;
      }

      const collection = key.slice(0, slashIndex);
      const rkey = key.slice(slashIndex + 1);
      const uri = `at://${did}/${collection}/${rkey}`;

      // Get record data
      const recordBytes = blocks.get(cid);
      if (!recordBytes) {
        console.warn(`Record block not found: ${cid}`);
        continue;
      }

      // Store record
      await actorStorage.putRecord(uri, cid, collection, rkey, recordBytes);
      recordCount++;
    }
  }

  // Store commit
  const prevCommit = await actorStorage.getLatestCommit();
  const seq = prevCommit ? prevCommit.seq + 1 : 1;
  await actorStorage.putCommit(
    seq,
    commitCid,
    rev,
    commit.prev ? cidToString(commit.prev) : null,
  );

  return {
    did,
    commitCid,
    rev,
    recordCount,
    blockCount: blocks.size,
  };
}

/**
 * Get repository info from CAR without loading into storage
 * @param {Uint8Array} carBytes - CAR file bytes
 * @returns {{ did: string, commitCid: string, rev: string }}
 */
export function getCarRepoInfo(carBytes) {
  const { roots, blocks } = parseCarFile(carBytes);

  if (roots.length === 0) {
    throw new Error('CAR file has no roots');
  }

  const commitCid = roots[0];
  const commitBytes = blocks.get(commitCid);

  if (!commitBytes) {
    throw new Error(`Commit block not found: ${commitCid}`);
  }

  const commit = cborDecode(commitBytes);

  return {
    did: commit.did,
    commitCid,
    rev: commit.rev,
  };
}

/**
 * Validate CAR file structure without fully loading
 * @param {Uint8Array} carBytes - CAR file bytes
 * @returns {{ valid: boolean, error?: string, did?: string }}
 */
export function validateCarFile(carBytes) {
  try {
    const { version, roots } = getCarHeader(carBytes);

    if (version !== 1) {
      return { valid: false, error: `Unsupported CAR version: ${version}` };
    }

    if (roots.length === 0) {
      return { valid: false, error: 'CAR file has no roots' };
    }

    // Quick parse to verify commit
    const info = getCarRepoInfo(carBytes);

    if (!info.did) {
      return { valid: false, error: 'Missing DID in commit' };
    }

    return { valid: true, did: info.did };
  } catch (err) {
    return {
      valid: false,
      error: err instanceof Error ? err.message : String(err),
    };
  }
}