OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / routes / registry.js

334 lines · javascript ref:
/**
 * 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 })
}