/**
* The drive key must be a deliberate input, not a random seed (org #145).
*
* `publishTree` opened `new Corestore(new Storage(storeDir))` with no primaryKey,
* so corestore minted a random seed and the drive key derived from it. Lose
* `<releaseDir>/<repo>/.vdrive` and the pear:// link died permanently — the
* secret existed only in the directory that was lost, and nothing anywhere
* recorded how to regenerate it.
*
* Deriving the primaryKey from the daemon's long-lived corestore plus the repo
* name makes the link a function of state that already survives. Losing the
* drive store stops being data loss and becomes a rebuild.
*
* Note corestore requires `unsafe: true` to accept a primaryKey at all, and
* refuses to open an existing store whose stored seed differs ("Another
* corestore is stored here"). Both are load-bearing and asserted here.
*/
import test from 'brittle'
import fsp from '#node:fs/promises'
import path from '#node:path'
import os from '#node:os'
import crypto from 'hypercore-crypto'
import {
publishVersionToDrive,
vdriveStoreDir,
deriveVdrivePrimaryKey
} from '../lib/vdrive-publish.js'
function tmpdir (name) {
return fsp.mkdtemp(path.join(os.tmpdir(), 'pgv3-vdkey-' + name + '-'))
}
const PAYLOAD = Buffer.alloc(256 * 1024, 5)
async function publish (releaseDir, repo, version, primaryKey) {
const versionDir = path.join(releaseDir, repo, 'versions', version)
await fsp.mkdir(versionDir, { recursive: true })
await fsp.writeFile(path.join(versionDir, 'app.bin'), PAYLOAD)
await fsp.writeFile(path.join(versionDir, 'notes.txt'), 'same bytes every time')
const { chunk } = await import('hyper-overlay/chunker')
return publishVersionToDrive({
releaseDir,
repo,
version,
channel: 'production',
vdrive: { enabled: true, channels: null },
primaryKey,
chunk
})
}
test('derivation is deterministic and namespaced per repo', (t) => {
const root = crypto.randomBytes(32)
const other = crypto.randomBytes(32)
const a = deriveVdrivePrimaryKey(root, 'demo')
const b = deriveVdrivePrimaryKey(root, 'demo')
t.alike(a, b, 'same inputs, same key')
t.is(a.byteLength, 32, 'and it is a 32-byte primaryKey')
t.unlike(deriveVdrivePrimaryKey(root, 'other'), a, 'a different repo differs')
t.unlike(deriveVdrivePrimaryKey(other, 'demo'), a, 'a different root differs')
})
test('losing the drive store no longer loses the link', async (t) => {
// The whole point. Publish, destroy the drive store, rebuild from the same
// deliberate key, and the pear:// link must come back identical.
const releaseDir = await tmpdir('recover')
const repo = 'demo'
const primaryKey = deriveVdrivePrimaryKey(crypto.randomBytes(32), repo)
const first = await publish(releaseDir, repo, '1.0.0', primaryKey)
t.is(first.ok, true, 'published')
t.ok(first.link.startsWith('pear://'), 'has a link')
// simulate losing the store (a temp dir this test created, not user state)
await fsp.rm(vdriveStoreDir(releaseDir, repo), { recursive: true, force: true })
const again = await publish(releaseDir, repo, '1.0.0', primaryKey)
t.is(again.ok, true, 'republished after the store was gone')
t.is(again.link, first.link, 'and the link is the SAME')
await fsp.rm(releaseDir, { recursive: true, force: true })
})
test('the same key and content reproduce the link on another machine', async (t) => {
// Two independent release dirs stand in for two machines holding the same
// artifacts. Same deliberate key + same bytes => same link.
const repo = 'demo'
const primaryKey = deriveVdrivePrimaryKey(crypto.randomBytes(32), repo)
const one = await tmpdir('m1')
const two = await tmpdir('m2')
const a = await publish(one, repo, '1.0.0', primaryKey)
const b = await publish(two, repo, '1.0.0', primaryKey)
t.is(a.ok && b.ok, true, 'both published')
t.is(b.link, a.link, 'identical link from an independent build')
await fsp.rm(one, { recursive: true, force: true })
await fsp.rm(two, { recursive: true, force: true })
})
test('a different key yields a different drive', async (t) => {
const repo = 'demo'
const one = await tmpdir('k1')
const two = await tmpdir('k2')
const a = await publish(one, repo, '1.0.0', deriveVdrivePrimaryKey(crypto.randomBytes(32), repo))
const b = await publish(two, repo, '1.0.0', deriveVdrivePrimaryKey(crypto.randomBytes(32), repo))
t.is(a.ok && b.ok, true, 'both published')
t.unlike(b.link, a.link, 'different keys, different links')
await fsp.rm(one, { recursive: true, force: true })
await fsp.rm(two, { recursive: true, force: true })
})
test('a store minted under a different key is refused, not silently rekeyed', async (t) => {
// corestore throws "Another corestore is stored here" rather than adopting the
// new key. Publishing must surface that as a clear, actionable skip instead of
// taking the release down with it.
const releaseDir = await tmpdir('mismatch')
const repo = 'demo'
const first = await publish(releaseDir, repo, '1.0.0', deriveVdrivePrimaryKey(crypto.randomBytes(32), repo))
t.is(first.ok, true, 'first publish created the store')
const second = await publish(releaseDir, repo, '2.0.0', deriveVdrivePrimaryKey(crypto.randomBytes(32), repo))
t.is(second.ok, false, 'a mismatched key does not silently rekey the drive')
t.ok(second.skipped, 'it is reported as skipped')
t.ok(/another corestore|stored here|key/i.test(second.reason || ''), 'with a reason naming the cause: ' + second.reason)
await fsp.rm(releaseDir, { recursive: true, force: true })
})