(function lendrySsoWidget(global) { 'use strict'; var MESSAGE_TYPE = 'lendry-sso-onetap'; var DEFAULT_SCOPE = 'openid profile email'; var widgetRootId = 'lendry-sso-onetap-root'; function trimSlash(value) { return String(value || '').replace(/\/+$/, ''); } function resolveScriptElement() { var current = global.document.currentScript; if (current) return current; var scripts = global.document.getElementsByTagName('script'); for (var i = scripts.length - 1; i >= 0; i -= 1) { var src = scripts[i].getAttribute('src') || ''; if (src.indexOf('sso-widget.js') !== -1) { return scripts[i]; } } return null; } function resolveIdpApiBase(script) { var override = script && script.getAttribute('data-idp-url'); if (override) return trimSlash(override); try { var origin = new URL(script.src).origin; return trimSlash(origin + '/idp-api'); } catch (_error) { return ''; } } function resolveFrontendBase(script, idpApiBase) { var override = script && script.getAttribute('data-idp-frontend-url'); if (override) return trimSlash(override); try { return new URL(script.src).origin; } catch (_error) { if (idpApiBase.indexOf('/idp-api') !== -1) { return idpApiBase.replace(/\/idp-api$/, ''); } return idpApiBase; } } function resolveProviderName(script) { return (script && script.getAttribute('data-provider-name')) || 'MVK ID'; } function dispatchSuccess(payload, script) { var callbackName = script && script.getAttribute('data-on-success'); if (callbackName && typeof global[callbackName] === 'function') { global[callbackName](payload); } try { global.dispatchEvent(new CustomEvent('lendry-sso-onetap-success', { detail: payload })); } catch (_error) { // IE fallback not required } } function supportsFedCM() { return 'IdentityCredential' in global && global.navigator && typeof global.navigator.credentials !== 'undefined'; } function isBrowserReachableBaseUrl(url) { try { var parsed = new URL(url); var host = parsed.hostname.toLowerCase(); if (host === 'api-gateway' || host === 'sso-core' || host === 'frontend' || host === 'docs') { return false; } if (host.indexOf('.') === -1 && host !== 'localhost') { return false; } return parsed.protocol === 'http:' || parsed.protocol === 'https:'; } catch (_error) { return false; } } function loginWithFedCM(idpApiBase, clientId) { if (!supportsFedCM()) { return Promise.resolve(null); } function requestFedCMConfig(configUrl) { return global.navigator.credentials .get({ identity: { providers: [ { configURL: configUrl, configUrl: configUrl, clientId: clientId } ] }, mediation: 'optional' }) .then(function (credential) { if (!credential || typeof credential !== 'object') return null; var token = credential.token; if (!token) return null; return { token: token, method: 'fedcm' }; }); } function requestFedCM(configBase) { return requestFedCMConfig(configBase + '/fedcm/config.json'); } return global.fetch(idpApiBase + '/fedcm/discover.json', { credentials: 'omit' }) .then(function (response) { if (!response.ok) { return { mode: 'apiBase', value: idpApiBase }; } return response.json().then(function (payload) { if (payload && payload.enabled === false) { return null; } if (payload && payload.configUrl && isBrowserReachableBaseUrl(payload.configUrl)) { return { mode: 'configUrl', value: payload.configUrl, webIdentityUrl: payload.webIdentityUrl }; } if (payload && payload.apiBase && isBrowserReachableBaseUrl(payload.apiBase)) { return { mode: 'apiBase', value: trimSlash(payload.apiBase), webIdentityUrl: payload.webIdentityUrl }; } if (isBrowserReachableBaseUrl(idpApiBase)) { return { mode: 'apiBase', value: idpApiBase }; } return null; }); }) .catch(function () { if (isBrowserReachableBaseUrl(idpApiBase)) { return { mode: 'apiBase', value: idpApiBase }; } return null; }) .then(function (resolved) { if (!resolved) { if (global.console && typeof global.console.warn === 'function') { global.console.warn( '[MVK ID] FedCM: URL IdP недоступен из браузера. Укажите публичный data-idp-url (например https://api.idpmvk.lpr), не внутренний Docker-хост.' ); } return null; } if (resolved.mode === 'configUrl') { return requestFedCMConfig(resolved.value); } return requestFedCM(resolved.value); }) .catch(function (error) { if (global.console && typeof global.console.warn === 'function') { global.console.warn( '[MVK ID] FedCM недоступен:', error, 'Проверьте публичный URL IdP в data-idp-url и доступность /.well-known/web-identity на домене IdP (не /idp-api/.well-known).' ); } return null; }); } function injectWidgetStyles() { if (global.document.getElementById('lendry-sso-onetap-style')) return; var style = global.document.createElement('style'); style.id = 'lendry-sso-onetap-style'; style.textContent = '#' + widgetRootId + '{position:fixed;top:16px;right:16px;z-index:2147483000;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}' + '.lendry-sso-onetap-btn{display:flex;align-items:center;gap:10px;padding:10px 16px;border-radius:999px;border:1px solid #e4e8ef;background:#fff;color:#1f2430;font-size:14px;font-weight:600;line-height:1;cursor:pointer;box-shadow:0 8px 24px rgba(31,36,48,.12);transition:transform .18s ease,box-shadow .18s ease}' + '.lendry-sso-onetap-btn:hover{transform:translateY(-1px);box-shadow:0 12px 28px rgba(31,36,48,.16)}' + '.lendry-sso-onetap-btn:active{transform:translateY(0)}' + '.lendry-sso-onetap-badge{display:inline-flex;align-items:center;justify-content:center;min-width:28px;height:28px;padding:0 8px;border-radius:999px;background:#111;color:#fff;font-size:12px;font-weight:700}'; global.document.head.appendChild(style); } function createWidget(providerName, onClick) { injectWidgetStyles(); if (global.document.getElementById(widgetRootId)) return; var root = global.document.createElement('div'); root.id = widgetRootId; var button = global.document.createElement('button'); button.type = 'button'; button.className = 'lendry-sso-onetap-btn'; button.setAttribute('aria-label', 'Войти через ' + providerName); var badge = global.document.createElement('span'); badge.className = 'lendry-sso-onetap-badge'; badge.textContent = 'ID'; var label = global.document.createElement('span'); label.textContent = 'Войти через ' + providerName; button.appendChild(badge); button.appendChild(label); button.addEventListener('click', onClick); root.appendChild(button); global.document.body.appendChild(root); } function buildPopupUrl(frontendBase, clientId, origin, redirectUri, scope) { var params = new URLSearchParams(); params.set('client_id', clientId); params.set('redirect_uri', redirectUri); params.set('response_type', 'code'); params.set('scope', scope || DEFAULT_SCOPE); params.set('display', 'popup'); params.set('popup_origin', origin); params.set('state', 'onetap_' + Math.random().toString(36).slice(2)); return frontendBase + '/auth/oauth/authorize?' + params.toString(); } function openPopup(url, expectedOrigin, onToken) { var width = 480; var height = 640; var left = Math.max(0, Math.round((global.screen.width - width) / 2)); var top = Math.max(0, Math.round((global.screen.height - height) / 2)); var features = 'popup=yes,width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',noopener,noreferrer'; var popup = global.open(url, 'lendry_sso_onetap', features); if (!popup) { if (global.console && typeof global.console.error === 'function') { global.console.error('[MVK ID] Не удалось открыть popup. Разрешите всплывающие окна.'); } return function cleanup() {}; } function handleMessage(event) { if (!event || !event.data || event.data.type !== MESSAGE_TYPE) return; if (expectedOrigin && event.origin !== expectedOrigin && event.origin !== trimSlash(expectedOrigin)) return; cleanup(); onToken(event.data); } function cleanup() { global.removeEventListener('message', handleMessage); if (popup && !popup.closed) { try { popup.close(); } catch (_error) { // ignore } } } global.addEventListener('message', handleMessage); return cleanup; } function initOneTap(options) { var script = resolveScriptElement(); var clientId = (options && options.clientId) || (script && script.getAttribute('data-client-id')); if (!clientId) { if (global.console && typeof global.console.error === 'function') { global.console.error('[MVK ID] Укажите data-client-id в теге script или clientId в LendryIdOneTap.init().'); } return; } var idpApiBase = trimSlash((options && options.idpUrl) || resolveIdpApiBase(script)); var frontendBase = trimSlash((options && options.frontendUrl) || resolveFrontendBase(script, idpApiBase)); var providerName = (options && options.providerName) || resolveProviderName(script); var scope = (options && options.scope) || (script && script.getAttribute('data-scope')) || DEFAULT_SCOPE; var redirectUri = (options && options.redirectUri) || (script && script.getAttribute('data-redirect-uri')) || global.location.origin + '/auth/callback'; loginWithFedCM(idpApiBase, clientId).then(function (result) { if (result && result.token) { dispatchSuccess( { token: result.token, method: 'fedcm' }, script ); return; } createWidget(providerName, function () { var popupUrl = buildPopupUrl(frontendBase, clientId, global.location.origin, redirectUri, scope); openPopup(popupUrl, frontendBase, function (payload) { dispatchSuccess( { token: payload.token, accessToken: payload.accessToken, idToken: payload.idToken, refreshToken: payload.refreshToken, code: payload.code, method: 'popup' }, script ); }); }); }); } global.LendryIdOneTap = { init: initOneTap, loginWithFedCM: loginWithFedCM, MESSAGE_TYPE: MESSAGE_TYPE }; var scriptEl = resolveScriptElement(); if (scriptEl && scriptEl.getAttribute('data-auto-init') !== 'false') { if (global.document.readyState === 'loading') { global.document.addEventListener('DOMContentLoaded', function () { initOneTap(); }); } else { initOneTap(); } } })(window);