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
97 lines
3.5 KiB
TypeScript
97 lines
3.5 KiB
TypeScript
import type { Host, SSHKey } from "../domain/models";
|
|
import { isEncryptedCredentialPlaceholder, sanitizeCredentialValue } from "../domain/credentials";
|
|
import { resolveVaultCsvHostKeyPath } from "../domain/vaultImport";
|
|
import { isPluginHostProtocol } from "../domain/pluginConnection";
|
|
import {
|
|
readExportableRememberedKeyPassphrases,
|
|
type DefaultKeyPassphraseVerificationRead,
|
|
} from "./defaultKeyPassphrases";
|
|
|
|
export interface VaultCsvCredentialOptions {
|
|
keyPathsById: Map<string, string>;
|
|
keyPassphrasesById: Map<string, string>;
|
|
keyPassphrases: Map<string, string>;
|
|
unreadablePassphraseCount: number;
|
|
}
|
|
|
|
export async function buildVaultCsvCredentialOptions(
|
|
hosts: Host[],
|
|
keys: SSHKey[],
|
|
readPassphrases?: (
|
|
keyPath: string,
|
|
) => Promise<DefaultKeyPassphraseVerificationRead>,
|
|
): Promise<VaultCsvCredentialOptions> {
|
|
const exportableHosts = hosts.filter((host) => (
|
|
host.protocol !== "serial" && !isPluginHostProtocol(host.protocol)
|
|
));
|
|
const referenceKeysById = new Map(keys.map((key) => [key.id, key] as const));
|
|
const keyPathsById = new Map(keys.flatMap((key) => (
|
|
key.source === "reference" && key.filePath?.trim()
|
|
? [[key.id, key.filePath.trim()] as const]
|
|
: []
|
|
)));
|
|
const keyPaths = Array.from(new Set(exportableHosts
|
|
.map((host) => resolveVaultCsvHostKeyPath(host, { keyPathsById }))
|
|
.filter(Boolean)));
|
|
const reads = new Map<string, DefaultKeyPassphraseVerificationRead>();
|
|
const readForExport = readPassphrases ?? (async (keyPath: string) => {
|
|
const remembered = await readExportableRememberedKeyPassphrases(keyPath, keys);
|
|
return {
|
|
...remembered,
|
|
present: remembered.unreadable || remembered.values.length > 0,
|
|
};
|
|
});
|
|
await Promise.all(keyPaths.map(async (keyPath) => {
|
|
try {
|
|
reads.set(keyPath, await readForExport(keyPath));
|
|
} catch {
|
|
reads.set(keyPath, { values: [], unreadable: true, present: true });
|
|
}
|
|
}));
|
|
|
|
const keyPassphrasesById = new Map<string, string>();
|
|
const keyPassphrases = new Map<string, string>();
|
|
const unreadableKeyPaths = new Set<string>();
|
|
for (const host of exportableHosts) {
|
|
const keyPath = resolveVaultCsvHostKeyPath(host, { keyPathsById });
|
|
if (!keyPath) continue;
|
|
const read = reads.get(keyPath);
|
|
const verifiedSideStoreValue = read?.values.length === 1 && !read.unreadable
|
|
? read.values[0]
|
|
: undefined;
|
|
const sideStoreIsAmbiguous = Boolean(
|
|
read && (read.unreadable || read.values.length > 1),
|
|
);
|
|
if (host.identityFileId) {
|
|
const key = referenceKeysById.get(host.identityFileId);
|
|
if (key?.source !== "reference" || key.savePassphrase === false) continue;
|
|
const keyValue = sanitizeCredentialValue(key.passphrase);
|
|
const hasSavedCredentialState = key.savePassphrase === true || Boolean(key.passphrase);
|
|
const selected = keyValue ?? (hasSavedCredentialState ? verifiedSideStoreValue : undefined);
|
|
if (selected) {
|
|
keyPassphrasesById.set(host.identityFileId, selected);
|
|
} else if (
|
|
isEncryptedCredentialPlaceholder(key.passphrase)
|
|
|| (hasSavedCredentialState && sideStoreIsAmbiguous)
|
|
) {
|
|
unreadableKeyPaths.add(keyPath);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const selected = verifiedSideStoreValue;
|
|
if (selected) {
|
|
keyPassphrases.set(keyPath, selected);
|
|
} else if (sideStoreIsAmbiguous || !reads.has(keyPath)) {
|
|
unreadableKeyPaths.add(keyPath);
|
|
}
|
|
}
|
|
|
|
return {
|
|
keyPathsById,
|
|
keyPassphrasesById,
|
|
keyPassphrases,
|
|
unreadablePassphraseCount: unreadableKeyPaths.size,
|
|
};
|
|
}
|