OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer / test / peer-write-rpc.test.js

115 lines · javascript ref:
// Model B write path: the peer applies + SIGNS a write requested over RPC (protomux-rpc,
// muxed over the swarm connection). A bridge (the DID's serviceEndpoint) relays the user's
// write; the PEER holds the key and signs — the bridge never does. Authorized by remote
// pubkey; unauthorized callers are refused (secure default: no writes without canWrite).

import '../lib/bare-globals.js' // MUST be first: Bare omits TextEncoder/TextDecoder, which cborg
                                // needs at MODULE LOAD time (via atproto-store) — without this the
                                // whole file throws before any test runs. Every sibling test does this.
import test from 'brittle'
import { mkdtempSync, rmSync } from 'bare-fs'
import { tmpdir } from 'bare-os'
import { join } from 'bare-path'
import b4a from 'b4a'
import c from 'compact-encoding'
import Hyperswarm from 'hyperswarm'
import createTestnet from '@hyperswarm/testnet'
import ProtomuxRPC from 'protomux-rpc'
import { decodeCommitFrame } from 'atproto-store/lib/firehose.js'
import { readCar, loadCommit } from 'atproto-repo/lib/repo.js'
import { verifyCommit } from 'atproto-repo/lib/commit.js'
import { decodeDidKey } from 'atproto-identity/lib/did-key.js'
import { Peer } from '../lib/peer.js'

const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
const DEVICE = 'legal winner thank year wave sausage worth useful legal winner thank yellow'
const NOW = '2026-07-03T00:00:00.000Z'
const RPC_PROTOCOL = 'hyper-proto-peer/rpc/v1'

async function clientRpcTo (swarm, serverKeyHex) {
  const conn = await new Promise((resolve) => {
    swarm.once('connection', resolve)
    swarm.join(b4a.from(serverKeyHex, 'hex'), { client: true, server: false })
    swarm.flush()
  })
  return new ProtomuxRPC(conn, { protocol: RPC_PROTOCOL, valueEncoding: c.json })
}

test('peer signs a write requested over RPC (authorized caller)', async (t) => {
  const testnet = await createTestnet(3)
  const dir = mkdtempSync(join(tmpdir(), 'pw-'))
  let micros = 1_700_000_000_000_000n
  const peer = await Peer.fromMnemonic({ storage: dir, mnemonic: MNEMONIC, handle: 'alice.7rnx.net', pdsUrl: 'https://pds.7rnx.net', clock: () => (micros += 1_000_000n), now: () => NOW })
  const peerSwarm = new Hyperswarm({ bootstrap: testnet.bootstrap })
  const clientSwarm = new Hyperswarm({ bootstrap: testnet.bootstrap })
  const clientHex = b4a.toString(clientSwarm.keyPair.publicKey, 'hex')
  const serving = peer.serve(peerSwarm, { canWrite: (hex) => hex === clientHex })
  await serving.ready()

  t.teardown(async () => {
    serving.close(); await peerSwarm.destroy(); await clientSwarm.destroy()
    await peer.close(); await testnet.destroy(); rmSync(dir, { recursive: true, force: true })
  })

  const rpc = await clientRpcTo(clientSwarm, serving.serverKey)
  const res = await rpc.request('apply-write', { action: 'create', collection: 'app.bsky.feed.post', rkey: '3a', record: { $type: 'app.bsky.feed.post', text: 'written via rpc', createdAt: NOW } })

  t.is(res.did, peer.did, 'response is from the peer DID')
  t.ok(res.commitCid, 'peer returned a signed commit for the RPC-relayed write')
  t.is((await peer.store.records()).length, 1, 'peer applied the write locally (it owns the repo + key)')

  // the write was SIGNED BY THE PEER and is on its own firehose — verify keyless vs its did:key
  const { body } = decodeCommitFrame(await peer.seq.core.get(peer.seq.core.length - 1))
  const signed = loadCommit(await readCar(body.blocks)).value
  const { pubkey } = decodeDidKey(peer.didKey)
  t.ok(verifyCommit(signed, pubkey), 'the RPC-requested write was signed by the PEER (verifies vs its did:key)')
})

test('peer refuses an unauthorized write over RPC (secure default)', async (t) => {
  const testnet = await createTestnet(3)
  const dir = mkdtempSync(join(tmpdir(), 'pw-deny-'))
  const peer = await Peer.fromMnemonic({ storage: dir, mnemonic: MNEMONIC, handle: 'alice.7rnx.net', pdsUrl: 'https://pds.7rnx.net' })
  const peerSwarm = new Hyperswarm({ bootstrap: testnet.bootstrap })
  const clientSwarm = new Hyperswarm({ bootstrap: testnet.bootstrap })
  const serving = peer.serve(peerSwarm, { canWrite: () => false }) // deny all callers
  await serving.ready()

  t.teardown(async () => {
    serving.close(); await peerSwarm.destroy(); await clientSwarm.destroy()
    await peer.close(); await testnet.destroy(); rmSync(dir, { recursive: true, force: true })
  })

  const rpc = await clientRpcTo(clientSwarm, serving.serverKey)
  // protomux-rpc wraps the handler throw as REQUEST_ERROR (our "unauthorized" is the cause),
  // so just assert it throws; the no-write-applied check below confirms it was actually denied.
  await t.exception(rpc.request('apply-write', { action: 'create', collection: 'app.bsky.feed.post', rkey: '3a', record: { $type: 'app.bsky.feed.post', text: 'nope', createdAt: NOW } }), 'unauthorized write rejected')
  t.is((await peer.store.records()).length, 0, 'no write was applied — peer refused the unauthorized caller')
})

test('peer mints a session over RPC for the app-password (create-session)', async (t) => {
  const testnet = await createTestnet(3)
  const dir = mkdtempSync(join(tmpdir(), 'ps-'))
  const peer = await Peer.init({ storage: dir, rootMnemonic: MNEMONIC, deviceMnemonic: DEVICE, handle: 'alice.7rnx.net', pdsUrl: 'https://pds.7rnx.net', appPassword: 'hunter2' })
  const peerSwarm = new Hyperswarm({ bootstrap: testnet.bootstrap })
  const clientSwarm = new Hyperswarm({ bootstrap: testnet.bootstrap })
  const serving = peer.serve(peerSwarm) // no canWrite; canLogin is true -> create-session is offered
  await serving.ready()

  t.teardown(async () => {
    serving.close(); await peerSwarm.destroy(); await clientSwarm.destroy()
    await peer.close(); await testnet.destroy(); rmSync(dir, { recursive: true, force: true })
  })

  const rpc = await clientRpcTo(clientSwarm, serving.serverKey)
  const sess = await rpc.request('create-session', { identifier: 'alice.7rnx.net', password: 'hunter2' })
  t.is(sess.did, peer.did, 'peer minted a session for its DID')
  t.ok(sess.accessJwt && sess.refreshJwt, 'peer returned access + refresh tokens (minted with ITS jwtSecret)')

  // a fresh session verifies against the peer's own jwtSecret, and refresh works
  const refreshed = await rpc.request('refresh-session', { refreshJwt: sess.refreshJwt })
  t.ok(refreshed.accessJwt, 'refresh-session re-minted from a valid refresh token')

  // wrong password is refused (peer is the sole credential authority)
  await t.exception(rpc.request('create-session', { identifier: 'alice.7rnx.net', password: 'wrong' }), 'wrong app-password rejected')
})