/**
* pgh device-linker — Phase 2c end-to-end.
*
* Mirrors pear-git-v3/test/device-linker.test.js's working pattern but
* exercises pgh's createInvite/joinInvite pair against a hyperident
* vault (rather than synthetic keet-identity-key calls), and asserts
* the result round-trips through pear-git-v3's createKeetVerifier.
*/
import test from 'brittle'
import process from '#node:process'
import fs from '#node:fs'
import path from '#node:path'
import os from '#node:os'
import b4a from 'b4a'
import createTestnet from '@hyperswarm/testnet'
import Hyperswarm from 'hyperswarm'
import { loadOrCreateIdentity } from '../lib/identity.js'
import { createInvite, joinInvite } from '../lib/device-linker.js'
function mktemp (prefix = 'pgh-dev-link') {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix + '-'))
}
test('device linker round-trip — target receives + chain verifies', async (t) => {
const vault = mktemp('pgh-vault')
const { FileManager } = await import('hyperident/lib/file-ops.js')
const prev = FileManager._customPath
FileManager.setBasePath(vault)
const testnet = await createTestnet(2)
const sourceSwarm = new Hyperswarm(testnet[0])
const targetSwarm = new Hyperswarm(testnet[1])
t.teardown(async () => {
try { await sourceSwarm.destroy() } catch {}
try { await targetSwarm.destroy() } catch {}
try { await testnet.destroy() } catch {}
if (prev) FileManager.setBasePath(prev)
else FileManager._customPath = null
fs.rmSync(vault, { recursive: true, force: true })
})
const source = await loadOrCreateIdentity(vault)
t.ok(source.chain, 'source has chain')
const { invite, linked, close } = await createInvite({
swarm: sourceSwarm,
sourceIdentity: source,
label: 'laptop-pgh'
})
t.ok(invite && invite.length > 20, 'invite hex non-trivial')
const [peerResult, payload] = await Promise.all([
linked,
joinInvite({ swarm: targetSwarm, invite, timeout: 30000 })
])
t.is(payload.version, 1)
t.is(payload.label, 'laptop-pgh')
t.is(payload.rootPubkeyHex, source.rootPubkeyHex, 'same root as source')
t.is(payload.devicePublicKeyHex, peerResult.newDevicePubkeyHex,
'source and target agree on new device pubkey')
t.not(payload.devicePublicKeyHex, source.publicKeyHex,
'new device differs from source device')
// Chain depth: source had N, extended should be N+1
const { ProofEncoding } = await import('keet-identity-key/lib/encoding.js')
const cenc = (await import('compact-encoding')).default
const srcDecoded = cenc.decode(ProofEncoding, b4a.from(source.chain, 'base64'))
const extDecoded = cenc.decode(ProofEncoding, b4a.from(payload.chainB64, 'base64'))
t.is(extDecoded.chain.length, srcDecoded.chain.length + 1, 'chain extends by one')
// End-to-end: does the extended chain authenticate as the same root
// via pear-git-v3's server-side verifier?
const verifierPath = path.resolve(
path.dirname(new URL(import.meta.url).pathname),
'..', '..', 'pear-git-v3', 'lib', 'auth', 'keet-verifier.js'
)
if (fs.existsSync(verifierPath)) {
const { createKeetVerifier } = await import(verifierPath)
const verify = createKeetVerifier()
const newDevicePubkeyB64 = b4a.toString(
b4a.from(payload.devicePublicKeyHex, 'hex'), 'base64'
)
const verified = verify(payload.chainB64, newDevicePubkeyB64)
t.is(verified.rootPubkey, source.rootPubkeyHex,
'chain-derived root matches source root')
} else {
t.pass('pear-git-v3 not alongside pgh — skipping verifier round-trip')
}
try { await close() } catch {}
})
test('loadOrCreateIdentity prefers linked-device.json when present', async (t) => {
const tmpHome = mktemp('pgh-linked-home')
const linkedPath = path.join(tmpHome, '.pgh', 'linked-device.json')
fs.mkdirSync(path.dirname(linkedPath), { recursive: true })
const fakePub = 'ab'.repeat(32)
const fakeSec = 'cd'.repeat(64)
const fakeChain = b4a.toString(Buffer.alloc(128, 0x11), 'base64')
const fakeRoot = 'ef'.repeat(32)
fs.writeFileSync(linkedPath, JSON.stringify({
version: 1,
label: 'laptop-pgh',
rootPubkeyHex: fakeRoot,
devicePublicKeyHex: fakePub,
deviceSecretKeyHex: fakeSec,
chainB64: fakeChain,
createdAt: 1700000000000
}))
const prevHome = process.env.HOME
const prevLinked = process.env.PGH_LINKED_DEVICE_FILE
process.env.HOME = tmpHome
// Point the loader at the fake via its explicit override. Relying on HOME +
// os.homedir() is not cross-runtime: bare-os homedir() reads the OS env at
// startup and ignores runtime process.env.HOME mutations (node respects them).
process.env.PGH_LINKED_DEVICE_FILE = linkedPath
t.teardown(() => {
process.env.HOME = prevHome
process.env.PGH_LINKED_DEVICE_FILE = prevLinked || ''
fs.rmSync(tmpHome, { recursive: true, force: true })
})
const mod = await import('../lib/identity.js?linkedtest=' + Date.now())
const id = await mod.loadOrCreateIdentity(tmpHome)
t.is(id.source, 'linked', 'loader picks the linked path')
t.is(id.publicKeyHex, fakePub)
t.is(id.rootPubkeyHex, fakeRoot)
t.is(id.chain, fakeChain)
t.is(id.label, 'laptop-pgh')
})