- Create web/middleware.ts to authenticate all API routes via bearer token and origin checks (previously unauthenticated due to missing middleware file) - Fix path traversal in browse-directories: replace startsWith with realpathSync + relative + isAbsolute containment checks - Fix XSS in session HTML export: escape raw HTML blocks via marked renderer - Fix PTY process leak: destroy session on SSE stream cancellation - Fix unhandled exception in terminal sessions POST: wrap getOrCreateSession in try/catch with structured JSON error response - Fix silent child-process failure in headless dispatch: add exit handler to write failed claim when sf headless triage exits non-zero - Fix TypeError on malformed claim JSON: add Array.isArray guard before accessing claim.ids.length All changes type-check cleanly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
69 lines
1.9 KiB
TypeScript
69 lines
1.9 KiB
TypeScript
/**
|
|
* Terminal session management.
|
|
*
|
|
* GET /api/terminal/sessions — list all sessions
|
|
* POST /api/terminal/sessions — create a new session (returns its id)
|
|
* DELETE /api/terminal/sessions?id=x — destroy a session
|
|
*/
|
|
|
|
import { requireProjectCwd } from "../../../../../src/web/bridge-service.ts";
|
|
import {
|
|
destroySession,
|
|
getOrCreateSession,
|
|
isAllowedTerminalCommand,
|
|
listSessions,
|
|
} from "../../../../lib/pty-manager";
|
|
|
|
export const runtime = "nodejs";
|
|
export const dynamic = "force-dynamic";
|
|
|
|
// Persist counter across HMR re-evaluations in dev
|
|
const g = globalThis as Record<string, unknown>;
|
|
if (!g.__sf_pty_next_index__) g.__sf_pty_next_index__ = 1;
|
|
function getNextIndex(): number {
|
|
return (g.__sf_pty_next_index__ as number)++;
|
|
}
|
|
|
|
export async function GET(): Promise<Response> {
|
|
return Response.json({ sessions: listSessions() });
|
|
}
|
|
|
|
export async function POST(request: Request): Promise<Response> {
|
|
const projectCwd = requireProjectCwd(request);
|
|
const id = `term-${getNextIndex()}`;
|
|
let command: string | undefined;
|
|
try {
|
|
const body = (await request.json()) as { command?: string };
|
|
command = body.command;
|
|
} catch {
|
|
// No body or invalid JSON — use default shell
|
|
}
|
|
|
|
if (command && !isAllowedTerminalCommand(command)) {
|
|
return Response.json(
|
|
{ error: `Command not allowed: ${command}` },
|
|
{ status: 403 },
|
|
);
|
|
}
|
|
|
|
try {
|
|
getOrCreateSession(id, projectCwd, command);
|
|
} catch (error) {
|
|
console.error("[pty-sessions] Failed to create session:", error);
|
|
return Response.json(
|
|
{ error: "Failed to create PTY session", detail: String(error) },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
return Response.json({ id });
|
|
}
|
|
|
|
export async function DELETE(request: Request): Promise<Response> {
|
|
const url = new URL(request.url);
|
|
const id = url.searchParams.get("id");
|
|
if (!id) {
|
|
return Response.json({ error: "id is required" }, { status: 400 });
|
|
}
|
|
const ok = destroySession(id);
|
|
return Response.json({ ok, id });
|
|
}
|