// @pds/core/loader - Repository loader from CAR files // Loads AT Protocol repositories from CAR archives into storage import { parseCarFile, getCarHeader } from './car.js'; import { cborDecode, cidToString } from './repo.js'; import { walkMst } from './mst.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} */ 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} */ 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), }; } }