import type { Request } from 'express'; import { firstValueFrom } from 'rxjs'; import { CoreGrpcService } from '../core-grpc.service'; import { resolveFrontendUrl, resolveOAuthIssuer, resolveProjectDomainApiUrl, resolveProjectDomainFrontendUrl } from './oauth-issuer'; import { isLocalDevBaseUrl, isValidFedcmEndpointUrl, normalizePublicBaseUrl, pickBestPublicBase, resolveFedcmWebIdentityOrigin } from './public-url'; export interface FedcmEndpoints { issuer: string; frontendUrl: string; projectName: string; configUrl: string; accountsEndpoint: string; clientMetadataEndpoint: string; idAssertionEndpoint: string; loginUrl: string; signupUrl: string; webIdentityUrl: string; } function buildFedcmEndpointUrls(issuer: string, frontendUrl: string) { const base = normalizePublicBaseUrl(issuer); const front = normalizePublicBaseUrl(frontendUrl); const webIdentityOrigin = resolveFedcmWebIdentityOrigin(base, front); return { configUrl: `${base}/fedcm/config.json`, accountsEndpoint: `${base}/fedcm/accounts`, clientMetadataEndpoint: `${base}/fedcm/client_metadata`, idAssertionEndpoint: `${base}/fedcm/id_assertion`, // FedCM: login_url MUST be same-origin with config.json (W3C FedCM / Chrome). // На split-domain UI проксируется через nginx: api.idpmvk.lpr/auth/login → frontend. loginUrl: `${base}/auth/login?fedcm=1`, signupUrl: `${front}/auth/register`, webIdentityUrl: `${webIdentityOrigin}/.well-known/web-identity` }; } async function resolveCanonicalFedcmIssuer(core: CoreGrpcService): Promise { const stored = await resolveOAuthIssuer(core); const fromProject = await resolveProjectDomainApiUrl(core); const issuer = pickBestPublicBase( [stored, fromProject], stored ); if (isValidFedcmEndpointUrl(issuer)) { return issuer; } if (fromProject && isValidFedcmEndpointUrl(fromProject)) { return fromProject; } return issuer; } async function resolveCanonicalFedcmFrontend(core: CoreGrpcService): Promise { const stored = await resolveFrontendUrl(core); const fromProject = await resolveProjectDomainFrontendUrl(core); const frontend = pickBestPublicBase( [stored, fromProject], stored ); if (isValidFedcmEndpointUrl(frontend) && !isLocalDevBaseUrl(frontend)) { return frontend; } if (fromProject && isValidFedcmEndpointUrl(fromProject)) { return fromProject; } return frontend; } export async function resolveFedcmEndpoints(core: CoreGrpcService, _req?: Request): Promise { // FedCM: web-identity (idpmvk.lpr) и config.json (api.idpmvk.lpr) ОБЯЗАНЫ // возвращать одинаковые login_url / accounts_endpoint / configUrl. // Хост запроса разный — никогда не выводим URL из req.headers.host. const issuer = await resolveCanonicalFedcmIssuer(core); const frontendUrl = await resolveCanonicalFedcmFrontend(core); 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) { const base = normalizePublicBaseUrl(endpoints.issuer); const front = normalizePublicBaseUrl(endpoints.frontendUrl); return { accounts_endpoint: `${base}/fedcm/accounts`, client_metadata_endpoint: `${base}/fedcm/client_metadata`, id_assertion_endpoint: `${base}/fedcm/id_assertion`, login_url: endpoints.loginUrl, branding: { name: endpoints.projectName, background_color: '#ffffff', color: '#1f2430', icons: [ { url: `${front}/icon.svg`, size: 40 }, { url: `${base}/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, // Поля для navigator.credentials.get({ identity: { fields: [...] } }) на стороне RP (Chrome 132+). suggestedFields: ['name', 'email', 'picture', 'tel'] }; } 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 };