Something went wrong. Try again.
An AT Protocol Personal Data Server written in JavaScript pdsjs.dev
pds atproto
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190// @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); }
// 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), }; }}