import type { FastifyInstance } from 'fastify' import { z } from 'zod' import { db, logEvent } from '../db/index.js' import { getStatus, getTunnelRow, startTunnel, stopTunnel, deleteTunnelRuntime, type TunnelRow, } from '../tunnels/manager.js' const createSchema = z.object({ name: z.string().min(1).max(128), integrationId: z.number().int(), mode: z.enum(['local', 'remote', 'dynamic']), sourcePort: z.number().int().min(1).max(65535), endpointHost: z.string().default(''), endpointPort: z.number().int().min(0).max(65535).default(0), autoStart: z.boolean().default(false), maxRetries: z.number().int().min(0).max(20).default(3), retryIntervalMs: z.number().int().min(500).max(60000).default(5000), }) function serialize(row: TunnelRow) { return { id: row.id, name: row.name, integrationId: row.integration_id, mode: row.mode, sourcePort: row.source_port, endpointHost: row.endpoint_host, endpointPort: row.endpoint_port, autoStart: !!row.auto_start, maxRetries: row.max_retries, retryIntervalMs: row.retry_interval_ms, createdAt: row.created_at, ...getStatus(row.id), } } export async function tunnelRoutes(app: FastifyInstance) { app.addHook('onRequest', app.authenticate) app.get('/api/tunnels', async () => { const rows = db.prepare('SELECT * FROM tunnels ORDER BY created_at').all() as TunnelRow[] return { tunnels: rows.map(serialize) } }) app.post('/api/tunnels', async (req, reply) => { const parsed = createSchema.safeParse(req.body) if (!parsed.success) { return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? 'Invalid input' }) } const d = parsed.data const result = db .prepare( `INSERT INTO tunnels (name, integration_id, mode, source_port, endpoint_host, endpoint_port, auto_start, max_retries, retry_interval_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) .run(d.name, d.integrationId, d.mode, d.sourcePort, d.endpointHost, d.endpointPort, d.autoStart ? 1 : 0, d.maxRetries, d.retryIntervalMs) const id = Number(result.lastInsertRowid) const row = getTunnelRow(id)! logEvent('tunnel_created', `${d.name} tunnel added`, d.mode) return reply.code(201).send({ tunnel: serialize(row) }) }) app.delete('/api/tunnels/:id', async (req, reply) => { const id = Number((req.params as { id: string }).id) const row = getTunnelRow(id) if (!row) return reply.code(404).send({ error: 'Tunnel not found' }) deleteTunnelRuntime(id) db.prepare('DELETE FROM tunnels WHERE id = ?').run(id) logEvent('tunnel_deleted', `${row.name} tunnel removed`, row.mode) return { ok: true } }) app.post('/api/tunnels/:id/connect', async (req, reply) => { const id = Number((req.params as { id: string }).id) const row = getTunnelRow(id) if (!row) return reply.code(404).send({ error: 'Tunnel not found' }) startTunnel(id) return { ok: true, ...getStatus(id) } }) app.post('/api/tunnels/:id/disconnect', async (req, reply) => { const id = Number((req.params as { id: string }).id) const row = getTunnelRow(id) if (!row) return reply.code(404).send({ error: 'Tunnel not found' }) stopTunnel(id) return { ok: true, ...getStatus(id) } }) }