/**
* NameRegistry tests — TOFU app-name registry.
*/
import test from 'brittle'
import fs from '#node:fs'
import path from '#node:path'
import os from '#node:os'
import Corestore from 'corestore'
import { NameRegistry } from '../lib/name-registry.js'
function mktempStore () {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'name-reg-'))
return { store: new Corestore(dir), dir }
}
function fakePublisher (overrides = {}) {
return {
devicePubkey: 'd'.repeat(64),
registryKey: 'r'.repeat(64),
serverKey: 'd'.repeat(64),
identityRoot: 'i'.repeat(64),
...overrides
}
}
// ── Lifecycle ───────────────────────────────────────────────
test('ready + close', async (t) => {
const { store } = mktempStore()
const reg = new NameRegistry(store)
await reg.ready()
t.ok(reg.keyHex && reg.keyHex.length === 64, 'registry key exposed')
t.ok(reg.writable, 'fresh registry is writable by its creator')
await reg.close()
await store.close()
})
// ── Claim + resolve ─────────────────────────────────────────
test('claim a name, resolve it', async (t) => {
const { store } = mktempStore()
const reg = new NameRegistry(store)
await reg.ready()
const publisher = fakePublisher()
await reg.claim({
name: 'pear-git-v3',
publisher,
description: 'p2p git forge'
})
const entry = await reg.resolve('pear-git-v3')
t.ok(entry, 'entry resolved')
t.is(entry.name, 'pear-git-v3')
t.is(entry.publisher.devicePubkey, publisher.devicePubkey)
t.is(entry.publisher.registryKey, publisher.registryKey)
t.is(entry.description, 'p2p git forge')
t.ok(entry.claimedAt > 0)
await reg.close()
await store.close()
})
test('resolve unknown name returns null', async (t) => {
const { store } = mktempStore()
const reg = new NameRegistry(store)
await reg.ready()
const res = await reg.resolve('does-not-exist')
t.is(res, null)
await reg.close()
await store.close()
})
// ── First-claim-wins ────────────────────────────────────────
test('duplicate claim is rejected (first-claim-wins)', async (t) => {
const { store } = mktempStore()
const reg = new NameRegistry(store)
await reg.ready()
const first = fakePublisher({ devicePubkey: '1'.repeat(64), identityRoot: 'a'.repeat(64) })
const second = fakePublisher({ devicePubkey: '2'.repeat(64), identityRoot: 'b'.repeat(64) })
await reg.claim({ name: 'squatter', publisher: first, description: 'first' })
await reg.claim({ name: 'squatter', publisher: second, description: 'second' })
const entry = await reg.resolve('squatter')
t.is(entry.description, 'first', 'second claim did not overwrite')
t.is(entry.publisher.identityRoot, 'a'.repeat(64), 'original publisher wins')
await reg.close()
await store.close()
})
// ── Update (publisher-only) ─────────────────────────────────
test('update: only publisher can rotate registryKey', async (t) => {
const { store } = mktempStore()
const reg = new NameRegistry(store)
await reg.ready()
const publisher = fakePublisher({ identityRoot: 'owner'.padEnd(64, '0') })
await reg.claim({ name: 'rotatable', publisher })
// Legit update (signedBy matches identityRoot)
await reg.updateName('rotatable', { registryKey: 'NEW'.padEnd(64, '0') }, 'owner'.padEnd(64, '0'))
const updated = await reg.resolve('rotatable')
t.is(updated.publisher.registryKey, 'NEW'.padEnd(64, '0'), 'publisher update applied')
// Hostile update (wrong signedBy) — must be ignored
await reg.updateName('rotatable', { registryKey: 'HIJACK'.padEnd(64, '0') }, 'attacker'.padEnd(64, '0'))
const afterHijackAttempt = await reg.resolve('rotatable')
t.is(
afterHijackAttempt.publisher.registryKey,
'NEW'.padEnd(64, '0'),
'hostile update rejected (signedBy mismatch)'
)
await reg.close()
await store.close()
})
// ── Release + reclaim ───────────────────────────────────────
test('release tombstones the claim; resolve returns null; reclaim works', async (t) => {
const { store } = mktempStore()
const reg = new NameRegistry(store)
await reg.ready()
const first = fakePublisher({ identityRoot: 'first'.padEnd(64, '0'), devicePubkey: 'f'.repeat(64) })
const second = fakePublisher({ identityRoot: 'second'.padEnd(64, '0'), devicePubkey: 's'.repeat(64) })
await reg.claim({ name: 'transient', publisher: first })
t.ok(await reg.resolve('transient'), 'claim visible')
// Wrong releaser cannot tombstone
await reg.release('transient', 'attacker'.padEnd(64, '0'))
t.ok(await reg.resolve('transient'), 'release by non-publisher was rejected')
// Publisher releases
await reg.release('transient', 'first'.padEnd(64, '0'))
t.is(await reg.resolve('transient'), null, 'tombstoned — resolve returns null')
// Second publisher reclaims
await reg.claim({ name: 'transient', publisher: second, description: 'reclaimed' })
const reclaimed = await reg.resolve('transient')
t.ok(reclaimed, 'reclaim succeeded')
t.is(reclaimed.publisher.identityRoot, 'second'.padEnd(64, '0'))
t.is(reclaimed.description, 'reclaimed')
await reg.close()
await store.close()
})
// ── List + by-publisher ─────────────────────────────────────
test('list returns all live claims; includeReleased surfaces tombstones', async (t) => {
const { store } = mktempStore()
const reg = new NameRegistry(store)
await reg.ready()
const pubA = fakePublisher({ identityRoot: 'A'.repeat(64), devicePubkey: '1'.repeat(64) })
const pubB = fakePublisher({ identityRoot: 'B'.repeat(64), devicePubkey: '2'.repeat(64) })
await reg.claim({ name: 'apple', publisher: pubA })
await reg.claim({ name: 'banana', publisher: pubA })
await reg.claim({ name: 'cherry', publisher: pubB })
const all = await reg.list()
t.is(all.length, 3, 'three live claims')
const onlyA = await reg.listByPublisher('A'.repeat(64))
t.is(onlyA.length, 2, 'A owns two names')
t.alike(onlyA.map(e => e.name).sort(), ['apple', 'banana'])
// Release banana — drops from live list, stays in includeReleased
await reg.release('banana', 'A'.repeat(64))
const liveAfterRelease = await reg.list()
t.is(liveAfterRelease.length, 2, 'banana tombstoned, not listed')
const withReleased = await reg.list({ includeReleased: true })
t.is(withReleased.length, 3, 'includeReleased surfaces tombstone')
await reg.close()
await store.close()
})
// ── Persistence ─────────────────────────────────────────────
test('claims persist across close+reopen of the same store', async (t) => {
const { store, dir } = mktempStore()
const reg1 = new NameRegistry(store)
await reg1.ready()
await reg1.claim({ name: 'durable', publisher: fakePublisher() })
const key = reg1.keyHex
await reg1.close()
await store.close()
// Reopen
const store2 = new Corestore(dir)
const reg2 = new NameRegistry(store2)
await reg2.ready()
t.is(reg2.keyHex, key, 'same autobase key')
const entry = await reg2.resolve('durable')
t.ok(entry, 'claim survived')
t.is(entry.name, 'durable')
await reg2.close()
await store2.close()
})