OverviewFilesCommitsIssuesDocsReleasesPeers

pgh / test / pgh-device-link.test.js

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