import { randomUUID } from 'node:crypto'; import { join } from 'node:path'; import { createRouter } from '@@/create-router'; import * as errors from '@@/custom-errors'; import { getUserIntegration, upsertUserIntegration, deleteUserIntegration } from 'officerdb'; import { deriveRelayToken, registerUserToken, unregisterUserToken } from './relay-auth'; import { getRelayPort, getUserRelayStatus, getUserTargets } from './relay'; import { captureScreenshot, evaluateJS, navigateTo } from './cdp'; export const browserRouter = createRouter(); const EXTENSION_DIR = join(import.meta.dir, '../../../extensions/browser-relay'); async function getOpts(userId: number) { const port = getRelayPort(); const integration = await getUserIntegration(userId, 'browser-relay'); const salt = (integration?.config as { tokenSalt?: string } | null)?.tokenSalt ?? randomUUID(); if (!integration) { await upsertUserIntegration({ userId, provider: 'browser-relay', config: { tokenSalt: salt } }); } const token = registerUserToken(userId, port, salt); return { relayPort: port, userToken: token }; } browserRouter.get('/extension-download', async (ctx) => { const proc = Bun.spawn(['zip', '-r', '-', '.'], { cwd: EXTENSION_DIR, stdout: 'pipe', stderr: 'pipe', }); const blob = await new Response(proc.stdout).blob(); await proc.exited; if (proc.exitCode !== 0) throw errors.INTERNAL_SERVER_ERROR('Failed to create zip'); return new Response(blob, { headers: { 'Content-Type': 'application/zip', 'Content-Disposition': 'attachment; filename="officer-browser-relay.zip"', }, }); }); browserRouter.get('/status', async (ctx) => { const user = ctx.get('user'); const status = getUserRelayStatus(user.id); return ctx.json(status); }); browserRouter.get('/relay-token', async (ctx) => { const user = ctx.get('user'); const port = getRelayPort(); let integration = await getUserIntegration(user.id, 'browser-relay'); if (!integration) { const salt = randomUUID(); integration = await upsertUserIntegration({ userId: user.id, provider: 'browser-relay', config: { tokenSalt: salt } }); } const salt = (integration.config as { tokenSalt: string }).tokenSalt; const token = registerUserToken(user.id, port, salt); return ctx.json({ token, port }); }); browserRouter.post('/relay-token/regenerate', async (ctx) => { const user = ctx.get('user'); const port = getRelayPort(); const oldIntegration = await getUserIntegration(user.id, 'browser-relay'); if (oldIntegration) { const oldSalt = (oldIntegration.config as { tokenSalt?: string })?.tokenSalt; if (oldSalt) { const oldToken = deriveRelayToken(user.id, port, oldSalt); unregisterUserToken(oldToken); } } const newSalt = randomUUID(); await upsertUserIntegration({ userId: user.id, provider: 'browser-relay', config: { tokenSalt: newSalt } }); const token = registerUserToken(user.id, port, newSalt); return ctx.json({ token, port }); }); browserRouter.delete('/relay-token', async (ctx) => { const user = ctx.get('user'); const port = getRelayPort(); const integration = await getUserIntegration(user.id, 'browser-relay'); if (integration) { const salt = (integration.config as { tokenSalt?: string })?.tokenSalt; if (salt) { const oldToken = deriveRelayToken(user.id, port, salt); unregisterUserToken(oldToken); } await deleteUserIntegration(user.id, 'browser-relay'); } return ctx.json({ ok: true }); }); browserRouter.get('/targets', async (ctx) => { const user = ctx.get('user'); const targets = getUserTargets(user.id); return ctx.json( targets.map((t) => ({ id: t.targetId, sessionId: t.sessionId, type: t.targetInfo.type ?? 'page', title: t.targetInfo.title ?? '', url: t.targetInfo.url ?? '', })), ); }); browserRouter.get('/targets/:id/screenshot', async (ctx) => { const user = ctx.get('user'); const targetId = ctx.req.param('id'); const format = (ctx.req.query('format') as 'png' | 'jpeg') || 'png'; const opts = await getOpts(user.id); // Find session ID for the target const targets = getUserTargets(user.id); const target = targets.find((t) => t.targetId === targetId); if (!target) throw errors.NOT_FOUND('Target not found'); try { const data = await captureScreenshot({ ...opts, targetId: target.sessionId }, format); return ctx.json({ data, format }); } catch (err) { throw errors.INTERNAL_SERVER_ERROR(err instanceof Error ? err.message : 'Screenshot failed'); } }); browserRouter.post('/targets/:id/evaluate', async (ctx) => { const user = ctx.get('user'); const targetId = ctx.req.param('id'); const body = ctx.get('body') as { expression?: string }; if (!body.expression) throw errors.BAD_REQUEST('expression is required'); const opts = await getOpts(user.id); const targets = getUserTargets(user.id); const target = targets.find((t) => t.targetId === targetId); if (!target) throw errors.NOT_FOUND('Target not found'); try { const result = await evaluateJS({ ...opts, targetId: target.sessionId }, body.expression); return ctx.json({ result }); } catch (err) { throw errors.INTERNAL_SERVER_ERROR(err instanceof Error ? err.message : 'Evaluation failed'); } }); browserRouter.post('/targets/:id/navigate', async (ctx) => { const user = ctx.get('user'); const targetId = ctx.req.param('id'); const body = ctx.get('body') as { url?: string }; if (!body.url) throw errors.BAD_REQUEST('url is required'); const opts = await getOpts(user.id); const targets = getUserTargets(user.id); const target = targets.find((t) => t.targetId === targetId); if (!target) throw errors.NOT_FOUND('Target not found'); try { await navigateTo({ ...opts, targetId: target.sessionId }, body.url); return ctx.json({ ok: true }); } catch (err) { throw errors.INTERNAL_SERVER_ERROR(err instanceof Error ? err.message : 'Navigation failed'); } }); browserRouter.post('/targets/:id/activate', async (ctx) => { const user = ctx.get('user'); const targetId = ctx.req.param('id'); const { relayPort, userToken } = await getOpts(user.id); try { const res = await fetch(`http://127.0.0.1:${relayPort}/json/activate/${encodeURIComponent(targetId)}`, { headers: { 'x-officer-relay-token': userToken }, }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return ctx.json({ ok: true }); } catch (err) { throw errors.INTERNAL_SERVER_ERROR(err instanceof Error ? err.message : 'Activate failed'); } }); browserRouter.post('/targets/:id/close', async (ctx) => { const user = ctx.get('user'); const targetId = ctx.req.param('id'); const { relayPort, userToken } = await getOpts(user.id); try { const res = await fetch(`http://127.0.0.1:${relayPort}/json/close/${encodeURIComponent(targetId)}`, { headers: { 'x-officer-relay-token': userToken }, }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return ctx.json({ ok: true }); } catch (err) { throw errors.INTERNAL_SERVER_ERROR(err instanceof Error ? err.message : 'Close failed'); } });