import type { Request } from 'express'; import { firstValueFrom } from 'rxjs'; import { CoreGrpcService } from '../core-grpc.service'; import { resolveFrontendUrl, resolveOAuthIssuer } from './oauth-issuer'; export interface FedcmEndpoints { issuer: string; frontendUrl: string; projectName: string; configUrl: string; accountsEndpoint: string; clientMetadataEndpoint: string; idAssertionEndpoint: string; loginUrl: string; signupUrl: string; webIdentityUrl: string; } function normalizeBaseUrl(url: string) { return url.trim().replace(/\/+$/, ''); } function readRequestHost(req?: Request): string | undefined { if (!req) { return undefined; } const forwarded = req.headers['x-forwarded-host']; const host = (Array.isArray(forwarded) ? forwarded[0] : forwarded) ?? req.headers.host; return host?.split(',')[0]?.trim() || undefined; } function readRequestProto(req?: Request): string { if (!req) { return 'https'; } const forwarded = req.headers['x-forwarded-proto']; const proto = (Array.isArray(forwarded) ? forwarded[0] : forwarded) ?? req.protocol; return proto?.split(',')[0]?.trim() || 'https'; } function hostsMatch(a: string, b: string) { if (a === b) { return true; } return a === 'localhost' && b === 'localhost'; } function resolveSameOriginFrontend(storedFrontend: string, req?: Request): string { const host = readRequestHost(req); if (!host) { return storedFrontend; } const requestOrigin = normalizeBaseUrl(`${readRequestProto(req)}://${host}`); try { const frontendUrl = new URL(storedFrontend); const requestUrl = new URL(requestOrigin); if (hostsMatch(frontendUrl.hostname, requestUrl.hostname)) { return requestOrigin; } } catch { return storedFrontend; } return storedFrontend; } export function resolveSameOriginIssuer(storedIssuer: string, storedFrontend: string, req?: Request): string { const host = readRequestHost(req); if (!host) { return storedIssuer; } const requestOrigin = normalizeBaseUrl(`${readRequestProto(req)}://${host}`); const sameOriginApi = `${requestOrigin}/idp-api`; try { const issuerUrl = new URL(storedIssuer); const frontendUrl = new URL(storedFrontend); const requestUrl = new URL(requestOrigin); const hostMatches = hostsMatch(issuerUrl.hostname, requestUrl.hostname) || hostsMatch(frontendUrl.hostname, requestUrl.hostname); if (!hostMatches) { return storedIssuer; } if (normalizeBaseUrl(storedIssuer) !== sameOriginApi) { return sameOriginApi; } } catch { return storedIssuer; } return storedIssuer; } function buildFedcmEndpointUrls(issuer: string, frontendUrl: string) { const base = normalizeBaseUrl(issuer); const front = normalizeBaseUrl(frontendUrl); let webIdentityOrigin = front; try { webIdentityOrigin = new URL(base).origin; } catch { // keep frontend origin } return { configUrl: `${base}/fedcm/config.json`, accountsEndpoint: `${base}/fedcm/accounts`, clientMetadataEndpoint: `${base}/fedcm/client_metadata`, idAssertionEndpoint: `${base}/fedcm/id_assertion`, loginUrl: `${front}/auth/login`, signupUrl: `${front}/auth/register`, webIdentityUrl: `${webIdentityOrigin}/.well-known/web-identity` }; } export async function resolveFedcmEndpoints(core: CoreGrpcService, req?: Request): Promise { const storedIssuer = await resolveOAuthIssuer(core); const storedFrontend = await resolveFrontendUrl(core); const frontendUrl = resolveSameOriginFrontend(storedFrontend, req); const issuer = resolveSameOriginIssuer(storedIssuer, storedFrontend, req); let projectName = 'MVK ID'; try { const setting = (await firstValueFrom(core.settings.GetSetting({ key: 'PROJECT_NAME' }))) as { value?: string }; if (setting.value?.trim()) { projectName = setting.value.trim(); } } catch { // fallback } return { issuer, frontendUrl, projectName, ...buildFedcmEndpointUrls(issuer, frontendUrl) }; } export function buildFedcmProviderConfig(endpoints: FedcmEndpoints) { return { accounts_endpoint: endpoints.accountsEndpoint, client_metadata_endpoint: endpoints.clientMetadataEndpoint, id_assertion_endpoint: endpoints.idAssertionEndpoint, login_url: endpoints.loginUrl, branding: { background_color: '#ffffff', color: '#3390ec', icons: [{ url: `${endpoints.frontendUrl}/favicon.ico`, size: 32 }] } }; } export function buildFedcmWebIdentityManifest(endpoints: FedcmEndpoints) { return { provider_urls: [endpoints.configUrl], accounts_endpoint: endpoints.accountsEndpoint, login_url: endpoints.loginUrl }; } export function buildFedcmDiscoverPayload(endpoints: FedcmEndpoints, enabled = true) { return { enabled, apiBase: endpoints.issuer, frontendUrl: endpoints.frontendUrl, projectName: endpoints.projectName, configUrl: endpoints.configUrl, webIdentityUrl: endpoints.webIdentityUrl, accountsEndpoint: endpoints.accountsEndpoint, loginUrl: endpoints.loginUrl }; } async function readOneTapEnabled(core: CoreGrpcService): Promise { try { const setting = (await firstValueFrom(core.settings.GetSetting({ key: 'ONE_TAP_ENABLED' }))) as { value?: string }; const raw = setting.value?.trim().toLowerCase(); if (!raw) return true; return ['true', '1', 'yes'].includes(raw); } catch { return true; } } export { readOneTapEnabled };