/** * Every SSH auth type the server knows how to connect with. * * Core registers password, key, credential, agent and none. The rest come from * plugins through ctx.auth.registerSshAuthProvider. A type with no provider fails the * connect with a message naming the plugin that would provide it. */ import type { KeyboardInteractiveInterceptor, SshAuthProvider, } from "./types.js"; const providers = new Map(); const interceptors = new Map(); export interface SshAuthTypeOwner { pluginId: string; pluginName: string; } /** * Which plugin declares which auth type, from every manifest on disk * including disabled ones. Set by the plugin runtime. */ let ownerSource: () => Array = () => []; export function setSshAuthTypeOwnerSource( source: () => Array, ): void { ownerSource = source; } export function listSshAuthTypeOwners(): Array< SshAuthTypeOwner & { type: string } > { return ownerSource(); } function findSshAuthTypeOwner(type: string): SshAuthTypeOwner | null { const owner = ownerSource().find((entry) => entry.type === type); return owner ? { pluginId: owner.pluginId, pluginName: owner.pluginName } : null; } export class SshAuthProviderMissingError extends Error { readonly code = "SSH_AUTH_PROVIDER_MISSING"; constructor( readonly authType: string, readonly owner: SshAuthTypeOwner | null, ) { super( owner ? `This host uses ${authType}, which needs the ${owner.pluginName} plugin` : `This host uses ${authType}, which no enabled plugin provides`, ); this.name = "SshAuthProviderMissingError"; } } export function registerSshAuthProvider(provider: SshAuthProvider): () => void { const existing = providers.get(provider.type); if (existing && existing.pluginId !== provider.pluginId) { throw new Error( `SSH auth type "${provider.type}" is already provided by ${existing.pluginId}`, ); } providers.set(provider.type, provider); return () => { if (providers.get(provider.type) === provider) { providers.delete(provider.type); } }; } export function getSshAuthProvider(type: string): SshAuthProvider | undefined { return providers.get(type); } /** Throws SshAuthProviderMissingError when nothing provides the type. */ export function requireSshAuthProvider(type: string): SshAuthProvider { const provider = providers.get(type); if (!provider) { throw new SshAuthProviderMissingError(type, findSshAuthTypeOwner(type)); } return provider; } export function listSshAuthProviders(): SshAuthProvider[] { return [...providers.values()]; } /** Whether a registered provider works for a host that is never saved. */ export function isQuickConnectAuthType(type: string): boolean { return providers.get(type)?.quickConnect === true; } /** Types a stored credential may use. */ export function listCredentialTypes(): string[] { return listSshAuthProviders() .filter((provider) => provider.credentialType) .map((provider) => provider.type); } export function registerKeyboardInteractiveInterceptor( interceptor: KeyboardInteractiveInterceptor, ): () => void { const existing = interceptors.get(interceptor.id); if (existing && existing.pluginId !== interceptor.pluginId) { throw new Error( `Keyboard-interactive handler "${interceptor.id}" is already provided by ${existing.pluginId}`, ); } interceptors.set(interceptor.id, interceptor); return () => { if (interceptors.get(interceptor.id) === interceptor) { interceptors.delete(interceptor.id); } }; } export function listKeyboardInteractiveInterceptors(): KeyboardInteractiveInterceptor[] { return [...interceptors.values()]; } /** Test helper. */ export function resetSshAuthRegistryForTests(): void { providers.clear(); interceptors.clear(); ownerSource = () => []; }