/**
* Registry Routes — P2P namespace registry management
*
* Handles:
* - POST /api/registry/share Create Autobase, migrate local data, return invite
* - POST /api/registry/join Join via invite hex
* - POST /api/registry/invite Create new invite for existing registry
* - DELETE /api/registry/unlink Disconnect from shared registry
* - GET /api/registry/status Registry status (active, bootstrap key, counts)
* - GET /api/registry/peers List writer peers
*/
import { json, badRequest, readJson } from '../response.js'
import { requireAuth } from '../middleware/require-auth.js'
import { GroupFacade } from '../facades/group-facade.js'
import { RepoLinker } from '../repo-link/repo-linker.js'
import { announceRegistry } from './namespaces.js'
/**
* Register registry routes
*/
export function registerRegistryRoutes (router) {
router.post('/api/registry/share', requireAuth, handleShareRegistry)
router.post('/api/registry/join', requireAuth, handleJoinRegistry)
router.post('/api/registry/invite', requireAuth, handleCreateInvite)
router.delete('/api/registry/unlink', requireAuth, handleUnlinkRegistry)
router.get('/api/registry/status', handleRegistryStatus)
router.get('/api/registry/peers', handleRegistryPeers)
}
/**
* POST /api/registry/share
* Create an Autobase-backed registry, migrate local data, return invite
*/
async function handleShareRegistry (req, res, server) {
const { namespaceRegistry } = server.namespaceData
if (namespaceRegistry) {
return badRequest(res, 'Registry already shared. Use /api/registry/invite to create new invites.')
}
const { store, registry } = server.coreData
const { repoRegistry } = server.repoData
const { serverIdentity } = server.p2p
try {
// Create new NamespaceRegistry
const nsRegistry = new GroupFacade({
metadataBase: server.metadataBase,
repoRegistry
})
// Add creator as owner writer
const userId = req.auth ? (req.auth.userId || req.auth.identityId) : 'anonymous'
const serverKey = serverIdentity
? serverIdentity.getPublicKeyHex()
: '0'.repeat(64)
// DANGER: serverKey bug — passes Ed25519 identity key, not Hypercore key.
await nsRegistry.addWriter(userId, serverKey, 'owner')
// Store bootstrap key in server registry
const bootstrapKey = nsRegistry.bootstrapKeyHex
await registry.put('config:namespaceRegistry', { bootstrapKey })
// Set as active — local groups stay in namespaceManager, visible via merged list
server.namespaceRegistry = nsRegistry
announceRegistry(server)
// Create invite via RepoLinker
let inviteHex = null
try {
const linker = new RepoLinker({ bootstrap: server._bootstrap })
const result = await linker.createInvite({
bootstrapKey,
type: 'namespace-registry',
role: 'writer'
})
inviteHex = result.inviteHex
// Handle waitForPeer in background — catch timeout to prevent crash
result.waitForPeer().then(joinerInfo => {
if (joinerInfo && joinerInfo.peerId && joinerInfo.serverKey) {
// DANGER: serverKey bug — passes Ed25519 identity key, not Hypercore key.
nsRegistry.addWriter(joinerInfo.peerId, joinerInfo.serverKey, 'writer')
.then(() => console.log(`[Registry] Peer joined: ${joinerInfo.peerId}`))
.catch(err => console.error('[Registry] Failed to add writer:', err.message))
}
}).catch(() => {
// Timeout — no peer connected, that's fine
linker.close().catch(() => {})
})
} catch (err) {
console.error('[Registry] Failed to create invite:', err.message)
}
json(res, {
success: true,
bootstrapKey,
invite: inviteHex
})
} catch (err) {
console.error('[Registry] Share error:', err)
json(res, { error: err.message }, 500)
}
}
/**
* POST /api/registry/join
* Join an existing P2P namespace registry via invite
*/
async function handleJoinRegistry (req, res, server) {
const { namespaceRegistry } = server.namespaceData
if (namespaceRegistry) {
return badRequest(res, 'Already connected to a shared registry')
}
const { store, registry } = server.coreData
const { repoRegistry } = server.repoData
const { serverIdentity } = server.p2p
const body = await readJson(req)
const { invite } = body
if (!invite) {
return badRequest(res, 'invite is required')
}
try {
// Join via blind-pairing
const linker = new RepoLinker({ bootstrap: server._bootstrap })
const userId = req.auth ? (req.auth.userId || req.auth.identityId) : 'anonymous'
const serverKey = serverIdentity
? serverIdentity.getPublicKeyHex()
: '0'.repeat(64)
const payload = await linker.joinWithInvite(invite, {
peerId: userId,
serverKey
})
if (!payload.bootstrapKey) {
return badRequest(res, 'Invalid invite — no bootstrap key received')
}
// Create NamespaceRegistry with received bootstrap key
const nsRegistry = new GroupFacade({
metadataBase: server.metadataBase,
repoRegistry
})
// Store config
await registry.put('config:namespaceRegistry', {
bootstrapKey: payload.bootstrapKey
})
// Set as active
server.namespaceRegistry = nsRegistry
announceRegistry(server)
json(res, {
success: true,
bootstrapKey: payload.bootstrapKey
})
} catch (err) {
console.error('[Registry] Join error:', err)
json(res, { error: err.message }, 500)
}
}
/**
* POST /api/registry/invite
* Create a new invite for the existing shared registry
*/
async function handleCreateInvite (req, res, server) {
const { namespaceRegistry } = server.namespaceData
if (!namespaceRegistry) {
return badRequest(res, 'No shared registry active. Use /api/registry/share first.')
}
try {
const linker = new RepoLinker({ bootstrap: server._bootstrap })
const result = await linker.createInvite({
bootstrapKey: namespaceRegistry.bootstrapKeyHex,
type: 'namespace-registry',
role: 'writer'
})
// Handle waitForPeer in background — catch timeout to prevent crash
result.waitForPeer().then(joinerInfo => {
if (joinerInfo && joinerInfo.peerId && joinerInfo.serverKey) {
// DANGER: serverKey bug — passes Ed25519 identity key, not Hypercore key.
namespaceRegistry.addWriter(joinerInfo.peerId, joinerInfo.serverKey, 'writer')
.then(() => console.log(`[Registry] Peer joined: ${joinerInfo.peerId}`))
.catch(err => console.error('[Registry] Failed to add writer:', err.message))
}
}).catch(() => {
linker.close().catch(() => {})
})
json(res, {
invite: result.inviteHex
})
} catch (err) {
console.error('[Registry] Invite error:', err)
json(res, { error: err.message }, 500)
}
}
/**
* DELETE /api/registry/unlink
* Disconnect from the shared registry, notifying peers
*/
async function handleUnlinkRegistry (req, res, server) {
const { namespaceRegistry } = server.namespaceData
if (!namespaceRegistry) {
return badRequest(res, 'No shared registry active')
}
const { registry } = server.coreData
const { serverIdentity, driveSwarm } = server.p2p
try {
const nsRegistry = namespaceRegistry
// 1. Remove self as writer and mark server key as revoked in registry
const serverKey = serverIdentity
? serverIdentity.getPublicKeyHex()
: null
const peerId = serverKey ? serverKey.slice(0, 16) : null
if (peerId) {
try {
await nsRegistry.removeWriter(peerId)
} catch (err) {
console.error('[Registry] remove-writer failed (continuing):', err.message)
}
}
// 2. Also remove all other writers — effectively revokes all peers
try {
const writers = await nsRegistry.listWriters()
for (const writer of writers) {
if (writer.peerId === peerId) continue
try {
await nsRegistry.removeWriter(writer.peerId)
} catch { /* ignore */ }
}
} catch (err) {
console.error('[Registry] remove other writers failed (continuing):', err.message)
}
// 3. Brief sync so peers receive the operations
await new Promise(resolve => setTimeout(resolve, 2000))
// 4. Revoke all federation records in local Hyperbee
try {
for await (const entry of registry.createReadStream({
gt: 'federation:',
lt: 'federation:\xff'
})) {
const record = entry.value
if (record.status === 'active') {
record.status = 'revoked'
record.revokedAt = Date.now()
await registry.put(entry.key, record)
}
}
} catch (err) {
console.error('[Registry] federation revoke failed (continuing):', err.message)
}
// 5. Leave the discovery key on driveSwarm
if (nsRegistry.discoveryKey && driveSwarm) {
await driveSwarm.leave(nsRegistry.discoveryKey)
}
// 6. Close the Autobase instance
await nsRegistry.close()
// 7. Delete config from local Hyperbee
await registry.del('config:namespaceRegistry')
// 8. Clear server reference
server.namespaceRegistry = null
console.log('[Registry] Unlinked from shared registry and revoked all federation peers')
json(res, { success: true })
} catch (err) {
console.error('[Registry] Unlink error:', err)
json(res, { error: err.message }, 500)
}
}
/**
* GET /api/registry/status
* Show registry status
*/
async function handleRegistryStatus (req, res, server) {
const { namespaceRegistry } = server.namespaceData
if (!namespaceRegistry) {
json(res, {
active: false,
mode: 'local'
})
return
}
const namespaces = await namespaceRegistry.list()
const writers = await namespaceRegistry.listWriters()
json(res, {
active: true,
mode: 'shared',
bootstrapKey: namespaceRegistry.bootstrapKeyHex,
writable: namespaceRegistry.writable,
writerCount: writers.length,
namespaceCount: namespaces.length
})
}
/**
* GET /api/registry/peers
* List writer peers
*/
async function handleRegistryPeers (req, res, server) {
const { namespaceRegistry } = server.namespaceData
if (!namespaceRegistry) {
json(res, { writers: [], count: 0 })
return
}
const writers = await namespaceRegistry.listWriters()
json(res, { writers, count: writers.length })
}