chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// @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),
};
}
}