Sudo autofill only read host.password and never resolved a host's reference to a Keychain identity (host.identityId). When the account password lived in a referenced identity, the autofill got nothing — while SSH login worked because it goes through resolveHostAuth, which resolves the identity. Add domain resolveHostAutofillPassword (same resolveHostAuth resolution: identity.password ?? host.password, honoring savePassword and dropping undecryptable placeholders) and use it as the terminal autofill password source. Login and autofill now share one resolution path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
141 lines
3.8 KiB
TypeScript
141 lines
3.8 KiB
TypeScript
import type { Host, Identity, SSHKey } from "./models";
|
|
import { sanitizeCredentialValue } from "./credentials";
|
|
|
|
type HostAuthMethod = "password" | "key" | "certificate";
|
|
|
|
type HostAuthOverride = {
|
|
authMethod?: HostAuthMethod;
|
|
username?: string;
|
|
password?: string;
|
|
keyId?: string;
|
|
passphrase?: string;
|
|
};
|
|
|
|
type ResolvedHostAuth = {
|
|
identity?: Identity;
|
|
authMethod: HostAuthMethod;
|
|
username: string;
|
|
password?: string;
|
|
keyId?: string;
|
|
key?: SSHKey;
|
|
passphrase?: string;
|
|
identityFilePath?: string;
|
|
};
|
|
|
|
const inferAuthMethod = (opts: {
|
|
explicit?: HostAuthMethod;
|
|
keyId?: string;
|
|
password?: string;
|
|
hostAuthMethod?: HostAuthMethod;
|
|
key?: SSHKey;
|
|
}): HostAuthMethod => {
|
|
if (opts.explicit) return opts.explicit;
|
|
if (opts.keyId) {
|
|
if (opts.hostAuthMethod === "key" || opts.hostAuthMethod === "certificate") {
|
|
return opts.hostAuthMethod;
|
|
}
|
|
return opts.key?.certificate ? "certificate" : "key";
|
|
}
|
|
if (opts.hostAuthMethod) return opts.hostAuthMethod;
|
|
if (opts.password) return "password";
|
|
return "password";
|
|
};
|
|
|
|
export const resolveHostAuth = (args: {
|
|
host: Host;
|
|
keys: SSHKey[];
|
|
identities?: Identity[];
|
|
override?: HostAuthOverride | null;
|
|
}): ResolvedHostAuth => {
|
|
const { host, keys, identities = [], override } = args;
|
|
|
|
const identity = host.identityId
|
|
? identities.find((i) => i.id === host.identityId)
|
|
: undefined;
|
|
|
|
const username =
|
|
override?.username?.trim() ||
|
|
identity?.username?.trim() ||
|
|
host.username?.trim() ||
|
|
"";
|
|
|
|
const selectedAuthMethod = (
|
|
override?.authMethod ||
|
|
identity?.authMethod ||
|
|
host.authMethod
|
|
) as HostAuthMethod | undefined;
|
|
|
|
// Don't load key when password auth is selected.
|
|
// This ensures the user's auth method selection is strictly respected.
|
|
const keyId = selectedAuthMethod === "password"
|
|
? undefined
|
|
: (override?.keyId || identity?.keyId || host.identityFileId || undefined);
|
|
|
|
|
|
const key = keyId ? keys.find((k) => k.id === keyId) : undefined;
|
|
|
|
const password = override?.password ?? identity?.password ?? host.password;
|
|
|
|
const authMethod = inferAuthMethod({
|
|
explicit: override?.authMethod,
|
|
hostAuthMethod: (identity?.authMethod || host.authMethod) as HostAuthMethod | undefined,
|
|
keyId,
|
|
password,
|
|
key,
|
|
});
|
|
|
|
const passphrase = override?.passphrase || key?.passphrase || undefined;
|
|
|
|
const identityFilePath = key?.source === 'reference' && key.filePath
|
|
? key.filePath
|
|
: undefined;
|
|
|
|
return {
|
|
identity,
|
|
authMethod,
|
|
username,
|
|
password,
|
|
keyId,
|
|
key,
|
|
passphrase,
|
|
identityFilePath,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Resolve the password to use for sudo autofill the same way SSH login does
|
|
* (through resolveHostAuth), so a password stored in a referenced Keychain
|
|
* identity (host.identityId) is found — not just host.password (issue #1284).
|
|
* Returns undefined when the host opts out of saving its password, or none is
|
|
* available (pure key auth, or an undecryptable placeholder).
|
|
*/
|
|
export const resolveHostAutofillPassword = (args: {
|
|
host: Host;
|
|
keys: SSHKey[];
|
|
identities?: Identity[];
|
|
}): string | undefined => {
|
|
if (args.host.savePassword === false) return undefined;
|
|
return sanitizeCredentialValue(resolveHostAuth(args).password) || undefined;
|
|
};
|
|
|
|
export const resolveBridgeKeyAuth = (args: {
|
|
key?: SSHKey | null;
|
|
fallbackIdentityFilePaths?: string[];
|
|
passphrase?: string;
|
|
}): {
|
|
privateKey?: string;
|
|
identityFilePaths?: string[];
|
|
passphrase?: string;
|
|
} => {
|
|
const { key, fallbackIdentityFilePaths, passphrase } = args;
|
|
const identityFilePaths = key?.source === "reference" && key.filePath
|
|
? [key.filePath]
|
|
: fallbackIdentityFilePaths;
|
|
|
|
return {
|
|
privateKey: key?.source === "reference" ? undefined : sanitizeCredentialValue(key?.privateKey),
|
|
identityFilePaths,
|
|
passphrase: sanitizeCredentialValue(passphrase ?? key?.passphrase),
|
|
};
|
|
};
|