OverviewFilesCommitsIssuesDocsReleasesPeers

favela-peer / test / bridge-observer.test.js

74 lines · javascript ref:
// bridge-observer.test.js — a node keeps the observation feed a bridge vouched for, and nothing it did not
// (hyper-social #82, I1 S4, operator decision O-2 b).
//
// ⛔ VERIFIED AGAINST THE BRIDGE KEY THIS NODE CLAIMED WITH. The grant arrives in the claim reply; it is stored
// only if it is signed by that bridge for this user and generation. A grant that fails is not stored, so the node
// never holds — and never renders — a feed the bridge did not vouch for.

import test from 'brittle'
import fs from '#node:fs'
import os from '#node:os'
import { join } from '#node:path'
import b4a from 'b4a'
import crypto from 'hypercore-crypto'
import { signObserver } from 'favela-identity/lib/observer-grant.js'
import { acceptObserver, observers, forgetObserver } from '../lib/bridge.js'

const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'favela-obs-'))

function claimReply (bridge, over = {}) {
  const grant = { username: 'fleeky', generation: 1, observationFeed: 'f6'.repeat(32), observerRoot: 'e4'.repeat(32), ...over }
  return { username: 'fleeky', generation: 1, acct: 'fleeky@ap.7rnx.net', observer: { observationFeed: grant.observationFeed, observerRoot: grant.observerRoot, sig: signObserver(grant, bridge) } }
}

test('★ a grant signed by the bridge this node claimed with is stored', (t) => {
  const storage = tmp()
  const bridge = crypto.keyPair()
  const key = b4a.toString(bridge.publicKey, 'hex')
  const o = acceptObserver(storage, key, claimReply(bridge))
  t.alike(o, { acct: 'fleeky@ap.7rnx.net', bridge: key, username: 'fleeky', generation: 1, observationFeed: 'f6'.repeat(32), observerRoot: 'e4'.repeat(32) })
  t.alike(observers(storage), [o], 'and listed')
})

test('⛔ a grant from another key, or for another user, is refused and NOT stored', (t) => {
  const storage = tmp()
  const bridge = crypto.keyPair()
  const other = crypto.keyPair()
  const key = b4a.toString(bridge.publicKey, 'hex')
  t.is(acceptObserver(storage, key, claimReply(other)), null, 'signed by someone else')
  const forged = claimReply(bridge)
  forged.username = 'ashatron' // the grant was for fleeky
  t.is(acceptObserver(storage, key, forged), null, 'replayed to another user')
  t.is(acceptObserver(storage, key, { username: 'fleeky', generation: 1, acct: 'fleeky@ap.7rnx.net' }), null, 'no grant at all')
  t.alike(observers(storage), [])
})

test('forgetting a bridge forgets its observation feed', (t) => {
  const storage = tmp()
  const bridge = crypto.keyPair()
  acceptObserver(storage, b4a.toString(bridge.publicKey, 'hex'), claimReply(bridge))
  forgetObserver(storage, 'fleeky@ap.7rnx.net')
  t.alike(observers(storage), [])
})

// ⛔ WHERE THE BOOT REFRESH LOOKS (claimTargets). This device's own bridges.json AND the identity's reach — so a
// device that never claimed anything itself still asks the bridge its identity names, and nothing already holding a
// grant is asked again.
test('★ claim targets: local bridges + the identity\'s reach, minus names that already have a grant', async (t) => {
  const { claimTargets, rememberBridge } = await import('../lib/bridge.js')
  const storage = tmp()
  const A = 'aa'.repeat(32)
  const B = 'bb'.repeat(32)
  rememberBridge(storage, 'local@ap.example', A)
  t.alike(claimTargets(storage, { fediverse: 'fleeky@ap.7rnx.net', fediverseBridge: B }).sort(),
    [['fleeky@ap.7rnx.net', B], ['local@ap.example', A]].sort(), 'both sources')
  t.alike(claimTargets(storage, { fediverse: 'local@ap.example', fediverseBridge: A }), [['local@ap.example', A]], 'deduplicated')
  t.alike(claimTargets(storage, { fediverse: 'x@y', fediverseBridge: null }), [['local@ap.example', A]], 'a name without a bridge is not a target')

  const bridge = crypto.keyPair()
  const key = b4a.toString(bridge.publicKey, 'hex')
  rememberBridge(storage, 'fleeky@ap.7rnx.net', key)
  acceptObserver(storage, key, claimReply(bridge))
  t.absent(claimTargets(storage, {}).some(([acct]) => acct === 'fleeky@ap.7rnx.net'), 'a name with a grant is not asked again')
})