import test from 'brittle' import fs from 'fs' import path from 'path' import os from 'os' import crypto from 'crypto' const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'peardock-peers-')) process.env.PEARDOCK_PEER_POLICY = path.join(tmp, 'peers.json') delete process.env.PEARDOCK_PEER_ALLOWLIST delete process.env.PEARDOCK_LEGACY_INVITES // Capability minting requires a seed-derived MAC key process.env.SERVER_SEED = crypto.randomBytes(32).toString('hex') const { createInvite, redeemInvite, redeemCapability, mintCapability, deleteInvite, revokePeer, unrevokePeer, clearRevokedPeers, listRevokedPeers, isPeerAllowed, isPeerRevoked, listPeers, listInvites, setPeerRole, registerPeer, } = await import('../server/core/peer-policy.js') const { initAuthKeys } = await import('../server/core/auth-keys.js') initAuthKeys({ seedHex: process.env.SERVER_SEED, publicKeyHex: 'aa'.repeat(32), }) const peerA = '11'.repeat(32) const peerB = '22'.repeat(32) test('capability invite redeem registers peer', (t) => { const inv = createInvite({ role: 'operator', maxUses: 1, ttlHours: 1 }) t.ok(inv.token.includes('.'), 'capability tokens are body.mac') t.is(inv.kind, 'capability') const entry = redeemInvite(inv.token, peerA) t.is(entry.role, 'operator') t.ok(isPeerAllowed(peerA)) t.exception(() => redeemInvite(inv.token, peerB)) }) test('mintCapability + redeemCapability elevates role', (t) => { const peerC = '33'.repeat(32) const cap = mintCapability({ role: 'admin', maxUses: 2, ttlHours: 1 }) t.ok(cap.capability.includes('.')) t.ok(cap.jti) const { role, jti } = redeemCapability(cap.capability, peerC) t.is(role, 'admin') t.is(jti, cap.jti) t.ok(listPeers().peers.some((p) => p.peerId === peerC && p.role === 'admin')) }) test('tampered capability is rejected', (t) => { const cap = mintCapability({ role: 'operator', ttlHours: 1 }) const [body, mac] = cap.capability.split('.') const flipped = body.slice(0, -1) + (body.endsWith('A') ? 'B' : 'A') + '.' + mac t.exception(() => redeemCapability(flipped, peerA)) }) test('listInvites includes unspent capabilities', (t) => { const before = listInvites().length mintCapability({ role: 'viewer', maxUses: 3, ttlHours: 1, note: 'list-test' }) const after = listInvites() t.ok(after.length >= before) t.ok(after.some((i) => i.kind === 'capability' || i.note === 'list-test' || i.jti)) }) test('revoke denies peer', (t) => { registerPeer(peerB, { role: 'viewer' }) t.ok(isPeerAllowed(peerB)) revokePeer(peerB) t.ok(isPeerRevoked(peerB)) t.not(isPeerAllowed(peerB)) t.ok(listRevokedPeers().includes(peerB)) }) test('unrevoke removes peer from revoke list', (t) => { const peerF = '77'.repeat(32) registerPeer(peerF, { role: 'operator' }) revokePeer(peerF) t.ok(isPeerRevoked(peerF)) const res = unrevokePeer(peerF) t.ok(res.success) t.ok(res.removed) t.absent(isPeerRevoked(peerF)) t.ok(isPeerAllowed(peerF)) t.absent(listRevokedPeers().includes(peerF)) }) test('clearRevokedPeers empties revoke list', (t) => { const a = '88'.repeat(32) const b = '99'.repeat(32) revokePeer(a) revokePeer(b) t.ok(listRevokedPeers().length >= 2) const res = clearRevokedPeers() t.ok(res.success) t.ok(res.cleared >= 2) t.is(listRevokedPeers().length, 0) }) test('setPeerRole updates', (t) => { registerPeer(peerA, { role: 'viewer' }) const e = setPeerRole(peerA, 'admin') t.is(e.role, 'admin') t.ok(listPeers().peers.some((p) => p.peerId === peerA && p.role === 'admin')) }) test('seed/capability auth modes bypass allowlist', (t) => { process.env.PEARDOCK_PEER_ALLOWLIST = '1' const unknown = '44'.repeat(32) t.not(isPeerAllowed(unknown)) t.ok(isPeerAllowed(unknown, { authMode: 'seed' })) t.ok(isPeerAllowed(unknown, { authMode: 'capability' })) delete process.env.PEARDOCK_PEER_ALLOWLIST }) test('deleteInvite revokes capability jti so redeem fails', (t) => { const peerD = '55'.repeat(32) const cap = mintCapability({ role: 'operator', maxUses: 5, ttlHours: 1 }) t.ok(listInvites().some((i) => i.jti === cap.jti)) const res = deleteInvite({ jti: cap.jti }) t.ok(res.success) t.ok(res.deleted.length > 0) t.absent(listInvites().some((i) => i.jti === cap.jti)) t.exception(() => redeemCapability(cap.capability, peerD)) }) test('persistent invite allows reconnect after first redeem', (t) => { const peerE = '66'.repeat(32) // Defaults: forever + unlimited uses const cap = mintCapability({ role: 'operator' }) t.ok(cap.persistent) t.is(cap.maxUses, 0) t.is(cap.expiresAt, null) const first = redeemCapability(cap.capability, peerE) t.is(first.role, 'operator') t.absent(first.reconnected) // Second handshake with same capability + same peer identity (app restart) const second = redeemCapability(cap.capability, peerE) t.is(second.role, 'operator') t.ok(second.reconnected) // Still listed as active capability (not spent) t.ok(listInvites().some((i) => i.jti === cap.jti)) })