import type { FastifyPluginAsync } from 'fastify' import { repos } from '@evofw/db' import { createInstallLinkBodySchema } from '@evofw/shared' import { AppError } from '../plugins/error-handler.js' import { mapInstallLink, randomToken, } from '../services/install-links.js' import { hashToken } from '../plugins/auth.js' import type { AppConfig } from '../config.js' import { auditMutation } from '../services/audit.js' import { applyPagination } from '../services/pagination.js' export const installLinksRoutes: FastifyPluginAsync<{ config: AppConfig }> = async ( app, opts, ) => { const { config } = opts app.get<{ Querystring: { limit?: string; offset?: string } }>( '/install-links', async (req) => { const items = repos .listInstallLinks(app.db) .map((row) => mapInstallLink(row, config.publicBaseUrl)) const paged = applyPagination(items, req.query) return { items: paged.items, total: paged.total } }, ) app.post('/install-links', async (req, reply) => { const body = createInstallLinkBodySchema.parse(req.body) const linkId = randomToken(32) const slug = randomToken(32) if ( repos.getInstallLink(app.db, linkId) || repos.getInstallLinkBySlug(app.db, slug) ) { throw new AppError('CONFLICT', 'Retry create (id collision)', 409) } const agentId = crypto.randomUUID() const inviteToken = `invite:${agentId}` const now = new Date().toISOString() const name = body.name.trim() const platform = body.platform ?? 'linux' app.sqlite.transaction(() => { repos.insertAgent(app.db, { id: agentId, name, hostname: null, platform, tokenPrefix: inviteToken.slice(0, 12), tokenHash: hashToken(inviteToken), status: 'invited', defaultAction: 'accept', policyGeneration: 1, clientVersion: null, settingsJson: '{}', createdAt: now, }) repos.insertInstallLink(app.db, { id: linkId, slug, clientName: name, platform, agentId, createdAt: now, expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), useCount: 0, }) })() const row = repos.getInstallLink(app.db, linkId)! auditMutation(app, config, req, { action: 'agent.create', targetType: 'app_resource', targetId: agentId, summary: `Создан агент (invite): ${name}`, details: { agent_id: agentId, platform, install_link_id: linkId }, }) return reply.code(201).send(mapInstallLink(row, config.publicBaseUrl)) }) app.delete<{ Params: { id: string } }>( '/install-links/:id', async (req) => { const row = repos.getInstallLink(app.db, req.params.id) if (!row) throw new AppError('NOT_FOUND', 'Install link not found', 404) const updated = repos.revokeInstallLink(app.db, row.id) return mapInstallLink(updated!, config.publicBaseUrl) }, ) }