Workspace Terminal tab: xterm front end + short-lived token route
- New Terminal tab in the workspace shell, backed by xterm.js, that connects to the worker's PTY WebSocket using a short-lived token minted by /api/agent-jobs/:id/terminal-token (owner-auth'd, never exposes the worker secret to the browser) - Site-matched xterm theme (light/dark, live theme switching), Victor Mono, binary stdin + JSON resize protocol, reconnect, graceful 'not configured' state when NEXT_PUBLIC_SPOON_AGENT_WORKER_WS_URL is unset - env: NEXT_PUBLIC_SPOON_AGENT_WORKER_WS_URL (client), SPOON_AGENT_TERMINAL_SECRET
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'server-only';
|
||||
|
||||
import { createHmac } from 'node:crypto';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { env } from '@/env';
|
||||
import { convexAuthNextjsToken } from '@convex-dev/auth/nextjs/server';
|
||||
@@ -8,6 +9,26 @@ import { fetchQuery } from 'convex/nextjs';
|
||||
import type { Id } from '@spoon/backend/convex/_generated/dataModel.js';
|
||||
import { api } from '@spoon/backend/convex/_generated/api.js';
|
||||
|
||||
const terminalSecret = () =>
|
||||
env.SPOON_AGENT_TERMINAL_SECRET ??
|
||||
env.SPOON_AGENT_WORKER_INTERNAL_TOKEN ??
|
||||
env.SPOON_WORKER_TOKEN;
|
||||
|
||||
// Mints a short-lived, job-scoped terminal token + the worker WS URL. Returns
|
||||
// null when the terminal feature is not configured. The 2-minute expiry is a
|
||||
// connect window only; an established PTY session persists past it.
|
||||
export const mintTerminalToken = (jobId: Id<'agentJobs'>) => {
|
||||
const secret = terminalSecret();
|
||||
const base = env.NEXT_PUBLIC_SPOON_AGENT_WORKER_WS_URL;
|
||||
if (!secret || !base) return null;
|
||||
const expiresAt = Date.now() + 2 * 60 * 1000;
|
||||
const payload = `${expiresAt}.${jobId}`;
|
||||
const signature = createHmac('sha256', secret).update(payload).digest('hex');
|
||||
const token = `${payload}.${signature}`;
|
||||
const url = `${base.replace(/\/$/, '')}/jobs/${encodeURIComponent(jobId)}/terminal?token=${encodeURIComponent(token)}`;
|
||||
return { url, expiresAt };
|
||||
};
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{ jobId: string }> | { jobId: string };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user