OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-repo commits / adfeffd

atproto-repo: extract as standalone repo (peer/bridge split, step 1)
Pure atproto repo data layer (MST / CAR / CID / commit / DAG-CBOR) extracted from atproto-bridge/repo per DECISION-2026-07-03-peer-bridge-split.md. Lib is pure; one whole-stack commit test dev-depends on atproto-identity, wired as file:../atproto-identity (test imports rewritten to the package). Standalone postinstall bare-deps (copied into scripts/). Tests: 10/10, 17/17 asserts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
adfeffd910a8371fd96a86aac4ccb9cc3f8062c4
by plankatron <fleeky@gmail.com> · 2026-07-03T20:14:01.000Z

Changed files (13) +1960 -0

.gitignore +1 -0
……@@ -0,0 +1 @@
1node_modules/
lib/car.js +72 -0
……@@ -0,0 +1,72 @@
1// Minimal CAR v1 codec — bare-safe, dependency-light replacement for @ipld/car.
2//
3// CAR v1 is a trivial container: varint(headerLen) ++ dag-cbor({version:1, roots:[…]})
4// then, per block, varint(len) ++ CIDbytes ++ blockBytes. We only need whole-buffer
5// read/write (no streaming, no file I/O), so this is ~40 lines of pure JS over
6// multiformats' CID (byte-exact with @ipld/car — verified by the repo tests + the
7// @atproto/repo conformance reader). Drops @ipld/car (which imports `fs`/streams and
8// won't load under Bare).
9//
10// ⚠️ LIMITATIONS vs @ipld/car (tracked in memory: bare-car-limitations). Fine for our
11// current use (whole-buffer, CARv1, OUR-OWN trusted output); add the feature before the
12// gate it guards:
13// - No streaming — whole CAR is buffered in memory. GATE: large repos (getRepo of a
14// big repo could be GBs). Add an async-iterator read/write path.
15// - No untrusted-input hardening — readCar trusts varints/lengths/CIDs. GATE: reading
16// UNTRUSTED CARs (com.atproto.repo.importRepo, ingress bridge). Add bounds/overflow/
17// truncation checks + duplicate-CID handling before then.
18// - CARv1 only (no CARv2/index). atproto is CARv1, so no gate today.
19// Fully-featured path: grow this file at each gate, OR extract a standalone bare-native
20// `bare-car` package (the holepunch ecosystem lacks one — a real contribution).
21
22import { CID } from 'multiformats/cid'
23import * as dagCbor from '@ipld/dag-cbor'
24
25function varintEncode (n) {
26 const out = []
27 while (n >= 0x80) { out.push((n & 0x7f) | 0x80); n >>>= 7 }
28 out.push(n)
29 return Uint8Array.from(out)
30}
31
32function varintDecode (bytes, offset) {
33 let n = 0; let shift = 0; let i = offset
34 for (;;) {
35 const b = bytes[i++]
36 n |= (b & 0x7f) << shift
37 if ((b & 0x80) === 0) break
38 shift += 7
39 }
40 return [n >>> 0, i]
41}
42
43// Serialise { root, blocks } to CAR bytes. `blocks` is an iterable of { cid, bytes }.
44export async function writeCar (rootCid, blocks) {
45 const header = dagCbor.encode({ version: 1, roots: [rootCid] })
46 const parts = [Buffer.from(varintEncode(header.length)), Buffer.from(header)]
47 for (const { cid, bytes } of blocks) {
48 const cidBytes = cid.bytes
49 const body = new Uint8Array(cidBytes.length + bytes.length)
50 body.set(cidBytes, 0)
51 body.set(bytes, cidBytes.length)
52 parts.push(Buffer.from(varintEncode(body.length)), Buffer.from(body))
53 }
54 return Buffer.concat(parts)
55}
56
57// Read a CAR into { roots, blocks } where blocks maps cid-string -> { cid, bytes }.
58export async function readCar (carBytes) {
59 const bytes = carBytes instanceof Uint8Array ? carBytes : new Uint8Array(carBytes)
60 let [hlen, off] = varintDecode(bytes, 0)
61 const header = dagCbor.decode(bytes.subarray(off, off + hlen))
62 off += hlen
63 const blocks = new Map()
64 while (off < bytes.length) {
65 const [blen, bodyStart] = varintDecode(bytes, off)
66 const end = bodyStart + blen
67 const [cid, rest] = CID.decodeFirst(bytes.subarray(bodyStart, end))
68 blocks.set(cid.toString(), { cid, bytes: rest })
69 off = end
70 }
71 return { roots: header.roots, blocks }
72}
lib/cid.js +41 -0
……@@ -0,0 +1,41 @@
1// atproto block CIDs — CIDv1, dag-cbor codec (0x71), sha2-256 multihash (0x12).
2// Every block in a repo (commit, MST node, record) is content-addressed this way,
3// so computeCid(dag-cbor(block)) IS the block's identity. This is the anchor for
4// byte-exact fidelity: if our re-encoded bytes hash to the same CID the reference
5// produced, our encoding is byte-identical.
6
7import { CID } from 'multiformats/cid'
8import * as Digest from 'multiformats/hashes/digest'
9import { sha256 as nobleSha256 } from '@noble/hashes/sha2'
10import * as dagCbor from '@ipld/dag-cbor'
11import * as raw from 'multiformats/codecs/raw'
12
13// raw block bytes -> CID. Uses noble's pure-JS sha256 (same as the sync path) rather
14// than multiformats' `crypto.subtle` path — identical CID, and no WebCrypto dependency
15// (so it loads + runs under Bare, which has no crypto.subtle).
16export async function cidForBytes (bytes) {
17 return cidForBytesSync(bytes)
18}
19
20// Synchronous variant — needed by the recursive MST build. Same result as
21// cidForBytes (CIDv1 / dag-cbor 0x71 / sha2-256 0x12), just without the async
22// WebCrypto digest.
23export function cidForBytesSync (bytes) {
24 const digest = Digest.create(0x12, nobleSha256(bytes)) // 0x12 = sha2-256 multihash code
25 return CID.createV1(dagCbor.code, digest)
26}
27
28// BLOB CID — CIDv1, RAW codec (0x55), sha2-256. atproto blobs (images/avatars) are
29// content-addressed with the raw codec (opaque bytes), NOT dag-cbor. This is the CID that
30// goes in a blob ref ({$type:'blob', ref:{$link:<this>}, mimeType, size}) and the key
31// com.atproto.sync.getBlob serves by.
32export function cidForBlobSync (bytes) {
33 const digest = Digest.create(0x12, nobleSha256(bytes))
34 return CID.createV1(raw.code, digest)
35}
36
37// a JS value -> its canonical dag-cbor bytes + CID
38export async function cidForValue (value) {
39 const bytes = dagCbor.encode(value)
40 return { bytes, cid: await cidForBytes(bytes) }
41}
lib/commit.js +36 -0
……@@ -0,0 +1,36 @@
1// atproto repo commit — sign + verify.
2//
3// A commit is { did, version: 3, data: <MST root CID>, rev, prev, sig }. The
4// signature is ECDSA-secp256k1 (low-S) over sha256(dag-cbor(unsignedCommit)), stored
5// as a raw 64-byte CBOR byte string (NOT base64url — that's the did:plc genesis).
6// The relay validates this sig against the DID doc's verificationMethod.
7
8import * as dagCbor from '@ipld/dag-cbor'
9import { sha256 as nobleSha256 } from '@noble/hashes/sha2'
10import { secp256k1 } from '@noble/curves/secp256k1'
11import { cidForBytesSync } from './cid.js'
12
13export const REPO_VERSION = 3
14
15// The unsigned commit (everything the signature covers).
16export function unsignedCommit ({ did, rev, data, prev = null }) {
17 return { did, version: REPO_VERSION, data, rev, prev }
18}
19
20// Sign an unsigned commit -> signed commit with `sig` (64 raw bytes).
21export function signCommit (unsigned, privateKey) {
22 const hash = nobleSha256(dagCbor.encode(unsigned))
23 const sig = secp256k1.sign(hash, privateKey, { lowS: true }).toCompactRawBytes()
24 return { ...unsigned, sig }
25}
26
27export function commitCid (signedCommit) {
28 return cidForBytesSync(dagCbor.encode(signedCommit))
29}
30
31// Verify a signed commit against a compressed secp256k1 pubkey (33 bytes).
32export function verifyCommit (signedCommit, publicKey) {
33 const { sig, ...unsigned } = signedCommit
34 const hash = nobleSha256(dagCbor.encode(unsigned))
35 return secp256k1.verify(sig, hash, publicKey, { lowS: true })
36}
lib/mst.js +337 -0
……@@ -0,0 +1,337 @@
1// atproto MST — WRITE path (slice 2).
2//
3// A faithful port of millipds's `atmst` (mst/node.py + node_wrangler.py) — the exact
4// code that produced our golden CAR. Byte-exactness is verified two ways: rebuilding
5// the golden repo's MST, and matching atmst's own root CIDs for a multi-height tree
6// (build + delete oracles). See test/mst-write.test.js.
7//
8// Node shape (interleaved): subtrees[0], (keys[0],vals[0]), subtrees[1], … ,subtrees[n].
9// key layer = leading-zero BITS of sha256(key), // 2 (fanout 4).
10
11import * as dagCbor from '@ipld/dag-cbor'
12import { sha256 as nobleSha256 } from '@noble/hashes/sha2'
13import { cidForBytesSync } from './cid.js'
14
15const cidEq = (a, b) => (a === null || b === null) ? a === b : a.equals(b)
16const replaceAt = (arr, i, v) => { const c = arr.slice(); c[i] = v; return c }
17const insertAt = (arr, i, v) => arr.slice(0, i).concat([v], arr.slice(i))
18const removeAt = (arr, i) => arr.slice(0, i).concat(arr.slice(i + 1))
19
20export function keyHeight (key) {
21 const h = nobleSha256(Buffer.from(key, 'utf8')) // 32 bytes
22 let zeros = 0
23 for (const byte of h) {
24 if (byte === 0) { zeros += 8; continue }
25 zeros += Math.clz32(byte) - 24 // leading zero bits within this byte
26 break
27 }
28 return zeros >> 1 // // 2
29}
30
31export class MSTNode {
32 constructor (keys, vals, subtrees) {
33 this.keys = keys // [string]
34 this.vals = vals // [CID]
35 this.subtrees = subtrees // [CID|null], length keys.length + 1
36 }
37
38 static empty () { return new MSTNode([], [], [null]) }
39
40 isEmpty () { return this.keys.length === 0 && this.subtrees.length === 1 && this.subtrees[0] === null }
41
42 serialise () {
43 const e = []
44 let prevKey = Buffer.alloc(0)
45 for (let i = 0; i < this.keys.length; i++) {
46 const keyBytes = Buffer.from(this.keys[i], 'utf8')
47 const max = Math.min(prevKey.length, keyBytes.length)
48 let p = 0
49 while (p < max && prevKey[p] === keyBytes[p]) p++
50 e.push({ k: keyBytes.subarray(p), p, t: this.subtrees[i + 1], v: this.vals[i] })
51 prevKey = keyBytes
52 }
53 return dagCbor.encode({ e, l: this.subtrees[0] }) // dag-cbor canonicalises map key order
54 }
55
56 cid () { return cidForBytesSync(this.serialise()) }
57
58 toOptional () { return this.isEmpty() ? null : this.cid() }
59
60 gteIndex (key) { let i = 0; while (i < this.keys.length && key > this.keys[i]) i++; return i }
61
62 height () {
63 if (this.keys.length) return keyHeight(this.keys[0])
64 if (this.subtrees[0] === null) return 0
65 return null // indeterminate (empty intermediate node)
66 }
67
68 static deserialise (bytes) {
69 const cbor = dagCbor.decode(bytes)
70 const subtrees = [cbor.l]
71 const keys = []
72 const vals = []
73 let prevKey = Buffer.alloc(0)
74 for (const e of cbor.e) {
75 const suffix = Buffer.from(e.k)
76 const thisKey = Buffer.concat([prevKey.subarray(0, e.p), suffix])
77 keys.push(thisKey.toString('utf8'))
78 vals.push(e.v)
79 subtrees.push(e.t)
80 prevKey = thisKey
81 }
82 return new MSTNode(keys, vals, subtrees)
83 }
84}
85
86// In-memory node store — maps CID string -> { cid, bytes }. get(null) returns the
87// (stored) empty node, matching atmst NodeStore.get_node(None).
88export class NodeStore {
89 constructor () { this.blocks = new Map() }
90
91 storedNode (node) {
92 const bytes = node.serialise()
93 const cid = cidForBytesSync(bytes)
94 this.blocks.set(cid.toString(), { cid, bytes })
95 return node
96 }
97
98 getNode (cid) {
99 if (cid === null || cid === undefined) return this.storedNode(MSTNode.empty())
100 const b = this.blocks.get(cid.toString())
101 if (!b) throw new Error(`missing MST node ${cid}`)
102 return MSTNode.deserialise(b.bytes)
103 }
104}
105
106export class NodeWrangler {
107 constructor (ns) { this.ns = ns }
108
109 putRecord (rootCid, key, val) {
110 const root = this.ns.getNode(rootCid)
111 if (root.isEmpty()) return this._putHere(root, key, val).cid()
112 return this._putRecursive(root, key, val, keyHeight(key), root.height()).cid()
113 }
114
115 delRecord (rootCid, key) {
116 const root = this.ns.getNode(rootCid)
117 const afterDel = this._deleteRecursive(root, key, keyHeight(key), root.height())
118 return this.ns.getNode(this._squashTop(afterDel)).cid()
119 }
120
121 _putHere (node, key, val) {
122 const i = node.gteIndex(key)
123 if (i < node.keys.length && node.keys[i] === key) {
124 if (cidEq(node.vals[i], val)) return node
125 return this.ns.storedNode(new MSTNode(node.keys.slice(), replaceAt(node.vals, i, val), node.subtrees.slice()))
126 }
127 const [lsub, rsub] = this._splitOnKey(node.subtrees[i], key)
128 return this.ns.storedNode(new MSTNode(
129 insertAt(node.keys, i, key),
130 insertAt(node.vals, i, val),
131 node.subtrees.slice(0, i).concat([lsub, rsub], node.subtrees.slice(i + 1))
132 ))
133 }
134
135 _putRecursive (node, key, val, keyH, treeH) {
136 if (keyH > treeH) { // grow the tree upward
137 return this.ns.storedNode(this._putRecursive(
138 this.ns.storedNode(new MSTNode([], [], [node.cid()])),
139 key, val, keyH, treeH + 1
140 ))
141 }
142 if (keyH < treeH) { // descend
143 const i = node.gteIndex(key)
144 return this.ns.storedNode(new MSTNode(
145 node.keys.slice(), node.vals.slice(),
146 replaceAt(node.subtrees, i, this._putRecursive(this.ns.getNode(node.subtrees[i]), key, val, keyH, treeH - 1).cid())
147 ))
148 }
149 return this._putHere(node, key, val) // keyH === treeH
150 }
151
152 _splitOnKey (nodeCid, key) {
153 if (nodeCid === null || nodeCid === undefined) return [null, null]
154 const node = this.ns.getNode(nodeCid)
155 const i = node.gteIndex(key)
156 const [lsub, rsub] = this._splitOnKey(node.subtrees[i], key)
157 const left = this.ns.storedNode(new MSTNode(node.keys.slice(0, i), node.vals.slice(0, i), node.subtrees.slice(0, i).concat([lsub]))).toOptional()
158 const right = this.ns.storedNode(new MSTNode(node.keys.slice(i), node.vals.slice(i), [rsub].concat(node.subtrees.slice(i + 1)))).toOptional()
159 return [left, right]
160 }
161
162 _squashTop (nodeCid) {
163 const node = this.ns.getNode(nodeCid)
164 if (node.keys.length) return nodeCid
165 if (node.subtrees[0] === null) return nodeCid
166 return this._squashTop(node.subtrees[0])
167 }
168
169 _deleteRecursive (node, key, keyH, treeH) {
170 if (keyH > treeH) return node.toOptional()
171 let i = node.gteIndex(key)
172 if (keyH < treeH) {
173 if (node.subtrees[i] === null) return node.toOptional()
174 return this.ns.storedNode(new MSTNode(
175 node.keys.slice(), node.vals.slice(),
176 replaceAt(node.subtrees, i, this._deleteRecursive(this.ns.getNode(node.subtrees[i]), key, keyH, treeH - 1))
177 )).toOptional()
178 }
179 i = node.gteIndex(key)
180 if (i === node.keys.length || node.keys[i] !== key) return node.toOptional()
181 return this.ns.storedNode(new MSTNode(
182 removeAt(node.keys, i), removeAt(node.vals, i),
183 node.subtrees.slice(0, i).concat([this._merge(node.subtrees[i], node.subtrees[i + 1])], node.subtrees.slice(i + 2))
184 )).toOptional()
185 }
186
187 _merge (leftCid, rightCid) {
188 if (leftCid === null) return rightCid
189 if (rightCid === null) return leftCid
190 const left = this.ns.getNode(leftCid)
191 const right = this.ns.getNode(rightCid)
192 return this.ns.storedNode(new MSTNode(
193 left.keys.concat(right.keys), left.vals.concat(right.vals),
194 left.subtrees.slice(0, -1).concat([this._merge(left.subtrees[left.subtrees.length - 1], right.subtrees[0])], right.subtrees.slice(1))
195 )).toOptional()
196 }
197}
198
199// Build an MST from records [{ key, value: CID }] (any order). Returns
200// { root: CID, ns: NodeStore }. ns.blocks holds every node created; use
201// reachableBlocks(ns, root) for just the live set (CAR output).
202export function buildMst (records) {
203 const ns = new NodeStore()
204 const w = new NodeWrangler(ns)
205 let root = ns.storedNode(MSTNode.empty()).cid()
206 for (const { key, value } of records) root = w.putRecord(root, key, value)
207 return { root, ns, wrangler: w }
208}
209
210// BFS the live MST nodes reachable from root (for CAR block sets).
211export function reachableBlocks (ns, rootCid) {
212 const out = new Map()
213 const stack = [rootCid]
214 while (stack.length) {
215 const cid = stack.pop()
216 if (cid === null || cid === undefined) continue
217 const key = cid.toString()
218 if (out.has(key)) continue
219 const b = ns.blocks.get(key)
220 if (!b) throw new Error(`missing node ${key}`)
221 out.set(key, b)
222 const node = MSTNode.deserialise(b.bytes)
223 for (const t of node.subtrees) if (t) stack.push(t)
224 }
225 return out
226}
227
228// ---- Firehose MST-inversion proof (slice 3b) ----
229//
230// A #commit's `blocks` CAR must be a MINIMAL, self-contained proof of the
231// old→new transition — not the whole tree (that would trip a relay's `tooBig`).
232// This ports millipds `repo_ops.py:apply_writes` step 2b + `atmst` proof.py:
233// blocks = created | deletion_proof_cids | inductive_proof_nodes (+ new records)
234// where `inductive_proof_nodes` = every MST node READ while replaying the ops
235// backward from the new root, which must reproduce the old (prevData) root.
236// The prior record CIDs travel in each op's `prev` (like millipds), so a cold
237// relay can invert with no old-tree state of its own.
238
239// A NodeStore overlay: writes land in an in-memory upper layer (NOT logged);
240// reads that fall through to `lower` are recorded in `.gets`. Mirrors millipds's
241// OverlayBlockStore(MemoryBlockStore, LoggingBlockStoreWrapper(bs)).
242export class OverlayNodeStore {
243 constructor (lower) { this.lower = lower; this.upper = new Map(); this.gets = new Set() }
244
245 storedNode (node) {
246 const bytes = node.serialise()
247 const cid = cidForBytesSync(bytes)
248 this.upper.set(cid.toString(), { cid, bytes })
249 return node
250 }
251
252 getNode (cid) {
253 if (cid === null || cid === undefined) return MSTNode.empty()
254 const k = cid.toString()
255 const u = this.upper.get(k)
256 if (u) return MSTNode.deserialise(u.bytes)
257 this.gets.add(k) // a read of a pre-existing node — part of the inductive proof
258 return this.lower.getNode(cid)
259 }
260}
261
262// Follow the search path for `key` from `rootCid`, collecting every node visited.
263// Returns { value: CID|null, proof: Set<cidStr> }. value===null ⇒ exclusion proof;
264// value!==null ⇒ inclusion proof. Port of atmst find_rpath_and_build_proof.
265export function findRpathProof (ns, rootCid, key) {
266 const proof = new Set()
267 let cid = rootCid
268 while (cid !== null && cid !== undefined) {
269 const node = ns.getNode(cid)
270 proof.add(cid.toString())
271 const i = node.gteIndex(key)
272 if (i < node.keys.length && node.keys[i] === key) return { value: node.vals[i], proof }
273 cid = node.subtrees[i] // descend the subtree left of the first key >= target
274 }
275 return { value: null, proof }
276}
277
278// Build a NodeStore holding exactly the given blocks (cid-string -> { cid, bytes }).
279export function nodeStoreFromBlocks (blocks) {
280 const ns = new NodeStore()
281 for (const [k, v] of blocks) ns.blocks.set(k, v)
282 return ns
283}
284
285// Compute the minimal firehose block set for a single commit's ops.
286// oldRecords / newRecords : [{ key, value: CID }] — the record sets BEFORE/AFTER.
287// ops : [{ action:'create'|'update'|'delete', path, cid: CID|null, prev: CID|null }]
288// Returns { blocks: Map<cidStr,{cid,bytes}>, dataCid, oldRoot } — MST nodes only
289// (record + commit blocks are added by the caller). Throws if the backward replay
290// does not reproduce the old root (i.e. the proof would be invalid).
291export function firehoseProofBlocks ({ oldRecords, newRecords, ops }) {
292 const oldB = buildMst(oldRecords)
293 const newB = buildMst(newRecords)
294 const oldRoot = oldB.root
295 const newRoot = newB.root
296
297 const oldSet = reachableBlocks(oldB.ns, oldRoot)
298 const newSet = reachableBlocks(newB.ns, newRoot)
299
300 // combined store holds every node of BOTH trees (proof reads span both)
301 const combined = new NodeStore()
302 for (const [k, v] of oldSet) combined.blocks.set(k, v)
303 for (const [k, v] of newSet) combined.blocks.set(k, v)
304
305 const proofCids = new Set()
306
307 // created: new-tree nodes not present in the old tree
308 for (const k of newSet.keys()) if (!oldSet.has(k)) proofCids.add(k)
309
310 // deletion proofs: for each delete, the exclusion-proof path in the NEW tree
311 for (const op of ops) {
312 if (op.action !== 'delete') continue
313 const { value, proof } = findRpathProof(combined, newRoot, op.path)
314 if (value !== null) throw new Error(`delete proof: ${op.path} still present in new tree`)
315 for (const c of proof) proofCids.add(c)
316 }
317
318 // inductive proof: replay ops backward from newRoot; every node read is required.
319 const overlay = new OverlayNodeStore(combined)
320 const w = new NodeWrangler(overlay)
321 let inv = newRoot
322 for (let i = ops.length - 1; i >= 0; i--) {
323 const op = ops[i]
324 if (op.action === 'create') {
325 inv = w.delRecord(inv, op.path)
326 } else { // update | delete → restore the prior value
327 if (op.prev === null || op.prev === undefined) throw new Error(`inversion: ${op.action} ${op.path} missing prev`)
328 inv = w.putRecord(inv, op.path, op.prev)
329 }
330 }
331 if (!inv.equals(oldRoot)) throw new Error('inversion proof failed: backward replay did not reproduce prevData root')
332 for (const k of overlay.gets) proofCids.add(k)
333
334 const blocks = new Map()
335 for (const k of proofCids) blocks.set(k, combined.blocks.get(k))
336 return { blocks, dataCid: newRoot, oldRoot }
337}
lib/repo.js +109 -0
……@@ -0,0 +1,109 @@
1// atproto repo — READ path (slice 1).
2//
3// A repo is a CAR whose root is a signed commit; the commit's `data` CID points at
4// the root of a Merkle Search Tree (MST) of records. This module reads a CAR, walks
5// the MST, and — most importantly — VERIFIES byte-exact fidelity by re-encoding every
6// block and checking its CID. Writing/mutating the MST is slice 2.
7
8import * as dagCbor from '@ipld/dag-cbor'
9import { cidForBytes, cidForBytesSync } from './cid.js'
10import { buildMst, reachableBlocks } from './mst.js'
11import { unsignedCommit, signCommit, commitCid } from './commit.js'
12import { readCar, writeCar } from './car.js'
13
14// CAR read/write moved to the bare-safe minimal codec in ./car.js (was @ipld/car).
15export { readCar, writeCar }
16
17export function getBlock (car, cid) {
18 const b = car.blocks.get(cid.toString())
19 if (!b) throw new Error(`missing block ${cid}`)
20 return b
21}
22
23export function decodeBlock (car, cid) {
24 return dagCbor.decode(getBlock(car, cid).bytes)
25}
26
27// The signed commit at the CAR root.
28export function loadCommit (car) {
29 if (car.roots.length !== 1) throw new Error(`expected 1 root, got ${car.roots.length}`)
30 const commit = decodeBlock(car, car.roots[0])
31 if (!commit || !commit.did || commit.data === undefined) throw new Error('root is not a commit')
32 return { cid: car.roots[0], value: commit }
33}
34
35// Walk the MST in key order, yielding { key, value: recordCid }. MST node shape:
36// { l: CID|null, e: [ { p: int, k: bytes, v: CID, t: CID|null } ] }
37// keys are prefix-compressed against the previous key in the SAME node.
38export function * walkMst (car, rootCid, _prevKey = '') {
39 const node = decodeBlock(car, rootCid)
40 if (!node || !Array.isArray(node.e)) throw new Error(`not an MST node: ${rootCid}`)
41 if (node.l) yield * walkMst(car, node.l)
42 let prev = ''
43 for (const e of node.e) {
44 const suffix = Buffer.from(e.k).toString('utf8')
45 const key = prev.slice(0, e.p) + suffix
46 prev = key
47 yield { key, value: e.v }
48 if (e.t) yield * walkMst(car, e.t)
49 }
50}
51
52// All records as { key, cid, value } in key order.
53export function listRecords (car) {
54 const commit = loadCommit(car)
55 const out = []
56 for (const { key, value } of walkMst(car, commit.value.data)) {
57 out.push({ key, cid: value, value: decodeBlock(car, value) })
58 }
59 return out
60}
61
62// Byte-exact fidelity check: re-encode every block and confirm its CID is unchanged.
63// Also confirms the commit's data CID and every MST value CID resolve to a present
64// block. Returns { blocks, records, ok, mismatches }.
65export async function verifyRepo (car) {
66 const mismatches = []
67 for (const [cidStr, { cid, bytes }] of car.blocks) {
68 // re-encode: decode then canonical-encode; must reproduce the exact bytes + CID
69 const reencoded = dagCbor.encode(dagCbor.decode(bytes))
70 const sameBytes = Buffer.from(reencoded).equals(Buffer.from(bytes))
71 const recomputed = await cidForBytes(reencoded)
72 if (!sameBytes || !recomputed.equals(cid)) {
73 mismatches.push({ cid: cidStr, sameBytes, recomputed: recomputed.toString() })
74 }
75 }
76 const commit = loadCommit(car)
77 getBlock(car, commit.value.data) // MST root must be present
78 const records = listRecords(car)
79 for (const r of records) getBlock(car, r.cid) // each record block present
80 return { blocks: car.blocks.size, records: records.length, ok: mismatches.length === 0, mismatches }
81}
82
83// ---- WRITE path (slice 3) ----
84
85// Originate a full atproto repo from records. Each record is
86// { collection, rkey, record } (or { key, value }). Returns the CAR bytes plus the
87// commit/data CIDs. `privateKey` is the atproto secp256k1 signing key.
88export async function createRepo ({ records, did, rev, privateKey }) {
89 const recordBlocks = new Map()
90 const mstRecords = []
91 for (const r of records) {
92 const key = r.key !== undefined ? r.key : `${r.collection}/${r.rkey}`
93 const value = r.record !== undefined ? r.record : r.value
94 const bytes = dagCbor.encode(value)
95 const cid = cidForBytesSync(bytes)
96 recordBlocks.set(cid.toString(), { cid, bytes })
97 mstRecords.push({ key, value: cid })
98 }
99 const { root: dataCid, ns } = buildMst(mstRecords)
100 const mstBlocks = reachableBlocks(ns, dataCid)
101
102 const signed = signCommit(unsignedCommit({ did, rev, data: dataCid }), privateKey)
103 const cCid = commitCid(signed)
104 const commitBytes = dagCbor.encode(signed)
105
106 const blocks = [{ cid: cCid, bytes: commitBytes }, ...mstBlocks.values(), ...recordBlocks.values()]
107 const carBytes = await writeCar(cCid, blocks)
108 return { carBytes, commitCid: cCid, dataCid, signedCommit: signed }
109}
package-lock.json +1114 -0
……@@ -0,0 +1,1114 @@
1{
2 "name": "atproto-repo",
3 "version": "0.0.1",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "atproto-repo",
9 "version": "0.0.1",
10 "hasInstallScript": true,
11 "dependencies": {
12 "@ipld/car": "^5.4.0",
13 "@ipld/dag-cbor": "^9.2.1",
14 "@noble/curves": "^1.9.7",
15 "@noble/hashes": "^1.5.0",
16 "multiformats": "^13.3.0"
17 },
18 "devDependencies": {
19 "atproto-identity": "file:../atproto-identity",
20 "brittle": "^4.0.2"
21 }
22 },
23 "../atproto-identity": {
24 "version": "0.0.1",
25 "dev": true,
26 "hasInstallScript": true,
27 "dependencies": {
28 "@ipld/dag-cbor": "^9.2.1",
29 "@noble/curves": "^1.6.0",
30 "@noble/hashes": "^1.5.0",
31 "@scure/bip32": "^1.5.0",
32 "@scure/bip39": "^1.4.0",
33 "keet-identity-key": "^3.2.0",
34 "multiformats": "^13.3.0"
35 },
36 "devDependencies": {
37 "brittle": "^4.0.2"
38 }
39 },
40 "node_modules/@ipld/car": {
41 "version": "5.4.6",
42 "resolved": "https://registry.npmjs.org/@ipld/car/-/car-5.4.6.tgz",
43 "integrity": "sha512-f1Iyb9ci8B/TGCyjAuSQ6BOLiy2u8en7rB+SumbyweqXDu3gxYJwYbGzoposKjOzc4CeGlkEbwEw8REoekYAKQ==",
44 "license": "Apache-2.0 OR MIT",
45 "dependencies": {
46 "@ipld/dag-cbor": "^10.0.1",
47 "cborg": "^5.0.0",
48 "multiformats": "^14.0.0",
49 "varint": "^6.0.0"
50 },
51 "engines": {
52 "node": ">=16.0.0",
53 "npm": ">=7.0.0"
54 }
55 },
56 "node_modules/@ipld/car/node_modules/@ipld/dag-cbor": {
57 "version": "10.0.1",
58 "resolved": "https://registry.npmjs.org/@ipld/dag-cbor/-/dag-cbor-10.0.1.tgz",
59 "integrity": "sha512-nF07iiZPqduSXyMxc0jGANArRHFa9hjMQpQlgLOV2O/3xI1CNb5sXhvbmigbMiz5owSGi0Oq10VtauupMojuiw==",
60 "license": "Apache-2.0 OR MIT",
61 "dependencies": {
62 "cborg": "^5.0.1",
63 "multiformats": "^14.0.0"
64 }
65 },
66 "node_modules/@ipld/car/node_modules/multiformats": {
67 "version": "14.0.4",
68 "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-14.0.4.tgz",
69 "integrity": "sha512-+SXItmOUWzT0kjlIfkZ4NawJaC9jW/mPlyn902V9Qgpc7YDwdEiwqbiZxTyvC0XWh1jZguXca3A/WQ+UOPRLUA==",
70 "license": "Apache-2.0 OR MIT"
71 },
72 "node_modules/@ipld/dag-cbor": {
73 "version": "9.2.7",
74 "resolved": "https://registry.npmjs.org/@ipld/dag-cbor/-/dag-cbor-9.2.7.tgz",
75 "integrity": "sha512-ZmfXmElRWATr+hoUTSAOr6HUcjVhOcNHDqgczc76qte2DHHFEK0ZhNzUcdTDQhF/VSIvf2ioaRTRLWwLc83sNw==",
76 "license": "Apache-2.0 OR MIT",
77 "dependencies": {
78 "cborg": "^5.0.1",
79 "multiformats": "^13.1.0"
80 }
81 },
82 "node_modules/@jridgewell/resolve-uri": {
83 "version": "3.1.2",
84 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
85 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
86 "dev": true,
87 "license": "MIT",
88 "engines": {
89 "node": ">=6.0.0"
90 }
91 },
92 "node_modules/@jridgewell/sourcemap-codec": {
93 "version": "1.5.5",
94 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
95 "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
96 "dev": true,
97 "license": "MIT"
98 },
99 "node_modules/@jridgewell/trace-mapping": {
100 "version": "0.3.31",
101 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
102 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
103 "dev": true,
104 "license": "MIT",
105 "dependencies": {
106 "@jridgewell/resolve-uri": "^3.1.0",
107 "@jridgewell/sourcemap-codec": "^1.4.14"
108 }
109 },
110 "node_modules/@noble/curves": {
111 "version": "1.9.7",
112 "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
113 "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
114 "license": "MIT",
115 "dependencies": {
116 "@noble/hashes": "1.8.0"
117 },
118 "engines": {
119 "node": "^14.21.3 || >=16"
120 },
121 "funding": {
122 "url": "https://paulmillr.com/funding/"
123 }
124 },
125 "node_modules/@noble/hashes": {
126 "version": "1.8.0",
127 "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
128 "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
129 "license": "MIT",
130 "engines": {
131 "node": "^14.21.3 || >=16"
132 },
133 "funding": {
134 "url": "https://paulmillr.com/funding/"
135 }
136 },
137 "node_modules/@types/istanbul-lib-coverage": {
138 "version": "2.0.6",
139 "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
140 "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
141 "dev": true,
142 "license": "MIT"
143 },
144 "node_modules/atproto-identity": {
145 "resolved": "../atproto-identity",
146 "link": true
147 },
148 "node_modules/b4a": {
149 "version": "1.8.1",
150 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
151 "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==",
152 "dev": true,
153 "license": "Apache-2.0",
154 "peerDependencies": {
155 "react-native-b4a": "*"
156 },
157 "peerDependenciesMeta": {
158 "react-native-b4a": {
159 "optional": true
160 }
161 }
162 },
163 "node_modules/bare-abort": {
164 "version": "2.0.13",
165 "resolved": "https://registry.npmjs.org/bare-abort/-/bare-abort-2.0.13.tgz",
166 "integrity": "sha512-zdc8l88eB11Jsz5rDd6sCAgv2kUFXgdrZWoMlgU6JMkfAi1/uuGFC3IEHswKbIRQTk5H3T5CMuechsXYxiaHlQ==",
167 "dev": true,
168 "license": "Apache-2.0"
169 },
170 "node_modules/bare-addon-resolve": {
171 "version": "1.10.0",
172 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
173 "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
174 "dev": true,
175 "license": "Apache-2.0",
176 "dependencies": {
177 "bare-module-resolve": "^1.10.0",
178 "bare-semver": "^1.0.0"
179 },
180 "peerDependencies": {
181 "bare-url": "*"
182 },
183 "peerDependenciesMeta": {
184 "bare-url": {
185 "optional": true
186 }
187 }
188 },
189 "node_modules/bare-ansi-escapes": {
190 "version": "2.2.3",
191 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
192 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
193 "dev": true,
194 "license": "Apache-2.0",
195 "dependencies": {
196 "bare-stream": "^2.6.5"
197 },
198 "peerDependencies": {
199 "bare-buffer": "*"
200 },
201 "peerDependenciesMeta": {
202 "bare-buffer": {
203 "optional": true
204 }
205 }
206 },
207 "node_modules/bare-assert": {
208 "version": "1.2.0",
209 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
210 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
211 "dev": true,
212 "license": "Apache-2.0",
213 "dependencies": {
214 "bare-inspect": "^3.1.2"
215 }
216 },
217 "node_modules/bare-buffer": {
218 "version": "3.6.1",
219 "resolved": "https://registry.npmjs.org/bare-buffer/-/bare-buffer-3.6.1.tgz",
220 "integrity": "sha512-8mzp60t6jdSD3cEGmyxAV1YrAN2+mnxiUvBPQHJJ4WNk0hSVLJGSIEolRj09ZP8yt/+oSj2N2f5kDVW8297n4A==",
221 "dev": true,
222 "license": "Apache-2.0",
223 "engines": {
224 "bare": ">=1.20.0"
225 }
226 },
227 "node_modules/bare-bundle": {
228 "version": "1.10.0",
229 "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
230 "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
231 "dev": true,
232 "license": "Apache-2.0",
233 "peerDependencies": {
234 "bare-buffer": "*",
235 "bare-url": "*"
236 },
237 "peerDependenciesMeta": {
238 "bare-buffer": {
239 "optional": true
240 },
241 "bare-url": {
242 "optional": true
243 }
244 }
245 },
246 "node_modules/bare-channel": {
247 "version": "5.2.4",
248 "resolved": "https://registry.npmjs.org/bare-channel/-/bare-channel-5.2.4.tgz",
249 "integrity": "sha512-enkaOtvXUiZsLBSbataxV/QBjehCOK+SUTd2JQWUYW2iIOufpPu9jyZ9bqJqkEquktrK/rpEMVEmKsPzoci/sA==",
250 "dev": true,
251 "license": "Apache-2.0",
252 "dependencies": {
253 "bare-events": "^2.0.0",
254 "bare-stream": "^2.7.0",
255 "bare-structured-clone": "^1.4.0"
256 },
257 "engines": {
258 "bare": ">=1.7.0"
259 }
260 },
261 "node_modules/bare-cov": {
262 "version": "1.2.2",
263 "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.2.tgz",
264 "integrity": "sha512-d/b4HdL5ohZDwBvDegJeSkmo1X2MGlm22jXzuY2D7wl2W1+7nk/b7+HMogwgUa+zi8rVVRJQKHCHPHYBJpvlvw==",
265 "dev": true,
266 "license": "Apache-2.0",
267 "dependencies": {
268 "bare-fs": "^4.1.2",
269 "bare-inspector": "^6.0.1",
270 "bare-path": "^3.0.0",
271 "bare-process": "^4.2.1",
272 "bare-url": "^2.1.5",
273 "bare-v8-to-istanbul": "^1.0.2",
274 "picomatch": "^4.0.2",
275 "which-runtime": "^1.2.1"
276 }
277 },
278 "node_modules/bare-crypto": {
279 "version": "1.15.3",
280 "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.15.3.tgz",
281 "integrity": "sha512-macV9lbyJTsLPRXJkBtz8ivTGEo3LCyJInLT9IB/PWJ7pRXwvHs/FP4bx/fWw+HZkiepIYCAV2cuU5CR92XWCw==",
282 "dev": true,
283 "license": "Apache-2.0",
284 "dependencies": {
285 "bare-assert": "^1.2.0",
286 "bare-stream": "^2.6.3"
287 },
288 "peerDependencies": {
289 "bare-buffer": "*"
290 },
291 "peerDependenciesMeta": {
292 "bare-buffer": {
293 "optional": true
294 }
295 }
296 },
297 "node_modules/bare-debug-log": {
298 "version": "2.0.0",
299 "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
300 "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
301 "dev": true,
302 "license": "Apache-2.0",
303 "dependencies": {
304 "bare-os": "^3.0.1"
305 }
306 },
307 "node_modules/bare-dns": {
308 "version": "2.1.4",
309 "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
310 "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==",
311 "dev": true,
312 "license": "Apache-2.0",
313 "engines": {
314 "bare": ">=1.7.0"
315 }
316 },
317 "node_modules/bare-encoding": {
318 "version": "1.0.3",
319 "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
320 "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
321 "dev": true,
322 "license": "Apache-2.0",
323 "peerDependencies": {
324 "bare-buffer": "*"
325 },
326 "peerDependenciesMeta": {
327 "bare-buffer": {
328 "optional": true
329 }
330 }
331 },
332 "node_modules/bare-env": {
333 "version": "3.0.0",
334 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
335 "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
336 "dev": true,
337 "license": "Apache-2.0",
338 "dependencies": {
339 "bare-os": "^3.0.1"
340 }
341 },
342 "node_modules/bare-events": {
343 "version": "2.9.1",
344 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz",
345 "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==",
346 "dev": true,
347 "license": "Apache-2.0",
348 "peerDependencies": {
349 "bare-abort-controller": "*"
350 },
351 "peerDependenciesMeta": {
352 "bare-abort-controller": {
353 "optional": true
354 }
355 }
356 },
357 "node_modules/bare-format": {
358 "version": "1.0.2",
359 "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.2.tgz",
360 "integrity": "sha512-GswdhnOnP9QtwRbrf4wLApw5widkaLMsLe2XOs35fQD2YfEN1ApoGka+cZ7PfvzxMgfYXmMhj/2OGlVn5/Dxgw==",
361 "dev": true,
362 "license": "Apache-2.0",
363 "dependencies": {
364 "bare-inspect": "^3.0.0"
365 }
366 },
367 "node_modules/bare-fs": {
368 "version": "4.7.3",
369 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.3.tgz",
370 "integrity": "sha512-xRgplks8SvcKkdlv2M6Z2LZmRsmqd+x0nXXGXeMEjwdibj1HSDrlnqBRLeYdMvsgCox7Bq0e+DHwfczOfsn6IA==",
371 "dev": true,
372 "license": "Apache-2.0",
373 "dependencies": {
374 "bare-events": "^2.5.4",
375 "bare-path": "^3.0.0",
376 "bare-stream": "^2.6.4",
377 "bare-url": "^2.2.2",
378 "fast-fifo": "^1.3.2"
379 },
380 "engines": {
381 "bare": ">=1.16.0"
382 },
383 "peerDependencies": {
384 "bare-buffer": "*"
385 },
386 "peerDependenciesMeta": {
387 "bare-buffer": {
388 "optional": true
389 }
390 }
391 },
392 "node_modules/bare-hrtime": {
393 "version": "2.1.1",
394 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
395 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
396 "dev": true,
397 "license": "Apache-2.0"
398 },
399 "node_modules/bare-http-parser": {
400 "version": "1.1.5",
401 "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.1.5.tgz",
402 "integrity": "sha512-sPaDetLbWRmth04JmuTCvw/hoNdWJvYUJN8n1tYdGW3HM0mMnCvK2f0oIxE6HK7iOq+WlsRzEMg1LT/b0wGbLQ==",
403 "dev": true,
404 "license": "Apache-2.0"
405 },
406 "node_modules/bare-http1": {
407 "version": "4.5.7",
408 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.5.7.tgz",
409 "integrity": "sha512-PRuzs9ywt4vUvrC3mnHhQyaQfLYzdFy8XKOH0oKeKmYfyYYUqXBkdbJE2csESLBxJEbKDBjxPGLU+Qa0l1ds4A==",
410 "dev": true,
411 "license": "Apache-2.0",
412 "dependencies": {
413 "bare-events": "^2.6.0",
414 "bare-http-parser": "^1.1.1",
415 "bare-stream": "^2.10.0",
416 "bare-tcp": "^2.2.0"
417 },
418 "peerDependencies": {
419 "bare-buffer": "*",
420 "bare-url": "*"
421 },
422 "peerDependenciesMeta": {
423 "bare-buffer": {
424 "optional": true
425 },
426 "bare-url": {
427 "optional": true
428 }
429 }
430 },
431 "node_modules/bare-https": {
432 "version": "3.0.0",
433 "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-3.0.0.tgz",
434 "integrity": "sha512-W1GRSCzn+xXKf5bMcPs/hg6Ga1bxPqb7owGfS+tvlBQfPe5Q2STcanRuKZrgU60v5uKrhXH5cgWwM+DLqvXZgQ==",
435 "dev": true,
436 "license": "Apache-2.0",
437 "dependencies": {
438 "bare-http1": "^4.4.0",
439 "bare-tcp": "^2.2.0",
440 "bare-tls": "^3.0.0"
441 }
442 },
443 "node_modules/bare-inspect": {
444 "version": "3.1.4",
445 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
446 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
447 "dev": true,
448 "license": "Apache-2.0",
449 "dependencies": {
450 "bare-ansi-escapes": "^2.1.0",
451 "bare-type": "^1.0.0"
452 },
453 "engines": {
454 "bare": ">=1.18.0"
455 }
456 },
457 "node_modules/bare-inspector": {
458 "version": "6.1.0",
459 "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-6.1.0.tgz",
460 "integrity": "sha512-PRxmZ4gF+K3TLzGubgRFvzdECybTCSKackgNsAdd4e7SdvICxMkGj+p5iX7bSKYSmgDnxgCR+2Z6UXmWykKhvw==",
461 "dev": true,
462 "license": "Apache-2.0",
463 "dependencies": {
464 "bare-events": "^2.1.0",
465 "bare-http1": "^4.0.0",
466 "bare-stream": "^2.0.0",
467 "bare-url": "^2.0.0",
468 "bare-ws": "^3.0.0"
469 },
470 "engines": {
471 "bare": ">=1.29.0"
472 },
473 "peerDependencies": {
474 "bare-tcp": "*"
475 },
476 "peerDependenciesMeta": {
477 "bare-tcp": {
478 "optional": true
479 }
480 }
481 },
482 "node_modules/bare-module": {
483 "version": "6.4.0",
484 "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.4.0.tgz",
485 "integrity": "sha512-Yn4V5g5EqGQL4LYUOmt7fjKzj2JPWyJOqE3lPoeZwfUH5rk4CKUfZj6JhDwbzhBYCqqmUgjgQ5aY8cihAPILLA==",
486 "dev": true,
487 "license": "Apache-2.0",
488 "dependencies": {
489 "bare-bundle": "^1.3.0",
490 "bare-module-lexer": "^1.0.0",
491 "bare-module-resolve": "^1.8.0",
492 "bare-path": "^3.0.0",
493 "bare-type-stripper": "^0.1.2",
494 "bare-url": "^2.0.1"
495 },
496 "engines": {
497 "bare": ">=1.29.4"
498 },
499 "peerDependencies": {
500 "bare-buffer": "*"
501 },
502 "peerDependenciesMeta": {
503 "bare-buffer": {
504 "optional": true
505 }
506 }
507 },
508 "node_modules/bare-module-lexer": {
509 "version": "1.6.0",
510 "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.6.0.tgz",
511 "integrity": "sha512-uVDdbh6EnypLg5K4Gz5GkFloXiJu0QN5S1JNiKdKbiLoyTjUqIXKnHR/8fb2QejDT/+5BNlnqlbkAAg1iEZSFA==",
512 "dev": true,
513 "license": "Apache-2.0",
514 "dependencies": {
515 "require-addon": "^1.0.2"
516 },
517 "peerDependencies": {
518 "bare-buffer": "*"
519 },
520 "peerDependenciesMeta": {
521 "bare-buffer": {
522 "optional": true
523 }
524 }
525 },
526 "node_modules/bare-module-resolve": {
527 "version": "1.12.2",
528 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.2.tgz",
529 "integrity": "sha512-j+hiD5k99qec4KjJvYsI67q5AOBifmy9JG3oeMVxTmvrhn2sIdp8StrUvZu4YNgwTpO+NhniQG16N1ETDe1k5w==",
530 "dev": true,
531 "license": "Apache-2.0",
532 "dependencies": {
533 "bare-semver": "^1.0.0"
534 },
535 "peerDependencies": {
536 "bare-url": "*"
537 },
538 "peerDependenciesMeta": {
539 "bare-url": {
540 "optional": true
541 }
542 }
543 },
544 "node_modules/bare-net": {
545 "version": "2.3.2",
546 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.3.2.tgz",
547 "integrity": "sha512-I+yz+pqbYsBkxDsnu5vkKvy7RSNY9CcAvu2jZT6PsmdXJQG1i3dmD5V7xc3334OVp2absgtUEYLmmuNFlphBzg==",
548 "dev": true,
549 "license": "Apache-2.0",
550 "dependencies": {
551 "bare-events": "^2.2.2",
552 "bare-pipe": "^4.0.0",
553 "bare-stream": "^2.0.0",
554 "bare-tcp": "^2.0.0"
555 }
556 },
557 "node_modules/bare-os": {
558 "version": "3.9.3",
559 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.3.tgz",
560 "integrity": "sha512-fF4Q7QsyKVF5Rj0qvI8BgUNjqzC2JvQlpTaPLjVJVxYVUX5Zr9un+y3w1HmA4nNKdFmRBT8z/WmrjvXzXVerKQ==",
561 "dev": true,
562 "license": "Apache-2.0",
563 "engines": {
564 "bare": ">=1.14.0"
565 }
566 },
567 "node_modules/bare-path": {
568 "version": "3.0.1",
569 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.1.tgz",
570 "integrity": "sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==",
571 "dev": true,
572 "license": "Apache-2.0",
573 "dependencies": {
574 "bare-os": "^3.0.1"
575 }
576 },
577 "node_modules/bare-pipe": {
578 "version": "4.2.2",
579 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.2.2.tgz",
580 "integrity": "sha512-Wkf6//CmKS8inHrkgi3X/xNMeaGfs2I0ayLM0B8cbPB4DxYHcoes6Ng+muCBfXNIJ9qmEjwDDJjAUz7nRxxjgQ==",
581 "dev": true,
582 "license": "Apache-2.0",
583 "peer": true,
584 "dependencies": {
585 "bare-events": "^2.0.0",
586 "bare-stream": "^2.0.0"
587 },
588 "engines": {
589 "bare": ">=1.16.0"
590 }
591 },
592 "node_modules/bare-posix": {
593 "version": "1.0.1",
594 "resolved": "https://registry.npmjs.org/bare-posix/-/bare-posix-1.0.1.tgz",
595 "integrity": "sha512-b4quA8dyRVvX0aFIAIiev5zFATHwIUGr42Gmt+CqiBNg/bdsNkrj4bbkeotMOMiljJS4DCME5kywJO/TFwKo1A==",
596 "dev": true,
597 "license": "Apache-2.0"
598 },
599 "node_modules/bare-process": {
600 "version": "4.5.0",
601 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.5.0.tgz",
602 "integrity": "sha512-NTtYDiwleJjWWNg4+yzS6B/RovL/LLPHVvoOM+18W+dYAXCmky0zFfN66hqqBbrlGAlb52/QgKrdzPR3x1QyHA==",
603 "dev": true,
604 "license": "Apache-2.0",
605 "dependencies": {
606 "bare-abort": "^2.0.13",
607 "bare-env": "^3.0.0",
608 "bare-events": "^2.3.1",
609 "bare-hrtime": "^2.0.0",
610 "bare-os": "^3.7.1",
611 "bare-posix": "^1.0.1",
612 "bare-signals": "^4.0.0",
613 "bare-stdio": "^1.0.1"
614 }
615 },
616 "node_modules/bare-semver": {
617 "version": "1.1.0",
618 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.1.0.tgz",
619 "integrity": "sha512-1Hw5qJ7hXdVt3uPUqjeFTuxyvBUJauvz5A1I2jk8gzjZMHp04n//6nV9MDbG9CMw78JHY2lGV0w6s//LrASm2w==",
620 "dev": true,
621 "license": "Apache-2.0"
622 },
623 "node_modules/bare-signals": {
624 "version": "4.2.0",
625 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz",
626 "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==",
627 "dev": true,
628 "license": "Apache-2.0",
629 "dependencies": {
630 "bare-events": "^2.5.3",
631 "bare-os": "^3.3.1"
632 },
633 "engines": {
634 "bare": ">=1.7.0"
635 }
636 },
637 "node_modules/bare-stdio": {
638 "version": "1.0.2",
639 "resolved": "https://registry.npmjs.org/bare-stdio/-/bare-stdio-1.0.2.tgz",
640 "integrity": "sha512-3WJDqtvVGP4f+j68kyEC05umOYNwKJ1xG+YAXL8yZ605WgNqiRhVaFq+mVIhBt2eKNp7pa5vCQdhOt1pNh79SA==",
641 "dev": true,
642 "license": "Apache-2.0",
643 "dependencies": {
644 "bare-fs": "^4.5.2",
645 "bare-pipe": "^4.1.5",
646 "bare-tty": "^5.0.3"
647 }
648 },
649 "node_modules/bare-stream": {
650 "version": "2.13.3",
651 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz",
652 "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==",
653 "dev": true,
654 "license": "Apache-2.0",
655 "dependencies": {
656 "b4a": "^1.8.1",
657 "streamx": "^2.25.0",
658 "teex": "^1.0.1"
659 },
660 "peerDependencies": {
661 "bare-abort-controller": "*",
662 "bare-buffer": "*",
663 "bare-events": "*"
664 },
665 "peerDependenciesMeta": {
666 "bare-abort-controller": {
667 "optional": true
668 },
669 "bare-buffer": {
670 "optional": true
671 },
672 "bare-events": {
673 "optional": true
674 }
675 }
676 },
677 "node_modules/bare-structured-clone": {
678 "version": "1.6.0",
679 "resolved": "https://registry.npmjs.org/bare-structured-clone/-/bare-structured-clone-1.6.0.tgz",
680 "integrity": "sha512-AZjEERyqF7kAuudlmgrweT2KwwWM0LsGG4Gx/bWyFwJrKU7E3wNG8c09z2DIrdw93p3vDtfOX8fyHOcXfy5m+g==",
681 "dev": true,
682 "license": "Apache-2.0",
683 "dependencies": {
684 "bare-buffer": "^3.6.0",
685 "bare-type": "^1.1.0",
686 "bare-url": "^2.4.0",
687 "compact-encoding": "^3.0.1"
688 },
689 "engines": {
690 "bare": ">=1.2.0"
691 }
692 },
693 "node_modules/bare-stylize": {
694 "version": "0.0.1",
695 "resolved": "https://registry.npmjs.org/bare-stylize/-/bare-stylize-0.0.1.tgz",
696 "integrity": "sha512-l3MjmIl476bWijYWf3RbE+osl4iuXSOMudzp0vAqzIK7gPgn/+G3oAxp8Oin9CFF911KBP0LO9kts8Ci8mGZaQ==",
697 "dev": true,
698 "license": "Apache-2.0",
699 "dependencies": {
700 "bare-ansi-escapes": "^2.2.3",
701 "bare-process": "^4.2.1"
702 }
703 },
704 "node_modules/bare-subprocess": {
705 "version": "5.2.3",
706 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.3.tgz",
707 "integrity": "sha512-07wwswlV7M3sC9IykbZRZ/jHAkrXFWVLqdBWGv1y0ojCimtRD9hGwxdHmR5FUFmDUZLNsBmTYJNQqgio5+A85Q==",
708 "dev": true,
709 "license": "Apache-2.0",
710 "dependencies": {
711 "bare-env": "^3.0.0",
712 "bare-events": "^2.5.4",
713 "bare-os": "^3.0.1",
714 "bare-pipe": "^4.0.0",
715 "bare-url": "^2.2.2"
716 },
717 "engines": {
718 "bare": ">=1.7.0"
719 },
720 "peerDependencies": {
721 "bare-buffer": "*"
722 },
723 "peerDependenciesMeta": {
724 "bare-buffer": {
725 "optional": true
726 }
727 }
728 },
729 "node_modules/bare-tcp": {
730 "version": "2.5.1",
731 "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.5.1.tgz",
732 "integrity": "sha512-N32OosAegmGzksr/huxIYLU/uaUARte6utO+vAu/EPQztMM7iAC55VeQSfzr2/XBJoz2OIETMvmU7/sdfiOGjw==",
733 "dev": true,
734 "license": "Apache-2.0",
735 "dependencies": {
736 "bare-dns": "^2.0.4",
737 "bare-events": "^2.5.4",
738 "bare-stream": "^2.6.4"
739 },
740 "engines": {
741 "bare": ">=1.16.0"
742 },
743 "peerDependencies": {
744 "bare-pipe": "*"
745 },
746 "peerDependenciesMeta": {
747 "bare-pipe": {
748 "optional": true
749 }
750 }
751 },
752 "node_modules/bare-tls": {
753 "version": "3.1.7",
754 "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-3.1.7.tgz",
755 "integrity": "sha512-AMw8tJlb3LhzAmhgXRcjDrTlNxR3gXXyj6G8eU9iwvCFtiUBD8MxAW7bwunA1gXDukgo40A970jX0APc2jMU7A==",
756 "dev": true,
757 "license": "Apache-2.0",
758 "dependencies": {
759 "bare-net": "^2.0.1",
760 "bare-stream": "^2.6.4"
761 },
762 "engines": {
763 "bare": ">=1.7.0"
764 }
765 },
766 "node_modules/bare-tty": {
767 "version": "5.1.1",
768 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.1.1.tgz",
769 "integrity": "sha512-cXCMAlcLCFTCWwCtXBIhDuXa4hutTEbrHHIK5pvSSb4BLaSGJ9tvxVN3mvGOvXXqONS7l04mTiP+eIUlZKI92g==",
770 "dev": true,
771 "license": "Apache-2.0",
772 "dependencies": {
773 "bare-events": "^2.2.0",
774 "bare-signals": "^4.0.0",
775 "bare-stream": "^2.0.0"
776 },
777 "engines": {
778 "bare": ">=1.16.0"
779 }
780 },
781 "node_modules/bare-type": {
782 "version": "1.1.0",
783 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
784 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
785 "dev": true,
786 "license": "Apache-2.0",
787 "engines": {
788 "bare": ">=1.2.0"
789 }
790 },
791 "node_modules/bare-type-stripper": {
792 "version": "0.1.4",
793 "resolved": "https://registry.npmjs.org/bare-type-stripper/-/bare-type-stripper-0.1.4.tgz",
794 "integrity": "sha512-FdZhp9XEnQpj8AWFmIft/sVUyKS9XSmB6PhcxBHhuEDxxZM5Kkt8+kFS7eEpLXR7TkaRkNpSENoGH/8lpAmtkA==",
795 "dev": true,
796 "license": "Apache-2.0",
797 "dependencies": {
798 "require-addon": "^1.0.2"
799 },
800 "peerDependencies": {
801 "bare-buffer": "*"
802 },
803 "peerDependenciesMeta": {
804 "bare-buffer": {
805 "optional": true
806 }
807 }
808 },
809 "node_modules/bare-url": {
810 "version": "2.4.5",
811 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz",
812 "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==",
813 "dev": true,
814 "license": "Apache-2.0",
815 "dependencies": {
816 "bare-path": "^3.0.0"
817 }
818 },
819 "node_modules/bare-utils": {
820 "version": "1.6.0",
821 "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.6.0.tgz",
822 "integrity": "sha512-WhQEIkkAxkSnW7u1QgrI0AfNm5JpMruETXeYsb5qnkBJ0TTfNKygZmsh6rkoHBANaV+C/7Jed7bJP9OmEHG7rQ==",
823 "dev": true,
824 "license": "Apache-2.0",
825 "dependencies": {
826 "bare-debug-log": "^2.0.0",
827 "bare-encoding": "^1.0.0",
828 "bare-format": "^1.0.0",
829 "bare-inspect": "^3.0.0",
830 "bare-stylize": "^0.0.1",
831 "bare-type": "^1.0.6"
832 }
833 },
834 "node_modules/bare-v8-to-istanbul": {
835 "version": "1.0.2",
836 "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
837 "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==",
838 "dev": true,
839 "license": "Apache-2.0",
840 "dependencies": {
841 "bare-assert": "^1.0.2",
842 "bare-fs": "^4.1.2",
843 "bare-module": "^6.1.2",
844 "bare-path": "^3.0.0",
845 "bare-process": "^4.2.0",
846 "bare-url": "^2.1.5",
847 "bare-utils": "^1.2.0",
848 "v8-to-istanbul": "^9.3.0",
849 "which-runtime": "^1.2.1"
850 }
851 },
852 "node_modules/bare-ws": {
853 "version": "3.0.0",
854 "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-3.0.0.tgz",
855 "integrity": "sha512-q2v0UIQ0cFQBXQMp+0FRaoSk1EoYgOhzO7yio0TqBV6Rkfot97mbBYxb1ssw5faVCisVaFxQYY8113SMLYQBow==",
856 "dev": true,
857 "license": "Apache-2.0",
858 "dependencies": {
859 "bare-crypto": "^1.2.0",
860 "bare-events": "^2.3.1",
861 "bare-http1": "^4.0.0",
862 "bare-https": "^3.0.0",
863 "bare-stream": "^2.1.2"
864 },
865 "peerDependencies": {
866 "bare-buffer": "*",
867 "bare-url": "*"
868 },
869 "peerDependenciesMeta": {
870 "bare-buffer": {
871 "optional": true
872 },
873 "bare-url": {
874 "optional": true
875 }
876 }
877 },
878 "node_modules/brittle": {
879 "version": "4.0.2",
880 "resolved": "https://registry.npmjs.org/brittle/-/brittle-4.0.2.tgz",
881 "integrity": "sha512-frnh7gB76uiSmFAf4+uIsaKjygMEvzUbN/GhxS+30WKz0pd/COBVuj+O9TmW9t0kIzJTafv8xquV6DriW316Vw==",
882 "dev": true,
883 "license": "Apache-2.0",
884 "dependencies": {
885 "b4a": "^1.8.0",
886 "bare-assert": "^1.2.0",
887 "bare-buffer": "^3.6.0",
888 "bare-channel": "^5.2.3",
889 "bare-cov": "^1.2.1",
890 "bare-fs": "^4.5.5",
891 "bare-os": "^3.7.1",
892 "bare-path": "^3.0.0",
893 "bare-process": "^4.3.0",
894 "bare-subprocess": "^5.2.2",
895 "bare-url": "^2.3.2",
896 "error-stack-parser": "^2.1.4",
897 "globbie": "^1.0.3",
898 "paparam": "^1.10.0",
899 "same-object": "^1.0.2",
900 "streamx": "^2.25.0",
901 "test-tmp": "^1.4.0",
902 "tmatch": "^5.0.0",
903 "which-runtime": "^1.3.2"
904 },
905 "bin": {
906 "brittle": "advise.js",
907 "brittle-bare": "brittle-bare.js",
908 "brittle-make-test": "make-test.js",
909 "brittle-node": "brittle-node.js"
910 }
911 },
912 "node_modules/cborg": {
913 "version": "5.1.6",
914 "resolved": "https://registry.npmjs.org/cborg/-/cborg-5.1.6.tgz",
915 "integrity": "sha512-apu/mOSODJ98JXBz3cbiv/gAFX4oaCZQQ53+8b4IPjc1wylYxd8eJis87QP7ic4AOE4O02j02jjfUgVZUJIIvA==",
916 "license": "Apache-2.0",
917 "bin": {
918 "cborg": "lib/bin.js"
919 }
920 },
921 "node_modules/compact-encoding": {
922 "version": "3.3.0",
923 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-3.3.0.tgz",
924 "integrity": "sha512-e64XyzlBvTRJ3iScuU/U2w25Dglkm7fhrRbrGaHIwuTlNnzjRKMaQBCVl7mJRvZg7wI5a9wX1IVTXCuaAANNkw==",
925 "dev": true,
926 "license": "Apache-2.0",
927 "dependencies": {
928 "b4a": "^1.3.0"
929 }
930 },
931 "node_modules/convert-source-map": {
932 "version": "2.0.0",
933 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
934 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
935 "dev": true,
936 "license": "MIT"
937 },
938 "node_modules/error-stack-parser": {
939 "version": "2.1.4",
940 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
941 "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
942 "dev": true,
943 "license": "MIT",
944 "dependencies": {
945 "stackframe": "^1.3.4"
946 }
947 },
948 "node_modules/events-universal": {
949 "version": "1.0.1",
950 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
951 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
952 "dev": true,
953 "license": "Apache-2.0",
954 "dependencies": {
955 "bare-events": "^2.7.0"
956 }
957 },
958 "node_modules/fast-fifo": {
959 "version": "1.3.2",
960 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
961 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
962 "dev": true,
963 "license": "MIT"
964 },
965 "node_modules/globbie": {
966 "version": "1.0.3",
967 "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz",
968 "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==",
969 "dev": true,
970 "license": "Apache-2.0",
971 "dependencies": {
972 "bare-fs": "^4.1.2",
973 "bare-path": "^3.0.0",
974 "bare-process": "^4.2.1",
975 "picomatch": "^4.0.2"
976 }
977 },
978 "node_modules/multiformats": {
979 "version": "13.4.2",
980 "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.2.tgz",
981 "integrity": "sha512-eh6eHCrRi1+POZ3dA+Dq1C6jhP1GNtr9CRINMb67OKzqW9I5DUuZM/3jLPlzhgpGeiNUlEGEbkCYChXMCc/8DQ==",
982 "license": "Apache-2.0 OR MIT"
983 },
984 "node_modules/paparam": {
985 "version": "1.10.1",
986 "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.1.tgz",
987 "integrity": "sha512-viyQI64VIja0Za3njIzhoEP8ZVkgPowhZPuG0E96NwBfYJ6ZIyrrlhWGtFPkdN7eYLl2L8CTWL+wla0evm1KKQ==",
988 "dev": true,
989 "license": "Apache-2.0"
990 },
991 "node_modules/picomatch": {
992 "version": "4.0.5",
993 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
994 "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
995 "dev": true,
996 "license": "MIT",
997 "engines": {
998 "node": ">=12"
999 },
1000 "funding": {
1001 "url": "https://github.com/sponsors/jonschlinkert"
1002 }
1003 },
1004 "node_modules/require-addon": {
1005 "version": "1.2.0",
1006 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
1007 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
1008 "dev": true,
1009 "license": "Apache-2.0",
1010 "dependencies": {
1011 "bare-addon-resolve": "^1.3.0"
1012 },
1013 "engines": {
1014 "bare": ">=1.10.0"
1015 }
1016 },
1017 "node_modules/same-object": {
1018 "version": "1.0.2",
1019 "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz",
1020 "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==",
1021 "dev": true,
1022 "license": "MIT"
1023 },
1024 "node_modules/stackframe": {
1025 "version": "1.3.4",
1026 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
1027 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
1028 "dev": true,
1029 "license": "MIT"
1030 },
1031 "node_modules/streamx": {
1032 "version": "2.28.0",
1033 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz",
1034 "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==",
1035 "dev": true,
1036 "license": "MIT",
1037 "dependencies": {
1038 "events-universal": "^1.0.0",
1039 "fast-fifo": "^1.3.2",
1040 "text-decoder": "^1.1.0"
1041 }
1042 },
1043 "node_modules/teex": {
1044 "version": "1.0.1",
1045 "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
1046 "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
1047 "dev": true,
1048 "license": "MIT",
1049 "dependencies": {
1050 "streamx": "^2.12.5"
1051 }
1052 },
1053 "node_modules/test-tmp": {
1054 "version": "1.4.0",
1055 "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz",
1056 "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==",
1057 "dev": true,
1058 "license": "MIT",
1059 "dependencies": {
1060 "bare-fs": "^4.0.1",
1061 "bare-os": "^3.3.0",
1062 "bare-path": "^3.0.0"
1063 }
1064 },
1065 "node_modules/text-decoder": {
1066 "version": "1.2.7",
1067 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
1068 "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
1069 "dev": true,
1070 "license": "Apache-2.0",
1071 "dependencies": {
1072 "b4a": "^1.6.4"
1073 }
1074 },
1075 "node_modules/tmatch": {
1076 "version": "5.0.0",
1077 "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz",
1078 "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==",
1079 "dev": true,
1080 "license": "ISC",
1081 "engines": {
1082 "node": ">=8"
1083 }
1084 },
1085 "node_modules/v8-to-istanbul": {
1086 "version": "9.3.0",
1087 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
1088 "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
1089 "dev": true,
1090 "license": "ISC",
1091 "dependencies": {
1092 "@jridgewell/trace-mapping": "^0.3.12",
1093 "@types/istanbul-lib-coverage": "^2.0.1",
1094 "convert-source-map": "^2.0.0"
1095 },
1096 "engines": {
1097 "node": ">=10.12.0"
1098 }
1099 },
1100 "node_modules/varint": {
1101 "version": "6.0.0",
1102 "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
1103 "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
1104 "license": "MIT"
1105 },
1106 "node_modules/which-runtime": {
1107 "version": "1.4.0",
1108 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.4.0.tgz",
1109 "integrity": "sha512-0ugbP4CJW4e2D20jvEcC4973dCgIaHI4Rw1PT+26U9zEve7FyYdWAIwUnoeOYvoCfn+wXHoHTKb1KhkYlb60Pw==",
1110 "dev": true,
1111 "license": "Apache-2.0"
1112 }
1113 }
1114}
package.json +22 -0
……@@ -0,0 +1,22 @@
1{
2 "name": "atproto-repo",
3 "version": "0.0.1",
4 "description": "Byte-exact atproto repo layer — CAR + DAG-CBOR + CID + MST — over Hypercore-friendly primitives. Verified against real relay-accepted repos.",
5 "type": "module",
6 "main": "lib/repo.js",
7 "scripts": {
8 "test": "brittle-node test/*.test.js",
9 "postinstall": "node scripts/bare-deps.mjs"
10 },
11 "dependencies": {
12 "@ipld/car": "^5.4.0",
13 "@ipld/dag-cbor": "^9.2.1",
14 "@noble/curves": "^1.9.7",
15 "@noble/hashes": "^1.5.0",
16 "multiformats": "^13.3.0"
17 },
18 "devDependencies": {
19 "atproto-identity": "file:../atproto-identity",
20 "brittle": "^4.0.2"
21 }
22}
scripts/bare-deps.mjs +53 -0
……@@ -0,0 +1,53 @@
1// Bare dep compat: add a "bare" export condition to @noble/hashes' ./crypto so it resolves
2// to the bare-safe crypto.js (globalThis.crypto ?? undefined) instead of cryptoNode.js
3// (`import 'node:crypto'`, which Bare can't load). @noble/curves + @scure/* import
4// @noble/hashes, so this one condition unblocks the whole crypto tree. We sign
5// deterministically (RFC6979), so the shim degrading to undefined is never called.
6//
7// node_modules is reinstall-wiped, so this runs on `postinstall` (see the package.json of
8// each package that depends on @noble). Idempotent. Run manually with explicit roots
9// (`node scripts/bare-deps.mjs identity repo pds`) to patch the whole dev tree at once;
10// with no args it patches the CURRENT package (process.cwd()) — the postinstall case.
11
12import { readdirSync, readFileSync, writeFileSync, statSync } from 'node:fs'
13import { join } from 'node:path'
14
15function patch (pjPath) {
16 let j
17 try { j = JSON.parse(readFileSync(pjPath, 'utf8')) } catch { return false }
18 const exp = j.exports && j.exports['./crypto']
19 if (exp && typeof exp === 'object' && !exp.bare) {
20 j.exports['./crypto'] = { bare: './esm/crypto.js', ...exp } // "bare" first → matched before "node"
21 writeFileSync(pjPath, JSON.stringify(j, null, 2) + '\n')
22 return true
23 }
24 return false
25}
26
27const isDir = (p) => { try { return statSync(p).isDirectory() } catch { return false } }
28
29// Walk a node_modules dir: patch any @noble/hashes found, recurse into nested node_modules.
30function walk (nm, found) {
31 let entries
32 try { entries = readdirSync(nm, { withFileTypes: true }) } catch { return }
33 for (const e of entries) {
34 if (!e.isDirectory()) continue
35 const dir = join(nm, e.name)
36 if (e.name === '@noble') {
37 const pj = join(dir, 'hashes', 'package.json')
38 if (patch(pj)) found.push(pj)
39 // scoped pkgs may carry their own nested node_modules
40 for (const p of readdirSync(dir)) { const nn = join(dir, p, 'node_modules'); if (isDir(nn)) walk(nn, found) }
41 } else if (e.name.startsWith('@')) {
42 for (const p of readdirSync(dir)) { const nn = join(dir, p, 'node_modules'); if (isDir(nn)) walk(nn, found) }
43 } else {
44 const nn = join(dir, 'node_modules'); if (isDir(nn)) walk(nn, found)
45 }
46 }
47}
48
49const roots = process.argv.slice(2)
50if (roots.length === 0) roots.push(process.cwd())
51const found = []
52for (const r of roots) { const nm = join(r, 'node_modules'); if (isDir(nm)) walk(nm, found) }
53console.log(found.length ? `bare-deps: patched ${found.length} @noble/hashes` : 'bare-deps: nothing to patch')
test/car-read.test.js +51 -0
……@@ -0,0 +1,51 @@
1// Slice 1: prove our CAR + DAG-CBOR + CID + MST-read stack is byte-exact against a
2// REAL, relay-accepted repo (getRepo output from our live PDS). The killer check:
3// every block re-encodes to identical bytes and the same CID.
4
5import test from 'brittle'
6import { readFileSync } from 'node:fs'
7import { fileURLToPath } from 'node:url'
8import { dirname, join } from 'node:path'
9import { readCar, loadCommit, listRecords, verifyRepo } from '../lib/repo.js'
10import { cidForValue } from '../lib/cid.js'
11
12const here = dirname(fileURLToPath(import.meta.url))
13const carBytes = readFileSync(join(here, 'vectors', 'bridgetest-repo.car'))
14
15const DID = 'did:plc:bh2hiuoeb34g4lif62zj2vx7'
16const ROOT = 'bafyreigiu7fhjqrqo3cxvg5ymdjazlat6n3go4h6akm4yh47iwodnebr2a'
17const POST_KEY = 'app.bsky.feed.post/3mpmksfzaqm7k'
18
19const car = await readCar(carBytes)
20
21test('CAR structure decodes to the signed commit', (t) => {
22 t.ok(car.roots.length === 1 && car.roots[0].toString() === ROOT,
23 'CAR has exactly one root (the commit)')
24
25 const c = loadCommit(car)
26 t.ok(c.value.did === DID && c.value.rev === '3mpmksfzaqm7k' && c.value.sig && c.value.data,
27 'root decodes to our signed commit')
28})
29
30// THE fidelity proof: every block re-encodes to identical bytes + same CID.
31test('every block is byte-exact (re-encode -> same CID)', async (t) => {
32 const r = await verifyRepo(car)
33 if (!r.ok) t.comment('mismatches: ' + JSON.stringify(r.mismatches))
34 t.comment(`verified ${r.blocks} blocks, ${r.records} record(s)`)
35 t.ok(r.ok, 'every block is byte-exact (re-encode -> same CID)')
36})
37
38test('MST traversal and record fidelity', async (t) => {
39 const recs = listRecords(car)
40 t.ok(recs.length === 1 && recs[0].key === POST_KEY, 'MST traversal yields the post key')
41
42 const rec = listRecords(car)[0].value
43 t.ok(rec.$type === 'app.bsky.feed.post' && typeof rec.text === 'string' && rec.text.includes('pds.7rnx.net'),
44 'the record is the expected app.bsky.feed.post')
45
46 // Independent CID check: recompute the record's CID from its value, compare to the
47 // CID the MST points at (proves cidForValue matches the on-wire addressing).
48 const rec2 = listRecords(car)[0]
49 const { cid } = await cidForValue(rec2.value)
50 t.ok(cid.equals(rec2.cid), 'recomputed record CID matches the MST pointer')
51})
test/commit-car.test.js +66 -0
……@@ -0,0 +1,66 @@
1// Slice 3: commit signing + full CAR origination, verified offline.
2// - sign/verify round-trip + tamper detection (secp256k1)
3// - createRepo -> re-read via slice-1 reader -> byte-exact, records intact
4// - whole-stack compose: mnemonic (identity) -> did:plc -> sign a repo commit ->
5// verify the sig against the DID's verificationMethod. No PLC/relay needed.
6
7import test from 'brittle'
8import * as dagCbor from '@ipld/dag-cbor'
9import { secp256k1 } from '@noble/curves/secp256k1'
10import { readCar, loadCommit, listRecords, verifyRepo, createRepo } from '../lib/repo.js'
11import { unsignedCommit, signCommit, verifyCommit } from '../lib/commit.js'
12import { cidForBytesSync } from '../lib/cid.js'
13// cross-package (dev dep; offline; deriveAtprotoKey needs no native deps):
14import { deriveAtprotoKey } from 'atproto-identity'
15import { decodeDidKey } from 'atproto-identity/lib/did-key.js'
16
17const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
18const DID = 'did:plc:yu4kgcktm7qtrwo5ofyiaf4c'
19const RECORDS = [
20 { collection: 'app.bsky.feed.post', rkey: '3aaa', record: { $type: 'app.bsky.feed.post', text: 'first', createdAt: '2026-07-01T00:00:00.000Z' } },
21 { collection: 'app.bsky.feed.post', rkey: '3bbb', record: { $type: 'app.bsky.feed.post', text: 'second', createdAt: '2026-07-01T00:00:01.000Z' } },
22 { collection: 'app.bsky.actor.profile', rkey: 'self', record: { $type: 'app.bsky.actor.profile', displayName: 'test' } }
23]
24
25// 1. sign / verify / tamper
26const key = deriveAtprotoKey(MNEMONIC)
27const dataCid = cidForBytesSync(dagCbor.encode('placeholder-mst-root')) // any valid CID for the sign/verify shape
28
29test('sign / verify / tamper (secp256k1)', (t) => {
30 const signed = signCommit(unsignedCommit({ did: DID, rev: '3zzz', data: dataCid }), key.privateKey)
31 t.ok(signed.sig instanceof Uint8Array && signed.sig.length === 64 && verifyCommit(signed, key.publicKey),
32 'sign -> verify round-trips (secp256k1, 64-byte sig)')
33
34 const signed2 = signCommit(unsignedCommit({ did: DID, rev: '3zzz', data: dataCid }), key.privateKey)
35 const tampered = { ...signed2, rev: '3zzy' }
36 t.ok(!verifyCommit(tampered, key.publicKey), 'tampered commit fails verification')
37})
38
39// 2. full CAR origination -> re-read with the slice-1 reader
40let repo
41test('createRepo -> re-read via slice-1 reader (byte-exact, records intact)', async (t) => {
42 repo = await createRepo({ records: RECORDS, did: DID, rev: '3ccc', privateKey: key.privateKey })
43 const car = await readCar(repo.carBytes)
44 const v = await verifyRepo(car)
45 const commit = loadCommit(car)
46 t.comment(`originated CAR: ${repo.carBytes.length}b, ${v.blocks} blocks, ${v.records} records`)
47 t.ok(v.ok && commit.cid.equals(repo.commitCid) && commit.value.did === DID && commit.value.data.equals(repo.dataCid),
48 'createRepo produces a byte-exact, re-readable CAR')
49
50 const car2 = await readCar(repo.carBytes)
51 const recs = listRecords(car2)
52 const keys = recs.map(r => r.key).sort()
53 const want = ['app.bsky.actor.profile/self', 'app.bsky.feed.post/3aaa', 'app.bsky.feed.post/3bbb']
54 const post = recs.find(r => r.key === 'app.bsky.feed.post/3aaa')
55 t.ok(JSON.stringify(keys) === JSON.stringify(want) && post.value.text === 'first',
56 're-read records match what we put in (keys + text)')
57})
58
59// 3. whole-stack compose: identity did -> our commit -> verify sig vs the DID key
60test('commit signed with mnemonic key verifies against the did:key', async (t) => {
61 const { pubkey, curve } = decodeDidKey(key.didKey)
62 const car = await readCar(repo.carBytes)
63 const signed = loadCommit(car).value
64 t.ok(curve === 'secp256k1' && verifyCommit(signed, pubkey),
65 'commit signed with mnemonic key verifies against the did:key')
66})
test/mst-write.test.js +58 -0
……@@ -0,0 +1,58 @@
1// Slice 2: prove the MST WRITE path is byte-exact, against two oracles:
2// A. rebuild the golden repo's MST from its records -> match commit.data + node bytes
3// B. atmst's own root CIDs for a 50-key, 3-height tree (build + delete)
4// The atmst roots were generated by running millipds's reference on stortron with
5// val(key) = cid(dag-cbor(key)); we reproduce that mapping here.
6
7import test from 'brittle'
8import { readFileSync } from 'node:fs'
9import { fileURLToPath } from 'node:url'
10import { dirname, join } from 'node:path'
11import { readCar, loadCommit, listRecords } from '../lib/repo.js'
12import { buildMst, keyHeight } from '../lib/mst.js'
13import { cidForBytesSync } from '../lib/cid.js'
14import * as dagCbor from '@ipld/dag-cbor'
15
16const here = dirname(fileURLToPath(import.meta.url))
17const carBytes = readFileSync(join(here, 'vectors', 'bridgetest-repo.car'))
18
19// atmst oracle (reference roots), val(k) = cid(dag-cbor(k)):
20const ATMST_KEYS = Array.from({ length: 50 }, (_, i) => `app.test.record/${String(i).padStart(4, '0')}`)
21const ATMST_ROOT = 'bafyreiaxxeqrlo6flqjewuug3locbq7mmf5cmswecbic3xj6h4lyajymta'
22const ATMST_ROOT_AFTER_DELETES = 'bafyreigwzj7pnllj3ggewdj4buwcaxqjhctasgcuwknjwjl7kbeiwlaot4'
23const DELETE_KEYS = ['app.test.record/0000', 'app.test.record/0025', 'app.test.record/0049']
24const valFor = (k) => cidForBytesSync(dagCbor.encode(k))
25
26test('A: rebuilt golden MST matches commit.data + byte-identical node', async (t) => {
27 const car = await readCar(carBytes)
28 const commit = loadCommit(car)
29 const records = listRecords(car).map(r => ({ key: r.key, value: r.cid }))
30 const builtGolden = buildMst(records)
31 t.ok(builtGolden.root.equals(commit.value.data), 'rebuilt golden MST root == commit.data')
32 const goldenNode = car.blocks.get(commit.value.data.toString())
33 const ourNode = builtGolden.ns.blocks.get(builtGolden.root.toString())
34 t.ok(Buffer.from(ourNode.bytes).equals(Buffer.from(goldenNode.bytes)),
35 'rebuilt MST node bytes are byte-identical to the golden block')
36})
37
38test('B: 50-key multi-height build matches atmst oracle (+ order independence)', (t) => {
39 const built = buildMst(ATMST_KEYS.map(k => ({ key: k, value: valFor(k) })))
40 const hist = {}
41 for (const k of ATMST_KEYS) { const h = keyHeight(k); hist[h] = (hist[h] || 0) + 1 }
42 t.comment('height histogram: ' + JSON.stringify(hist))
43 t.is(built.root.toString(), ATMST_ROOT, '50-key build root matches atmst oracle')
44
45 const shuffled = buildMst(ATMST_KEYS.slice().reverse().map(k => ({ key: k, value: valFor(k) })))
46 t.is(shuffled.root.toString(), ATMST_ROOT, 'root is insertion-order independent')
47})
48
49test('D: delete path matches atmst oracle', (t) => {
50 const { root, wrangler } = buildMst(ATMST_KEYS.map(k => ({ key: k, value: valFor(k) })))
51 let r = root
52 for (const k of DELETE_KEYS) r = wrangler.delRecord(r, k)
53 t.is(r.toString(), ATMST_ROOT_AFTER_DELETES, 'root after 3 deletes matches atmst oracle')
54})
55
56test('E: key heights span >1 level (splits exercised)', (t) => {
57 t.ok(new Set(ATMST_KEYS.map(keyHeight)).size >= 2, 'height histogram spans >1 level')
58})
test/vectors/bridgetest-repo.car +0 -0
(no content changes — binary or rename)