// The membrane's PERSISTENT swarm identity (hyper-social#21 P0).
//
// The property under test is the one the P0 turned on: the Noise public key a peer would allowlist
// in PEER_WRITERS must be the SAME after a restart. The old `new Hyperswarm()` failed exactly here —
// a fresh random key per boot, so an allowlist entry authorised one boot and then silently stopped.
//
// The control (second test) is what makes the first meaningful: it demonstrates the pre-fix
// behaviour, so a regression that quietly reverts to random keys cannot pass.
import test from 'brittle'
import { mkdtempSync, rmSync, readFileSync, statSync, existsSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import b4a from 'b4a'
import Hyperswarm from 'hyperswarm'
import { loadOrCreateSwarmKeyPair } from '../lib/swarm-keypair.js'
test('swarm keypair: created once, then STABLE across restarts (the PEER_WRITERS contract)', async (t) => {
const dir = mkdtempSync(join(tmpdir(), 'bridge-swarmkey-'))
t.teardown(() => rmSync(dir, { recursive: true, force: true }))
const first = loadOrCreateSwarmKeyPair(dir)
const file = join(dir, 'swarm-keypair.json')
t.ok(existsSync(file), 'first boot persists <storage>/swarm-keypair.json')
t.is(first.publicKey.length, 32, 'publicKey is 32 bytes')
t.is(first.secretKey.length, 64, 'secretKey is 64 bytes')
// The restart. Same storage dir → the SAME key, which is the whole point.
const second = loadOrCreateSwarmKeyPair(dir)
t.ok(b4a.equals(first.publicKey, second.publicKey), 'RESTART: public key is unchanged')
t.ok(b4a.equals(first.secretKey, second.secretKey), 'RESTART: secret key is unchanged')
// It must be the key Hyperswarm actually presents to a peer — a persisted file that the swarm
// ignores would pass every assertion above and still fail in the field.
const swarm = new Hyperswarm({ keyPair: second })
t.teardown(() => swarm.destroy())
t.ok(b4a.equals(swarm.keyPair.publicKey, first.publicKey), 'the swarm presents the persisted key')
const saved = JSON.parse(readFileSync(file, 'utf8'))
t.is(saved.publicKey, b4a.toString(first.publicKey, 'hex'), 'file holds the hex public key an operator pastes into PEER_WRITERS')
t.is(statSync(file).mode & 0o777, 0o600, 'secret-bearing file is 0600')
// A different storage dir is a different membrane, not the same one.
const other = mkdtempSync(join(tmpdir(), 'bridge-swarmkey-'))
t.teardown(() => rmSync(other, { recursive: true, force: true }))
t.absent(b4a.equals(loadOrCreateSwarmKeyPair(other).publicKey, first.publicKey), 'separate storage → separate identity')
})
test('control: plain `new Hyperswarm()` mints a NEW key per boot — the bug being fixed', async (t) => {
const a = new Hyperswarm()
const b = new Hyperswarm()
t.teardown(async () => { await a.destroy(); await b.destroy() })
t.absent(b4a.equals(a.keyPair.publicKey, b.keyPair.publicKey),
'two unkeyed swarms differ — an allowlist entry would authorise exactly one boot')
})
test('storage must be a path — refuse rather than write a keypair somewhere unintended', async (t) => {
t.exception(() => loadOrCreateSwarmKeyPair(null), /storage must be a directory path/)
t.exception(() => loadOrCreateSwarmKeyPair({ storage: '/tmp' }), /storage must be a directory path/)
})