import type { StoredCredential } from "./types"; /** * Derives a credential key from a remote URL, scoped to host + organisation — * the same granularity Azure DevOps / GitHub use. Examples: * https://github.com/owner/repo.git -> github.com/owner * https://dev.azure.com/org/project/_git/repo -> dev.azure.com/org * git@github.com:owner/repo.git -> github.com/owner * ssh://git@host:2222/owner/repo -> host/owner * Returns null when nothing usable can be parsed. */ export function orgKeyFromUrl(raw: string): string | null { const url = raw.trim(); if (!url) return null; let host = ""; let path = ""; // scp-like syntax: user@host:owner/repo.git (no scheme, single colon segment) const scp = url.match(/^[^@/]+@([^:/]+):(.+)$/); if (scp && !url.includes("://")) { host = scp[1]; path = scp[2]; } else { try { const parsed = new URL(url); host = parsed.host; // host:port, without userinfo path = parsed.pathname; } catch { return null; } } host = host.toLowerCase(); const org = path.replace(/^\/+/, "").split("/").filter(Boolean)[0] ?? ""; if (!host) return null; return org ? `${host}/${org}` : host; } /** A stored credential is expired only if it carries a past expiry date. */ export function isCredentialExpired(cred: StoredCredential): boolean { if (!cred.expiresAt) return false; const time = new Date(cred.expiresAt).getTime(); return !Number.isNaN(time) && time < Date.now(); } const AUTH_PREFIX = "AUTH_FAILED:"; export function isAuthError(message: string): boolean { return message.startsWith(AUTH_PREFIX); } export function stripAuthPrefix(message: string): string { return message.startsWith(AUTH_PREFIX) ? message.slice(AUTH_PREFIX.length).trim() : message; } /** * Condenses raw git stderr to the single most relevant line for the * credential dialog. Prefers the server's own explanation ("remote: ...") * over git's generic wrapper ("fatal: Authentication failed for ..."). */ export function summarizeGitError(message: string): string { const lines = message .split("\n") .map((line) => line.trim()) .filter(Boolean); const remote = lines.find((line) => line.toLowerCase().startsWith("remote:")); return remote ?? lines[0] ?? ""; }