152 lines
5.1 KiB
TypeScript
152 lines
5.1 KiB
TypeScript
import { ErrorCodes, errorShape } from "../protocol/index.js";
|
|
import {
|
|
assertGatewayPtyOwnership,
|
|
createGatewayPtySession,
|
|
destroyGatewayPtySession,
|
|
GatewayPtyError,
|
|
listGatewayPtySessionsByOwner,
|
|
resizeGatewayPtySession,
|
|
writeGatewayPtySession,
|
|
} from "../pty-manager.js";
|
|
import type { GatewayRequestHandlers } from "./types.js";
|
|
|
|
function getPtyOwner(client: { connect?: { device?: { id?: string } }; connId?: string } | null): {
|
|
ownerKey: string;
|
|
connId: string;
|
|
deviceId?: string;
|
|
} {
|
|
const connId = client?.connId?.trim();
|
|
if (!connId) {
|
|
throw new GatewayPtyError(
|
|
"PTY_INVALID_ARGS",
|
|
"PTY requires an authenticated gateway connection",
|
|
);
|
|
}
|
|
const deviceId = client?.connect?.device?.id?.trim() || undefined;
|
|
return {
|
|
ownerKey: deviceId ? `device:${deviceId}` : `conn:${connId}`,
|
|
connId,
|
|
deviceId,
|
|
};
|
|
}
|
|
|
|
function invalidParams(message: string) {
|
|
return errorShape(ErrorCodes.INVALID_PARAMS, message);
|
|
}
|
|
|
|
function asString(value: unknown): string | undefined {
|
|
return typeof value === "string" ? value : undefined;
|
|
}
|
|
|
|
function asNumber(value: unknown): number | undefined {
|
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
}
|
|
|
|
function mapPtyError(error: unknown) {
|
|
if (error instanceof GatewayPtyError) {
|
|
switch (error.code) {
|
|
case "PTY_NOT_FOUND":
|
|
return errorShape(ErrorCodes.NOT_FOUND, error.message);
|
|
case "PTY_ACCESS_DENIED":
|
|
return errorShape(ErrorCodes.FORBIDDEN, error.message);
|
|
case "PTY_LIMIT_REACHED":
|
|
return errorShape(ErrorCodes.RESOURCE_LIMIT, error.message);
|
|
case "PTY_INPUT_TOO_LARGE":
|
|
return errorShape(ErrorCodes.PAYLOAD_TOO_LARGE, error.message);
|
|
case "PTY_INVALID_ARGS":
|
|
default:
|
|
return invalidParams(error.message);
|
|
}
|
|
}
|
|
return invalidParams(error instanceof Error ? error.message : String(error));
|
|
}
|
|
|
|
export const ptyHandlers: GatewayRequestHandlers = {
|
|
"pty.create": async ({ client, params, respond, context }) => {
|
|
try {
|
|
const owner = getPtyOwner(client);
|
|
const session = await createGatewayPtySession({
|
|
owner,
|
|
cols: asNumber(params.cols),
|
|
rows: asNumber(params.rows),
|
|
cwd: asString(params.cwd),
|
|
shell: asString(params.shell),
|
|
onOutput: ({ sessionId, data, connId }) => {
|
|
context.broadcastToConnIds("pty.output", { sessionId, data }, new Set([connId]));
|
|
},
|
|
onExit: ({ sessionId, code, connId }) => {
|
|
context.broadcastToConnIds("pty.exit", { sessionId, code }, new Set([connId]));
|
|
},
|
|
});
|
|
respond(true, { sessionId: session.sessionId, cwd: session.cwd, shell: session.shell });
|
|
} catch (error) {
|
|
respond(false, undefined, mapPtyError(error));
|
|
}
|
|
},
|
|
"pty.write": ({ client, params, respond }) => {
|
|
try {
|
|
const owner = getPtyOwner(client);
|
|
const sessionId = asString(params.sessionId)?.trim();
|
|
const data = asString(params.data);
|
|
if (!sessionId) {
|
|
respond(false, undefined, invalidParams("pty.write requires sessionId"));
|
|
return;
|
|
}
|
|
if (typeof data !== "string") {
|
|
respond(false, undefined, invalidParams("pty.write requires string data"));
|
|
return;
|
|
}
|
|
assertGatewayPtyOwnership({ sessionId, ownerKey: owner.ownerKey, connId: owner.connId });
|
|
writeGatewayPtySession(sessionId, data);
|
|
respond(true, { ok: true });
|
|
} catch (error) {
|
|
respond(false, undefined, mapPtyError(error));
|
|
}
|
|
},
|
|
"pty.resize": ({ client, params, respond }) => {
|
|
try {
|
|
const owner = getPtyOwner(client);
|
|
const sessionId = asString(params.sessionId)?.trim();
|
|
if (!sessionId) {
|
|
respond(false, undefined, invalidParams("pty.resize requires sessionId"));
|
|
return;
|
|
}
|
|
assertGatewayPtyOwnership({ sessionId, ownerKey: owner.ownerKey, connId: owner.connId });
|
|
resizeGatewayPtySession(sessionId, asNumber(params.cols), asNumber(params.rows));
|
|
respond(true, { ok: true });
|
|
} catch (error) {
|
|
respond(false, undefined, mapPtyError(error));
|
|
}
|
|
},
|
|
"pty.kill": ({ client, params, respond }) => {
|
|
try {
|
|
const owner = getPtyOwner(client);
|
|
const sessionId = asString(params.sessionId)?.trim();
|
|
if (!sessionId) {
|
|
respond(false, undefined, invalidParams("pty.kill requires sessionId"));
|
|
return;
|
|
}
|
|
assertGatewayPtyOwnership({ sessionId, ownerKey: owner.ownerKey, connId: owner.connId });
|
|
destroyGatewayPtySession(sessionId);
|
|
respond(true, { ok: true });
|
|
} catch (error) {
|
|
respond(false, undefined, mapPtyError(error));
|
|
}
|
|
},
|
|
"pty.list": ({ client, respond }) => {
|
|
try {
|
|
const owner = getPtyOwner(client);
|
|
const sessions = listGatewayPtySessionsByOwner(owner.ownerKey).map((session) => ({
|
|
sessionId: session.sessionId,
|
|
createdAt: session.createdAt,
|
|
lastActive: session.lastActive,
|
|
cols: session.cols,
|
|
rows: session.rows,
|
|
}));
|
|
respond(true, { sessions });
|
|
} catch (error) {
|
|
respond(false, undefined, mapPtyError(error));
|
|
}
|
|
},
|
|
};
|