Files
NetMesh/domain/passwordPromptAssist.ts
zhaolei 3c72efcb7f
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
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

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;
};