// A Corestore FROM A DIFFERENT COPY OF THE MODULE is still a Corestore.
//
// `instanceof` compares against one module instance's class object. Bundle two copies of corestore —
// which is the normal outcome of a nested node_modules tree, and exactly what bare-pack ships to the
// phone — and a store built by copy A fails `instanceof` against copy B. Every discriminator in this
// package was written as `!(storage instanceof Corestore)`, i.e. "not a store, so it must be a PATH":
//
// TypeError: Cannot read properties of undefined (reading 'length')
// at Object.join (bare-path/lib/posix.js:67)
// at new CorestoreStorage (hypercore-storage/index.js:496)
// at new Corestore (corestore/index.js:242)
// at new RepoStore (atproto-store/lib/repo-store.js)
//
// Measured on a real device (hyper-social #40): the daemon passed
// `shared.store.namespace('membrane:' + did)` into serve-membrane's RepoStore, the check said "path",
// and Corestore tried to path.join an OBJECT. The node had already opened its record store, taken the
// joiner posture, opened blobs and served two control connections — it died at the very last step, and
// the message named a JS idiom rather than a cause.
//
// It never reproduced on a workstation because a deduped/symlinked tree has exactly one copy. The
// difference between the two environments is the module graph, not the code — so the discriminator
// must not depend on module identity. A store is identified by what it DOES.
//
// Run: brittle-node test/corestore-cross-copy.test.js
import test from 'brittle'
import fs from 'fs'
import os from 'os'
import path from 'path'
import Corestore from 'corestore'
import { isCorestore, openCorestore } from '../lib/corestore.js'
import { RepoStore } from '../lib/repo-store.js'
import { BlobStore } from '../lib/blob-store.js'
const tmp = (t) => {
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'atproto-store-crosscopy-'))
t.teardown(() => { try { fs.rmSync(d, { recursive: true, force: true }) } catch {} })
return d
}
// A store from ANOTHER copy of corestore. Same shape, same behaviour, different class object — which
// is all a second copy in the bundle amounts to. Delegating to a real store keeps the test honest:
// whatever the code does with it has to actually work, not merely type-check.
function foreignCopy (store) {
const surface = {}
for (const m of ['get', 'namespace', 'replicate', 'ready', 'close', 'session']) {
if (typeof store[m] === 'function') surface[m] = store[m].bind(store)
}
return surface
}
test('the premise: a foreign-copy store fails instanceof', async (t) => {
const store = new Corestore(tmp(t))
await store.ready()
const foreign = foreignCopy(store)
t.absent(foreign instanceof Corestore, 'it is NOT instanceof the local copy — this is the whole bug')
t.ok(typeof foreign.get === 'function' && typeof foreign.namespace === 'function', 'but it is a store by every behaviour that matters')
await store.close()
})
test('isCorestore recognises a store by behaviour, not module identity', async (t) => {
const store = new Corestore(tmp(t))
await store.ready()
t.ok(isCorestore(store), 'the local copy')
t.ok(isCorestore(foreignCopy(store)), 'and one from another copy of the module')
t.absent(isCorestore('/some/path'), 'a path is not a store')
t.absent(isCorestore(null), 'and neither is null')
t.absent(isCorestore({ get: 1, namespace: 2 }), 'nor an object that merely has the names')
await store.close()
})
test('RepoStore accepts a foreign-copy store instead of path.join-ing it', async (t) => {
const store = new Corestore(tmp(t))
await store.ready()
const repo = await new RepoStore(foreignCopy(store), { name: 'membrane-repo:did:plc:test' }).ready()
// Before the fix this threw inside the constructor. Assert it is USABLE, not merely constructed.
await repo.put('app.bsky.feed.post', 'abc', { $type: 'app.bsky.feed.post', text: 'hello', createdAt: '2026-08-10T00:00:00.000Z' })
const got = await repo.get('app.bsky.feed.post', 'abc')
t.is(got && got.text, 'hello', 'a record round-trips through the foreign-copy store')
await repo.close()
await store.close()
})
test('BlobStore too — the same discriminator, the same trap', async (t) => {
const store = new Corestore(tmp(t))
await store.ready()
const blobs = await new BlobStore(foreignCopy(store), { name: 'membrane-blobs:did:plc:test' }).ready()
const put = await blobs.put(Buffer.from('some bytes'), 'application/octet-stream')
const back = await blobs.get(put.cid.toString())
t.is(back && back.size, 10, 'a blob round-trips through the foreign-copy store')
t.is(back && Buffer.from(back.bytes).toString(), 'some bytes', 'and its bytes are intact')
await store.close()
})
test('a PATH still gets its own store — the other half of the discriminator', async (t) => {
const dir = tmp(t)
const repo = await new RepoStore(dir).ready()
await repo.put('app.bsky.feed.post', 'p1', { $type: 'app.bsky.feed.post', text: 'from a path', createdAt: '2026-08-10T00:00:00.000Z' })
t.is((await repo.get('app.bsky.feed.post', 'p1')).text, 'from a path', 'a string path is still opened as a directory')
await repo.close()
t.ok(fs.existsSync(dir), 'and it wrote there')
})
test('openCorestore is idempotent on a foreign-copy store', async (t) => {
const store = new Corestore(tmp(t))
await store.ready()
const foreign = foreignCopy(store)
t.is(openCorestore(foreign), foreign, 'it is handed back untouched, not wrapped in a second Corestore')
await store.close()
})