OverviewFilesCommitsIssuesDocsReleasesPeers

pgh / test / pgh-peer-client.test.js

151 lines · javascript ref:
/**
 * 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')
  }
})