Worker: interactive terminal WebSocket bridge (PTY in workspace container)
- attachTerminalServer() upgrades /jobs/:id/terminal WS connections, verifying a short-lived job-scoped HMAC token (verifyTerminalToken) so the browser never holds the worker secret - Bridges the socket to a bash PTY via dockerode exec (Tty) in a persistent per-job shell container (spoon-agent-term-<id>) mounting the workspace; binary frames = stdin, JSON text frames = resize; idle containers reaped after 30m - New env: SPOON_AGENT_TERMINAL_IMAGE/SECRET/IDLE_MS (secret falls back to the shared worker internal token)
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { createHmac, timingSafeEqual } from 'node:crypto';
|
||||
|
||||
// Short-lived, job-scoped token authorizing a browser terminal connection.
|
||||
// Minted server-side by the Next app (which has verified job ownership) and
|
||||
// verified here so the browser never sees the shared worker secret. Format:
|
||||
// `${expiresAtMs}.${jobId}.${hmacSha256Hex}`
|
||||
const signature = (payload: string, secret: string) =>
|
||||
createHmac('sha256', secret).update(payload).digest('hex');
|
||||
|
||||
export const verifyTerminalToken = (
|
||||
token: string,
|
||||
jobId: string,
|
||||
secret: string,
|
||||
): boolean => {
|
||||
if (!token || !secret) return false;
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) return false;
|
||||
const [expRaw, tokenJobId, provided] = parts;
|
||||
if (tokenJobId !== jobId) return false;
|
||||
const exp = Number.parseInt(expRaw ?? '', 10);
|
||||
if (!Number.isFinite(exp) || Date.now() > exp) return false;
|
||||
const expected = signature(`${expRaw}.${tokenJobId}`, secret);
|
||||
const providedBuf = Buffer.from(provided ?? '', 'hex');
|
||||
const expectedBuf = Buffer.from(expected, 'hex');
|
||||
return (
|
||||
providedBuf.length === expectedBuf.length &&
|
||||
timingSafeEqual(providedBuf, expectedBuf)
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user