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

+15 -14
View File
@@ -2,9 +2,9 @@
* The Docker panel's interactive connect.
*
* Core's pipeline does the handshake; this only bridges its prompt channel to
* HTTP. A connect that needs a person (a TOTP code, a Warpgate sign-in, a
* HTTP. A connect that needs a person (a TOTP code, a browser sign-in, a
* password the host does not store) answers the request that is waiting with
* what to ask, and the next request (connect-totp, connect-warpgate) carries
* what to ask, and the next request (connect-totp, connect-browser-sign-in) carries
* the answer back and waits for whatever comes after it: the session, another
* prompt, or an error.
*/
@@ -31,10 +31,10 @@ export interface ConnectStep {
}
const TOTP_WAIT_MS = 3 * 60 * 1000;
const WARPGATE_WAIT_MS = 5 * 60 * 1000;
const BROWSER_SIGN_IN_WAIT_MS = 5 * 60 * 1000;
const STEP_WAIT_MS = 90 * 1000;
// Covers the longest a person may take on a prompt, plus the handshake.
const CONNECT_TIMEOUT_MS = WARPGATE_WAIT_MS + 30 * 1000;
const CONNECT_TIMEOUT_MS = BROWSER_SIGN_IN_WAIT_MS + 30 * 1000;
const PASSWORD_PROMPT = /password/i;
@@ -47,7 +47,7 @@ export class PendingConnect {
expiresAt = Date.now() + TOTP_WAIT_MS;
/** What the open prompt expects, or null when nothing is being asked. */
awaiting: "totp" | "warpgate" | null = null;
awaiting: "totp" | "browser" | null = null;
abandoned = false;
private waiter: ((step: ConnectStep) => void) | null = null;
private queued: ConnectStep | null = null;
@@ -87,7 +87,7 @@ export class PendingConnect {
});
}
ask(kind: "totp" | "warpgate", waitMs: number): Promise<string | null> {
ask(kind: "totp" | "browser", waitMs: number): Promise<string | null> {
this.awaiting = kind;
this.expiresAt = Date.now() + waitMs;
return new Promise((resolve) => {
@@ -203,22 +203,23 @@ export async function startConnect(
switch (request.kind) {
case "totp":
return parkTotp(request.prompt, request.retry ? { retry: true } : {});
case "warpgate": {
case "browser": {
logs.push(
connectionLog(
"info",
"docker_auth",
"Warpgate authentication required",
`${request.label} sign-in required`,
),
);
const answer = pending.ask("warpgate", WARPGATE_WAIT_MS);
const answer = pending.ask("browser", BROWSER_SIGN_IN_WAIT_MS);
pending.emit({
status: 200,
body: {
requires_warpgate: true,
requires_browser_sign_in: true,
sessionId,
label: request.label,
url: request.url,
securityKey: request.securityKey,
code: request.code,
connectionLogs: logs,
},
});
@@ -378,7 +379,7 @@ export async function answerConnect(
sessions: DockerSessions,
sessionId: string,
userId: string,
kind: "totp" | "warpgate",
kind: "totp" | "browser",
value: string,
): Promise<ConnectStep> {
const pending = sessions.getPending(sessionId);
@@ -393,8 +394,8 @@ export async function answerConnect(
status: 400,
body: {
error:
kind === "warpgate"
? "Session is not a Warpgate session"
kind === "browser"
? "Session is not waiting for a browser sign-in"
: "Session is not waiting for a code",
},
};
+7 -7
View File
@@ -104,7 +104,7 @@ export function registerRoutes(router: Router, { ctx, sessions, log }: Deps) {
* /plugin-api/docker/ssh/connect:
* post:
* summary: Establish SSH session for Docker
* description: Opens an SSH session to a host for Docker operations. When the host asks for a code, a Warpgate sign-in or a password, the reply says so and the session waits for connect-totp or connect-warpgate.
* description: Opens an SSH session to a host for Docker operations. When the host asks for a code, a browser sign-in or a password, the reply says so and the session waits for connect-totp or connect-browser-sign-in.
* tags:
* - Docker
* requestBody:
@@ -311,10 +311,10 @@ export function registerRoutes(router: Router, { ctx, sessions, log }: Deps) {
/**
* @openapi
* /plugin-api/docker/ssh/connect-warpgate:
* /plugin-api/docker/ssh/connect-browser-sign-in:
* post:
* summary: Complete Warpgate authentication
* description: Continues a connect after the user finished the Warpgate sign-in in the browser.
* summary: Continue after a browser sign-in
* description: Continues a connect after the user finished signing in in the browser (an SSH gateway's approval, for example).
* tags:
* - Docker
* requestBody:
@@ -332,13 +332,13 @@ export function registerRoutes(router: Router, { ctx, sessions, log }: Deps) {
* 200:
* description: Connected, or another prompt.
* 400:
* description: Session ID required, or the session is not a Warpgate session.
* description: Session ID required, or the session is not waiting for a browser sign-in.
* 404:
* description: The connect expired.
* 408:
* description: The host did not answer in time.
*/
router.post("/ssh/connect-warpgate", async (req, res) => {
router.post("/ssh/connect-browser-sign-in", async (req, res) => {
const userId = userOf(res);
if (!userId) return;
const { sessionId } = (req.body ?? {}) as Record<string, unknown>;
@@ -349,7 +349,7 @@ export function registerRoutes(router: Router, { ctx, sessions, log }: Deps) {
sessions,
sessionId,
userId,
"warpgate",
"browser",
"",
);
return res.status(step.status).json(step.body);
+1 -1
View File
@@ -20,7 +20,7 @@ export interface DockerSession {
cancelIdle?: () => void;
}
/** A connect still waiting on a person (TOTP, Warpgate). */
/** A connect still waiting on a person (TOTP, a browser sign-in). */
export interface PendingEntry {
userId: string;
hostId: number;
+23 -16
View File
@@ -26,7 +26,7 @@ import {
Select2,
Separator,
TOTPDialog,
WarpgateDialog,
BrowserSignInDialog,
logActivity,
useAdaptivePolling,
useAreaPreferences,
@@ -57,12 +57,13 @@ interface DockerManagerProps {
}
interface PromptState {
kind: "totp" | "warpgate";
kind: "totp" | "browser";
sessionId: string;
prompt?: string;
isPassword?: boolean;
url?: string;
securityKey?: string;
code?: string;
label?: string;
}
function newSessionId(): string {
@@ -193,12 +194,13 @@ function DockerManagerInner({
) => {
if (result.connectionLogs) setLogs(result.connectionLogs);
if (result.requires_warpgate) {
if (result.requires_browser_sign_in) {
setPrompt({
kind: "warpgate",
kind: "browser",
sessionId: sid,
url: result.url || "",
securityKey: result.securityKey || "",
code: result.code || "",
label: result.label || "",
});
return;
}
@@ -357,15 +359,19 @@ function DockerManagerInner({
}
};
const handleWarpgateContinue = async () => {
if (!prompt || prompt.kind !== "warpgate") return;
const handleBrowserSignInContinue = async () => {
if (!prompt || prompt.kind !== "browser") return;
const sid = prompt.sessionId;
setPrompt(null);
setIsConnecting(true);
try {
await applyConnectResult(sid, await docker.continueWarpgate(sid), false);
await applyConnectResult(
sid,
await docker.continueBrowserSignIn(sid),
false,
);
} catch (error) {
reportFailure(error, t("docker.warpgateVerificationFailed"));
reportFailure(error, t("docker.browserSignInFailed"));
} finally {
setIsConnecting(false);
}
@@ -379,7 +385,7 @@ function DockerManagerInner({
closeTab();
};
const handleWarpgateOpenUrl = () => {
const handleBrowserSignInOpenUrl = () => {
if (prompt?.url) window.open(prompt.url, "_blank", "noopener,noreferrer");
};
@@ -475,13 +481,14 @@ function DockerManagerInner({
onSubmit={handleTotpSubmit}
onCancel={handlePromptCancel}
/>
<WarpgateDialog
isOpen={prompt?.kind === "warpgate"}
<BrowserSignInDialog
isOpen={prompt?.kind === "browser"}
label={prompt?.label ?? ""}
url={prompt?.url ?? ""}
securityKey={prompt?.securityKey ?? ""}
onContinue={handleWarpgateContinue}
code={prompt?.code ?? ""}
onContinue={handleBrowserSignInContinue}
onCancel={handlePromptCancel}
onOpenUrl={handleWarpgateOpenUrl}
onOpenUrl={handleBrowserSignInOpenUrl}
/>
{currentHost && (
<SSHAuthDialog
+6 -5
View File
@@ -28,9 +28,10 @@ export interface ConnectResult {
prompt?: string;
isPassword?: boolean;
retry?: boolean;
requires_warpgate?: boolean;
requires_browser_sign_in?: boolean;
url?: string | null;
securityKey?: string;
code?: string;
label?: string;
connectionLogs?: ApiConnectionLog[];
}
@@ -101,9 +102,9 @@ export function createDockerApi(api: PluginApiClient) {
api.post("/ssh/connect-totp", { sessionId, totpCode }),
),
continueWarpgate: (sessionId: string) =>
call<ConnectResult>("complete Warpgate authentication", () =>
api.post("/ssh/connect-warpgate", { sessionId }),
continueBrowserSignIn: (sessionId: string) =>
call<ConnectResult>("continue after a browser sign-in", () =>
api.post("/ssh/connect-browser-sign-in", { sessionId }),
),
disconnect: (sessionId: string) =>