/**
* pgh peer-client ↔ pear-git-v3 round-trip — Phase 3 Tier 2.
*
* Boots a pear-git-v3 daemon in the same process, creates a synthetic
* keet identity + attestation chain, constructs a pgh identity that
* exposes the chain (matching the shape loadOrCreateIdentity would
* return), and calls `list-my-devices` + `revoke-device` through the
* peer-client.
*
* Verifies:
* 1. HTTP /api/rpc accepts pgh's envelope-signed request
* 2. TOFU-registers the device under the chain's root
* 3. list-my-devices returns both entries
* 4. revoke-device rejects subsequent calls from the revoked device
*/
import test from 'brittle'
import fs from '#node:fs'
import path from '#node:path'
import os from '#node:os'
import b4a from 'b4a'
import crypto from 'hypercore-crypto'
import { PghPeerClient } from '../lib/peer-client.js'
function mktemp (prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix + '-'))
}
async function startPgvDaemon (t) {
// Lazy-import pear-git-v3 so this test is skipped cleanly when the
// sibling repo isn't checked out (e.g. stand-alone pgh build).
const pgvPath = path.resolve(
path.dirname(new URL(import.meta.url).pathname),
'..', '..', 'pear-git-v3', 'index.js'
)
if (!fs.existsSync(pgvPath)) return null
const { startPearGit } = await import(pgvPath)
const node = await startPearGit({
storage: mktemp('pgh-peer-client-storage'),
uds: path.join(mktemp('pgh-peer-client-sock'), 'svc.sock'),
http: { port: 0 },
swarm: false,
autoImportSSH: false
})
t.teardown(async () => { try { await node.close() } catch {} })
return node
}
/**
* Construct a pgh-shape identity bound to a freshly generated keet
* identity + one attested device. Matches the fields pgh's peer-client
* reads: publicKey, secretKey, chain (base64), rootPubkeyHex.
*/
async function makePghIdentity () {
const IdentityKey = (await import('keet-identity-key')).default
const mnemonic = IdentityKey.generateMnemonic()
const identityKey = await IdentityKey.from({ mnemonic })
const devKp = crypto.keyPair()
const proof = await identityKey.bootstrap(devKp.publicKey)
return {
publicKey: devKp.publicKey,
secretKey: devKp.secretKey,
publicKeyHex: b4a.toString(devKp.publicKey, 'hex'),
rootPubkeyHex: b4a.toString(identityKey.identityKeyPair.publicKey, 'hex'),
chain: b4a.toString(proof, 'base64'),
source: 'test'
}
}
test('pgh peer-client: envelope round-trip to pear-git-v3 /api/rpc', async (t) => {
const node = await startPgvDaemon(t)
if (!node) {
t.pass('pear-git-v3 not checked out alongside pgh — skipping round-trip')
return
}
const url = 'http://127.0.0.1:' + node.svc.http.port
const deviceA = await makePghIdentity()
const clientA = await PghPeerClient.connect({ url, identity: deviceA })
t.teardown(() => clientA.close())
// First call → TOFU creates user + registers root + device
const listA = await clientA.call('list-my-devices', {})
t.ok(listA && Array.isArray(listA.devices), 'list returns devices array')
const pubkeys = listA.devices.map(d => d.publicKey).sort()
t.ok(pubkeys.includes(deviceA.rootPubkeyHex), 'root in devices')
t.ok(pubkeys.includes(deviceA.publicKeyHex), 'device in devices')
const devEntry = listA.devices.find(d => d.publicKey === deviceA.publicKeyHex)
t.is(devEntry.method, 'keet')
t.is(devEntry.label, null)
t.ok(devEntry.firstLinkedAt, 'firstLinkedAt present')
// Second independent device under the same identity, so we can
// cross-revoke it from device A.
const IdentityKey = (await import('keet-identity-key')).default
const devBKp = crypto.keyPair()
// Reuse deviceA's mnemonic? We didn't retain it — so spin a second
// chain via deviceA's device keypair attesting deviceB under the
// same root. Technique: decode deviceA's proof, use the identity
// key stored there indirectly... actually simpler: attest deviceB
// WITH deviceA's keypair (deviceA is a writer per the chain).
const deviceAProof = b4a.from(deviceA.chain, 'base64')
const deviceBProof = IdentityKey.attestDevice(
devBKp.publicKey,
{ publicKey: deviceA.publicKey, secretKey: deviceA.secretKey },
deviceAProof
)
const deviceB = {
publicKey: devBKp.publicKey,
secretKey: devBKp.secretKey,
publicKeyHex: b4a.toString(devBKp.publicKey, 'hex'),
rootPubkeyHex: deviceA.rootPubkeyHex,
chain: b4a.toString(deviceBProof, 'base64'),
source: 'test'
}
const clientB = await PghPeerClient.connect({ url, identity: deviceB })
t.teardown(() => clientB.close())
// Register device B by making a call
await clientB.call('list-my-devices', {})
// Label device B from device A
const labelRes = await clientA.call('label-device', {
devicePubkey: deviceB.publicKeyHex,
label: 'laptop-pgh'
})
t.ok(labelRes.ok, 'label-device succeeded')
const listA2 = await clientA.call('list-my-devices', {})
const bEntry = listA2.devices.find(d => d.publicKey === deviceB.publicKeyHex)
t.is(bEntry.label, 'laptop-pgh', 'label persisted via remote call')
// Revoke B from A
const revRes = await clientA.call('revoke-device', {
devicePubkey: deviceB.publicKeyHex
})
t.ok(revRes.ok, 'revoke-device succeeded')
// B can no longer authenticate
try {
await clientB.call('list-my-devices', {})
t.fail('revoked device should have been rejected')
} catch (err) {
t.is(err.code, 'EREVOKED', 'revoked device gets EREVOKED')
}
})