export function escapeHtml(value: string): string { return value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } // Replicates openpubkey's client/choosers/web_chooser.go IssuerToName(). // OPKSSH's /select handler keys its providerMap by this derived name, NOT by the // `alias` field in config.yml. We need the same mapping so we can normalize any // `op=` query param we receive (which can be alias, issuer with protocol, or // issuer without protocol depending on client version) to what OPKSSH expects. function opksshIssuerToName(issuer: string): string | null { if (!issuer) return null; const withScheme = issuer.startsWith("http://") || issuer.startsWith("https://") ? issuer : `https://${issuer}`; if (withScheme.startsWith("https://accounts.google.com")) return "google"; if (withScheme.startsWith("https://login.microsoftonline.com")) return "azure"; if (withScheme.startsWith("https://gitlab.com")) return "gitlab"; if (withScheme.startsWith("https://issuer.hello.coop")) return "hello"; if (withScheme.startsWith("https://")) { const host = withScheme.slice("https://".length).split("/")[0]; return host || null; } return null; } export function normalizeSelectOpParam( rawOp: string, providers: Array<{ alias: string; issuer: string }>, ): string { if (!rawOp) return rawOp; const knownNames = new Set( providers .map((p) => opksshIssuerToName(p.issuer)) .filter((n): n is string => typeof n === "string" && n.length > 0), ); if (knownNames.has(rawOp)) return rawOp; const derivedFromRaw = opksshIssuerToName(rawOp); if (derivedFromRaw && knownNames.has(derivedFromRaw)) return derivedFromRaw; const matchByAlias = providers.find((p) => p.alias === rawOp); if (matchByAlias) { const name = opksshIssuerToName(matchByAlias.issuer); if (name) return name; } return rawOp; } type OpksshErrorPageOptions = { title: string; heading: string; message: string; details?: string; requestId?: string; statusCode?: number; }; export function renderErrorPage(opts: OpksshErrorPageOptions): string { const title = escapeHtml(opts.title); const heading = escapeHtml(opts.heading); const message = escapeHtml(opts.message); const detailsBlock = opts.details ? `
${escapeHtml(opts.details)}`
: "";
const requestIdBlock = opts.requestId
? `Request ID: ${escapeHtml(opts.requestId)}
` : ""; return `${message}
${detailsBlock} ${requestIdBlock}