// device-registry.test.js — the persistent, revocable browser-sub-key registry (Option 3): delegated
// sub-keys recorded as RESERVED StateBase entries (never in the public repo), listable + revocable via
// a soft tombstone. The atproto-keyless analog of hyper-identity's IdentityRegistry device records +
// `revoked:` tombstone, riding the StateBase we already run (no second autobase).
import '../lib/bare-globals.js'
import test from 'brittle'
import crypto from 'hypercore-crypto'
import fs from 'bare-fs'
import { StateBaseV3 } from 'atproto-room/lib/state-base.js'
import { SwarmDevice } from '../lib/swarm-device.js'
const PRIV = new Uint8Array(Buffer.from('9085d2bef69286a6cbb51623c8fa258629945cd55ca705cc4e66700396894e0c', 'hex'))
const DID = 'did:plc:test22222222222222222222'
function tmp () { const d = '/tmp/hpp-devreg-' + Math.floor(Math.random() * 1e9); fs.mkdirSync(d, { recursive: true }); return d }
const subKeyFrom = (b) => '02' + b.repeat(32) // valid 33-byte compressed pubkey shape (66 hex)
test('device registry: add → list → revoke → restore (reserved, never public)', async (t) => {
const sb = new StateBaseV3(tmp(), crypto.randomBytes(32), { roomSeed: crypto.randomBytes(32), network: false })
await sb.ready()
t.teardown(() => sb.close())
const dev = new SwarmDevice(sb, { did: DID, privateKey: PRIV, self: 'deviceA' })
const sk1 = subKeyFrom('aa')
const sk2 = subKeyFrom('bb')
await dev.addDevice(sk1, { label: 'laptop' })
await dev.addDevice(sk2)
let list = await dev.listDevices()
t.is(list.length, 2, 'both delegated devices listed')
const d1 = list.find((d) => d.subKey === sk1)
t.is(d1.label, 'laptop', 'label preserved')
t.ok(d1.addedAt > 0, 'addedAt recorded')
t.absent(d1.revoked, 'not revoked initially')
t.absent(await dev.isDeviceRevoked(sk1), 'isDeviceRevoked false initially')
// re-adding preserves the original addedAt (idempotent re-delegate)
const firstAddedAt = d1.addedAt
await dev.addDevice(sk1, { label: 'laptop-renamed' })
const d1b = (await dev.listDevices()).find((d) => d.subKey === sk1)
t.is(d1b.addedAt, firstAddedAt, 're-delegate keeps original addedAt')
t.is(d1b.label, 'laptop-renamed', 're-delegate can relabel')
// revoke sk1
await dev.revokeDevice(sk1, { reason: 'lost laptop' })
t.ok(await dev.isDeviceRevoked(sk1), 'sk1 now revoked')
t.absent(await dev.isDeviceRevoked(sk2), 'sk2 still valid')
list = await dev.listDevices()
t.ok(list.find((d) => d.subKey === sk1).revoked, 'sk1 shows revoked in list')
t.ok(list.find((d) => d.subKey === sk1).revokedAt > 0, 'revokedAt recorded')
// restore sk1 (soft, reversible tombstone — never a destructive delete of the record)
await dev.restoreDevice(sk1)
t.absent(await dev.isDeviceRevoked(sk1), 'sk1 restored')
t.ok((await dev.listDevices()).find((d) => d.subKey === sk1), 'sk1 still listed after restore')
// the registry entries are RESERVED — they never surface as public repo records
const records = await dev.repo.listRecords()
const leaked = records.filter((r) => String(r.collection || '').startsWith('__device'))
t.is(leaked.length, 0, 'device registry entries never appear as public repo records')
// input validation
await t.exception(async () => { await dev.addDevice('nothex') }, /subKey/, 'bad subKey rejected')
await t.exception(async () => { await dev.revokeDevice('nothex') }, /subKey/, 'revoke bad subKey rejected')
})