// The operator's read-out. Loopback, read-mostly, and it mints nothing.
//
// ⛔ WHAT THIS PINS. The bridge is keyless for atproto and holds only its own AP signing key. An admin
// screen that created identities would make it a key holder for other people and reverse the peer/bridge
// split the whole architecture rests on. So every action here is a flow that ALREADY EXISTS — issue an
// invite, list who registered, release a name whose namespace the operator owns.
import test from 'brittle'
import { adminRoutes, renderOverview } from '../lib/admin.js'
import { MemoryRegistrations } from '../lib/registrations.js'
const ROOT_A = 'a'.repeat(64)
const ROOT_B = 'b'.repeat(64)
const config = { host: 'ap.7rnx.net', registration: 'open', inboundCap: 500 }
const get = (path) => ({ method: 'GET', path })
const post = (path, form) => ({ method: 'POST', path, form })
test('★★ the overview lists who holds a name on this bridge', async (t) => {
const registrations = new MemoryRegistrations({ policy: 'open' })
await registrations.claim({ username: 'fleeky', root: ROOT_A, feedKey: 'd'.repeat(64) })
await registrations.claim({ username: 'ashatron', root: ROOT_B })
const res = await adminRoutes(get('/admin'), { registrations, inbound: null, config })
t.is(res.status, 200)
t.ok(/fleeky@ap\.7rnx\.net/.test(res.body), 'the name as a person actually types it')
t.ok(/ashatron@ap\.7rnx\.net/.test(res.body))
t.ok(/Registration policy.*open/s.test(res.body), 'and the policy, which decides who may join')
})
test('★★★ a released name is shown, not hidden', async (t) => {
// ⛔ A burned name is NOT a free name: the generation counter makes a reused one a different actor so
// it cannot inherit the previous holder's audience. An operator seeing only the live list would read
// a burned name as merely absent.
const registrations = new MemoryRegistrations({ policy: 'open' })
await registrations.claim({ username: 'gone', root: ROOT_A })
await registrations.release('gone', { root: ROOT_A })
const res = await adminRoutes(get('/admin'), { registrations, inbound: null, config })
t.ok(/Released names/.test(res.body))
t.ok(/>gone</.test(res.body), 'the tombstone is visible')
})
test('★★★ the operator may release a name, and it burns', async (t) => {
const registrations = new MemoryRegistrations({ policy: 'open' })
await registrations.claim({ username: 'squatted', root: ROOT_A })
const res = await adminRoutes(post('/admin/release', { username: 'squatted' }), { registrations, inbound: null, config })
t.is(res.status, 303, 'back to the overview')
t.absent(await registrations.get('squatted'), 'the name is no longer held')
// ⚠ Burned, not freed — so this is not a way to hand somebody else's name to a third party.
const dead = await registrations.released('squatted')
t.ok(dead, 'a tombstone remains')
t.is(dead.generation, 1)
})
test('★★ issuing an invite is the existing flow, surfaced', async (t) => {
const registrations = new MemoryRegistrations({ policy: 'invite' })
const res = await adminRoutes(post('/admin/invite', {}), { registrations, inbound: null, config })
t.is(res.status, 200)
t.ok(/single-use/.test(res.body), 'and it says what an invite is')
})
test('★★★ the cap is stated, and so is the fact that it cannot be backfilled', (t) => {
// atproto inbound can be rebuilt from Constellation — a backlink index answering "what links to this
// subject?" for any point in the past. ActivityPub is push-only delivery and has no such index, so an
// eviction here may destroy the only copy. An operator choosing a cap has to be told which of the two
// they are choosing.
const body = renderOverview({ host: 'ap.7rnx.net', cap: 500, inbound: [] })
t.ok(/Keeping the newest/.test(body))
t.ok(/no backlink index/.test(body), 'the fediverse asymmetry is said out loud')
t.ok(/peer holds the durable copy/.test(body), 'and that the cap is a buffer, not the memory')
})
test('★★ nothing here mints an identity', async (t) => {
const registrations = new MemoryRegistrations({ policy: 'open' })
const res = await adminRoutes(get('/admin'), { registrations, inbound: null, config })
t.absent(/mint/i.test(res.body), 'no minting offered — the bridge holds no key for anybody else')
const missing = await adminRoutes(post('/admin/mint', {}), { registrations, inbound: null, config })
t.is(missing.status, 404, 'and no such route exists')
})
// ── pages another role of the process adds (the favela gateway's status page, hyper-social #80) ──────
test('★ an added page is served behind the same gate, even when registration is off', async (t) => {
const pages = new Map([['/admin/status', async () => ({ title: 'Gateway status', body: '<p id="x">all roles up</p>' })]])
const res = await adminRoutes(get('/admin/status'), { registrations: null, inbound: null, config, pages })
t.is(res.status, 200)
t.ok(res.body.includes('<p id="x">all roles up</p>'), 'the page body, as rendered by its owner')
t.ok(res.body.includes('<title>Gateway status</title>'), 'inside the admin chrome')
})
test('the overview links every added page', async (t) => {
const registrations = new MemoryRegistrations({ policy: 'open' })
const pages = new Map([['/admin/status', async () => ({ title: 'Gateway status', body: '' })]])
const res = await adminRoutes(get('/admin'), { registrations, inbound: null, config, pages })
t.ok(res.body.includes('href="/admin/status"') && res.body.includes('Gateway status'))
})
test('an added page that throws answers 500 with the reason, not a dead admin', async (t) => {
const pages = new Map([['/admin/status', async () => { throw new Error('metrics unavailable') }]])
const res = await adminRoutes(get('/admin/status'), { registrations: null, inbound: null, config, pages })
t.is(res.status, 500)
t.ok(res.body.includes('metrics unavailable'))
})