from a full audit of all 179 route definitions under src/servers/api, tracing
consumers through useClient, raw fetch, EventSource, the capabilities repo and
the mobile monorepo. only routes with zero consumers anywhere are removed.
server-settings/applications.ts whole file — an app install/update registry
with no settings section to drive it
server-settings/claude-code.ts whole file — the ai settings screen talks
to chat-providers/* exclusively
GET browser/extension-download superseded by a static asset; BrowserRelay
links at /browser-relay-extension.zip
GET integrations/ a stub returning []
GET chat-providers/auth /api-keys says the same thing with more detail
GET desktop/vnc-status and with it the vnc:status command and reply,
which existed only to serve this route.
docs/sidecar-audit-2026-07.md called this
one dead months ago
deliberately KEPT, because "no caller" turned out not to mean "dead":
POST activity/announce not orphaned — it is the missing PRODUCER for the
detached[] list GET activity/tasks already returns
and ActivityScreen already renders. an unbuilt
feature, not dead code, and finishing or dropping
it is a product decision.
GET agents/runs three days old. part of agent grounds, still being
built. "not yet consumed" is not "dead".
PUT/GET vault/unlock-key six days old, storage half of a feature whose
client half is unwritten. the vault is off limits.
DELETE integrations/google/connection caller exists but is deliberately
commented out of the tree. dormant on purpose.
vnc-manager's getSession is now orphaned too, but it is sidecar-internal and
was not in scope; noted rather than chased.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
186 lines
6.6 KiB
TypeScript
186 lines
6.6 KiB
TypeScript
import { randomUUID } from 'node:crypto';
|
|
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();
|
|
|
|
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 };
|
|
}
|
|
|
|
// GET /extension-download used to zip the extension directory on the fly, shelling out to `zip`. It had
|
|
// no caller: BrowserRelay.tsx links straight at /browser-relay-extension.zip, a real file in public/.
|
|
|
|
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');
|
|
}
|
|
});
|