OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-store commits / 324ffdd

atproto-store: getRepoFromCommit — keyless full-repo CAR assembly
Add read-view.getRepoFromCommit(store, signedCommit): assemble the full atproto repo CAR from the store's records + a PROVIDED signed commit (as a bridge reads from a peer's replicated firehose). MST/records rebuild without the key; the signature travels in the commit. Integrity guard: refuses a commit whose data CID != the rebuilt MST root. Unit test 24/24 (incl. guard rejects a mismatched commit). This is the keyless path for com.atproto.sync.getRepo — the bridge can serve a signed repo it never had the key to sign. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
324ffdd97ca14564b3f798dffcd7fa79ed2dbd4c
by plankatron <fleeky@gmail.com> · 2026-07-03T21:19:01.000Z

Changed files (2) +68 -0

lib/read-view.js +26 -0
……@@ -38,6 +38,32 @@ export async function getRepo (store, meta) {
3838 return { carBytes, commitCid: r.commitCid, dataCid: r.dataCid, signedCommit: r.signedCommit }
3939}
4040
41// KEYLESS getRepo (peer/bridge split): assemble the full CAR from the store's records + a
42// PROVIDED signed commit — e.g. one a bridge read from a peer's replicated firehose. The
43// MST/records are curve-independent so they rebuild without the key; the signature travels
44// IN the commit, never re-derived. Integrity: the provided commit must commit to exactly
45// these records (its data CID == the rebuilt MST root), else we refuse to serve a mismatched
46// repo. Signature verification (vs the peer's did:key) is the caller's concern.
47export async function getRepoFromCommit (store, signedCommit) {
48 const recs = await store.records()
49 const recordBlocks = []
50 const mstRecords = []
51 for (const r of recs) {
52 const cid = cidForBytesSync(r.bytes)
53 recordBlocks.push({ cid, bytes: r.bytes })
54 mstRecords.push({ key: r.key, value: cid })
55 }
56 const { root: dataCid, ns } = buildMst(mstRecords)
57 if (!dataCid.equals(signedCommit.data)) {
58 throw new Error('getRepoFromCommit: signed commit data CID != rebuilt MST root (repo/commit mismatch)')
59 }
60 const cCid = commitCid(signedCommit)
61 const commitBlock = { cid: cCid, bytes: dagCbor.encode(signedCommit) }
62 const mstBlocks = [...reachableBlocks(ns, dataCid).values()]
63 const carBytes = await writeCar(cCid, [commitBlock, ...mstBlocks, ...recordBlocks])
64 return { carBytes, commitCid: cCid, dataCid }
65}
66
4167// com.atproto.sync.getLatestCommit -> { cid, rev }.
4268export async function getLatestCommit (store, meta) {
4369 const r = await assembleRepo(store, meta)
test/keyless-getrepo.test.js +42 -0
……@@ -0,0 +1,42 @@
1// getRepoFromCommit: assemble a full repo CAR KEYLESSLY from the store's records + a PROVIDED
2// signed commit (as a bridge does with a commit read from a peer's replicated firehose). The
3// MST/records rebuild without the key; the signature travels in the commit. Includes the
4// integrity guard (a commit that doesn't match the records is refused).
5
6import test from 'brittle'
7import { mkdtempSync, rmSync } from 'node:fs'
8import { tmpdir } from 'node:os'
9import { join } from 'node:path'
10import { openCorestore } from '../lib/corestore.js'
11import { RepoStore } from '../lib/repo-store.js'
12import { RepoManager } from '../lib/repo-manager.js'
13import { getRepoFromCommit } from '../lib/read-view.js'
14import { readCar, loadCommit, listRecords } from 'atproto-repo/lib/repo.js'
15import { cidForBytesSync } from 'atproto-repo/lib/cid.js'
16
17const PRIV = new Uint8Array(32).fill(7)
18const NOW = '2026-07-03T00:00:00.000Z'
19
20test('getRepoFromCommit assembles a byte-exact CAR keylessly from a provided signed commit', async (t) => {
21 const dir = mkdtempSync(join(tmpdir(), 'store-keyless-'))
22 const store = openCorestore(dir)
23 const repo = await new RepoStore(store, { name: 'atproto-repo' }).ready()
24 let micros = 1_700_000_000_000_000n
25 const mgr = await new RepoManager(repo, { did: 'did:plc:x', privateKey: PRIV, clock: () => (micros += 1_000_000n) }).init()
26 const w = await mgr.applyWrite({ action: 'create', collection: 'app.bsky.feed.post', rkey: '3a', record: { $type: 'app.bsky.feed.post', text: 'hi', createdAt: NOW } })
27
28 t.teardown(async () => { await repo.close(); await store.close(); rmSync(dir, { recursive: true, force: true }) })
29
30 // KEYLESS: no privateKey — the signed commit is provided (as read from a firehose).
31 const { carBytes, commitCid: cc } = await getRepoFromCommit(repo, w.signedCommit)
32 t.ok(cc.equals(w.commitCid), 'keyless CAR is rooted at the provided signed commit')
33 const car = await readCar(carBytes)
34 t.ok(loadCommit(car).value.sig, 'CAR carries the signed commit (signature present, not re-derived)')
35 const recs = await listRecords(car)
36 t.ok(recs.some(r => r.value.text === 'hi'), 'records present in the keyless CAR')
37
38 // integrity guard: a commit that does not commit to these records is refused
39 const bogus = cidForBytesSync(new Uint8Array([1, 2, 3]))
40 await t.exception(getRepoFromCommit(repo, { ...w.signedCommit, data: bogus }),
41 'refuses a signed commit whose data CID != the rebuilt MST root')
42})