// Referenced-blob convergence — spec/convergence.md §6.
//
// A node can hold a byte-perfect, fully verified record set and still render no images: blobs live
// OUTSIDE the MST. Nothing checked that until 0.3.44.
//
// The walk is GENERIC on purpose, and that is the property most worth pinning. A hardcoded list of known
// paths (profile.avatar, embed.images[].image, …) is wrong the day a lexicon adds a field, and wrong
// SILENTLY — a blob nobody enumerates is a blob nobody notices is missing.
//
// Run: node test/blob-refs.test.mjs
import { collectBlobRefs, collectRepoBlobRefs, diffBlobs } from '../lib/blob-refs.js'
const assert = (cond, msg) => { if (!cond) throw new Error('FAIL: ' + msg); console.log(' ok -', msg) }
const CID1 = 'bafkreiabcdefghijklmnopqrstuvwxyz234567abcdefghijklmnop'
const CID2 = 'bafkreizyxwvutsrqponmlkjihgfedcba765432zyxwvutsrqponml'
const blob = (cid, mime = 'image/jpeg') => ({ $type: 'blob', ref: { $link: cid }, mimeType: mime, size: 1234 })
console.log('# the four ref shapes all normalise')
{
const shapes = [
{ $type: 'blob', ref: { $link: CID1 }, mimeType: 'image/png' }, // decoded {$link}
{ $type: 'blob', ref: { '/': CID1 }, mimeType: 'image/png' }, // dag-json
{ $type: 'blob', ref: CID1, mimeType: 'image/png' }, // bare string
{ $type: 'blob', ref: { asCID: null, toString: () => CID1 }, mimeType: 'image/png' } // CID-ish
]
shapes[3].ref.asCID = shapes[3].ref // a multiformats CID is its own .asCID
for (const s of shapes) {
const r = collectBlobRefs(s)
assert(r.length === 1 && r[0].cid === CID1, 'ref shape normalised to a cid')
}
}
console.log('\n# blobs are found wherever they sit — no path list')
{
const profile = { $type: 'app.bsky.actor.profile', displayName: 'x', avatar: blob(CID1), banner: blob(CID2) }
assert(collectBlobRefs(profile).length === 2, 'profile avatar + banner')
const post = {
$type: 'app.bsky.feed.post',
text: 'hi',
embed: { $type: 'app.bsky.embed.images', images: [{ alt: 'a', image: blob(CID1) }, { alt: 'b', image: blob(CID2) }] }
}
assert(collectBlobRefs(post).length === 2, 'both embedded images')
const rwm = {
$type: 'app.bsky.feed.post',
embed: { $type: 'app.bsky.embed.recordWithMedia', media: { $type: 'app.bsky.embed.images', images: [{ image: blob(CID1) }] } }
}
assert(collectBlobRefs(rwm).length === 1, 'the media side of a quote-with-media')
// ⛔ THE POINT: a field no one has written an extractor for is still found.
const future = { $type: 'app.bsky.feed.post', embed: { $type: 'com.example.notyetinvented', deeply: { nested: { thing: blob(CID1) } } } }
assert(collectBlobRefs(future).length === 1, 'a blob in a lexicon that does not exist yet is STILL found')
}
console.log('\n# the legacy (pre-lexicon) blob form is taken too')
{
const legacy = { $type: 'app.bsky.actor.profile', avatar: { cid: CID1, mimeType: 'image/jpeg' } }
assert(collectBlobRefs(legacy).length === 1, 'a legacy {cid,mimeType} blob counts — wj62 predates the cutover')
}
console.log('\n# not-a-blob is not taken')
{
const post = {
$type: 'app.bsky.feed.post',
text: 'a reply',
reply: { parent: { uri: 'at://x/y/z', cid: CID1 }, root: { uri: 'at://x/y/z', cid: CID2 } }
}
// A strongRef also has a `cid`, but no mimeType — treating it as a blob would invent gaps for every
// reply in the repo.
assert(collectBlobRefs(post).length === 0, 'a strongRef (cid without mimeType) is NOT a blob')
assert(collectBlobRefs({ text: 'plain' }).length === 0, 'a record with no blobs yields none')
assert(collectBlobRefs(null).length === 0, 'null is safe')
}
console.log('\n# the same blob in two records is ONE blob')
{
const records = [
{ collection: 'app.bsky.feed.post', rkey: 'a', record: { embed: { $type: 'app.bsky.embed.images', images: [{ image: blob(CID1) }] } } },
{ collection: 'app.bsky.feed.post', rkey: 'b', record: { embed: { $type: 'app.bsky.embed.images', images: [{ image: blob(CID1) }] } } }
]
const m = collectRepoBlobRefs(records)
assert(m.size === 1, 'content-addressed: one CID, one entry')
assert(m.get(CID1).refs.length === 2, 'but both referring records are named')
}
console.log('\n# ⛔ ACCEPTANCE (#32): an absent avatar is reported, naming the CID')
{
const records = [{ collection: 'app.bsky.actor.profile', rkey: 'self', record: { $type: 'app.bsky.actor.profile', avatar: blob(CID1), banner: blob(CID2) } }]
const d = await diffBlobs({ records, hasBlob: async (cid) => cid !== CID1 }) // avatar bytes absent
assert(d.referenced === 2, 'two blobs referenced')
assert(d.present === 1 && d.missing.length === 1, 'one present, one missing')
assert(d.missing[0].cid === CID1, 'the MISSING one is named by CID')
assert(d.missing[0].refs[0].collection === 'app.bsky.actor.profile', 'and the record that wants it is named')
assert(/avatar/.test(d.missing[0].refs[0].where), 'down to the FIELD — "' + d.missing[0].refs[0].where + '"')
}
console.log('\n# a fully-present repo reports no gap')
{
const records = [{ collection: 'app.bsky.actor.profile', rkey: 'self', record: { avatar: blob(CID1) } }]
const d = await diffBlobs({ records, hasBlob: async () => true })
assert(d.referenced === 1 && d.missing.length === 0, 'nothing missing')
}
console.log('\n# ⛔ with no inventory, orphans are NOT claimed to be checked')
{
const d = await diffBlobs({ records: [], hasBlob: async () => true })
assert(d.orphansChecked === false, '"we did not look" is reported as such')
assert(d.orphans.length === 0 && d.inventory === null, 'and NOT as "there are none" — different answers')
assert(/listBlobs/.test(d.orphanNote), 'names what it would take — the class that lost the 2026-07-03 image')
}
console.log('\n# ⛔ ACCEPTANCE (#32 slice 2): an ORPHAN is reported as orphaned, not as missing')
{
const records = [{ collection: 'app.bsky.actor.profile', rkey: 'self', record: { avatar: blob(CID1) } }]
// The authority holds TWO blobs; only CID1 is referenced by any record. CID2 is an orphan — invisible
// to any walk of records, which is the whole point of asking the authority.
const d = await diffBlobs({
records,
hasBlob: async () => true,
listAuthorityBlobs: async () => ({ cids: [CID1, CID2] })
})
assert(d.orphansChecked === true, 'the inventory was consulted')
assert(d.inventory === 2 && d.referenced === 1, 'authority holds 2, records reference 1')
assert(d.orphans.length === 1 && d.orphans[0].cid === CID2, 'the ORPHAN is named — and it is CID2')
assert(d.missing.length === 0, 'and it is NOT reported as missing — we have every blob we need')
}
console.log('\n# an inventory that fails degrades to unchecked, never to zero')
{
const d = await diffBlobs({ records: [], hasBlob: async () => true, listAuthorityBlobs: async () => { throw new Error('501') } })
assert(d.orphansChecked === false, 'a throwing inventory reads as unchecked')
const d2 = await diffBlobs({ records: [], hasBlob: async () => true, listAuthorityBlobs: async () => null })
assert(d2.orphansChecked === false, 'so does a null one — an old peer must not look converged')
}
console.log('\n# a throwing hasBlob counts as absent, never as a crash')
{
const records = [{ collection: 'app.bsky.actor.profile', rkey: 'self', record: { avatar: blob(CID1) } }]
const d = await diffBlobs({ records, hasBlob: async () => { throw new Error('disk gone') } })
assert(d.missing.length === 1, 'an unreadable store reads as missing, not as healthy')
}
console.log('\nall blob-refs assertions passed')
// ---- content-address verification -------------------------------------------------------------------
import { verifyBlobCid } from '../lib/blob-refs.js'
import { cidForBlobSync } from 'atproto-repo/lib/cid.js'
console.log('\n# ⛔ fetched blob bytes are verified against their CID before being written')
{
const bytes = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])
const realCid = String(cidForBlobSync(bytes))
assert(await verifyBlobCid(bytes, realCid) === true, 'the right bytes under the right CID verify')
assert(await verifyBlobCid(new Uint8Array([9, 9, 9]), realCid) === false, 'WRONG bytes under that CID are refused — a bad server cannot poison the store')
assert(await verifyBlobCid(bytes, 'bafkreinotacid') === false, 'a mismatched CID is refused')
assert(await verifyBlobCid(new Uint8Array(0), realCid) === false, 'empty bytes are refused')
assert(await verifyBlobCid(bytes, null) === false, 'a missing CID is refused')
}
console.log('\nall blob verification assertions passed')