Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
187 lines
5.5 KiB
TypeScript
187 lines
5.5 KiB
TypeScript
import type { Terminal as XTerm } from "@xterm/xterm";
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import type { RefObject } from "react";
|
|
import type { Host, TerminalSession } from "../../../types";
|
|
import type { PendingAuth } from "../runtime/createTerminalSessionStarters";
|
|
import type { TerminalAuthMethod } from "../TerminalAuthDialog";
|
|
import { logger } from "../../../lib/logger";
|
|
|
|
/**
|
|
* Password auth is valid when the user typed something — including a single
|
|
* space. SSH passwords may be whitespace-only; do not trim before this check
|
|
* (issue #2036).
|
|
*/
|
|
export const isAuthPasswordProvided = (password: string): boolean =>
|
|
password.length > 0;
|
|
|
|
export const buildSavedAuthHostUpdate = (
|
|
host: Host,
|
|
auth: {
|
|
authMethod: TerminalAuthMethod;
|
|
username: string;
|
|
password: string;
|
|
keyId: string | null;
|
|
},
|
|
): Host => ({
|
|
...host,
|
|
username: auth.username,
|
|
authMethod: auth.authMethod,
|
|
password: auth.authMethod === "password" ? auth.password : undefined,
|
|
savePassword: auth.authMethod === "password" ? true : host.savePassword,
|
|
identityFileId:
|
|
auth.authMethod === "key" || auth.authMethod === "certificate"
|
|
? (auth.keyId ?? undefined)
|
|
: undefined,
|
|
// Detach stale Keychain identity on explicit credential save (#1956):
|
|
// resolveHostAuth prefers identity credentials over host fields.
|
|
// Empty string (not undefined) so applyGroupDefaults treats this as an explicit
|
|
// host-level override and does not re-inherit a group-level identity; consumers
|
|
// check host.identityId truthiness so "" behaves as "no identity".
|
|
identityId: "",
|
|
});
|
|
|
|
export const useTerminalAuthState = ({
|
|
host,
|
|
pendingAuthRef,
|
|
termRef,
|
|
onUpdateHost,
|
|
onStartSession,
|
|
setStatus,
|
|
setProgressLogs,
|
|
}: {
|
|
host: Host;
|
|
pendingAuthRef: RefObject<PendingAuth>;
|
|
termRef: RefObject<XTerm | null>;
|
|
onUpdateHost?: (host: Host) => void;
|
|
onStartSession: (term: XTerm) => void;
|
|
setStatus: (status: TerminalSession["status"]) => void;
|
|
setProgressLogs: (next: string[] | ((prev: string[]) => string[])) => void;
|
|
}) => {
|
|
const [needsAuth, setNeedsAuth] = useState(false);
|
|
const [authRetryMessage, setAuthRetryMessage] = useState<string | null>(null);
|
|
const [authUsername, setAuthUsername] = useState(host.username || "root");
|
|
const [authMethod, setAuthMethod] = useState<TerminalAuthMethod>("password");
|
|
const [authPassword, setAuthPassword] = useState("");
|
|
const [authKeyId, setAuthKeyId] = useState<string | null>(null);
|
|
const [authPassphrase, setAuthPassphrase] = useState("");
|
|
const [showAuthPassword, setShowAuthPassword] = useState(false);
|
|
const [showAuthPassphrase, setShowAuthPassphrase] = useState(false);
|
|
const [saveCredentials, setSaveCredentials] = useState(true);
|
|
|
|
useEffect(() => {
|
|
setNeedsAuth(false);
|
|
setAuthRetryMessage(null);
|
|
setAuthUsername(host.username || "root");
|
|
setAuthPassword("");
|
|
setAuthKeyId(null);
|
|
setAuthPassphrase("");
|
|
setShowAuthPassword(false);
|
|
setShowAuthPassphrase(false);
|
|
setSaveCredentials(true);
|
|
}, [host.id, host.username]);
|
|
|
|
const isValid = useMemo(() => {
|
|
if (!authUsername.trim()) return false;
|
|
if (authMethod === "password") return isAuthPasswordProvided(authPassword);
|
|
if (authMethod === "key" || authMethod === "certificate") return !!authKeyId;
|
|
return false;
|
|
}, [authKeyId, authMethod, authPassword, authUsername]);
|
|
|
|
const resetForRetry = useCallback(() => {
|
|
setNeedsAuth(false);
|
|
setAuthRetryMessage(null);
|
|
pendingAuthRef.current = null;
|
|
}, [pendingAuthRef]);
|
|
|
|
const submit = useCallback(
|
|
(opts?: { saveToHost?: boolean }) => {
|
|
if (!isValid) return;
|
|
|
|
const shouldSave = opts?.saveToHost ?? saveCredentials;
|
|
pendingAuthRef.current = {
|
|
authMethod,
|
|
username: authUsername,
|
|
password: authMethod === "password" ? authPassword : undefined,
|
|
keyId:
|
|
authMethod === "key" || authMethod === "certificate"
|
|
? (authKeyId ?? undefined)
|
|
: undefined,
|
|
passphrase:
|
|
authMethod === "key" || authMethod === "certificate"
|
|
? authPassphrase || undefined
|
|
: undefined,
|
|
savedToHost: shouldSave && Boolean(onUpdateHost),
|
|
};
|
|
|
|
if (shouldSave && onUpdateHost) {
|
|
onUpdateHost(
|
|
buildSavedAuthHostUpdate(host, {
|
|
authMethod,
|
|
username: authUsername,
|
|
password: authPassword,
|
|
keyId: authKeyId,
|
|
}),
|
|
);
|
|
}
|
|
|
|
setNeedsAuth(false);
|
|
setAuthRetryMessage(null);
|
|
setStatus("connecting");
|
|
setProgressLogs(["Authenticating with provided credentials..."]);
|
|
|
|
const term = termRef.current;
|
|
if (!term) return;
|
|
|
|
try {
|
|
term.clear?.();
|
|
} catch (err) {
|
|
logger.warn("Failed to clear terminal", err);
|
|
}
|
|
|
|
onStartSession(term);
|
|
},
|
|
[
|
|
authKeyId,
|
|
authMethod,
|
|
authPassphrase,
|
|
authPassword,
|
|
authUsername,
|
|
host,
|
|
isValid,
|
|
onStartSession,
|
|
onUpdateHost,
|
|
pendingAuthRef,
|
|
saveCredentials,
|
|
setProgressLogs,
|
|
setStatus,
|
|
termRef,
|
|
],
|
|
);
|
|
|
|
return {
|
|
needsAuth,
|
|
setNeedsAuth,
|
|
authRetryMessage,
|
|
setAuthRetryMessage,
|
|
authUsername,
|
|
setAuthUsername,
|
|
authMethod,
|
|
setAuthMethod,
|
|
authPassword,
|
|
setAuthPassword,
|
|
authKeyId,
|
|
setAuthKeyId,
|
|
authPassphrase,
|
|
setAuthPassphrase,
|
|
showAuthPassword,
|
|
setShowAuthPassword,
|
|
showAuthPassphrase,
|
|
setShowAuthPassphrase,
|
|
saveCredentials,
|
|
setSaveCredentials,
|
|
isValid,
|
|
resetForRetry,
|
|
submit,
|
|
};
|
|
};
|