import test from 'brittle'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import { IdentityManager } from '../../lib/identity/identity-manager.js'
function tmpDir() {
const dir = path.join(os.tmpdir(), 'hn-firstrun-' + Date.now() + '-' + Math.random().toString(36).slice(2))
fs.mkdirSync(dir, { recursive: true })
return dir
}
function rmrf(dir) {
try {
for (const f of fs.readdirSync(dir)) {
const fp = path.join(dir, f)
const stat = fs.statSync(fp)
if (stat.isDirectory()) rmrf(fp)
else fs.unlinkSync(fp)
}
fs.rmdirSync(dir)
} catch {}
}
test('first-run - ensureKeypair creates identity file', async (t) => {
const dir = tmpDir()
const mgr = new IdentityManager({
storagePath: dir,
identityFile: path.join(dir, 'device-identity.json')
})
const isNew = await mgr.ensureKeypair()
t.ok(isNew, 'newly generated')
// Read back
const data = JSON.parse(fs.readFileSync(path.join(dir, 'device-identity.json'), 'utf-8'))
t.ok(data.publicKey, 'has publicKey')
t.ok(data.secretKey, 'has secretKey')
t.is(data.publicKey.length, 64, 'publicKey is 32 bytes hex')
// Second call is a no-op
const isNew2 = await mgr.ensureKeypair()
t.absent(isNew2, 'second call returns false')
rmrf(dir)
})
test('first-run - setup creates vault + proof', async (t) => {
const dir = tmpDir()
const mgr = new IdentityManager({
storagePath: dir,
identityFile: path.join(dir, 'device-identity.json')
})
await mgr.ensureKeypair()
const password = Buffer.from('test-password-123')
const result = await mgr.setup(password)
t.ok(result.mnemonic, 'returns mnemonic')
t.ok(result.mnemonic.split(' ').length >= 12, 'mnemonic has 12+ words')
t.ok(result.identityPublicKey, 'returns identity public key')
// Check vault exists
t.ok(mgr.vault.exists(), 'vault file exists')
// Check proof stored
const status = mgr.status()
t.ok(status.hasKeypair, 'has keypair')
t.ok(status.hasVault, 'has vault')
t.ok(status.hasProof, 'has proof')
t.ok(status.identityPublicKey, 'has identity public key')
rmrf(dir)
})
test('first-run - verifyProof validates stored proof', async (t) => {
const dir = tmpDir()
const mgr = new IdentityManager({
storagePath: dir,
identityFile: path.join(dir, 'device-identity.json')
})
await mgr.ensureKeypair()
await mgr.setup(Buffer.from('pass123'))
const verification = mgr.verifyProof()
t.ok(verification.valid, 'proof is valid')
t.ok(verification.identityPublicKey, 'has identity public key')
t.ok(verification.devicePublicKey, 'has device public key')
rmrf(dir)
})
test('first-run - exportMnemonic requires correct password', async (t) => {
const dir = tmpDir()
const mgr = new IdentityManager({
storagePath: dir,
identityFile: path.join(dir, 'device-identity.json')
})
await mgr.ensureKeypair()
const password = Buffer.from('export-test-pw')
const result = await mgr.setup(password)
// Export with correct password
const exported = await mgr.exportMnemonic(password)
t.is(exported, result.mnemonic, 'exported mnemonic matches')
// Export with wrong password fails
try {
await mgr.exportMnemonic(Buffer.from('wrong'))
t.fail('should have thrown')
} catch (err) {
t.pass('wrong password rejected')
}
rmrf(dir)
})
test('first-run - setupFromMnemonic restores identity on new device', async (t) => {
// Simulate first device
const dir1 = tmpDir()
const mgr1 = new IdentityManager({
storagePath: dir1,
identityFile: path.join(dir1, 'device-identity.json')
})
await mgr1.ensureKeypair()
const result1 = await mgr1.setup(Buffer.from('pw1'))
// Simulate second device
const dir2 = tmpDir()
const mgr2 = new IdentityManager({
storagePath: dir2,
identityFile: path.join(dir2, 'device-identity.json')
})
await mgr2.ensureKeypair()
const result2 = await mgr2.setupFromMnemonic(result1.mnemonic, Buffer.from('pw2'))
// Same identity public key, different device keys
t.is(result2.identityPublicKey, result1.identityPublicKey, 'same identity key')
const status1 = mgr1.status()
const status2 = mgr2.status()
t.not(status1.devicePublicKey, status2.devicePublicKey, 'different device keys')
rmrf(dir1)
rmrf(dir2)
})