(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.debug === 'function') { global.console.debug( '[MVK ID] FedCM недоступен (будет использован popup):', error, 'Для One Tap без popup: примите SSL для корневого домена IdP (например idpmvk.lpr) и проверьте /.well-known/web-identity.' ); } return null; }); } function exchangeAuthorizationCode(idpApiBase, clientId, clientSecret, code, redirectUri) { if (!code || !clientId || !clientSecret) { return Promise.reject(new Error('Для обмена code нужны clientId, clientSecret и authorization code')); } var body = new URLSearchParams(); body.set('grant_type', 'authorization_code'); body.set('code', code); body.set('client_id', clientId); body.set('client_secret', clientSecret); body.set('redirect_uri', redirectUri); return global.fetch(trimSlash(idpApiBase) + '/oauth/token', { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString() }).then(function (response) { return response.json().then(function (payload) { if (!response.ok) { var message = payload && (payload.message || payload.error_description || payload.error); throw new Error(message || 'Не удалось обменять authorization code'); } return payload; }); }); } var SIZE_PRESETS = { s: { height: 32, font: 13, padX: 12, gap: 8, badge: 20, radius: 16 }, m: { height: 40, font: 14, padX: 16, gap: 10, badge: 24, radius: 20 }, l: { height: 44, font: 15, padX: 18, gap: 10, badge: 28, radius: 22 }, xl: { height: 52, font: 16, padX: 22, gap: 12, badge: 32, radius: 26 } }; function themePalette(theme) { if (theme === 'dark') { return { bg: '#1f2430', bgHover: '#2a3040', border: 'transparent', borderHover: 'transparent', text: '#ffffff', badgeBg: '#ffffff', badgeColor: '#1f2430' }; } return { bg: '#ffffff', bgHover: '#f6f8fb', border: '#e4e8ef', borderHover: '#d4dae6', text: '#1f2430', badgeBg: '#111111', badgeColor: '#ffffff' }; } function readButtonOptions(script, options) { function attr(name) { return script ? script.getAttribute(name) : null; } var o = options || {}; var size = String(o.buttonSize || attr('data-button-size') || 'xl').toLowerCase(); if (!SIZE_PRESETS[size]) size = 'xl'; var radiusRaw = o.buttonRadius != null ? o.buttonRadius : attr('data-button-radius'); return { size: size, theme: String(o.buttonTheme || attr('data-button-theme') || 'light').toLowerCase(), view: String(o.buttonView || attr('data-button-view') || 'main').toLowerCase(), icon: String(o.buttonIcon || attr('data-button-icon') || 'id').toLowerCase(), radius: radiusRaw != null && radiusRaw !== '' ? parseInt(radiusRaw, 10) : null, container: o.buttonContainer || attr('data-button-container') || null, colors: { bg: o.buttonBg || attr('data-button-bg') || null, bgHover: o.buttonBgHover || attr('data-button-bg-hover') || null, border: o.buttonBorder || attr('data-button-border') || null, borderHover: o.buttonBorderHover || attr('data-button-border-hover') || null, text: o.buttonText || attr('data-button-text') || 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}' + '.lendry-sso-onetap-btn{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;font-weight:600;line-height:1;cursor:pointer;white-space:nowrap;transition:transform .18s ease,box-shadow .18s ease,background .18s ease,border-color .18s ease;box-shadow:0 8px 24px rgba(31,36,48,.12)}' + '.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;border-radius:999px;font-weight:700}'; global.document.head.appendChild(style); } function applyButtonStyles(button, badge, providerName, btn) { var preset = SIZE_PRESETS[btn.size] || SIZE_PRESETS.xl; var palette = themePalette(btn.theme === 'dark' ? 'dark' : 'light'); var bg = btn.colors.bg || palette.bg; var bgHover = btn.colors.bgHover || palette.bgHover; var border = btn.colors.border || palette.border; var borderHover = btn.colors.borderHover || palette.borderHover; var textColor = btn.colors.text || palette.text; var radius = btn.radius != null && !isNaN(btn.radius) ? btn.radius : preset.radius; var iconOnly = btn.view === 'icon'; button.style.height = preset.height + 'px'; button.style.fontSize = preset.font + 'px'; button.style.gap = preset.gap + 'px'; button.style.padding = iconOnly ? '0' : '0 ' + preset.padX + 'px'; button.style.width = iconOnly ? preset.height + 'px' : 'auto'; button.style.borderRadius = radius + 'px'; button.style.background = bg; button.style.color = textColor; button.style.border = '1px solid ' + border; button.addEventListener('mouseenter', function () { button.style.background = bgHover; button.style.borderColor = borderHover; }); button.addEventListener('mouseleave', function () { button.style.background = bg; button.style.borderColor = border; }); if (badge) { badge.style.minWidth = preset.badge + 'px'; badge.style.height = preset.badge + 'px'; badge.style.padding = '0 ' + Math.round(preset.badge / 4) + 'px'; badge.style.fontSize = Math.round(preset.font * 0.85) + 'px'; badge.style.background = palette.badgeBg; badge.style.color = palette.badgeColor; } } function createWidget(providerName, onClick, options) { injectWidgetStyles(); var btn = options || readButtonOptions(null, null); var iconOnly = btn.view === 'icon'; var mount = null; if (btn.container) { mount = global.document.getElementById(btn.container); if (mount) { mount.innerHTML = ''; } } if (!mount) { if (global.document.getElementById(widgetRootId)) return; mount = global.document.createElement('div'); mount.id = widgetRootId; global.document.body.appendChild(mount); } var button = global.document.createElement('button'); button.type = 'button'; button.className = 'lendry-sso-onetap-btn'; button.setAttribute('aria-label', 'Войти через ' + providerName); var badge = null; if (btn.icon !== 'none') { badge = global.document.createElement('span'); badge.className = 'lendry-sso-onetap-badge'; badge.textContent = 'ID'; button.appendChild(badge); } if (!iconOnly) { var label = global.document.createElement('span'); label.textContent = 'Войти через ' + providerName; button.appendChild(label); } applyButtonStyles(button, badge, providerName, btn); button.addEventListener('click', onClick); mount.appendChild(button); } 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)); // Без noopener/noreferrer: popup должен видеть window.opener для postMessage после «Разрешить». var features = 'popup=yes,width=' + width + ',height=' + height + ',left=' + left + ',top=' + top; 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'; var buttonOptions = readButtonOptions(script, options); loginWithFedCM(idpApiBase, clientId).then(function (result) { if (result && result.token) { dispatchSuccess( { token: result.token, idToken: result.token, grantType: 'id_token', method: 'fedcm' }, script ); return; } createWidget( providerName, function () { var popupUrl = buildPopupUrl(frontendBase, clientId, global.location.origin, redirectUri, scope); openPopup(popupUrl, frontendBase, function (payload) { dispatchSuccess( { code: payload.code, state: payload.state, grantType: payload.grantType || (payload.code ? 'authorization_code' : undefined), idToken: payload.idToken, accessToken: payload.accessToken, refreshToken: payload.refreshToken, tokenEndpoint: trimSlash(idpApiBase) + '/oauth/token', redirectUri: redirectUri, method: 'popup' }, script ); }); }, buttonOptions ); }); } global.LendryIdOneTap = { init: initOneTap, loginWithFedCM: loginWithFedCM, exchangeAuthorizationCode: exchangeAuthorizationCode, 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);