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
90 lines
3.0 KiB
TypeScript
90 lines
3.0 KiB
TypeScript
import type { Host, Identity, SSHKey } from "./models";
|
|
import { sanitizeCredentialValue } from "./credentials";
|
|
import { resolveHostAuth, resolveHostAutofillPassword } from "./sshAuth";
|
|
|
|
export type PasswordPromptFillCandidate = {
|
|
id: string;
|
|
source: "host" | "identity";
|
|
label: string;
|
|
username?: string;
|
|
/** Sanitized plaintext. Callers must not log or render this in UI. */
|
|
password: string;
|
|
};
|
|
|
|
const hostCandidateId = "host";
|
|
const identityCandidateId = (identityId: string) => `identity:${identityId}`;
|
|
|
|
/**
|
|
* Build the credential list for sudo/su password-prompt assist (#2156).
|
|
* Host session password first, then every decryptable keychain password
|
|
* identity. Dedupes by password value so the same secret is not listed twice.
|
|
*/
|
|
export const listPasswordPromptFillCandidates = (args: {
|
|
host: Host;
|
|
keys: SSHKey[];
|
|
identities?: Identity[];
|
|
}): PasswordPromptFillCandidate[] => {
|
|
const candidates: PasswordPromptFillCandidate[] = [];
|
|
const seenPasswords = new Set<string>();
|
|
|
|
const push = (candidate: PasswordPromptFillCandidate) => {
|
|
if (!candidate.password || seenPasswords.has(candidate.password)) return;
|
|
seenPasswords.add(candidate.password);
|
|
candidates.push(candidate);
|
|
};
|
|
|
|
// Same resolution path as login so group-inherited identities and identityId
|
|
// references surface the correct username next to the session password.
|
|
const resolvedAuth = resolveHostAuth(args);
|
|
const hostPassword = resolveHostAutofillPassword(args);
|
|
if (hostPassword) {
|
|
const hostLabel =
|
|
args.host.label?.trim()
|
|
|| args.host.hostname?.trim()
|
|
|| resolvedAuth.username?.trim()
|
|
|| args.host.username?.trim()
|
|
|| "Host";
|
|
push({
|
|
id: hostCandidateId,
|
|
source: "host",
|
|
label: hostLabel,
|
|
username: resolvedAuth.username?.trim() || args.host.username?.trim() || undefined,
|
|
password: hostPassword,
|
|
});
|
|
}
|
|
|
|
const identities = args.identities ?? [];
|
|
// Stable order: explicit order field, then created, then id.
|
|
const ordered = [...identities].sort((a, b) => {
|
|
const ao = a.order ?? Number.MAX_SAFE_INTEGER;
|
|
const bo = b.order ?? Number.MAX_SAFE_INTEGER;
|
|
if (ao !== bo) return ao - bo;
|
|
if (a.created !== b.created) return a.created - b.created;
|
|
return a.id.localeCompare(b.id);
|
|
});
|
|
|
|
for (const identity of ordered) {
|
|
if (identity.authMethod !== "password") continue;
|
|
const password = sanitizeCredentialValue(identity.password);
|
|
if (!password) continue;
|
|
const label = identity.label?.trim() || identity.username?.trim() || identity.id;
|
|
push({
|
|
id: identityCandidateId(identity.id),
|
|
source: "identity",
|
|
label,
|
|
username: identity.username?.trim() || undefined,
|
|
password,
|
|
});
|
|
}
|
|
|
|
return candidates;
|
|
};
|
|
|
|
/** Prefer host candidate password; fall back to first available candidate. */
|
|
export const resolveDefaultPasswordPromptFillPassword = (
|
|
candidates: PasswordPromptFillCandidate[],
|
|
): string | undefined => {
|
|
const host = candidates.find((c) => c.source === "host");
|
|
return host?.password ?? candidates[0]?.password;
|
|
};
|