···22// Re-export all utilities from submodules
3344export * from './auth.js';
55+export * from './car.js';
56export * from './crypto.js';
77+export * from './loader.js';
68export * from './mst.js';
79export * from './oauth.js';
810export {
+184
packages/core/src/loader.js
···11+// @pds/core/loader - Repository loader from CAR files
22+// Loads AT Protocol repositories from CAR archives into storage
33+44+import { parseCarFile, getCarHeader } from './car.js';
55+import { cborDecode, cidToString } from './repo.js';
66+import { walkMst } from './mst.js';
77+88+/**
99+ * @typedef {import('./ports.js').ActorStoragePort} ActorStoragePort
1010+ */
1111+1212+/**
1313+ * @typedef {Object} CommitData
1414+ * @property {string} did - Repository DID
1515+ * @property {number} version - Commit version (usually 3)
1616+ * @property {string} rev - Revision string (TID)
1717+ * @property {string|null} prev - Previous commit CID
1818+ * @property {string|null} data - MST root CID
1919+ * @property {Uint8Array} [sig] - Signature bytes
2020+ */
2121+2222+/**
2323+ * @typedef {Object} LoadResult
2424+ * @property {string} did - Repository DID
2525+ * @property {string} commitCid - Root commit CID
2626+ * @property {string} rev - Revision string
2727+ * @property {number} recordCount - Number of records loaded
2828+ * @property {number} blockCount - Number of blocks loaded
2929+ */
3030+3131+/**
3232+ * Load a repository from CAR bytes into storage
3333+ * @param {Uint8Array} carBytes - CAR file bytes
3434+ * @param {ActorStoragePort} actorStorage - Storage to populate
3535+ * @returns {Promise<LoadResult>}
3636+ */
3737+export async function loadRepositoryFromCar(carBytes, actorStorage) {
3838+ // Parse the CAR file
3939+ const { roots, blocks } = parseCarFile(carBytes);
4040+4141+ if (roots.length === 0) {
4242+ throw new Error('CAR file has no roots');
4343+ }
4444+4545+ const commitCid = roots[0];
4646+ const commitBytes = blocks.get(commitCid);
4747+4848+ if (!commitBytes) {
4949+ throw new Error(`Commit block not found: ${commitCid}`);
5050+ }
5151+5252+ // Decode commit
5353+ const commit = cborDecode(commitBytes);
5454+ const did = commit.did;
5555+ const rev = commit.rev;
5656+ const version = commit.version;
5757+5858+ if (!did || typeof did !== 'string') {
5959+ throw new Error('Invalid commit: missing DID');
6060+ }
6161+6262+ if (version !== 3 && version !== 2) {
6363+ throw new Error(`Unsupported commit version: ${version}`);
6464+ }
6565+6666+ // Get MST root CID
6767+ const mstRootCid = commit.data ? cidToString(commit.data) : null;
6868+6969+ // Store all blocks first
7070+ for (const [cid, data] of blocks) {
7171+ await actorStorage.putBlock(cid, data);
7272+ }
7373+7474+ // Set metadata
7575+ await actorStorage.setDid(did);
7676+7777+ // Walk MST and extract records
7878+ let recordCount = 0;
7979+ if (mstRootCid) {
8080+ /**
8181+ * @param {string} cid
8282+ * @returns {Promise<Uint8Array|null>}
8383+ */
8484+ const getBlock = async (cid) => blocks.get(cid) || null;
8585+8686+ for await (const { key, cid } of walkMst(mstRootCid, getBlock)) {
8787+ // key format: "collection/rkey"
8888+ const slashIndex = key.indexOf('/');
8989+ if (slashIndex === -1) {
9090+ console.warn(`Invalid record key format: ${key}`);
9191+ continue;
9292+ }
9393+9494+ const collection = key.slice(0, slashIndex);
9595+ const rkey = key.slice(slashIndex + 1);
9696+ const uri = `at://${did}/${collection}/${rkey}`;
9797+9898+ // Get record data
9999+ const recordBytes = blocks.get(cid);
100100+ if (!recordBytes) {
101101+ console.warn(`Record block not found: ${cid}`);
102102+ continue;
103103+ }
104104+105105+ // Store record
106106+ await actorStorage.putRecord(uri, cid, collection, rkey, recordBytes);
107107+ recordCount++;
108108+ }
109109+ }
110110+111111+ // Store commit
112112+ const prevCommit = await actorStorage.getLatestCommit();
113113+ const seq = prevCommit ? prevCommit.seq + 1 : 1;
114114+ await actorStorage.putCommit(seq, commitCid, rev, commit.prev ? cidToString(commit.prev) : null);
115115+116116+ return {
117117+ did,
118118+ commitCid,
119119+ rev,
120120+ recordCount,
121121+ blockCount: blocks.size,
122122+ };
123123+}
124124+125125+/**
126126+ * Get repository info from CAR without loading into storage
127127+ * @param {Uint8Array} carBytes - CAR file bytes
128128+ * @returns {{ did: string, commitCid: string, rev: string }}
129129+ */
130130+export function getCarRepoInfo(carBytes) {
131131+ const { roots, blocks } = parseCarFile(carBytes);
132132+133133+ if (roots.length === 0) {
134134+ throw new Error('CAR file has no roots');
135135+ }
136136+137137+ const commitCid = roots[0];
138138+ const commitBytes = blocks.get(commitCid);
139139+140140+ if (!commitBytes) {
141141+ throw new Error(`Commit block not found: ${commitCid}`);
142142+ }
143143+144144+ const commit = cborDecode(commitBytes);
145145+146146+ return {
147147+ did: commit.did,
148148+ commitCid,
149149+ rev: commit.rev,
150150+ };
151151+}
152152+153153+/**
154154+ * Validate CAR file structure without fully loading
155155+ * @param {Uint8Array} carBytes - CAR file bytes
156156+ * @returns {{ valid: boolean, error?: string, did?: string }}
157157+ */
158158+export function validateCarFile(carBytes) {
159159+ try {
160160+ const { version, roots } = getCarHeader(carBytes);
161161+162162+ if (version !== 1) {
163163+ return { valid: false, error: `Unsupported CAR version: ${version}` };
164164+ }
165165+166166+ if (roots.length === 0) {
167167+ return { valid: false, error: 'CAR file has no roots' };
168168+ }
169169+170170+ // Quick parse to verify commit
171171+ const info = getCarRepoInfo(carBytes);
172172+173173+ if (!info.did) {
174174+ return { valid: false, error: 'Missing DID in commit' };
175175+ }
176176+177177+ return { valid: true, did: info.did };
178178+ } catch (err) {
179179+ return {
180180+ valid: false,
181181+ error: err instanceof Error ? err.message : String(err),
182182+ };
183183+ }
184184+}