/**
* Sync Routes — P2P replication control endpoints
*/
import { json, readJson } from '../response.js'
import { UnauthorizedError, ForbiddenError, NotFoundError, ValidationError } from '../errors.js'
async function requireAuth (req, server, action, notebook) {
const { authorized, auth, reason } = await server.authManager.authorizeRequest(req, action, notebook)
if (!auth) throw new UnauthorizedError('Authentication required')
if (!authorized) throw new ForbiddenError(reason, action)
return auth
}
export function registerSyncRoutes (router, server) {
// Sync status overview
router.get('/api/sync/status', async (req, res) => {
await requireAuth(req, server, 'admin')
const status = server.replicationManager.getStatus()
json(res, status)
})
// Announce notebook on swarm
router.post('/api/notebooks/:notebook/announce', async (req, res) => {
await requireAuth(req, server, 'admin', req.params.notebook)
const slug = req.params.notebook
const info = await server.driveManager.getDriveInfo(slug)
if (!info) throw new NotFoundError(`Notebook '${slug}' not found`, 'notebook')
await server.replicationManager.announce(slug)
json(res, { announced: true, slug, discoveryKey: info.discoveryKey })
})
// Stop announcing notebook
router.post('/api/notebooks/:notebook/unannounce', async (req, res) => {
await requireAuth(req, server, 'admin', req.params.notebook)
const slug = req.params.notebook
await server.replicationManager.unannounce(slug)
json(res, { announced: false, slug })
})
// Add remote notebook
router.post('/api/sync/add', async (req, res) => {
await requireAuth(req, server, 'admin')
const body = await readJson(req)
if (!body.driveKey || typeof body.driveKey !== 'string') {
throw new ValidationError('driveKey is required (64-char hex)', 'driveKey')
}
if (body.driveKey.length !== 64) {
throw new ValidationError('driveKey must be 64-char hex string', 'driveKey')
}
const slug = body.slug || 'remote-' + body.driveKey.substring(0, 8)
await server.driveManager.addRemoteDrive(body.driveKey, slug)
await server.replicationManager.joinRemote(slug)
json(res, { added: true, slug, driveKey: body.driveKey }, 201)
})
// Remove remote notebook
router.delete('/api/sync/remote/:notebook', async (req, res) => {
await requireAuth(req, server, 'admin')
const slug = req.params.notebook
const info = await server.driveManager.getDriveInfo(slug)
if (!info) throw new NotFoundError(`Remote notebook '${slug}' not found`, 'notebook')
await server.replicationManager.unannounce(slug)
await server.driveManager.removeRemoteDrive(slug)
json(res, { removed: true, slug })
})
// Drive info for a notebook (open read)
router.get('/api/notebooks/:notebook/drive-info', async (req, res) => {
const slug = req.params.notebook
const info = await server.driveManager.getDriveInfo(slug)
if (!info) throw new NotFoundError(`Notebook '${slug}' not found`, 'notebook')
const status = server.replicationManager.getStatus()
json(res, {
...info,
announced: status.announced.includes(slug)
})
})
}
export default registerSyncRoutes