// get-blob responder (#16): blobs could be UPLOADED but never read back — the get-blob RPC responder was
// never implemented, so peer-front.getBlob got null and the bridge 404'd every blob (BlobNotFound), regardless
// of who held the bytes. This proves the round-trip now works over the swarm control surface: upload-blob then
// get-blob returns the exact bytes; a public (non-writer) client can read; an unknown CID returns null (→404).
import '../lib/bare-globals.js'
import test from 'brittle'
import crypto from 'hypercore-crypto'
import b4a from 'b4a'
import c from 'compact-encoding'
import Hyperswarm from 'hyperswarm'
import ProtomuxRPC from 'protomux-rpc'
import createTestnet from '@hyperswarm/testnet'
import { SwarmDevice } from '../lib/swarm-device.js'
import { serveDevice, DEVICE_PROTOCOL } from '../lib/serve-device.js'
import { BlobStore } from 'atproto-store/lib/blob-store.js'
const DID = 'did:plc:blob22222222222222222222'
function memKV () {
const m = new Map()
return {
async set (k, v) { m.set(k, v) },
async del (k) { m.delete(k) },
async get (k) { return m.has(k) ? m.get(k) : null },
async list (prefix = '') { const o = {}; for (const [k, v] of m) if (k.startsWith(prefix)) o[k] = v; return o }
}
}
function clientRpc (swarm, serverKeyHex) {
return new Promise((resolve) => {
swarm.on('connection', (conn) => resolve(new ProtomuxRPC(conn, { protocol: DEVICE_PROTOCOL, valueEncoding: c.json })))
swarm.join(b4a.from(serverKeyHex, 'hex'), { client: true, server: false })
})
}
test('get-blob: peer serves stored blobs (the previously-missing read path)', async (t) => {
const testnet = await createTestnet(3)
const device = new SwarmDevice(memKV(), { did: DID, privateKey: crypto.randomBytes(32), self: 'deviceA' })
const blobs = await new BlobStore('data/blob-test-' + Date.now(), { name: 'test-blobs:' + DID }).ready()
const serverSwarm = new Hyperswarm({ bootstrap: testnet.bootstrap })
const authedClient = new Hyperswarm({ bootstrap: testnet.bootstrap })
const strangerClient = new Hyperswarm({ bootstrap: testnet.bootstrap })
const authedPub = b4a.toString(authedClient.keyPair.publicKey, 'hex')
const serving = serveDevice(device, serverSwarm, { canWrite: (hex) => hex === authedPub, blobs })
await serving.ready()
t.teardown(async () => {
serving.close()
await serverSwarm.destroy(); await authedClient.destroy(); await strangerClient.destroy()
try { await blobs.close() } catch {}
await testnet.destroy()
})
const rpc = await clientRpc(authedClient, serving.serverKey)
// upload (writer-gated) then read back (public) — the exact round-trip that used to 404.
const bytes = b4a.from('the actual image bytes for the avatar and post')
const up = await rpc.request('upload-blob', { bytes: b4a.toString(bytes, 'hex'), mimeType: 'image/png' })
t.ok(up.cid, 'upload-blob returned a content-addressed CID')
const got = await rpc.request('get-blob', { cid: up.cid })
t.ok(got, 'get-blob returned a blob (was null before the responder existed → BlobNotFound)')
t.is(got.mimeType, 'image/png', 'mime type preserved')
t.is(got.bytes, b4a.toString(bytes, 'hex'), 'bytes round-trip exactly (hex)')
t.alike(b4a.from(got.bytes, 'hex'), bytes, 'decoded bytes equal the original')
// get-blob is PUBLIC: a non-allowlisted client (cannot upload) can still read.
const rpc2 = await clientRpc(strangerClient, serving.serverKey)
const pub = await rpc2.request('get-blob', { cid: up.cid })
t.is(pub && pub.bytes, b4a.toString(bytes, 'hex'), 'a public (non-writer) client can read a blob')
// a miss is null (server-bare maps null → 404 BlobNotFound), not an error.
const miss = await rpc.request('get-blob', { cid: 'bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' })
t.is(miss, null, 'unknown CID returns null (→ 404)')
})