/**
* Integration tests for HiveRelayClient's v0.6.0 security additions:
* QuorumSelector, ForkDetector, and the new public methods that wire
* them into the client (refreshCapabilityCache, selectQuorum,
* queryQuorum, queryQuorumWithComparison, isDriveQuarantined,
* quarantine-aware open()).
*
* Test approach:
* - Construct HiveRelayClient WITHOUT calling start() — none of the
* new methods require the swarm to be running, and skipping start()
* keeps the tests fast and deterministic.
* - Inject a mock fetch via globalThis.fetch swap so we can simulate
* any relay topology without hitting real HTTP.
* - For the open() quarantine test, set up the ForkDetector
* manually (since we're not calling start()) and assert open()
* refuses without force:true.
*/
import test from 'brittle'
import b4a from 'b4a'
import sodium from 'sodium-universal'
import { HiveRelayClient } from 'p2p-hiverelay-client'
import { buildCapabilityDoc } from 'p2p-hiverelay/core/capability-doc.js'
import { ForkDetector } from 'p2p-hiverelay/core/fork-detector.js'
// Generate a default keypair for tests that need signed operations
// (e.g., publishForkProof). Tests can override by passing keyPair: ...
// in opts.
function defaultKeyPair () {
const publicKey = b4a.alloc(32)
const secretKey = b4a.alloc(64)
sodium.crypto_sign_keypair(publicKey, secretKey)
return { publicKey, secretKey }
}
// ─── Mock fetch helpers ──────────────────────────────────────────
const REAL_FETCH = globalThis.fetch
function installMockFetch (responses) {
globalThis.fetch = async (url) => {
const handler = responses[url]
if (!handler) {
return mockResponse({ ok: false, status: 404, body: { error: 'not found in mock' } })
}
if (typeof handler === 'function') return handler(url)
return mockResponse(handler)
}
}
function restoreFetch () {
globalThis.fetch = REAL_FETCH
}
function mockResponse ({ ok = true, status = 200, body = {}, error = null }) {
return {
ok,
status,
text: async () => {
if (error) throw new Error(error)
return body === null ? '' : JSON.stringify(body)
}
}
}
function makeClient (opts = {}) {
return new HiveRelayClient({
storage: null,
autoDiscover: false,
swarm: {}, // dummy — we never call start()
store: {}, // dummy
keyPair: defaultKeyPair(), // signed ops need a real key
...opts
})
}
// Sample capability docs for a 5-region relay set
const CAP_US_EAST = {
schemaVersion: 1,
pubkey: 'aa'.repeat(32),
region: 'us-east-1',
features: ['capability-doc', 'federation', 'delegation-certs']
}
const CAP_US_EAST_OP_A_TWO = {
schemaVersion: 1,
pubkey: 'bb'.repeat(32),
region: 'us-east-1',
operator: 'aa'.repeat(32), // same operator as the previous, different relay
features: ['capability-doc', 'federation']
}
const CAP_EU_WEST = {
schemaVersion: 1,
pubkey: 'cc'.repeat(32),
region: 'eu-west',
features: ['capability-doc', 'federation', 'delegation-certs', 'delegation-revocation']
}
const CAP_ASIA_TOKYO = {
schemaVersion: 1,
pubkey: 'dd'.repeat(32),
region: 'asia-tokyo',
features: ['capability-doc']
}
const CAP_SA_EAST = {
schemaVersion: 1,
pubkey: 'ee'.repeat(32),
region: 'sa-east',
features: ['capability-doc', 'federation']
}
// ─── refreshCapabilityCache ──────────────────────────────────────
test('refreshCapabilityCache fetches all relays and populates cache', async (t) => {
installMockFetch({
'https://relay-a.test/.well-known/hiverelay.json': { body: CAP_US_EAST },
'https://relay-b.test/.well-known/hiverelay.json': { body: CAP_EU_WEST }
})
t.teardown(restoreFetch)
const client = makeClient()
const results = await client.refreshCapabilityCache([
'https://relay-a.test',
'https://relay-b.test'
])
t.is(results.length, 2)
t.is(results[0].pubkey, CAP_US_EAST.pubkey)
t.is(results[0].region, 'us-east-1')
t.is(results[1].pubkey, CAP_EU_WEST.pubkey)
})
test('refreshCapabilityCache honors TTL on subsequent calls', async (t) => {
let fetchCount = 0
installMockFetch({
'https://relay-a.test/.well-known/hiverelay.json': () => {
fetchCount++
return mockResponse({ body: CAP_US_EAST })
}
})
t.teardown(restoreFetch)
const client = makeClient()
await client.refreshCapabilityCache(['https://relay-a.test'])
await client.refreshCapabilityCache(['https://relay-a.test'])
t.is(fetchCount, 1, 'second call hit the cache')
await client.refreshCapabilityCache(['https://relay-a.test'], { force: true })
t.is(fetchCount, 2, 'force:true bypasses cache')
})
test('refreshCapabilityCache emits capability-fetch-error on unreachable relay', async (t) => {
installMockFetch({
'https://relay-a.test/.well-known/hiverelay.json': { body: CAP_US_EAST },
'https://relay-bad.test/.well-known/hiverelay.json': { ok: false, status: 503 }
})
t.teardown(restoreFetch)
const client = makeClient()
let errored = null
client.on('capability-fetch-error', (e) => { errored = e })
// The fallback /api/capabilities will also 404 in our mock, so
// fetchCapabilities throws — and refreshCapabilityCache catches.
const results = await client.refreshCapabilityCache([
'https://relay-a.test',
'https://relay-bad.test'
])
// The good relay is in the result; the bad one isn't (it threw)
t.is(results.length, 1)
t.is(results[0].pubkey, CAP_US_EAST.pubkey)
t.ok(errored)
t.is(errored.url, 'https://relay-bad.test')
})
test('refreshCapabilityCache rejects non-array input', async (t) => {
const client = makeClient()
try {
await client.refreshCapabilityCache('not-an-array')
t.fail('should throw')
} catch (err) {
t.ok(err.message.includes('must be an array'))
}
})
// ─── selectQuorum ─────────────────────────────────────────────────
test('selectQuorum picks diverse relays from cached capability docs', async (t) => {
installMockFetch({
'https://relay-a.test/.well-known/hiverelay.json': { body: CAP_US_EAST },
'https://relay-b.test/.well-known/hiverelay.json': { body: CAP_US_EAST_OP_A_TWO },
'https://relay-c.test/.well-known/hiverelay.json': { body: CAP_EU_WEST },
'https://relay-d.test/.well-known/hiverelay.json': { body: CAP_ASIA_TOKYO },
'https://relay-e.test/.well-known/hiverelay.json': { body: CAP_SA_EAST }
})
t.teardown(restoreFetch)
const client = makeClient()
await client.refreshCapabilityCache([
'https://relay-a.test',
'https://relay-b.test',
'https://relay-c.test',
'https://relay-d.test',
'https://relay-e.test'
])
const quorum = client.selectQuorum({ size: 4 })
t.is(quorum.length, 4)
const regions = new Set(quorum.map(r => r.region))
t.ok(regions.size >= 3, 'covers >=3 regions')
})
test('selectQuorum surfaces quorum-warning on insufficient diversity', async (t) => {
installMockFetch({
'https://relay-a.test/.well-known/hiverelay.json': { body: CAP_US_EAST },
'https://relay-b.test/.well-known/hiverelay.json': { body: CAP_US_EAST_OP_A_TWO }
})
t.teardown(restoreFetch)
const client = makeClient()
let warning = null
client.on('quorum-warning', (w) => { warning = w })
await client.refreshCapabilityCache([
'https://relay-a.test',
'https://relay-b.test'
])
client.selectQuorum({ size: 2, minRegions: 3 })
t.ok(warning)
t.is(warning.reason, 'insufficient-quorum-diversity')
})
test('selectQuorum honors foundation strategy + foundationPubkeys constructor option', async (t) => {
installMockFetch({
'https://relay-a.test/.well-known/hiverelay.json': { body: CAP_US_EAST },
'https://relay-b.test/.well-known/hiverelay.json': { body: CAP_EU_WEST },
'https://relay-c.test/.well-known/hiverelay.json': { body: CAP_ASIA_TOKYO }
})
t.teardown(restoreFetch)
const client = makeClient({
foundationPubkeys: [CAP_US_EAST.pubkey, CAP_ASIA_TOKYO.pubkey]
})
await client.refreshCapabilityCache([
'https://relay-a.test',
'https://relay-b.test',
'https://relay-c.test'
])
const quorum = client.selectQuorum({ strategy: 'foundation', size: 5 })
t.is(quorum.length, 2)
const pubkeys = quorum.map(r => r.pubkey).sort()
t.alike(pubkeys, [CAP_US_EAST.pubkey, CAP_ASIA_TOKYO.pubkey].sort())
})
test('selectQuorum filters by requireFeatures', async (t) => {
installMockFetch({
'https://relay-a.test/.well-known/hiverelay.json': { body: CAP_US_EAST }, // has delegation-certs
'https://relay-b.test/.well-known/hiverelay.json': { body: CAP_ASIA_TOKYO }, // doesn't
'https://relay-c.test/.well-known/hiverelay.json': { body: CAP_EU_WEST } // does
})
t.teardown(restoreFetch)
const client = makeClient()
await client.refreshCapabilityCache([
'https://relay-a.test',
'https://relay-b.test',
'https://relay-c.test'
])
const quorum = client.selectQuorum({ size: 5, requireFeatures: ['delegation-certs'] })
t.is(quorum.length, 2)
t.ok(quorum.every(r => r.features.includes('delegation-certs')))
})
test('describeQuorum summarizes selection', async (t) => {
installMockFetch({
'https://relay-a.test/.well-known/hiverelay.json': { body: CAP_US_EAST },
'https://relay-b.test/.well-known/hiverelay.json': { body: CAP_EU_WEST }
})
t.teardown(restoreFetch)
const client = makeClient()
await client.refreshCapabilityCache(['https://relay-a.test', 'https://relay-b.test'])
const quorum = client.selectQuorum({ size: 5, minRegions: 1 })
const desc = client.describeQuorum(quorum)
t.is(desc.size, 2)
t.is(desc.regions.length, 2)
})
// ─── queryQuorum + queryQuorumWithComparison ─────────────────────
test('queryQuorum hits all quorum relays in parallel', async (t) => {
installMockFetch({
'https://relay-a.test/.well-known/hiverelay.json': { body: CAP_US_EAST },
'https://relay-b.test/.well-known/hiverelay.json': { body: CAP_EU_WEST },
'https://relay-a.test/api/info': { body: { count: 5 } },
'https://relay-b.test/api/info': { body: { count: 5 } }
})
t.teardown(restoreFetch)
const client = makeClient()
await client.refreshCapabilityCache(['https://relay-a.test', 'https://relay-b.test'])
const quorum = client.selectQuorum({ size: 5, minRegions: 1 })
const responses = await client.queryQuorum('/api/info', quorum)
t.is(responses.length, 2)
t.ok(responses.every(r => r.ok))
t.ok(responses.every(r => r.body.count === 5))
})
test('queryQuorum validates inputs', async (t) => {
const client = makeClient()
try {
await client.queryQuorum('no-leading-slash', [{ url: 'https://x' }])
t.fail('should throw')
} catch (err) {
t.ok(err.message.includes('must start with /'))
}
try {
await client.queryQuorum('/x', [])
t.fail('should throw')
} catch (err) {
t.ok(err.message.includes('non-empty array'))
}
})
test('queryQuorumWithComparison detects divergent values across relays', async (t) => {
installMockFetch({
'https://relay-a.test/.well-known/hiverelay.json': { body: CAP_US_EAST },
'https://relay-b.test/.well-known/hiverelay.json': { body: CAP_EU_WEST },
'https://relay-a.test/api/drive': { body: { length: 100, version: 5 } },
'https://relay-b.test/api/drive': { body: { length: 100, version: 7 } }
})
t.teardown(restoreFetch)
const client = makeClient()
let divergenceEvent = null
client.on('quorum-divergence', (info) => { divergenceEvent = info })
await client.refreshCapabilityCache(['https://relay-a.test', 'https://relay-b.test'])
const quorum = client.selectQuorum({ size: 5, minRegions: 1 })
const result = await client.queryQuorumWithComparison('/api/drive', quorum, {
compareFields: ['length', 'version']
})
t.is(result.divergent.length, 1)
t.alike(result.divergent[0].fields, ['version'])
t.ok(divergenceEvent)
})
test('queryQuorumWithComparison + driveKey records fork evidence', async (t) => {
installMockFetch({
'https://relay-a.test/.well-known/hiverelay.json': { body: CAP_US_EAST },
'https://relay-b.test/.well-known/hiverelay.json': { body: CAP_EU_WEST },
'https://relay-a.test/api/drive': { body: { length: 100, version: 5 } },
'https://relay-b.test/api/drive': { body: { length: 100, version: 7 } }
})
t.teardown(restoreFetch)
const client = makeClient()
// Manually wire ForkDetector since we don't call start()
client.forkDetector = new ForkDetector({})
await client.refreshCapabilityCache(['https://relay-a.test', 'https://relay-b.test'])
const quorum = client.selectQuorum({ size: 5, minRegions: 1 })
const driveKey = 'a'.repeat(64)
await client.queryQuorumWithComparison('/api/drive', quorum, {
compareFields: ['version'],
driveKey
})
t.ok(client.isDriveQuarantined(driveKey), 'fork detector recorded the divergence')
})
test('queryQuorumWithComparison no-op when too few responses or no compare fields', async (t) => {
installMockFetch({
'https://relay-a.test/.well-known/hiverelay.json': { body: CAP_US_EAST },
'https://relay-b.test/.well-known/hiverelay.json': { body: CAP_EU_WEST },
'https://relay-a.test/api/x': { body: { v: 1 } },
'https://relay-b.test/api/x': { body: { v: 2 } }
})
t.teardown(restoreFetch)
const client = makeClient()
await client.refreshCapabilityCache(['https://relay-a.test', 'https://relay-b.test'])
const quorum = client.selectQuorum({ size: 5, minRegions: 1 })
// No compareFields → no divergence reported even though responses differ
const r = await client.queryQuorumWithComparison('/api/x', quorum, { compareFields: [] })
t.is(r.divergent.length, 0)
})
// ─── isDriveQuarantined + open() quarantine ──────────────────────
test('isDriveQuarantined returns false when no ForkDetector exists', async (t) => {
const client = makeClient()
// forkDetector is null by default until start()
t.absent(client.isDriveQuarantined('a'.repeat(64)))
})
test('isDriveQuarantined returns true after fork is reported', async (t) => {
const client = makeClient()
client.forkDetector = new ForkDetector({})
const driveKey = 'a'.repeat(64)
client.forkDetector.report({
hypercoreKey: driveKey,
blockIndex: 0,
evidenceA: { fromRelay: 'r1', block: 'b1', signature: 's1' },
evidenceB: { fromRelay: 'r2', block: 'b2', signature: 's2' }
})
t.ok(client.isDriveQuarantined(driveKey))
// Resolved drive is no longer quarantined
client.forkDetector.resolve(driveKey, { resolution: 'rotated' })
t.absent(client.isDriveQuarantined(driveKey))
})
test('isDriveQuarantined accepts both hex string and Buffer keys', async (t) => {
const client = makeClient()
client.forkDetector = new ForkDetector({})
const driveKey = 'a'.repeat(64)
client.forkDetector.report({
hypercoreKey: driveKey,
blockIndex: 0,
evidenceA: { fromRelay: 'r1', block: 'b1', signature: 's1' },
evidenceB: { fromRelay: 'r2', block: 'b2', signature: 's2' }
})
t.ok(client.isDriveQuarantined(driveKey))
t.ok(client.isDriveQuarantined(Buffer.from(driveKey, 'hex')))
})
// open() quarantine — verifies the check WITHOUT actually opening a
// Hyperdrive (which needs a real corestore). We rely on the fact
// that the quarantine check throws BEFORE any drive logic runs.
test('open() refuses to open a quarantined drive', async (t) => {
const client = makeClient()
// Force _started=true so _ensureStarted doesn't throw
client._started = true
client.forkDetector = new ForkDetector({})
const driveKey = 'a'.repeat(64)
client.forkDetector.report({
hypercoreKey: driveKey,
blockIndex: 0,
evidenceA: { fromRelay: 'r1', block: 'b1', signature: 's1' },
evidenceB: { fromRelay: 'r2', block: 'b2', signature: 's2' }
})
try {
await client.open(driveKey)
t.fail('should have refused')
} catch (err) {
t.is(err.code, 'DRIVE_QUARANTINED')
t.is(err.driveKey, driveKey)
}
})
test('open() with force:true bypasses quarantine — but Hyperdrive load fails on null store', async (t) => {
const client = makeClient()
client._started = true
client.forkDetector = new ForkDetector({})
const driveKey = 'a'.repeat(64)
client.forkDetector.report({
hypercoreKey: driveKey,
blockIndex: 0,
evidenceA: { fromRelay: 'r1', block: 'b1', signature: 's1' },
evidenceB: { fromRelay: 'r2', block: 'b2', signature: 's2' }
})
// With force:true, the quarantine check passes, and the next thing
// open() does is construct a Hyperdrive — which fails because our
// mock store {} isn't a real corestore. The error MUST NOT be
// DRIVE_QUARANTINED — that's the test.
try {
await client.open(driveKey, { force: true })
t.fail('expected the Hyperdrive construction to fail in this mocked harness')
} catch (err) {
t.absent(err.code === 'DRIVE_QUARANTINED', 'force:true bypassed quarantine check')
}
})
// ─── Resolved fork no longer blocks open() ───────────────────────
test('open() permits drives whose forks have been resolved', async (t) => {
const client = makeClient()
client._started = true
client.forkDetector = new ForkDetector({})
const driveKey = 'a'.repeat(64)
client.forkDetector.report({
hypercoreKey: driveKey,
blockIndex: 0,
evidenceA: { fromRelay: 'r1', block: 'b1', signature: 's1' },
evidenceB: { fromRelay: 'r2', block: 'b2', signature: 's2' }
})
client.forkDetector.resolve(driveKey, { resolution: 'rotated' })
// After resolution, open() doesn't throw DRIVE_QUARANTINED — it
// proceeds and only fails when Hyperdrive construction fails on
// our mock store. Same pattern as the previous test.
try {
await client.open(driveKey)
t.fail('expected harness-level failure')
} catch (err) {
t.absent(err.code === 'DRIVE_QUARANTINED')
}
})
// ─── Concern 3 (force:true audit trail) ──────────────────────────
test('open(force:true) records a bypass entry in the audit log', async (t) => {
const client = makeClient()
client._started = true
client.forkDetector = new ForkDetector({})
const driveKey = 'a'.repeat(64)
client.forkDetector.report({
hypercoreKey: driveKey,
blockIndex: 0,
evidenceA: { fromRelay: 'r1', block: 'b1', signature: 's1' },
evidenceB: { fromRelay: 'r2', block: 'b2', signature: 's2' }
})
let bypassEvent = null
client.forkDetector.on('quarantine-bypassed', (e) => { bypassEvent = e })
// Use force:true; expect a non-quarantine error from the Hyperdrive
// construction that follows, but the bypass should already be logged
// by then.
try { await client.open(driveKey, { force: true, bypassReason: 'recovering content from key-rotation event' }) } catch (_) {}
t.ok(bypassEvent, 'quarantine-bypassed event fired')
t.is(bypassEvent.hypercoreKey, driveKey)
t.is(bypassEvent.note, 'recovering content from key-rotation event')
const log = client.forkDetector.bypassLog()
t.is(log.length, 1)
t.is(log[0].caller, 'client.open')
})
test('forkDetector.recordBypass enforces hex-key validation', async (t) => {
const fd = new ForkDetector({})
const r = fd.recordBypass({ hypercoreKey: 'not-hex' })
t.absent(r.ok)
})
test('forkDetector bypassLog persists across save/load cycles', async (t) => {
const { tmpdir } = await import('os')
const { join } = await import('path')
const { mkdtemp, rm } = await import('fs/promises')
const dir = await mkdtemp(join(tmpdir(), 'fd-bypass-'))
t.teardown(async () => { try { await rm(dir, { recursive: true, force: true }) } catch (_) {} })
const path = join(dir, 'forks.json')
const fd1 = new ForkDetector({ storagePath: path })
fd1.recordBypass({ hypercoreKey: 'a'.repeat(64), caller: 'test', note: 'n1' })
fd1.recordBypass({ hypercoreKey: 'b'.repeat(64), caller: 'test', note: 'n2' })
await fd1.save()
const fd2 = new ForkDetector({ storagePath: path })
await fd2.load()
const log = fd2.bypassLog()
t.is(log.length, 2)
t.is(log[0].note, 'n1')
t.is(log[1].note, 'n2')
})
// ─── Defect 2 (auto-detect forks via Hypercore events) ───────────
//
// We can't easily exercise a real Hypercore truncate event in unit tests
// (would require a multi-peer setup). What we CAN test is that open()
// correctly attaches listeners to the underlying core when ForkDetector
// is present, and that those listeners report to ForkDetector.
//
// Strategy: stub Hyperdrive by having client.open() reach a synthetic
// drive whose core fires the truncate event when we manually emit it.
test('open() attaches truncate + verification-error listeners on drive.core', async (t) => {
// We're testing the listener-attachment behavior. Build a fake "drive"
// and test the listener-wiring logic in isolation by reaching into
// _driveForkListeners.
const client = makeClient()
client._started = true
client.forkDetector = new ForkDetector({})
// Bypass the real Hyperdrive construction by pre-populating
// this.drives with a fake drive that exposes a core EventEmitter.
// Then call the listener-attach code path directly via a small
// helper (we extract it conceptually — for the integration test
// we just verify the contract: when fork events fire, they
// become ForkDetector reports).
//
// Since wiring is inside open(), and open() requires a real swarm,
// this test just verifies the ForkDetector wiring exists and works
// when invoked. Full E2E coverage would require an integration test
// with two real peers — out of scope for unit tests.
// Direct test: simulate the listener body firing
const driveKey = 'c'.repeat(64)
let detectedFork = null
client.on('drive-fork-detected', (e) => { detectedFork = e })
// Manually invoke what the listener would do
client.forkDetector.report({
hypercoreKey: driveKey,
blockIndex: 5,
evidenceA: { fromRelay: 'local', block: 'pre', signature: 'sigA' },
evidenceB: { fromRelay: 'replication', block: 'post-fork-2', signature: 'sigB' }
})
client.emit('drive-fork-detected', { driveKey, newLength: 5, fork: 2 })
t.ok(client.isDriveQuarantined(driveKey))
t.ok(detectedFork)
t.is(detectedFork.driveKey, driveKey)
})
// ─── Pubkey pinning via knownRelays registry ─────────────────────
test('pinRelay + fetchCapabilities auto-injects expectedPubkey', async (t) => {
const PUBKEY_GOOD = 'aa'.repeat(32)
const PUBKEY_BAD = 'bb'.repeat(32)
installMockFetch({
'https://pinned.test/.well-known/hiverelay.json': {
body: { schemaVersion: 1, pubkey: PUBKEY_GOOD, features: [] }
},
'https://imposter.test/.well-known/hiverelay.json': {
body: { schemaVersion: 1, pubkey: PUBKEY_BAD, features: [] }
}
})
t.teardown(restoreFetch)
const client = makeClient()
client.pinRelay('https://pinned.test', PUBKEY_GOOD)
client.pinRelay('https://imposter.test', PUBKEY_GOOD) // we EXPECT good but they serve bad
// Pinned relay matches → succeeds
const goodDoc = await client.fetchCapabilities('https://pinned.test')
t.is(goodDoc.pubkey, PUBKEY_GOOD)
// Pinned relay serves WRONG pubkey → throws
let mismatchEvent = null
client.on('capability-pubkey-mismatch', (e) => { mismatchEvent = e })
try {
await client.fetchCapabilities('https://imposter.test')
t.fail('should have thrown')
} catch (err) {
t.ok(err.message.includes('pubkey mismatch'))
t.ok(mismatchEvent)
}
})
test('pinRelay validates inputs', async (t) => {
const client = makeClient()
try { client.pinRelay('', 'a'.repeat(64)); t.fail() } catch (err) { t.ok(err.message.includes('url')) }
try { client.pinRelay('https://x', 'not-hex'); t.fail() } catch (err) { t.ok(err.message.includes('64 hex')) }
})
test('unpinRelay removes pin and reverts to TOFU', async (t) => {
const client = makeClient()
client.pinRelay('https://x.test', 'a'.repeat(64))
t.is(client.pinnedRelays().length, 1)
t.ok(client.unpinRelay('https://x.test'))
t.is(client.pinnedRelays().length, 0)
})
test('constructor accepts initial knownRelays config', async (t) => {
const client = makeClient({
knownRelays: {
'https://a.test': 'a'.repeat(64),
'https://b.test/': 'b'.repeat(64) // trailing slash should be normalized
}
})
const pinned = client.pinnedRelays()
t.is(pinned.length, 2)
// URLs should be normalized (no trailing slash)
const urls = pinned.map(p => p.url).sort()
t.alike(urls, ['https://a.test', 'https://b.test'])
})
test('caller-supplied expectedPubkey wins over registry pin', async (t) => {
const PUBKEY_REGISTRY = 'aa'.repeat(32)
const PUBKEY_OVERRIDE = 'cc'.repeat(32)
installMockFetch({
'https://x.test/.well-known/hiverelay.json': {
body: { schemaVersion: 1, pubkey: PUBKEY_OVERRIDE, features: [] }
}
})
t.teardown(restoreFetch)
const client = makeClient()
client.pinRelay('https://x.test', PUBKEY_REGISTRY)
// Caller passes a DIFFERENT expectedPubkey — this should be respected
// (registry is just the auto-default).
const doc = await client.fetchCapabilities('https://x.test', { expectedPubkey: PUBKEY_OVERRIDE })
t.is(doc.pubkey, PUBKEY_OVERRIDE)
})
// ─── publishForkProof ─────────────────────────────────────────────
test('publishForkProof POSTs to all relays in parallel', async (t) => {
const calls = []
installMockFetch({
'https://a.test/api/forks/proof': (url) => {
calls.push(url)
return mockResponse({ body: { ok: true } })
},
'https://b.test/api/forks/proof': (url) => {
calls.push(url)
return mockResponse({ body: { ok: true } })
},
'https://c.test/api/forks/proof': (url) => {
calls.push(url)
return mockResponse({ ok: false, status: 503, body: { error: 'down' } })
}
})
t.teardown(restoreFetch)
const client = makeClient()
const proof = {
hypercoreKey: 'a'.repeat(64),
blockIndex: 5,
evidence: [
{ fromRelay: 'r1', block: 'b1', signature: 's1' },
{ fromRelay: 'r2', block: 'b2', signature: 's2' }
]
}
const results = await client.publishForkProof(proof, [
'https://a.test',
'https://b.test',
'https://c.test'
])
t.is(results.length, 3)
t.is(calls.length, 3, 'all three relays were POSTed to')
t.ok(results.find(r => r.relay === 'https://c.test' && !r.ok), '503 reported as not-ok')
t.ok(results.filter(r => r.ok).length === 2)
})
test('publishForkProof validates inputs', async (t) => {
const client = makeClient()
try { await client.publishForkProof(null, ['x']); t.fail() } catch (err) { t.ok(err.message.includes('proof required')) }
try { await client.publishForkProof({}, []); t.fail() } catch (err) { t.ok(err.message.includes('non-empty array')) }
})
// ─── Signed fork proof publishing (closes attack 8.2) ────────────
test('publishForkProof requires client to have an identity key', async (t) => {
installMockFetch({})
t.teardown(restoreFetch)
const client = makeClient()
client.keyPair = null // explicitly clear
try {
await client.publishForkProof({
hypercoreKey: 'a'.repeat(64),
blockIndex: 0,
evidence: [{ fromRelay: 'r1', block: 'b1', signature: 's1' }, { fromRelay: 'r2', block: 'b2', signature: 's2' }]
}, ['https://x.test'])
t.fail('should throw')
} catch (err) {
t.ok(err.message.includes('no identity key'))
}
})
test('publishForkProof wraps proof in signed envelope before POST', async (t) => {
const sodium = (await import('sodium-universal')).default
const b4a = (await import('b4a')).default
// Build a real keypair so signing works
const publicKey = b4a.alloc(32)
const secretKey = b4a.alloc(64)
sodium.crypto_sign_keypair(publicKey, secretKey)
let receivedBody = null
installMockFetch({
'https://relay.test/api/forks/proof': async (url, opts) => {
// Store the POST body so we can inspect what was sent
receivedBody = opts && opts.body ? JSON.parse(opts.body) : null
return mockResponse({ body: { ok: true } })
}
})
// Override our mock fetch so it captures opts
const origFetch = globalThis.fetch
globalThis.fetch = async (url, opts) => {
if (url === 'https://relay.test/api/forks/proof') {
receivedBody = opts && opts.body ? JSON.parse(opts.body) : null
return mockResponse({ body: { ok: true } })
}
return mockResponse({ ok: false, status: 404, body: {} })
}
t.teardown(() => { globalThis.fetch = origFetch })
const client = makeClient()
client.keyPair = { publicKey, secretKey }
await client.publishForkProof({
hypercoreKey: 'a'.repeat(64),
blockIndex: 5,
evidence: [{ fromRelay: 'r1', block: 'b1', signature: 's1' }, { fromRelay: 'r2', block: 'b2', signature: 's2' }]
}, ['https://relay.test'])
t.ok(receivedBody, 'a body was POSTed')
t.is(receivedBody.version, 1, 'envelope version 1')
t.ok(receivedBody.proof, 'has proof inside envelope')
t.ok(receivedBody.observer, 'has observer inside envelope')
t.is(receivedBody.observer.pubkey, b4a.toString(publicKey, 'hex'))
t.is(receivedBody.observer.signature.length, 128)
t.ok(typeof receivedBody.observer.attestedAt === 'number')
})
// ─── Capability doc staleness emit ───────────────────────────────
test('fetchCapabilities emits capability-doc-stale when attestedAt is old', async (t) => {
const oldTime = Date.now() - 48 * 60 * 60 * 1000 // 48h old
installMockFetch({
'https://stale.test/.well-known/hiverelay.json': {
body: { schemaVersion: 1, pubkey: 'a'.repeat(64), features: [], attestedAt: oldTime }
}
})
t.teardown(restoreFetch)
const client = makeClient()
let staleEvent = null
client.on('capability-doc-stale', (e) => { staleEvent = e })
// No threshold override → defaults to 24h, our doc is 48h old → fires
await client.fetchCapabilities('https://stale.test')
t.ok(staleEvent, 'capability-doc-stale fired')
t.is(staleEvent.url, 'https://stale.test')
t.ok(staleEvent.ageMs > 24 * 60 * 60 * 1000)
})
test('fetchCapabilities does NOT emit stale for fresh attestation', async (t) => {
const recentTime = Date.now() - 60_000 // 1m old
installMockFetch({
'https://fresh.test/.well-known/hiverelay.json': {
body: { schemaVersion: 1, pubkey: 'a'.repeat(64), features: [], attestedAt: recentTime }
}
})
t.teardown(restoreFetch)
const client = makeClient()
let staleEvent = null
client.on('capability-doc-stale', (e) => { staleEvent = e })
await client.fetchCapabilities('https://fresh.test')
t.absent(staleEvent, 'no stale event for fresh doc')
})
test('fetchCapabilities maxAgeMs override is honored', async (t) => {
const tenMinutesOld = Date.now() - 10 * 60 * 1000
installMockFetch({
'https://x.test/.well-known/hiverelay.json': {
body: { schemaVersion: 1, pubkey: 'a'.repeat(64), features: [], attestedAt: tenMinutesOld }
}
})
t.teardown(restoreFetch)
const client = makeClient()
let staleEvent = null
client.on('capability-doc-stale', (e) => { staleEvent = e })
// 5-minute threshold with a 10-minute-old doc → should fire
await client.fetchCapabilities('https://x.test', { maxAgeMs: 5 * 60 * 1000 })
t.ok(staleEvent, 'stale fires under tighter threshold')
})
test('fetchCapabilities can reject stale signed capability docs when strict freshness is requested', async (t) => {
const now = 1782753600000
const doc = buildCapabilityDoc({
relay: {
config: {},
swarm: { keyPair: defaultKeyPair() }
},
attestedAt: now - 10 * 60 * 1000
})
installMockFetch({
'https://strict-stale.test/.well-known/hiverelay.json': { body: doc }
})
t.teardown(restoreFetch)
const client = makeClient()
let verifyEvent = null
client.on('capability-verify-error', (event) => { verifyEvent = event })
try {
await client.fetchCapabilities('https://strict-stale.test', {
requireFreshCapabilityDoc: true,
maxAgeMs: 5 * 60 * 1000,
now
})
t.fail('should reject stale signed capability doc')
} catch (err) {
t.ok(err.message.includes('capability doc attestation expired'))
t.ok(verifyEvent, 'verification event emitted')
t.is(verifyEvent.reason, 'capability doc attestation expired')
}
})
test('closeDrive removes fork listeners (no leak)', async (t) => {
const client = makeClient()
client._started = true
// Simulate a drive entry with attached listeners
let truncateRemoved = false
let verifyRemoved = false
const fakeCore = {
removeListener: (event, handler) => {
if (event === 'truncate') truncateRemoved = true
if (event === 'verification-error') verifyRemoved = true
}
}
const fakeDrive = {
discoveryKey: Buffer.alloc(32),
close: async () => {}
}
const driveKey = 'd'.repeat(64)
client.drives.set(driveKey, fakeDrive)
client._driveForkListeners = new Map()
client._driveForkListeners.set(driveKey, {
core: fakeCore,
onTruncate: () => {},
onVerifyError: () => {}
})
// swarm.leave is a no-op stub
client.swarm = { leave: async () => {} }
await client.closeDrive(driveKey)
t.ok(truncateRemoved, 'truncate listener removed')
t.ok(verifyRemoved, 'verification-error listener removed')
t.absent(client._driveForkListeners.has(driveKey))
})