feat: move opkssh and warpgate into plugins (C3)

This commit is contained in:
LukeGus committed 2026-09-24 19:28:19 -05:00
1 parent 925da4a661
commit 98d367d7e2
336 files changed
+22380 -4746

No files matched your search

+28 -31
View File
@@ -200,7 +200,7 @@ export async function activate(ctx: PluginContext) {
);
}
// Pending TOTP/Warpgate parking sessions expire on their own; sweep stale
// Pending TOTP and browser sign-in parking sessions expire on their own; sweep stale
// ones so a session left mid-handshake doesn't leak an open ssh2 Client.
const pendingSweep = setInterval(() => {
const now = Date.now();
@@ -386,7 +386,7 @@ export async function activate(ctx: PluginContext) {
if (
req.path === "/connect" ||
req.path === "/connect-totp" ||
req.path === "/connect-warpgate"
req.path === "/connect-browser-sign-in"
) {
return next();
}
@@ -463,7 +463,7 @@ export async function activate(ctx: PluginContext) {
* type: array
* responses:
* 200:
* description: SSH connection established, or requires TOTP/Warpgate authentication.
* description: SSH connection established, or requires TOTP or a browser sign-in.
* 400:
* description: Missing required parameters or invalid SSH key format.
* 401:
@@ -983,7 +983,7 @@ export async function activate(ctx: PluginContext) {
finish: (responses: string[]) => void,
prompts: Array<{ prompt: string; echo: boolean }>,
promptIndex: number,
isWarpgate = false,
isBrowserSignIn = false,
) => {
pendingTOTPSessions[sessionId] = {
client,
@@ -1000,7 +1000,7 @@ export async function activate(ctx: PluginContext) {
totpPromptIndex: promptIndex,
resolvedPassword: resolvedCredentials.password,
totpAttempts: 0,
...(isWarpgate ? { isWarpgate: true } : {}),
...(isBrowserSignIn ? { isBrowserSignIn: true } : {}),
};
};
@@ -1025,23 +1025,24 @@ export async function activate(ctx: PluginContext) {
return;
}
if (decision.kind === "warpgate") {
if (decision.kind === "browser") {
if (responseSent) return;
responseSent = true;
connectionLogs.push(
createConnectionLog(
"info",
"sftp_auth",
"Warpgate authentication required",
`${decision.label} sign-in required`,
{ url: decision.url },
),
);
parkForAnswer(finish, prompts, -1, true);
res.json({
requires_warpgate: true,
requires_browser_sign_in: true,
sessionId,
label: decision.label,
url: decision.url,
securityKey: decision.securityKey,
code: decision.code,
connectionLogs,
});
return;
@@ -1347,10 +1348,10 @@ export async function activate(ctx: PluginContext) {
/**
* @openapi
* /plugin-api/file-manager/connect-warpgate:
* /plugin-api/file-manager/connect-browser-sign-in:
* post:
* summary: Complete Warpgate authentication
* description: Submits empty response to complete Warpgate authentication after user completes browser auth.
* summary: Continue after a browser sign-in
* description: Answers the parked keyboard-interactive round once the user finished signing in in the browser (an SSH gateway's approval, for example).
* tags:
* - File Manager
* requestBody:
@@ -1366,15 +1367,15 @@ export async function activate(ctx: PluginContext) {
* type: string
* responses:
* 200:
* description: Warpgate authentication completed successfully.
* description: Signed in, SSH connection established.
* 401:
* description: Authentication failed or unauthorized.
* 404:
* description: Warpgate session expired.
* description: Sign-in session expired.
* 408:
* description: Warpgate session timeout.
* description: Sign-in session timeout.
*/
app.post("/connect-warpgate", async (req, res) => {
app.post("/connect-browser-sign-in", async (req, res) => {
const { sessionId } = req.body;
const userId = ctx.currentActor();
@@ -1389,12 +1390,12 @@ export async function activate(ctx: PluginContext) {
if (!session) {
return res
.status(404)
.json({ error: "Warpgate session expired. Please reconnect." });
.json({ error: "Sign-in session expired. Please reconnect." });
}
if (!session.isWarpgate) {
if (!session.isBrowserSignIn) {
return res
.status(400)
.json({ error: "Session is not a Warpgate session" });
.json({ error: "Session is not waiting for a browser sign-in" });
}
if (Date.now() - session.createdAt > 300000) {
delete pendingTOTPSessions[sessionId];
@@ -1405,7 +1406,7 @@ export async function activate(ctx: PluginContext) {
}
return res
.status(408)
.json({ error: "Warpgate session timeout. Please reconnect." });
.json({ error: "Sign-in session timeout. Please reconnect." });
}
let responseSent = false;
@@ -1413,7 +1414,7 @@ export async function activate(ctx: PluginContext) {
if (!responseSent) {
responseSent = true;
delete pendingTOTPSessions[sessionId];
res.status(408).json({ error: "Warpgate verification timeout" });
res.status(408).json({ error: "Sign-in verification timeout" });
}
}, 60000);
@@ -1440,7 +1441,7 @@ export async function activate(ctx: PluginContext) {
res.json({
status: "success",
message: "Warpgate verified, SSH connection established",
message: "Signed in, SSH connection established",
});
if (session.hostId && session.userId) {
@@ -1460,9 +1461,7 @@ export async function activate(ctx: PluginContext) {
responseSent = true;
clearTimeout(responseTimeout);
delete pendingTOTPSessions[sessionId];
res
.status(401)
.json({ status: "error", message: "Warpgate authentication failed" });
res.status(401).json({ status: "error", message: "Sign-in failed" });
});
session.finish([""]);
@@ -1567,12 +1566,10 @@ export async function activate(ctx: PluginContext) {
}
const session = sshSessions[sessionId];
if (!session || !session.isConnected) {
return res
.status(400)
.json({
error: "SSH session not found or not connected",
connected: false,
});
return res.status(400).json({
error: "SSH session not found or not connected",
connected: false,
});
}
if (!userId || !verifySessionOwnership(session, userId)) {
return res.status(403).json({ error: "Session access denied" });
+1 -1
View File
@@ -57,7 +57,7 @@ export interface PendingTOTPSession {
totpPromptIndex?: number;
resolvedPassword?: string;
totpAttempts: number;
isWarpgate?: boolean;
isBrowserSignIn?: boolean;
}
export function execWithSudo(
+1 -1
View File
@@ -38,7 +38,7 @@ export function connectSsh(
/**
* Lower-level primitives for the interactive /connect route, which drives its
* own ssh2 Client through the TOTP/Warpgate parking flow rather than letting
* own ssh2 Client through the TOTP and browser sign-in parking flow rather than letting
* ctx.ssh.connect finish the handshake itself.
*/
export function prepareSsh(
@@ -59,7 +59,7 @@ import {
moveSSHItem,
connectSSH,
verifySSHTOTP,
verifySSHWarpgate,
verifySSHBrowserSignIn,
getSSHStatus,
keepSSHAlive,
identifySSHSymlink,
@@ -199,12 +199,13 @@ function FileManagerContent({
const [totpRequired, setTotpRequired] = useState(false);
const [totpSessionId, setTotpSessionId] = useState<string | null>(null);
const [totpPrompt, setTotpPrompt] = useState<string>("");
const [warpgateRequired, setWarpgateRequired] = useState(false);
const [warpgateSessionId, setWarpgateSessionId] = useState<string | null>(
null,
);
const [warpgateUrl, setWarpgateUrl] = useState<string>("");
const [warpgateSecurityKey, setWarpgateSecurityKey] = useState<string>("");
const [browserSignInRequired, setBrowserSignInRequired] = useState(false);
const [browserSignInSessionId, setBrowserSignInSessionId] = useState<
string | null
>(null);
const [browserSignInUrl, setBrowserSignInUrl] = useState<string>("");
const [browserSignInCode, setBrowserSignInCode] = useState<string>("");
const [browserSignInLabel, setBrowserSignInLabel] = useState<string>("");
const [showAuthDialog, setShowAuthDialog] = useState(false);
const [authDialogReason, setAuthDialogReason] = useState<
"no_keyboard" | "auth_failed" | "timeout"
@@ -421,7 +422,7 @@ function FileManagerContent({
!!currentHost &&
!sshSessionId &&
!totpRequired &&
!warpgateRequired &&
!browserSignInRequired &&
!showAuthDialog &&
!showPassphraseDialog,
autoStart: false,
@@ -627,11 +628,12 @@ function FileManagerContent({
socks5ProxyChain: currentHost.socks5ProxyChain,
});
if (result?.requires_warpgate) {
setWarpgateRequired(true);
setWarpgateSessionId(sessionId);
setWarpgateUrl(result.url || "");
setWarpgateSecurityKey(result.securityKey || "N/A");
if (result?.requires_browser_sign_in) {
setBrowserSignInRequired(true);
setBrowserSignInSessionId(sessionId);
setBrowserSignInUrl(result.url || "");
setBrowserSignInCode(result.code || "N/A");
setBrowserSignInLabel(result.label || "");
setIsLoading(false);
return;
}
@@ -698,11 +700,14 @@ function FileManagerContent({
setIsLoading(false);
return;
}
if (sshError.requires_warpgate) {
setWarpgateRequired(true);
setWarpgateSessionId(sshError.sessionId || currentHost.id.toString());
setWarpgateUrl(sshError.url || "");
setWarpgateSecurityKey(sshError.securityKey || "N/A");
if (sshError.requires_browser_sign_in) {
setBrowserSignInRequired(true);
setBrowserSignInSessionId(
sshError.sessionId || currentHost.id.toString(),
);
setBrowserSignInUrl(sshError.url || "");
setBrowserSignInCode(sshError.code || "N/A");
setBrowserSignInLabel(sshError.label || "");
setIsLoading(false);
return;
}
@@ -2660,23 +2665,26 @@ function FileManagerContent({
if (onClose) onClose();
}
async function handleWarpgateContinue() {
if (!warpgateSessionId) return;
async function handleBrowserSignInContinue() {
if (!browserSignInSessionId) return;
try {
setIsLoading(true);
const result = await verifySSHWarpgate(warpgateSessionId);
const result = await verifySSHBrowserSignIn(browserSignInSessionId);
if (result?.status === "success") {
setWarpgateRequired(false);
setWarpgateUrl("");
setWarpgateSecurityKey("");
setSshSessionId(warpgateSessionId);
setWarpgateSessionId(null);
setBrowserSignInRequired(false);
setBrowserSignInUrl("");
setBrowserSignInCode("");
setSshSessionId(browserSignInSessionId);
setBrowserSignInSessionId(null);
connectRetryRef.current.markConnected();
try {
const response = await listSSHFiles(warpgateSessionId, currentPath);
const response = await listSSHFiles(
browserSignInSessionId,
currentPath,
);
const files = Array.isArray(response)
? response
: response?.files || [];
@@ -2691,24 +2699,24 @@ function FileManagerContent({
}
}
} catch (error: unknown) {
console.error("Warpgate verification failed:", error);
toast.error(t("fileManager.warpgateVerificationFailed"));
console.error("Browser sign-in failed:", error);
toast.error(t("fileManager.browserSignInFailed"));
} finally {
setIsLoading(false);
}
}
function handleWarpgateCancel() {
setWarpgateRequired(false);
setWarpgateUrl("");
setWarpgateSecurityKey("");
setWarpgateSessionId(null);
function handleBrowserSignInCancel() {
setBrowserSignInRequired(false);
setBrowserSignInUrl("");
setBrowserSignInCode("");
setBrowserSignInSessionId(null);
if (onClose) onClose();
}
function handleWarpgateOpenUrl() {
if (warpgateUrl) {
window.open(warpgateUrl, "_blank", "noopener,noreferrer");
function handleBrowserSignInOpenUrl() {
if (browserSignInUrl) {
window.open(browserSignInUrl, "_blank", "noopener,noreferrer");
}
}
@@ -2745,11 +2753,12 @@ function FileManagerContent({
socks5ProxyChain: currentHost.socks5ProxyChain,
});
if (result?.requires_warpgate) {
setWarpgateRequired(true);
setWarpgateSessionId(sessionId);
setWarpgateUrl(result.url || "");
setWarpgateSecurityKey(result.securityKey || "N/A");
if (result?.requires_browser_sign_in) {
setBrowserSignInRequired(true);
setBrowserSignInSessionId(sessionId);
setBrowserSignInUrl(result.url || "");
setBrowserSignInCode(result.code || "N/A");
setBrowserSignInLabel(result.label || "");
setIsLoading(false);
return;
}
@@ -3626,12 +3635,13 @@ function FileManagerContent({
totpPrompt={totpPrompt}
handleTotpSubmit={handleTotpSubmit}
handleTotpCancel={handleTotpCancel}
warpgateRequired={warpgateRequired}
warpgateUrl={warpgateUrl}
warpgateSecurityKey={warpgateSecurityKey}
handleWarpgateContinue={handleWarpgateContinue}
handleWarpgateCancel={handleWarpgateCancel}
handleWarpgateOpenUrl={handleWarpgateOpenUrl}
browserSignInRequired={browserSignInRequired}
browserSignInUrl={browserSignInUrl}
browserSignInCode={browserSignInCode}
browserSignInLabel={browserSignInLabel}
handleBrowserSignInContinue={handleBrowserSignInContinue}
handleBrowserSignInCancel={handleBrowserSignInCancel}
handleBrowserSignInOpenUrl={handleBrowserSignInOpenUrl}
currentHost={currentHost}
showAuthDialog={showAuthDialog}
authDialogReason={authDialogReason}
@@ -2,7 +2,7 @@ import type { FileItem, SSHHost } from "@/types/index";
import {
TOTPDialog,
SSHAuthDialog,
WarpgateDialog,
BrowserSignInDialog,
} from "@termix/plugin-sdk/ui";
import { PermissionsDialog } from "./components/PermissionsDialog.tsx";
import { CompressDialog } from "./components/CompressDialog.tsx";
@@ -17,12 +17,13 @@ type FileManagerDialogsProps = {
totpPrompt: string;
handleTotpSubmit: (code: string) => void | Promise<void>;
handleTotpCancel: () => void;
warpgateRequired: boolean;
warpgateUrl: string;
warpgateSecurityKey: string;
handleWarpgateContinue: () => void | Promise<void>;
handleWarpgateCancel: () => void;
handleWarpgateOpenUrl: () => void;
browserSignInRequired: boolean;
browserSignInUrl: string;
browserSignInCode: string;
browserSignInLabel: string;
handleBrowserSignInContinue: () => void | Promise<void>;
handleBrowserSignInCancel: () => void;
handleBrowserSignInOpenUrl: () => void;
currentHost: SSHHost | null;
showAuthDialog: boolean;
authDialogReason: "no_keyboard" | "auth_failed" | "timeout";
@@ -49,12 +50,13 @@ export function FileManagerDialogs({
totpPrompt,
handleTotpSubmit,
handleTotpCancel,
warpgateRequired,
warpgateUrl,
warpgateSecurityKey,
handleWarpgateContinue,
handleWarpgateCancel,
handleWarpgateOpenUrl,
browserSignInRequired,
browserSignInUrl,
browserSignInCode,
browserSignInLabel,
handleBrowserSignInContinue,
handleBrowserSignInCancel,
handleBrowserSignInOpenUrl,
currentHost,
showAuthDialog,
authDialogReason,
@@ -85,13 +87,14 @@ export function FileManagerDialogs({
backgroundColor="var(--bg-canvas)"
/>
<WarpgateDialog
isOpen={warpgateRequired}
url={warpgateUrl}
securityKey={warpgateSecurityKey}
onContinue={handleWarpgateContinue}
onCancel={handleWarpgateCancel}
onOpenUrl={handleWarpgateOpenUrl}
<BrowserSignInDialog
isOpen={browserSignInRequired}
label={browserSignInLabel}
url={browserSignInUrl}
code={browserSignInCode}
onContinue={handleBrowserSignInContinue}
onCancel={handleBrowserSignInCancel}
onOpenUrl={handleBrowserSignInOpenUrl}
backgroundColor="var(--bg-canvas)"
/>
@@ -30,11 +30,12 @@ type ConnectErrorResponse = {
message?: string;
connectionLogs?: ApiConnectionLog[];
requires_totp?: boolean;
requires_warpgate?: boolean;
requires_browser_sign_in?: boolean;
sessionId?: string;
prompt?: string;
url?: string;
securityKey?: string;
code?: string;
label?: string;
status?: string;
reason?: string;
};
@@ -48,11 +49,12 @@ export interface SSHConnectResult {
status?: string;
reason?: "timeout" | "no_keyboard" | "auth_failed";
requires_totp?: boolean;
requires_warpgate?: boolean;
requires_browser_sign_in?: boolean;
sessionId?: string;
prompt?: string;
url?: string;
securityKey?: string;
code?: string;
label?: string;
}
function triggerBlobDownload(blob: Blob, fileName: string): void {
@@ -125,12 +127,13 @@ export async function connectSSH(
prompt: data.prompt,
});
}
if (data.requires_warpgate) {
if (data.requires_browser_sign_in) {
Object.assign(errorWithLogs, {
requires_warpgate: true,
requires_browser_sign_in: true,
sessionId: data.sessionId,
url: data.url,
securityKey: data.securityKey,
code: data.code,
label: data.label,
});
}
if (data.status === "auth_required") {
@@ -176,17 +179,17 @@ export async function verifySSHTOTP(
}
}
export async function verifySSHWarpgate(
export async function verifySSHBrowserSignIn(
sessionId: string,
): Promise<Record<string, unknown>> {
try {
const response = await getFileManagerApiForSession(sessionId).post(
"/ssh/connect-warpgate",
"/ssh/connect-browser-sign-in",
{ sessionId },
);
return response.data;
} catch (error) {
handleApiError(error, "verify SSH Warpgate");
handleApiError(error, "verify SSH browser sign-in");
}
}
@@ -1020,7 +1023,7 @@ export async function ensureSSHSessionForHost(
if (
result?.requires_totp ||
result?.requires_warpgate ||
result?.requires_browser_sign_in ||
result?.status === "auth_required"
) {
return { state: "auth_required", sessionId };
@@ -16,11 +16,12 @@ export type ConnectionLogPayload = Omit<LogEntry, "id" | "timestamp">;
export type SSHConnectionError = Error & {
connectionLogs?: ConnectionLogPayload[];
requires_totp?: boolean;
requires_warpgate?: boolean;
requires_browser_sign_in?: boolean;
sessionId?: string;
prompt?: string;
url?: string;
securityKey?: string;
code?: string;
label?: string;
status?: string;
reason?: "no_keyboard" | "auth_failed" | "timeout";
};