davidarellano / Swagger UI Middleware Dev Tools

// ==UserScript==
// @name         Swagger UI Middleware Dev Tools
// @namespace    https://publicis.com/
// @version      3.1.5
// @description  Redux-only auth token helper, auto-try-it-out, and endpoint UI cleanup for Swagger UI.
// @author       darellano
// @match        https://*.publicis.com/*
// @match        http://localhost:*/*
// @match        http://127.0.0.1:*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @connect      authorization.proconnect.tst.publicis.com
// @connect      authorization.proconnect.dev.publicis.com
// @connect      authorization.proconnect.local.publicis.com
// @updateURL    https://openuserjs.org/meta/davidarellano/Swagger_UI_Middleware_Dev_Tools.meta.js
// @downloadURL  https://openuserjs.org/install/davidarellano/Swagger_UI_Middleware_Dev_Tools.user.js
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // Shipped defaults. Users array carries a dummy entry shown in the config
    // blob so users see the expected shape (no real credentials in the source).
    var DEFAULT_CONFIG = {
        users: [
            { username: 'you@example.com', password: 'your-password' },
        ],
        domains: {
            TST:   'https://authorization.proconnect.tst.publicis.com',
            DEV:   'https://authorization.proconnect.dev.publicis.com',
            LOCAL: 'https://authorization.proconnect.dev.publicis.com',
        },
        tokenEndpoint:              '/api/authorization/users/token',
        usersSearchEndpoint:         '/api/users/search',
        customClaimTokenEndpoint:    '/api/authorization/users/{referenceIdentityId}/token',
        customClaim: {
            application:    'string',
            businessGroups: ['string'],
            jobFamily:      'string',
            region:         'string',
            client:         'string',
        },
        fullTokenTtl:       2700,
        renewalPostExpiryBufferSecs: 5,
        showMainPageBanner: false,
    };

    var CONFIG_KEY = 'mdt-config';

    // Effective config = stored over defaults. users/domains/tokenEndpoint are a
    // full replacement when stored; the scalars deep-merge so new defaults reach
    // existing users on update.
    function buildEffectiveConfig(stored) {
        var c = {
            users:         DEFAULT_CONFIG.users,
            domains:       DEFAULT_CONFIG.domains,
            tokenEndpoint:           DEFAULT_CONFIG.tokenEndpoint,
            usersSearchEndpoint:      DEFAULT_CONFIG.usersSearchEndpoint,
            customClaimTokenEndpoint: DEFAULT_CONFIG.customClaimTokenEndpoint,
            customClaim:              DEFAULT_CONFIG.customClaim,
            fullTokenTtl:             DEFAULT_CONFIG.fullTokenTtl,
            renewalPostExpiryBufferSecs: DEFAULT_CONFIG.renewalPostExpiryBufferSecs,
            showMainPageBanner: DEFAULT_CONFIG.showMainPageBanner,
        };
        if (stored && typeof stored === 'object') {
            if (Array.isArray(stored.users)) { c.users = stored.users; }
            if (stored.domains && typeof stored.domains === 'object') { c.domains = stored.domains; }
            if (typeof stored.tokenEndpoint === 'string') { c.tokenEndpoint = stored.tokenEndpoint; }
            if (typeof stored.usersSearchEndpoint === 'string') { c.usersSearchEndpoint = stored.usersSearchEndpoint; }
            if (typeof stored.customClaimTokenEndpoint === 'string') { c.customClaimTokenEndpoint = stored.customClaimTokenEndpoint; }
            if (stored.customClaim && typeof stored.customClaim === 'object' && !Array.isArray(stored.customClaim)) { c.customClaim = stored.customClaim; }
            if (typeof stored.fullTokenTtl === 'number') { c.fullTokenTtl = stored.fullTokenTtl; }
            if (typeof stored.renewalPostExpiryBufferSecs === 'number') { c.renewalPostExpiryBufferSecs = stored.renewalPostExpiryBufferSecs; }
            if (typeof stored.showMainPageBanner === 'boolean') { c.showMainPageBanner = stored.showMainPageBanner; }
        }
        return c;
    }

    function hasStoredConfig() {
        try { return !!GM_getValue(CONFIG_KEY, ''); } catch (e) { return false; }
    }

    function loadStoredConfig() {
        try {
            var raw = GM_getValue(CONFIG_KEY, '');
            return raw ? JSON.parse(raw) : null;
        } catch (e) { return null; }
    }

    function saveStoredConfig(obj) {
        GM_setValue(CONFIG_KEY, JSON.stringify(obj));
    }

    var CONFIG = buildEffectiveConfig(loadStoredConfig());

    var LS_KEY = 'mdt-enabled';

    // Enabled flag (persisted)
    function isEnabled() {
        var val = localStorage.getItem(LS_KEY);
        return val === null || val === 'true';
    }

    function setEnabled(val) {
        localStorage.setItem(LS_KEY, val ? 'true' : 'false');
    }

    // Logging
    function ts() { return new Date().toISOString(); }

    function sprintf(fmt) {
        var i = 1, args = arguments;
        var result = String(fmt).replace(/%([sdfo])/g, function (match, type) {
            if (i >= args.length) { return match; }
            var val = args[i++];
            if (type === 'd') { return String(parseInt(val, 10)); }
            if (type === 'f') { return String(parseFloat(val)); }
            if (type === 'o') { try { return JSON.stringify(val); } catch (e) { return String(val); } }
            return (val === null || val === undefined) ? String(val) : String(val);
        });
        while (i < args.length) {
            var surplus = args[i++], extra;
            if (surplus !== null && typeof surplus === 'object') {
                try { extra = JSON.stringify(surplus); } catch (e) { extra = String(surplus); }
            } else { extra = String(surplus); }
            result += ' ' + extra;
        }
        return result;
    }

    function logMsg(level, fmt) {
        var fmtArgs = [fmt];
        for (var i = 2; i < arguments.length; i++) { fmtArgs.push(arguments[i]); }
        var msg = '[MDT][' + ts() + '] ' + sprintf.apply(null, fmtArgs);
        if (level === 'warn') { console.warn(msg); return; }
        if (level === 'error') { console.error(msg); return; }
        if (level === 'debug') { console.debug(msg); return; }
        console.info(msg);
    }

    var log = {
        info:  function (fmt) { logMsg.apply(null, ['info'].concat(Array.prototype.slice.call(arguments))); },
        warn:  function (fmt) { logMsg.apply(null, ['warn'].concat(Array.prototype.slice.call(arguments))); },
        error: function (fmt) { logMsg.apply(null, ['error'].concat(Array.prototype.slice.call(arguments))); },
        debug: function (fmt) { logMsg.apply(null, ['debug'].concat(Array.prototype.slice.call(arguments))); },
    };

    // Environment detection
    function resolveAuthDomain() {
        var hostname = window.location.hostname.toLowerCase();
        if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.includes('local')) {
            log.info('Local environment detected - using LOCAL auth domain: %s', CONFIG.domains.LOCAL);
            return CONFIG.domains.LOCAL;
        }
        if (hostname.includes('dev')) { return CONFIG.domains.DEV; }
        if (hostname.includes('tst')) { return CONFIG.domains.TST; }
        log.warn('Could not detect environment from hostname "%s". Falling back to TST.', hostname);
        return CONFIG.domains.TST;
    }

    var AUTH_BASE_URL = resolveAuthDomain();
    var TOKEN_URL = AUTH_BASE_URL + CONFIG.tokenEndpoint;

    // Recompute the auth endpoint after a config change (live-apply on save).
    function recomputeAuthEndpoint() {
        AUTH_BASE_URL = resolveAuthDomain();
        TOKEN_URL = AUTH_BASE_URL + CONFIG.tokenEndpoint;
    }

    // Utils
    function buildBasicAuthHeader(username, password) {
        return 'Basic ' + btoa(unescape(encodeURIComponent(username + ':' + password)));
    }

    function getPasswordForUser(username) {
        var entry = CONFIG.users.find(function (u) { return u.username === username; });
        return entry ? entry.password : null;
    }

    function formatExpiryDate(date) { return date.toLocaleString(); }

    // Theme: inherit page colours (.swagger-ui bg is transparent)
    function parseRGB(str) {
        if (!str) { return null; }
        var m = str.match(/(-?\d+(?:\.\d+)?)/g);
        if (!m || m.length < 3) { return null; }
        var a = (m.length >= 4) ? parseFloat(m[3]) : 1;
        return { r: parseInt(m[0], 10), g: parseInt(m[1], 10), b: parseInt(m[2], 10), a: a };
    }

    function isTransparent(rgb) {
        return !rgb || rgb.a === 0;
    }

    function rgbToCss(rgb) {
        return 'rgb(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ')';
    }

    function relLuminance(rgb) {
        var ch = [rgb.r, rgb.g, rgb.b].map(function (v) {
            var c = v / 255;
            return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
        });
        return 0.2126 * ch[0] + 0.7152 * ch[1] + 0.0722 * ch[2];
    }

    function resolveBackground(el) {
        var node = el;
        var guard = 0;
        while (node && guard < 30) {
            var bg = parseRGB(window.getComputedStyle(node).backgroundColor);
            if (!isTransparent(bg)) { return bg; }
            node = node.parentElement;
            guard++;
        }
        var bodyBg = parseRGB(window.getComputedStyle(document.body).backgroundColor);
        if (!isTransparent(bodyBg)) { return bodyBg; }
        return { r: 255, g: 255, b: 255, a: 1 };
    }

    function mix(a, b, t) {
        return {
            r: Math.round(a.r + (b.r - a.r) * t),
            g: Math.round(a.g + (b.g - a.g) * t),
            b: Math.round(a.b + (b.b - a.b) * t),
            a: 1,
        };
    }

    function getThemeTokens() {
        var dialog = document.querySelector('.modal-ux-content');
        var swagger = document.querySelector('.swagger-ui') || document.body;
        var probe = dialog || swagger;

        var pageBg = resolveBackground(probe);
        var dark = relLuminance(pageBg) < 0.4;

        var textEl = probe.querySelector('h4, h5, p, label, .opblock-tag') || probe;
        var textRGB = parseRGB(window.getComputedStyle(textEl).color) || (dark ? { r: 230, g: 237, b: 243, a: 1 } : { r: 36, g: 41, b: 47, a: 1 });

        var inputEl = probe.querySelector('input[type="text"], input:not([type="password"]):not([type="checkbox"])');
        var inputBgRGB, inputBorderRGB, inputTextRGB;
        if (inputEl) {
            var cs = window.getComputedStyle(inputEl);
            inputBgRGB = parseRGB(cs.backgroundColor);
            if (isTransparent(inputBgRGB)) { inputBgRGB = null; }
            inputBorderRGB = parseRGB(cs.borderColor);
            inputTextRGB = parseRGB(cs.color);
        }

        var containerBg = mix(pageBg, textRGB, dark ? 0.06 : 0.03);
        var borderRGB = mix(pageBg, textRGB, dark ? 0.22 : 0.18);
        var subtextRGB = mix(pageBg, textRGB, 0.55);

        var greenText = dark ? { r: 76, g: 175, b: 119, a: 1 } : { r: 26, g: 110, b: 60, a: 1 };
        var statusBg = mix(pageBg, greenText, dark ? 0.18 : 0.14);
        var statusBorder = mix(pageBg, greenText, dark ? 0.45 : 0.4);

        return {
            bg:           rgbToCss(containerBg),
            border:       rgbToCss(borderRGB),
            text:         rgbToCss(textRGB),
            subtext:      rgbToCss(subtextRGB),
            inputBg:      rgbToCss(inputBgRGB || (dark ? mix(pageBg, textRGB, 0.1) : { r: 255, g: 255, b: 255, a: 1 })),
            inputBorder:  rgbToCss(inputBorderRGB && !isTransparent(inputBorderRGB) ? inputBorderRGB : borderRGB),
            inputText:    rgbToCss(inputTextRGB && !isTransparent(inputTextRGB) ? inputTextRGB : textRGB),
            statusBg:     rgbToCss(statusBg),
            statusBorder: rgbToCss(statusBorder),
            statusText:   rgbToCss(greenText),
        };
    }

    // Config validation + modal
    function validateConfigObject(obj) {
        if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
            return 'Config must be a JSON object.';
        }
        if (!Array.isArray(obj.users)) { return '"users" must be an array.'; }
        for (var i = 0; i < obj.users.length; i++) {
            var u = obj.users[i];
            if (!u || typeof u !== 'object') { return 'users[' + i + '] must be an object.'; }
            if (typeof u.username !== 'string' || u.username.trim() === '') { return 'users[' + i + '].username must be a non-empty string.'; }
            if (typeof u.password !== 'string' || u.password === '') { return 'users[' + i + '].password must be a non-empty string.'; }
        }
        if (!obj.domains || typeof obj.domains !== 'object' || Array.isArray(obj.domains)) { return '"domains" must be an object.'; }
        var keys = Object.keys(obj.domains);
        if (keys.length === 0) { return '"domains" must have at least one entry.'; }
        for (var k = 0; k < keys.length; k++) {
            if (typeof obj.domains[keys[k]] !== 'string' || obj.domains[keys[k]].trim() === '') {
                return 'domains.' + keys[k] + ' must be a non-empty string.';
            }
        }
        if (typeof obj.tokenEndpoint !== 'string' || obj.tokenEndpoint.trim() === '') { return '"tokenEndpoint" must be a non-empty string.'; }
        if (obj.fullTokenTtl !== undefined && typeof obj.fullTokenTtl !== 'number') { return '"fullTokenTtl" must be a number.'; }
        if (obj.renewalPostExpiryBufferSecs !== undefined && typeof obj.renewalPostExpiryBufferSecs !== 'number') { return '"renewalPostExpiryBufferSecs" must be a number.'; }
        if (obj.showMainPageBanner !== undefined && typeof obj.showMainPageBanner !== 'boolean') { return '"showMainPageBanner" must be a boolean.'; }
        if (obj.usersSearchEndpoint !== undefined && (typeof obj.usersSearchEndpoint !== 'string' || obj.usersSearchEndpoint.trim() === '')) { return '"usersSearchEndpoint" must be a non-empty string.'; }
        if (obj.customClaimTokenEndpoint !== undefined && (typeof obj.customClaimTokenEndpoint !== 'string' || obj.customClaimTokenEndpoint.trim() === '')) { return '"customClaimTokenEndpoint" must be a non-empty string.'; }
        if (obj.customClaim !== undefined && (obj.customClaim === null || typeof obj.customClaim !== 'object' || Array.isArray(obj.customClaim))) { return '"customClaim" must be an object.'; }
        return null;
    }

    // Current effective config as a pretty JSON blob for the textarea.
    function currentConfigBlob() {
        return JSON.stringify({
            users:                    CONFIG.users,
            domains:                  CONFIG.domains,
            tokenEndpoint:            CONFIG.tokenEndpoint,
            usersSearchEndpoint:      CONFIG.usersSearchEndpoint,
            customClaimTokenEndpoint: CONFIG.customClaimTokenEndpoint,
            customClaim:              CONFIG.customClaim,
            fullTokenTtl:             CONFIG.fullTokenTtl,
            renewalPostExpiryBufferSecs: CONFIG.renewalPostExpiryBufferSecs,
            showMainPageBanner:       CONFIG.showMainPageBanner,
        }, null, 2);
    }

    function defaultConfigBlob() {
        return JSON.stringify(DEFAULT_CONFIG, null, 2);
    }

    // Apply a validated config object live (no reload).
    function applyConfig(obj) {
        CONFIG = buildEffectiveConfig(obj);
        recomputeAuthEndpoint();
        // Rebuild the dialog UI if open so the user dropdown / env badge refresh.
        var stale = document.getElementById(IDS.container);
        if (stale) { stale.remove(); injectHelperUI(); }
        // Reflect config changes that affect the main-page banner immediately.
        updateMainPageBanner();
    }

    var CONFIG_MODAL_ID = 'mdt-config-modal';

    function closeConfigModal() {
        var m = document.getElementById(CONFIG_MODAL_ID);
        if (m) { m.remove(); }
    }

    function openConfigModal() {
        closeConfigModal();
        var t = getThemeTokens();

        var overlay = document.createElement('div');
        overlay.id = CONFIG_MODAL_ID;
        Object.assign(overlay.style, {
            position: 'fixed', inset: '0', zIndex: '100000',
            background: 'rgba(0,0,0,0.5)', display: 'flex',
            alignItems: 'center', justifyContent: 'center',
        });

        var panel = document.createElement('div');
        Object.assign(panel.style, {
            position: 'relative',
            width: 'min(680px, 92vw)', maxHeight: '88vh', overflow: 'auto',
            background: t.bg, color: t.text, border: '1px solid ' + t.border,
            borderRadius: '8px', padding: '18px 20px', fontFamily: 'sans-serif',
            boxShadow: '0 10px 40px rgba(0,0,0,0.35)',
        });

        var closeX = document.createElement('button');
        closeX.type = 'button';
        closeX.textContent = 'X';
        closeX.title = 'Close without saving';
        Object.assign(closeX.style, {
            position: 'absolute', top: '12px', right: '12px',
            background: 'none', border: 'none', cursor: 'pointer',
            fontSize: '15px', lineHeight: '1', color: 'inherit', opacity: '0.7', padding: '4px',
        });
        closeX.addEventListener('mouseenter', function () { closeX.style.opacity = '1'; });
        closeX.addEventListener('mouseleave', function () { closeX.style.opacity = '0.7'; });
        closeX.addEventListener('click', closeConfigModal);
        panel.appendChild(closeX);

        var title = document.createElement('h3');
        title.textContent = 'Middleware Dev Tools - Configuration';
        Object.assign(title.style, { margin: '0 40px 6px 0', fontSize: '16px' });
        panel.appendChild(title);

        var hint = document.createElement('p');
        hint.textContent = 'Edit the JSON below. users, domains and tokenEndpoint replace the defaults entirely. Stored privately in your browser; not affected by script updates.';
        Object.assign(hint.style, { margin: '0 0 10px', fontSize: '12px', opacity: '0.8' });
        panel.appendChild(hint);

        var textarea = document.createElement('textarea');
        textarea.value = currentConfigBlob();
        textarea.spellcheck = false;
        Object.assign(textarea.style, {
            width: '100%', height: '320px', boxSizing: 'border-box',
            fontFamily: 'monospace', fontSize: '12px', lineHeight: '1.45',
            padding: '10px', borderRadius: '6px',
            border: '1px solid ' + t.inputBorder, background: t.inputBg, color: t.inputText,
            resize: 'vertical', whiteSpace: 'pre',
        });
        panel.appendChild(textarea);

        var errorLine = document.createElement('div');
        Object.assign(errorLine.style, { minHeight: '18px', margin: '8px 0 4px', fontSize: '12px', color: '#cf222e' });
        panel.appendChild(errorLine);

        var resetConfirm = document.createElement('div');
        Object.assign(resetConfirm.style, { fontSize: '12px', margin: '0 0 8px' });

        var btnRow = document.createElement('div');
        Object.assign(btnRow.style, { display: 'flex', gap: '10px', justifyContent: 'flex-end', flexWrap: 'wrap' });

        var resetBtn = document.createElement('button');
        resetBtn.type = 'button';
        resetBtn.className = 'btn';
        resetBtn.textContent = 'Reset to defaults';
        Object.assign(resetBtn.style, { marginRight: 'auto' });

        var cancelBtn = document.createElement('button');
        cancelBtn.type = 'button';
        cancelBtn.className = 'btn';
        cancelBtn.textContent = 'Cancel';

        var saveBtn = document.createElement('button');
        saveBtn.type = 'button';
        saveBtn.className = 'btn authorize';
        saveBtn.textContent = 'Save';

        btnRow.appendChild(resetBtn);
        btnRow.appendChild(cancelBtn);
        btnRow.appendChild(saveBtn);
        panel.appendChild(resetConfirm);
        panel.appendChild(btnRow);
        overlay.appendChild(panel);
        document.body.appendChild(overlay);

        function parseCurrent() {
            var obj;
            try { obj = JSON.parse(textarea.value); } catch (e) { return { error: 'Invalid JSON: ' + e.message }; }
            var err = validateConfigObject(obj);
            return err ? { error: err } : { obj: obj };
        }

        function liveValidate() {
            var r = parseCurrent();
            if (r.error) {
                errorLine.textContent = r.error;
                saveBtn.disabled = true;
                saveBtn.style.opacity = '0.45';
                saveBtn.style.cursor = 'not-allowed';
            } else {
                errorLine.textContent = '';
                saveBtn.disabled = false;
                saveBtn.style.opacity = '1';
                saveBtn.style.cursor = 'pointer';
            }
        }

        textarea.addEventListener('input', liveValidate);
        liveValidate();

        cancelBtn.addEventListener('click', closeConfigModal);

        saveBtn.addEventListener('click', function () {
            var r = parseCurrent();
            if (r.error) { liveValidate(); return; }
            saveStoredConfig(r.obj);
            applyConfig(r.obj);
            closeConfigModal();
            showSwaggerToast('Configuration saved.');
        });

        resetBtn.addEventListener('click', function () {
            resetConfirm.textContent = '';
            var q = document.createElement('span');
            q.textContent = 'Reset to shipped defaults? ';
            var yes = document.createElement('button');
            yes.type = 'button'; yes.className = 'btn'; yes.textContent = 'Yes';
            var no = document.createElement('button');
            no.type = 'button'; no.className = 'btn'; no.textContent = 'No';
            Object.assign(yes.style, { marginLeft: '6px' });
            Object.assign(no.style, { marginLeft: '6px' });
            resetConfirm.appendChild(q);
            resetConfirm.appendChild(yes);
            resetConfirm.appendChild(no);
            no.addEventListener('click', function () { resetConfirm.textContent = ''; });
            yes.addEventListener('click', function () {
                textarea.value = defaultConfigBlob();
                resetConfirm.textContent = '';
                liveValidate();
            });
        });

        textarea.focus();
    }

    // Auth state (single source of truth)
    var authStatus = { username: null, expiryDate: null, token: null };

    function setAuthStatus(username, expiryDate, token) {
        authStatus.username = username;
        authStatus.expiryDate = expiryDate;

        if (token !== undefined) { authStatus.token = token; }
        if (!username) { authStatus.token = null; }
        updateDialogAuthorizedLine();
        updateMainPageBanner();
    }

    // Auto-renewal: fire just after expiry (server caches token until then)
    var renewalState = {
        renewalTimer:        null,
        activeUsername:      null,
        apiKeyInput:         null,
        tokenExpiryTime:     null,
        useCustomClaims:     false,
        referenceIdentityId: null,
        customClaimsBody:    null,
    };

    function cancelRenewal() {
        if (renewalState.renewalTimer !== null) {
            clearTimeout(renewalState.renewalTimer);
            renewalState.renewalTimer = null;
        }
        renewalState.tokenExpiryTime = null;
        renewalState.useCustomClaims = false;
        renewalState.referenceIdentityId = null;
        renewalState.customClaimsBody = null;
    }

    function scheduleRenewal(username, expiresInSecs) {
        cancelRenewal();

        if (expiresInSecs == null || expiresInSecs < 0) {
            log.warn('scheduleRenewal: invalid/absent expiresIn (%s) - renewing soon.', expiresInSecs);
            expiresInSecs = 0;
        }

        var buffer = CONFIG.renewalPostExpiryBufferSecs;
        // fire after expiry so server mints a fresh token
        var fireDelaySecs = Math.max(expiresInSecs + buffer, 1);
        var fireAt = new Date(Date.now() + fireDelaySecs * 1000).toISOString();

        renewalState.tokenExpiryTime = Date.now() + expiresInSecs * 1000;
        renewalState.activeUsername = username;
        renewalState.renewalTimer = setTimeout(function () {
            if (renewalState.useCustomClaims && renewalState.referenceIdentityId) {
                log.info('[Custom claims] Auto-renewing token for %s (token expired; scheduled fire at %s)', username, fireAt);
                silentRenewTokenWithCustomClaims(username);
            } else {
                log.info('Auto-renewing token for %s (token expired; scheduled fire at %s)', username, fireAt);
                silentRenewToken(username);
            }
        }, fireDelaySecs * 1000);

        var renewalType = (renewalState.useCustomClaims && renewalState.referenceIdentityId) ? '[Custom claims] ' : '';
        log.info(
            '%sToken renewal scheduled: token expires in %ds, renewal fires in %ds (buffer=%ds) at %s, user=%s.',
            renewalType, expiresInSecs, fireDelaySecs, buffer, fireAt, username
        );
    }

    function silentRenewToken(username) {
        var password = getPasswordForUser(username);
        if (!password) {
            log.error('Silent renewal aborted: no password configured for %s.', username);
            return;
        }

        GM_xmlhttpRequest({
            method:  'POST',
            url:     TOKEN_URL,
            headers: { 'Authorization': buildBasicAuthHeader(username, password), 'Content-Type': 'application/json' },
            onload: function (response) {
                if (response.status < 200 || response.status >= 300) {
                    log.error('Silent renewal HTTP error: status=%d, body=%s.', response.status, response.responseText);
                    return;
                }
                var body;
                try { body = JSON.parse(response.responseText); }
                catch (parseError) {
                    log.error('Silent renewal JSON parse error: %s | raw: %s', parseError, response.responseText);
                    return;
                }
                var accessToken = body.accessToken;
                var expiresIn = body.expiresIn;
                if (!accessToken) {
                    log.error('Silent renewal: accessToken missing. Body: %o', body);
                    return;
                }
                var isFresh = (expiresIn != null && expiresIn >= CONFIG.fullTokenTtl);
                log.info('Silent renewal token received: expiresIn=%ds, fullTtl=%ds, isFresh=%s.',
                    expiresIn, CONFIG.fullTokenTtl, isFresh);

                authorizeBearer(accessToken);

                updateTokenDisplay(accessToken);

                if (isFresh) {
                    var renewedExpiryDate = new Date(Date.now() + expiresIn * 1000);
                    setAuthStatus(username, renewedExpiryDate);
                    showSwaggerToast('[KEY] Token auto-renewed for ' + username + '.<br>Expires at ' + formatExpiryDate(renewedExpiryDate) + '.');
                }

                scheduleRenewal(username, expiresIn);
            },
            onerror: function (err) {
                log.error('Silent renewal network error for %s: %o', username, err);
                renewalState.renewalTimer = setTimeout(function () { silentRenewToken(username); }, 30000);
            },
            ontimeout: function () {
                log.error('Silent renewal timed out for %s. Will retry in 30 s.', username);
                renewalState.renewalTimer = setTimeout(function () { silentRenewToken(username); }, 30000);
            },
            timeout: 15000,
        });
    }

    var _cachedSystem = null;

    // Swagger Redux access via ui.getSystem()
    function getSwaggerSystem() {
        if (_cachedSystem && _cachedSystem.authActions) { return _cachedSystem; }
        var pw = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
        var ui = pw.ui || window.ui;
        if (!ui) { return null; }
        try {
            var sys = (typeof ui.getSystem === 'function') ? ui.getSystem() : ui;
            if (sys && sys.authActions && sys.authSelectors && sys.specSelectors) {
                _cachedSystem = sys;
                return sys;
            }
        } catch (e) {
            log.error('getSwaggerSystem error: %o', e);
        }
        return null;
    }

    function getSecurityDefinitions() {
        var sys = getSwaggerSystem();
        if (!sys) { return {}; }
        try {
            var defs = sys.specSelectors.securityDefinitions();
            if (defs && typeof defs.toJS === 'function') { return defs.toJS(); }
            return defs || {};
        } catch (e) {
            log.error('getSecurityDefinitions error: %o', e);
            return {};
        }
    }

    // split bearer vs basic by real scheme names
    function classifySchemes() {
        var defs = getSecurityDefinitions();
        var bearer = [], basic = [];
        Object.keys(defs).forEach(function (name) {
            var d = defs[name] || {};
            if (d.type === 'http' && /basic/i.test(d.scheme || '')) {
                basic.push(name);
            } else if (d.type === 'apiKey' || (d.type === 'http' && /bearer/i.test(d.scheme || ''))) {
                bearer.push(name);
            }
        });
        return { bearer: bearer, basic: basic, all: defs };
    }

    function isWorkingStore(s) {
        return !!(s && typeof s.getState === 'function' && typeof s.dispatch === 'function');
    }

    function pageWin() {
        return (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
    }

    // Store-resolution strategies (diagnostic only)
    function storeViaElementProperty() {
        var pw = pageWin();
        var els = [pw.document.querySelector('.swagger-ui'), document.querySelector('.swagger-ui')];
        for (var i = 0; i < els.length; i++) {
            if (els[i] && isWorkingStore(els[i].__reduxStore)) { return els[i].__reduxStore; }
        }
        return null;
    }

    function storeViaWindowUi() {
        var pw = pageWin();
        var ui = pw.ui || window.ui;
        if (!ui) { return null; }
        try {
            if (typeof ui.getStore === 'function' && isWorkingStore(ui.getStore())) { return ui.getStore(); }
        } catch (e) { /* not on this build */ }
        try {
            if (typeof ui.getSystem === 'function') {
                var sys = ui.getSystem();
                if (sys && typeof sys.getStore === 'function' && isWorkingStore(sys.getStore())) {
                    return sys.getStore();
                }
            }
        } catch (e) { /* not on this build */ }
        return null;
    }

    function storeViaReactFiber() {
        var root = document.querySelector('.swagger-ui');
        if (!root) { return null; }

        var fiberKey = null;
        var keys = Object.keys(root);
        for (var i = 0; i < keys.length; i++) {
            if (keys[i].indexOf('__reactContainer$') === 0 ||
                keys[i].indexOf('__reactInternalInstance$') === 0 ||
                keys[i].indexOf('__reactFiber$') === 0) {
                fiberKey = keys[i];
                break;
            }
        }
        if (!fiberKey) { return null; }

        var node = root[fiberKey];
        var guard = 0;
        var visited = 0;

        function extractStore(fiber) {
            if (!fiber) { return null; }
            var p = fiber.memoizedProps;
            if (p) {
                if (isWorkingStore(p.store)) { return p.store; }
                if (p.value && isWorkingStore(p.value.store)) { return p.value.store; }
                if (isWorkingStore(p.value)) { return p.value; }
            }
            var s = fiber.memoizedState;
            if (s && isWorkingStore(s.store)) { return s.store; }
            return null;
        }

        var cur = node;
        while (cur && cur.return && guard < 10000) { cur = cur.return; guard++; }

        var queue = [cur];
        while (queue.length && visited < 20000) {
            var f = queue.shift();
            visited++;
            var found = extractStore(f);
            if (found) { return found; }
            if (f.child) { queue.push(f.child); }
            if (f.sibling) { queue.push(f.sibling); }
        }
        return null;
    }

    var STORE_STRATEGIES = [
        { name: 'element.__reduxStore', fn: storeViaElementProperty },
        { name: 'window.ui', fn: storeViaWindowUi },
        { name: 'react fiber', fn: storeViaReactFiber },
    ];

    function getReduxStoreDiagnostic() {
        log.info('=== Redux store diagnostic START ===');
        var anyWorking = null;
        for (var i = 0; i < STORE_STRATEGIES.length; i++) {
            var name = STORE_STRATEGIES[i].name;
            var s = null, err = null;
            try { s = STORE_STRATEGIES[i].fn(); } catch (e) { err = e; }
            if (err) {
                log.warn('Strategy "%s": threw %o', name, err);
            } else if (isWorkingStore(s)) {
                log.info('Strategy "%s": WORKING store found (has getState + dispatch).', name);
                if (!anyWorking) { anyWorking = s; }

                try {
                    var state = s.getState();
                    var hasAuth = !!(state && (state.auth || state.security));
                    log.info('Strategy "%s": getState() ok, auth slice present=%s.', name, hasAuth);
                } catch (e2) {
                    log.warn('Strategy "%s": getState() threw %o', name, e2);
                }
            } else if (s) {
                log.warn('Strategy "%s": returned an object but it is NOT a working store.', name);
            } else {
                log.info('Strategy "%s": no store found.', name);
            }
        }

        var pw = pageWin();
        log.info('Context probe: typeof unsafeWindow=%s, window.ui=%s, pageWin.ui=%s, .swagger-ui present=%s',
            typeof unsafeWindow,
            (typeof window.ui),
            (pw ? typeof pw.ui : 'n/a'),
            !!document.querySelector('.swagger-ui'));

        try {
            if (anyWorking) {
                var st = anyWorking.getState();
                var topKeys;
                if (st && typeof st.keySeq === 'function') { topKeys = st.keySeq().toArray(); }
                else if (st && typeof st.keys === 'function') { topKeys = Array.from(st.keys()); }
                else { topKeys = st ? Object.keys(st) : []; }
                log.info('STATE top-level keys: %o', topKeys);

                var authKeys = ['auth', 'authorization', 'security'];
                for (var ak = 0; ak < authKeys.length; ak++) {
                    var key = authKeys[ak], slice = null;
                    if (st && typeof st.get === 'function') { slice = st.get(key); }
                    else if (st) { slice = st[key]; }
                    if (slice !== undefined && slice !== null) {
                        var asJs = (typeof slice.toJS === 'function') ? slice.toJS() : slice;
                        log.info('STATE["%s"] present: %o', key, asJs);
                    }
                }
            }
        } catch (deepErr) {
            log.warn('Deep state probe threw: %o', deepErr);
        }

        try {
            var uiInst = pw.ui || window.ui;
            if (uiInst) {
                var sys = (typeof uiInst.getSystem === 'function') ? uiInst.getSystem() : uiInst;
                log.info('ui API: authSelectors=%s, authActions=%s, specSelectors=%s',
                    !!(sys && sys.authSelectors), !!(sys && sys.authActions), !!(sys && sys.specSelectors));
                if (sys && sys.authSelectors) {
                    log.info('ui authSelectors methods: %o', Object.keys(sys.authSelectors));
                    try {
                        var authorized = sys.authSelectors.authorized();
                        var authJs = (authorized && typeof authorized.toJS === 'function') ? authorized.toJS() : authorized;
                        log.info('ui authSelectors.authorized(): %o', authJs);
                    } catch (asErr) { log.warn('authSelectors.authorized() threw: %o', asErr); }
                }
                if (sys && sys.authActions) {
                    log.info('ui authActions methods: %o', Object.keys(sys.authActions));
                }
                if (sys && sys.specSelectors) {
                    try {
                        var schemes = sys.specSelectors.securityDefinitions
                            ? sys.specSelectors.securityDefinitions() : null;
                        var schemesJs = (schemes && typeof schemes.toJS === 'function') ? schemes.toJS() : schemes;
                        log.info('ui specSelectors.securityDefinitions(): %o', schemesJs);
                    } catch (ssErr) { log.warn('securityDefinitions() threw: %o', ssErr); }
                }
            }
        } catch (uiErr) {
            log.warn('ui API probe threw: %o', uiErr);
        }

        log.info('=== Redux store diagnostic END (anyWorking=%s) ===', !!anyWorking);
        return anyWorking;
    }

    // Redux-only auth ops
    function authorizeBearer(accessToken) {
        var sys = getSwaggerSystem();
        if (!sys) { log.warn('authorizeBearer: Swagger system not available.'); return false; }
        var schemes = classifySchemes();
        if (schemes.bearer.length === 0) {
            log.warn('authorizeBearer: no bearer-like scheme found in spec.');
            return false;
        }
        var payload = {};
        schemes.bearer.forEach(function (name) {
            payload[name] = { name: name, schema: schemes.all[name], value: 'Bearer ' + accessToken };
        });
        try {
            sys.authActions.authorize(payload);
            return true;
        } catch (e) {
            log.error('authorizeBearer error: %o', e);
            return false;
        }
    }

    function authorizeBasic(username, password) {
        var sys = getSwaggerSystem();
        if (!sys) { log.warn('authorizeBasic: Swagger system not available.'); return false; }
        var schemes = classifySchemes();
        if (schemes.basic.length === 0) {
            return false;
        }
        var payload = {};
        schemes.basic.forEach(function (name) {
            payload[name] = { name: name, schema: schemes.all[name], value: { username: username, password: password } };
        });
        try {
            sys.authActions.authorize(payload);
            return true;
        } catch (e) {
            log.error('authorizeBasic error: %o', e);
            return false;
        }
    }

    function logoutSchemes(names) {
        var sys = getSwaggerSystem();
        if (!sys) { log.warn('logoutSchemes: Swagger system not available.'); return false; }
        var toLogout = names;
        if (!toLogout) {
            var schemes = classifySchemes();
            toLogout = schemes.bearer.concat(schemes.basic);
        }
        if (!toLogout || toLogout.length === 0) { return false; }
        try {
            sys.authActions.logout(toLogout);
            return true;
        } catch (e) {
            log.error('logoutSchemes error: %o', e);
            return false;
        }
    }

    function logoutAll() {
        return logoutSchemes(null);
    }

    function getAuthorizedSchemes() {
        var sys = getSwaggerSystem();
        if (!sys) { return {}; }
        try {
            var a = sys.authSelectors.authorized();
            if (a && typeof a.toJS === 'function') { return a.toJS(); }
            return a || {};
        } catch (e) {
            log.error('getAuthorizedSchemes error: %o', e);
            return {};
        }
    }

    // Toast
    function showSwaggerToast(htmlMessage) {
        var TOAST_ID = 'auth-helper-toast';
        var existing = document.getElementById(TOAST_ID);
        if (existing) { existing.remove(); }
        var t = getThemeTokens();
        var toast = document.createElement('div');
        toast.id = TOAST_ID;
        Object.assign(toast.style, {
            position: 'fixed', bottom: '24px', right: '24px', zIndex: '99999',
            maxWidth: '400px', padding: '12px 16px', borderRadius: '6px',
            fontSize: '13px', lineHeight: '1.5', boxShadow: '0 4px 14px rgba(0,0,0,0.18)',
            transition: 'opacity 0.4s ease', opacity: '1', cursor: 'pointer',
            background: t.statusBg, color: t.statusText, border: '1px solid ' + t.statusBorder,
        });
        toast.innerHTML = htmlMessage;
        document.body.appendChild(toast);
        var fadeTimer = setTimeout(function () {
            toast.style.opacity = '0';
            setTimeout(function () { toast.remove(); }, 400);
        }, 5600);
        toast.addEventListener('click', function () { clearTimeout(fadeTimer); toast.remove(); });
    }

    // Shared authorized message (used by dialog line and main-page banner)
    function authorizedMessage() {
        var claimsNote = '';
        if (renewalState.useCustomClaims && renewalState.customClaimsBody) {
            try { claimsNote = ' with custom claims ' + JSON.stringify(renewalState.customClaimsBody); } catch (e) { /* ignore */ }
        }
        return authStatus.expiryDate
            ? 'Authorized as ' + authStatus.username + claimsNote + ' - expires at ' + formatExpiryDate(authStatus.expiryDate) + '. Auto-renewal active.'
            : 'Authorized as ' + authStatus.username + claimsNote + '.';
    }

    // Authorized line (success + expiry + renewal)
    function updateDialogAuthorizedLine() {
        var LINE_ID = 'auth-helper-authorized-line';
        var container = document.getElementById(IDS.container);
        var existing = document.getElementById(LINE_ID);
        var t = getThemeTokens();
        if (!authStatus.username) { if (existing) { existing.remove(); } return; }
        var text = authorizedMessage();
        if (existing) {
            if (existing.textContent !== text) { existing.textContent = text; }
            existing.style.color = t.statusText; existing.style.background = t.statusBg;
            existing.style.border = '1px solid ' + t.statusBorder;
            return;
        }
        if (!container) { return; }
        var line = document.createElement('p');
        line.id = LINE_ID;
        Object.assign(line.style, {
            margin: '10px 0 2px', padding: '6px 10px', fontSize: '13px', fontWeight: '600',
            borderRadius: '4px', color: t.statusText, background: t.statusBg,
            border: '1px solid ' + t.statusBorder,
        });
        line.textContent = text;
        container.appendChild(line);
    }

    // Main-page banner (optional)
    function updateMainPageBanner() {
        var BANNER_ID = 'auth-helper-main-banner';
        var SECTION_ID = 'auth-helper-main-banner-section';
        var existing = document.getElementById(BANNER_ID);
        var t = getThemeTokens();
        if (!isEnabled() || !CONFIG.showMainPageBanner || !authStatus.username) {
            var sec = document.getElementById(SECTION_ID);
            if (sec) { sec.remove(); } else if (existing) { existing.remove(); }
            return;
        }
        var text = authorizedMessage();
        if (existing) {
            if (existing.textContent !== text) { existing.textContent = text; }
            existing.style.color = t.statusText; existing.style.background = t.statusBg;
            existing.style.border = '1px solid ' + t.statusBorder;
            return;
        }
        // Place the banner in its own Swagger-style section inside the
        // scheme-container, so the container holds two sibling sections.
        var schemeContainer = document.querySelector('.swagger-ui .scheme-container');
        if (!schemeContainer) {
            var authWrapperEl = document.querySelector('.swagger-ui .auth-wrapper');
            schemeContainer = authWrapperEl ? authWrapperEl.closest('.scheme-container') : null;
        }
        if (!schemeContainer) { return; }
        var staleSection = document.getElementById(SECTION_ID);
        if (staleSection) { staleSection.remove(); }
        var section = document.createElement('section');
        section.id = SECTION_ID;
        section.className = 'schemes wrapper block col-12';
        section.style.marginTop = '0.3em';
        var banner = document.createElement('div');
        banner.id = BANNER_ID;
        Object.assign(banner.style, {
            padding: '8px 12px', borderRadius: '6px',
            fontSize: '13px', fontWeight: '500', background: t.statusBg,
            border: '1px solid ' + t.statusBorder, color: t.statusText,
        });
        banner.textContent = text;
        section.appendChild(banner);
        schemeContainer.appendChild(section);
    }

    // Injected element IDs
    var IDS = {
        container:    'auth-helper-container',
        selector:     'auth-helper-user-select',
        getTokenBtn:  'auth-helper-get-token-btn',
        expiresInfo:  'auth-helper-expires-info',
        tokenDisplay:      'auth-helper-token-display',
        customClaimsCheck: 'auth-helper-custom-claims-check',
        customClaimsArea:  'auth-helper-custom-claims-area',
        customClaimsError: 'auth-helper-custom-claims-error',
    };

    // Token display (persisted across reopen)
    function updateTokenDisplay(accessToken) {
        if (accessToken !== undefined && accessToken !== null) { authStatus.token = accessToken; }
        var el = document.getElementById(IDS.tokenDisplay);
        if (el) { el.value = authStatus.token ? ('Bearer ' + authStatus.token) : ''; }
    }

    function restoreTokenDisplay() {
        var el = document.getElementById(IDS.tokenDisplay);
        if (el) { el.value = authStatus.token ? ('Bearer ' + authStatus.token) : ''; }
    }

    var BODY_ACTIVE_CLASS = 'mdt-active';
    var STYLE_ID = 'mdt-endpoint-style';

    // Endpoint tweaks (enable-gated)
    function injectEndpointStyles() {
        if (document.getElementById(STYLE_ID)) { return; }
        var style = document.createElement('style');
        style.id = STYLE_ID;
        style.textContent = [
            'body.mdt-active .opblock .responses-wrapper:not([data-mdt-live]) { display: none !important; }',
            'body.mdt-active .opblock .responses-wrapper .responses-inner > .responses-table,',
            'body.mdt-active .opblock .responses-wrapper .responses-inner > table { display: none !important; }',
            '.swagger-ui .dialog-ux .modal-ux-content { max-height: 725px; }',
        ].join('\n');
        document.head.appendChild(style);
    }

    function isTryItOutActive(opblock) {
        var tryBtn = opblock.querySelector('.try-out__btn');
        if (tryBtn && tryBtn.textContent.trim().toLowerCase() === 'cancel') { return true; }
        var resetBtn = opblock.querySelector('.btn.reset');
        return !!(resetBtn && resetBtn.style.display !== 'none');
    }

    function applyButtonVisibility(opblock) {
        var tryBtn = opblock.querySelector('.try-out__btn');
        var resetBtn = opblock.querySelector('.btn.reset');
        if (tryBtn) { tryBtn.style.display = 'none'; }
        if (resetBtn) { resetBtn.style.display = ''; }
    }

    function restoreButtonVisibility(opblock) {
        var tryBtn = opblock.querySelector('.try-out__btn');
        var resetBtn = opblock.querySelector('.btn.reset');
        if (tryBtn) { tryBtn.style.display = ''; }
        if (resetBtn) { resetBtn.style.display = 'none'; }
        var wrapper = opblock.querySelector('.responses-wrapper');
        if (wrapper) {
            wrapper.removeAttribute('data-mdt-live');
            wrapper.style.removeProperty('display');
        }
    }

    function applyEndpointTweaks() {
        if (!isEnabled()) { return; }
        document.body.classList.add(BODY_ACTIVE_CLASS);
        updateMainPageBanner();
        clickAllTryItOutButtons();

        if (document.querySelector('.modal-ux-content')) {
            injectHelperUI();
            injectBasicAuthToggle();
        }
    }

    function removeEndpointTweaks() {
        document.body.classList.remove(BODY_ACTIVE_CLASS);
        updateMainPageBanner();
        deactivateAllTryItOut();

        var helper = document.getElementById(IDS.container);
        if (helper) { helper.remove(); }
        var basicToggle = document.getElementById('auth-helper-basic-toggle');
        if (basicToggle) { basicToggle.remove(); }
        var modalContent = document.querySelector('.modal-ux-content');
        if (modalContent) { modalContent.classList.remove('auth-helper-show-basic'); }
    }

    function clickAllTryItOutButtons() {
        var opblocks = document.querySelectorAll('.opblock');
        for (var i = 0; i < opblocks.length; i++) {
            var opblock = opblocks[i];
            if (isTryItOutActive(opblock)) { applyButtonVisibility(opblock); continue; }
            var btn = opblock.querySelector('.try-out__btn');
            if (!btn) { continue; }
            btn.click();
            (function (ob) { setTimeout(function () { applyButtonVisibility(ob); }, 50); }(opblock));
        }
    }

    function deactivateAllTryItOut() {
        var opblocks = document.querySelectorAll('.opblock');
        for (var i = 0; i < opblocks.length; i++) {
            var opblock = opblocks[i];
            restoreButtonVisibility(opblock);
            if (!isTryItOutActive(opblock)) { continue; }
            var btn = opblock.querySelector('.try-out__btn');
            if (!btn) { continue; }
            btn.click();
            (function (ob) { setTimeout(function () { restoreButtonVisibility(ob); }, 50); }(opblock));
        }
    }

    var TOPBAR_TOGGLE_ID = 'mdt-topbar-toggle';

    var MDT_ICON_PATH = 'M19.8565,9 C19.9616,9.00974 20.0667,9.0243 20.1714,9.04377'
        + ' L18.521,11.9024 C18.1988,12.4604 18.39,13.1739 18.948,13.4961'
        + ' C19.506,13.8183 20.2195,13.6271 20.5417,13.0691 L22.1921,10.2105'
        + ' C23.1291,11.3063 23.3244,12.916 22.5624,14.2357'
        + ' C21.8586,15.4548 20.5321,16.0903 19.2166,15.9724 L16.1855,21.2224'
        + ' C15.7828,21.9199 14.8908,22.1589 14.1933,21.7562'
        + ' C13.5724,21.3977 13.3149,20.6516 13.5499,20'
        + ' C13.5788,19.9196 13.6153,19.8407 13.6595,19.7641 L16.685,14.5239'
        + ' C15.9109,13.4415 15.7915,11.9632 16.5002,10.7357'
        + ' C17.0907,9.71296 18.1194,9.10098 19.2123,9'
        + ' C19.4255,8.9803 19.6411,8.98005 19.8565,9 Z'
        + ' M16.5,2 C17.3803385,2 18.1846722,2.3249884 18.7997413,2.86152441'
        + ' L18.9495,3 L19,3 L19,3.05051 C19.5710154,3.63322077'
        + ' 19.9399195,4.4147197 19.9932952,5.28157387 L20,5.5 L20,8.00934'
        + ' C19.404825,7.947355 18.8064344,8.00568031 18.240468,8.17497053'
        + ' L18,8.25429 L18,7 L4,7 L4,16.5 C4,17.2796706 4.59488554,17.9204457'
        + ' 5.35553954,17.9931332 L5.5,18 L13.5234,18 L12.7935,19.2641'
        + ' C12.69165,19.440575 12.6141563,19.6243625 12.5599641,19.8116656'
        + ' L12.5135,20 L5.5,20 C3.63143333,20 2.10487333,18.5357256'
        + ' 2.00517885,16.6920355 L2,16.5 L2,5.5 C2,3.61970769 2.3249884,3.81531787'
        + ' 2.86152441,3.20026347 L3,3.05051 L3,3 L3.05051,3'
        + ' C3.63322077,2.42899385 4.4147197,2.06008189 5.28157387,2.00670494'
        + ' L5.5,2 L16.5,2 Z'
        + ' M12.1508,9.17782 C12.3244889,9.00437111 12.5939407,8.98527284'
        + ' 12.7886996,9.12040579 L12.8579,9.17831 L15.1463,11.4698'
        + ' C15.0037625,12.08405 14.9949797,12.7132297 15.1088979,13.3172777'
        + ' L15.1652,13.5745 L12.8579,15.8849 C12.6628,16.0803 12.3462,16.0805'
        + ' 12.1508,15.8854 C11.9771111,15.7119778 11.9576543,15.4425556'
        + ' 12.0924999,15.2475893 L12.1503,15.1783 L14.7934,12.5316 L12.1503,9.88493'
        + ' C11.9552,9.68953 11.9554,9.37295 12.1508,9.17782 Z'
        + ' M9.85711,9.17826 C10.0523,9.37363 10.0521,9.69021 9.85672,9.88537'
        + ' L7.2075,12.5316 L9.85672,15.1779 C10.0521,15.373 10.0523,15.6896'
        + ' 9.85711,15.885 C9.66196,16.0803 9.34538,16.0805 9.15001,15.8854'
        + ' L6.14665,12.8854 C6.05276,12.7916 6,12.6643 6,12.5316'
        + ' C6,12.3989 6.05275,12.2717 6.14664,12.1779 L9.15001,9.17787'
        + ' C9.34538,8.98271 9.66196,8.98289 9.85711,9.17826 Z';

    // Topbar toggle icon
    function buildToggleSVG(enabled) {
        var strike = enabled ? '' : (
            '<line x1="2" y1="2" x2="22" y2="22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>'
        );
        return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" style="display:block;">'
            + '<path d="' + MDT_ICON_PATH + '" fill="currentColor" fill-rule="nonzero"/>'
            + strike + '</svg>';
    }

    function getToggleColor(enabled) {
        return enabled ? 'rgba(255,255,255,0.75)' : 'rgba(255,255,255,0.35)';
    }

    function updateToggleIcon(btn, enabled) {
        btn.innerHTML = buildToggleSVG(enabled);
        btn.style.color = getToggleColor(enabled);
        btn.title = 'Middleware Dev Tools - ' + (enabled ? 'enabled' : 'disabled');
    }

    function injectTopbarToggle() {
        if (document.getElementById(TOPBAR_TOGGLE_ID)) { return; }
        var topbarWrapper = document.querySelector('.topbar-wrapper');
        if (!topbarWrapper) { return; }

        var wrapper = document.createElement('div');
        wrapper.id = TOPBAR_TOGGLE_ID;
        Object.assign(wrapper.style, { display: 'flex', justifyContent: 'center', lineHeight: '1' });

        var btn = document.createElement('button');
        Object.assign(btn.style, {
            background: 'none', border: 'none', padding: '2px', margin: '0',
            cursor: 'pointer', lineHeight: '1', display: 'block', borderRadius: '3px', transition: 'opacity 0.2s',
        });

        updateToggleIcon(btn, isEnabled());

        btn.addEventListener('mouseenter', function () { btn.style.opacity = '1'; });
        btn.addEventListener('mouseleave', function () { btn.style.opacity = ''; });
        btn.addEventListener('click', function (e) {
            e.stopPropagation();
            toggleTopbarMenu(wrapper, btn);
        });

        wrapper.appendChild(btn);
        topbarWrapper.appendChild(wrapper);
    }

    var TOPBAR_MENU_ID = 'mdt-topbar-menu';

    function closeTopbarMenu() {
        var m = document.getElementById(TOPBAR_MENU_ID);
        if (m) { m.remove(); }
    }

    function toggleTopbarMenu(wrapper, btn) {
        if (document.getElementById(TOPBAR_MENU_ID)) { closeTopbarMenu(); return; }
        var t = getThemeTokens();
        var menu = document.createElement('div');
        menu.id = TOPBAR_MENU_ID;
        Object.assign(menu.style, {
            position: 'absolute', top: '100%', left: '50%', transform: 'translateX(-50%)',
            marginTop: '4px', minWidth: '150px', zIndex: '100000',
            background: t.bg, color: t.text, border: '1px solid ' + t.border,
            borderRadius: '6px', boxShadow: '0 6px 20px rgba(0,0,0,0.3)',
            fontFamily: 'sans-serif', fontSize: '13px', overflow: 'hidden',
        });

        function item(label, onClick) {
            var el = document.createElement('div');
            el.textContent = label;
            Object.assign(el.style, { padding: '8px 12px', cursor: 'pointer', whiteSpace: 'nowrap' });
            el.addEventListener('mouseenter', function () { el.style.background = 'rgba(128,128,128,0.18)'; });
            el.addEventListener('mouseleave', function () { el.style.background = ''; });
            el.addEventListener('click', function (e) { e.stopPropagation(); closeTopbarMenu(); onClick(); });
            return el;
        }

        menu.appendChild(item(isEnabled() ? 'Disable' : 'Enable', function () {
            var nowEnabled = !isEnabled();
            setEnabled(nowEnabled);
            updateToggleIcon(btn, nowEnabled);
            log.info('Dev Tools toggled: enabled=%s', nowEnabled);
            if (nowEnabled) { applyEndpointTweaks(); } else { removeEndpointTweaks(); }
        }));
        menu.appendChild(item('Configuration', openConfigModal));

        if (getComputedStyle(wrapper).position === 'static') { wrapper.style.position = 'relative'; }
        wrapper.appendChild(menu);

        setTimeout(function () {
            document.addEventListener('click', function onDoc(e) {
                if (!menu.contains(e.target)) { closeTopbarMenu(); document.removeEventListener('click', onDoc); }
            });
        }, 0);
    }

    var BASIC_HIDE_STYLE_ID = 'auth-helper-basic-auth-hide';

    // Basic Auth section show/hide
    function injectBasicAuthHideStyle() {
        if (document.getElementById(BASIC_HIDE_STYLE_ID)) { return; }
        var style = document.createElement('style');
        style.id = BASIC_HIDE_STYLE_ID;

        style.textContent = 'body.mdt-active .modal-ux-content:not(.auth-helper-show-basic) .auth-container[data-auth-helper="basic"] { display: none !important; }';
        document.head.appendChild(style);
    }

    function injectBasicAuthToggle() {
        if (!isEnabled()) { return; }
        var TOGGLE_ID = 'auth-helper-basic-toggle';
        if (document.getElementById(TOGGLE_ID)) { return; }
        var header = document.querySelector('.modal-ux-header');
        if (!header) { return; }
        var modalContent = document.querySelector('.modal-ux-content');
        if (!modalContent) { return; }

        var basicContainer = findBasicAuthContainer();
        var bearerContainer = findBearerAuthContainer();

        if (!basicContainer) {
            var stale = document.getElementById(TOGGLE_ID);
            if (stale) { stale.remove(); }
            return;
        }

        basicContainer.setAttribute('data-auth-helper', 'basic');
        if (bearerContainer) { bearerContainer.setAttribute('data-auth-helper', 'bearer'); }

        var isShown = modalContent.classList.contains('auth-helper-show-basic');
        var link = document.createElement('button');
        link.id = TOGGLE_ID;
        link.type = 'button';
        link.textContent = isShown ? 'Hide Basic Auth' : 'Show Basic Auth';

        Object.assign(link.style, {
            position: 'absolute', top: '50%', right: '52px', transform: 'translateY(-50%)',
            background: 'none', border: 'none', padding: '2px 4px', fontSize: '11px',
            cursor: 'pointer', color: 'inherit',
            textDecoration: 'underline', textUnderlineOffset: '2px',
            opacity: '0.7', whiteSpace: 'nowrap', zIndex: '10',
        });
        link.addEventListener('mouseenter', function () { link.style.opacity = '1'; });
        link.addEventListener('mouseleave', function () { link.style.opacity = '0.7'; });
        link.addEventListener('click', function () {
            var showing = modalContent.classList.toggle('auth-helper-show-basic');
            link.textContent = showing ? 'Hide Basic Auth' : 'Show Basic Auth';
        });
        if (getComputedStyle(header).position === 'static') { header.style.position = 'relative'; }
        header.appendChild(link);
    }

    // Auth container finders
    function findAuthContainerByTitle(pattern) {
        var containers = document.querySelectorAll('.modal-ux-content .auth-container');
        for (var i = 0; i < containers.length; i++) {
            var heading = containers[i].querySelector('h4') || containers[i].querySelector('h5');
            if (heading && pattern.test(heading.textContent)) { return containers[i]; }
        }
        return null;
    }

    function findBearerAuthContainer() {
        var tagged = document.querySelector('.modal-ux-content .auth-container[data-auth-helper="bearer"]');
        return tagged || findAuthContainerByTitle(/bearer|apikey/i);
    }

    function findBasicAuthContainer() {
        var tagged = document.querySelector('.modal-ux-content .auth-container[data-auth-helper="basic"]');
        return tagged || findAuthContainerByTitle(/basic/i);
    }

    // Dialog UI injection
    function injectHelperUI() {
        if (!isEnabled()) { return; }
        if (document.getElementById(IDS.container)) { return; }

        var bearerContainer = findBearerAuthContainer();
        if (!bearerContainer) { log.warn('Bearer auth container not found - UI injection skipped.'); return; }

        var apiKeyInput = document.getElementById('api_key_value');
        if (apiKeyInput) {
            renewalState.apiKeyInput = apiKeyInput;

            prefillBearerInput(apiKeyInput);
        }

        var form = bearerContainer.querySelector('form');
        var h4 = bearerContainer.querySelector('form div h4');
        var h4Wrapper = h4 ? h4.parentNode : null;
        var insertParent = h4Wrapper || form || bearerContainer;
        var insertBefore = h4Wrapper ? h4.nextSibling : insertParent.firstChild;
        if (!insertParent) { log.warn('Could not find insertion anchor - aborting injection.'); return; }

        var container = document.createElement('div');
        container.id = IDS.container;

        Object.assign(container.style, {
            margin: '10px 0 14px', padding: '12px 14px',
            border: '1px solid', borderColor: 'rgba(128,128,128,0.35)',
            borderRadius: '6px',
        });

        var envName = Object.keys(CONFIG.domains).find(function (k) { return CONFIG.domains[k] === AUTH_BASE_URL; }) || 'UNKNOWN';
        var envColors = { TST: '#b08000', DEV: '#1a6e3c', LOCAL: '#5a3e9e', UNKNOWN: '#888' };
        var envBadge = document.createElement('div');
        envBadge.textContent = 'Auth endpoint: ' + AUTH_BASE_URL;
        Object.assign(envBadge.style, {
            marginBottom: '10px', padding: '3px 8px', background: envColors[envName] || envColors.UNKNOWN,
            color: '#fff', borderRadius: '4px', display: 'inline-block',
            fontWeight: '600', fontSize: '11px', letterSpacing: '0.03em',
        });
        container.appendChild(envBadge);

        var selectorLabel = document.createElement('label');
        selectorLabel.textContent = 'Pre-configured user ';
        selectorLabel.htmlFor = IDS.selector;
        Object.assign(selectorLabel.style, { display: 'block', marginBottom: '5px', fontWeight: '600' });
        var cfgLink = document.createElement('a');
        cfgLink.textContent = '(Configure)';
        cfgLink.href = '#';
        Object.assign(cfgLink.style, {
            fontWeight: '400', fontSize: '11px', textDecoration: 'underline',
            textUnderlineOffset: '2px', cursor: 'pointer', color: 'inherit', opacity: '0.85',
        });
        cfgLink.addEventListener('click', function (e) { e.preventDefault(); openConfigModal(); });
        selectorLabel.appendChild(cfgLink);
        container.appendChild(selectorLabel);

        var selector = document.createElement('select');
        selector.id = IDS.selector;
        Object.assign(selector.style, { width: '100%', marginBottom: '8px', boxSizing: 'border-box' });
        var placeholderOption = document.createElement('option');
        placeholderOption.value = '';
        placeholderOption.textContent = '-- Select a user --';
        selector.appendChild(placeholderOption);
        CONFIG.users.forEach(function (u) {
            var option = document.createElement('option');
            option.value = u.username; option.textContent = u.username;
            selector.appendChild(option);
        });
        container.appendChild(selector);

        var claimsContainer = document.createElement('div');
        Object.assign(claimsContainer.style, { display: 'none', marginBottom: '8px' });

        var claimsArea = document.createElement('textarea');
        claimsArea.id = IDS.customClaimsArea;
        claimsArea.spellcheck = false;
        claimsArea.value = JSON.stringify(CONFIG.customClaim, null, 2);
        Object.assign(claimsArea.style, {
            width: '100%', minHeight: '110px', boxSizing: 'border-box',
            fontFamily: 'monospace', fontSize: '11px', lineHeight: '1.4',
            padding: '6px 8px', borderRadius: '4px', resize: 'vertical', whiteSpace: 'pre',
        });

        var claimsError = document.createElement('span');
        claimsError.id = IDS.customClaimsError;
        Object.assign(claimsError.style, { display: 'block', fontSize: '11px', color: '#cf222e', minHeight: '16px', marginTop: '3px' });

        claimsContainer.appendChild(claimsArea);
        claimsContainer.appendChild(claimsError);
        container.appendChild(claimsContainer);

        var row = document.createElement('div');
        Object.assign(row.style, { display: 'flex', alignItems: 'center', gap: '10px', flexWrap: 'wrap', marginBottom: '4px' });

        var getTokenBtn = document.createElement('button');
        getTokenBtn.id = IDS.getTokenBtn;
        getTokenBtn.className = 'btn authorize';
        getTokenBtn.type = 'button';
        getTokenBtn.textContent = 'Get token';
        getTokenBtn.disabled = true;
        Object.assign(getTokenBtn.style, { opacity: '0.45', whiteSpace: 'nowrap' });

        var customClaimsCheck = document.createElement('input');
        customClaimsCheck.type = 'checkbox';
        customClaimsCheck.id = IDS.customClaimsCheck;

        var customClaimsLabel = document.createElement('label');
        customClaimsLabel.htmlFor = IDS.customClaimsCheck;
        customClaimsLabel.textContent = 'with custom claims';
        Object.assign(customClaimsLabel.style, { fontSize: '12px', cursor: 'pointer', userSelect: 'none' });

        row.appendChild(getTokenBtn);
        row.appendChild(customClaimsCheck);
        row.appendChild(customClaimsLabel);
        container.appendChild(row);

        var expiresInfo = document.createElement('span');
        expiresInfo.id = IDS.expiresInfo;
        Object.assign(expiresInfo.style, { display: 'block', fontSize: '12px', fontStyle: 'italic', opacity: '0.85', marginBottom: '4px', minHeight: '16px' });
        container.appendChild(expiresInfo);

        var tokenLabel = document.createElement('label');
        tokenLabel.textContent = 'Token';
        tokenLabel.htmlFor = IDS.tokenDisplay;
        Object.assign(tokenLabel.style, { display: 'block', marginTop: '6px', marginBottom: '4px', fontWeight: '600', opacity: '0.85' });
        container.appendChild(tokenLabel);

        var tokenDisplay = document.createElement('input');
        tokenDisplay.id = IDS.tokenDisplay;
        tokenDisplay.type = 'text';
        tokenDisplay.readOnly = true;
        tokenDisplay.placeholder = 'Fetched token will appear here';
        Object.assign(tokenDisplay.style, {
            width: '100%', fontFamily: 'monospace', fontSize: '11px', boxSizing: 'border-box',
        });

        tokenDisplay.addEventListener('click', function () { tokenDisplay.select(); });
        container.appendChild(tokenDisplay);

        insertParent.insertBefore(container, insertBefore);

        // Enable/disable Get token based on selector + optional claims validity.
        function updateGetTokenBtnState() {
            var hasUser = selector.value !== '';
            var claimsOk = true;
            if (customClaimsCheck.checked) {
                try { JSON.parse(claimsArea.value); } catch (e) { claimsOk = false; }
            }
            var ok = hasUser && claimsOk;
            getTokenBtn.disabled = !ok;
            getTokenBtn.style.opacity = ok ? '1' : '0.45';
            getTokenBtn.style.cursor = ok ? 'pointer' : 'not-allowed';
        }

        customClaimsCheck.addEventListener('change', function () {
            claimsContainer.style.display = customClaimsCheck.checked ? '' : 'none';
            if (!customClaimsCheck.checked) { claimsError.textContent = ''; }
            updateGetTokenBtnState();
        });

        claimsArea.addEventListener('input', function () {
            try {
                JSON.parse(claimsArea.value);
                claimsError.textContent = '';
            } catch (e) {
                claimsError.textContent = 'Invalid JSON: ' + e.message;
            }
            updateGetTokenBtnState();
        });

        selector.addEventListener('change', function () {
            cancelRenewal();
            expiresInfo.textContent = '';
            updateGetTokenBtnState();
        });
        getTokenBtn.addEventListener('click', function () {
            if (customClaimsCheck.checked) {
                handleGetTokenWithCustomClaims(selector, expiresInfo, getTokenBtn);
            } else {
                handleGetToken(selector, apiKeyInput, expiresInfo, getTokenBtn);
            }
        });

        if (authStatus.username) { updateDialogAuthorizedLine(); }

        restoreTokenDisplay();

        syncNativeAuthState();

    }

    // Re-apply auth if native dialog drifted out of sync
    function syncNativeAuthState() {
        if (!authStatus.username || !authStatus.token) { return; }
        var authorized = getAuthorizedSchemes();
        var schemes = classifySchemes();
        var bearerAuthorized = schemes.bearer.some(function (name) {
            var e = authorized[name];
            return e && (e.value || e.token);
        });
        var basicAuthorized = schemes.basic.length === 0 || schemes.basic.some(function (name) {
            var e = authorized[name];
            return e && e.value;
        });
        if (bearerAuthorized && basicAuthorized) { return; }

        if (!bearerAuthorized) { authorizeBearer(authStatus.token); }
        if (!basicAuthorized) {
            var pwd = getPasswordForUser(authStatus.username);
            if (pwd) { authorizeBasic(authStatus.username, pwd); }
        }
    }

    // Manual token fetch
    function handleGetToken(selector, apiKeyInput, expiresInfo, getTokenBtn) {
        var username = selector.value;
        if (!username) { return; }
        var password = getPasswordForUser(username);
        if (!password) {
            setExpiresText(expiresInfo, 'Error: no password configured for this user.', '#cf222e');
            return;
        }

        if (authStatus.username && authStatus.username !== username) {
            log.info('Logging out current user "%s" before authorizing "%s".', authStatus.username, username);
            cancelRenewal();
            logoutAll();
            setAuthStatus(null, null);
            resetSelectorUI();
            var tdEl = document.getElementById(IDS.tokenDisplay);
            if (tdEl) { tdEl.value = ''; }
        }

        setControlsBusy(true);
        setExpiresText(expiresInfo, '', '');

        GM_xmlhttpRequest({
            method:  'POST',
            url:     TOKEN_URL,
            headers: { 'Authorization': buildBasicAuthHeader(username, password), 'Content-Type': 'application/json' },
            onload: function (response) {
                restoreButton(getTokenBtn);
                if (response.status < 200 || response.status >= 300) {
                    setExpiresText(expiresInfo, 'Error ' + response.status + ': ' + (response.statusText || 'request failed'), '#cf222e');
                    log.error('Manual token request failed: status=%d, body=%s', response.status, response.responseText);
                    return;
                }
                var body;
                try { body = JSON.parse(response.responseText); }
                catch (parseError) {
                    setExpiresText(expiresInfo, 'Error: could not parse the response JSON.', '#cf222e');
                    log.error('Manual token JSON parse error: %s | raw: %s', parseError, response.responseText);
                    return;
                }
                var accessToken = body.accessToken;
                var expiresIn = body.expiresIn;
                if (!accessToken) {
                    setExpiresText(expiresInfo, 'Error: "accessToken" not found in the response.', '#cf222e');
                    log.error('Manual token: unexpected response shape: %o', body);
                    return;
                }

                updateTokenDisplay(accessToken);
                authorizeBearer(accessToken);
                authorizeBasic(username, password);

                var expiryDate = expiresIn != null ? new Date(Date.now() + expiresIn * 1000) : null;

                setExpiresText(expiresInfo, '', '');
                setAuthStatus(username, expiryDate);
                log.info('Manual token applied. expiresIn=%ds, post-expiry buffer=%ds.', expiresIn, CONFIG.renewalPostExpiryBufferSecs);
                scheduleRenewal(username, expiresIn);
            },
            onerror: function (error) {
                restoreButton(getTokenBtn);
                setExpiresText(expiresInfo, 'Network error - could not reach the auth server.', '#cf222e');
                log.error('Manual token network error for %s: %o', username, error);
            },
            ontimeout: function () {
                restoreButton(getTokenBtn);
                setExpiresText(expiresInfo, 'Request timed out.', '#cf222e');
                log.error('Manual token request timed out for %s.', username);
            },
            timeout: 15000,
        });
    }

    // Custom claims flow = regular flow + 2 extra steps.
    // Step 1 (regular): POST tokenEndpoint with Basic Auth -> bearer token
    //   -> authorize Swagger with regular token + schedule regular renewal.
    // Step 2 (extra): POST usersSearchEndpoint with Basic Auth -> referenceIdentityId
    // Step 3 (extra): POST customClaimTokenEndpoint with Basic Auth -> custom token
    //   -> overwrite Swagger auth + switch renewal to custom claims path.
    // Steps 2-3 fail gracefully: user stays logged in with the regular token.
    function handleGetTokenWithCustomClaims(selector, expiresInfo, getTokenBtn) {
        var username = selector.value;
        if (!username) { return; }
        var password = getPasswordForUser(username);
        if (!password) {
            setExpiresText(expiresInfo, 'Error: no password configured for this user.', '#cf222e');
            return;
        }

        var claimsAreaEl = document.getElementById(IDS.customClaimsArea);
        var claimsBody;
        try { claimsBody = JSON.parse(claimsAreaEl ? claimsAreaEl.value : '{}'); } catch (e) {
            setExpiresText(expiresInfo, 'Error: invalid custom claims JSON.', '#cf222e');
            return;
        }

        // Persist customClaim to stored config if not already there (once only).
        var stored = loadStoredConfig() || {};
        if (!stored.customClaim) {
            stored.customClaim = claimsBody;
            stored.usersSearchEndpoint = stored.usersSearchEndpoint || CONFIG.usersSearchEndpoint;
            stored.customClaimTokenEndpoint = stored.customClaimTokenEndpoint || CONFIG.customClaimTokenEndpoint;
            saveStoredConfig(stored);
            CONFIG = buildEffectiveConfig(stored);
            log.info('customClaim saved to stored config for the first time.');
        }

        if (authStatus.username && authStatus.username !== username) {
            log.info('Logging out current user "%s" before authorizing "%s".', authStatus.username, username);
            cancelRenewal();
            logoutAll();
            setAuthStatus(null, null, null);
            resetSelectorUI();
            var tdEl = document.getElementById(IDS.tokenDisplay);
            if (tdEl) { tdEl.value = ''; }
        }

        setControlsBusy(true);
        setExpiresText(expiresInfo, '', '');

        // === Step 1: regular bearer token (same endpoint + auth as handleGetToken) ===
        GM_xmlhttpRequest({
            method:  'POST',
            url:     TOKEN_URL,
            headers: { 'Authorization': buildBasicAuthHeader(username, password), 'Content-Type': 'application/json' },
            onload: function (r1) {
                if (r1.status < 200 || r1.status >= 300) {
                    restoreButton(getTokenBtn);
                    log.warn('Custom claims step 1/3 (bearer token) failed: status=%d, body=%s', r1.status, r1.responseText);
                    setExpiresText(expiresInfo, 'Custom claims failed at bearer token - error ' + r1.status + (r1.statusText ? ' ' + r1.statusText : '') + responseSnippet(r1.responseText) + '.', '#cf222e');
                    return;
                }
                var b1;
                try { b1 = JSON.parse(r1.responseText); } catch (e) {
                    restoreButton(getTokenBtn);
                    setExpiresText(expiresInfo, 'Custom claims failed: could not parse bearer token response (status ' + r1.status + ').', '#cf222e');
                    return;
                }
                var bearerToken = b1.accessToken;
                var expiresIn = b1.expiresIn;
                if (!bearerToken) {
                    restoreButton(getTokenBtn);
                    setExpiresText(expiresInfo, 'Custom claims failed: accessToken missing in bearer response (status ' + r1.status + ').', '#cf222e');
                    return;
                }

                // Authorize Swagger with the regular token first (same as regular flow).
                updateTokenDisplay(bearerToken);
                authorizeBearer(bearerToken);
                authorizeBasic(username, password);
                var expiryDate = expiresIn != null ? new Date(Date.now() + expiresIn * 1000) : null;
                setAuthStatus(username, expiryDate);
                // Schedule regular renewal as a fallback while extra steps execute.
                renewalState.useCustomClaims = false;
                renewalState.referenceIdentityId = null;
                renewalState.customClaimsBody = null;
                scheduleRenewal(username, expiresIn);

                // Progress hint: regular token is in place; claims still loading.
                setExpiresText(expiresInfo, 'Regular token obtained. Fetching custom claims...', getThemeTokens().subtext);

                // === Step 2: search for user to get referenceIdentityId ===
                var searchUrl = AUTH_BASE_URL + CONFIG.usersSearchEndpoint;
                GM_xmlhttpRequest({
                    method:  'POST',
                    url:     searchUrl,
                    headers: { 'X-Middleware-Authorization': 'Bearer ' + bearerToken, 'Content-Type': 'application/json' },
                    data:    JSON.stringify({ search: username }),
                    onload: function (r2) {
                        if (r2.status < 200 || r2.status >= 300) {
                            restoreButton(getTokenBtn);
                            log.warn('Custom claims step 2/3 (user search) failed: status=%d, body=%s', r2.status, r2.responseText);
                            setExpiresText(expiresInfo, 'Custom claims failed at user search - error ' + r2.status + (r2.statusText ? ' ' + r2.statusText : '') + responseSnippet(r2.responseText) + '. Using regular token.', '#cf222e');
                            return;
                        }
                        var b2;
                        try { b2 = JSON.parse(r2.responseText); } catch (e) {
                            restoreButton(getTokenBtn);
                            log.warn('Custom claims step 2/3 (user search): parse error - body=%s', r2.responseText);
                            setExpiresText(expiresInfo, 'Custom claims failed: could not parse user search response (status ' + r2.status + '). Using regular token.', '#cf222e');
                            return;
                        }
                        var users = Array.isArray(b2) ? b2 : (b2.data || b2.users || b2.content || []);
                        if (!users.length) {
                            restoreButton(getTokenBtn);
                            log.warn('Custom claims step 2/3 (user search): no user found for "%s" - keeping regular token.', username);
                            setExpiresText(expiresInfo, 'Custom claims failed: no user found for "' + username + '" (status ' + r2.status + '). Using regular token.', '#cf222e');
                            return;
                        }
                        var referenceIdentityId = users[0].referenceIdentityId;
                        if (!referenceIdentityId) {
                            restoreButton(getTokenBtn);
                            log.warn('Custom claims step 2/3 (user search): referenceIdentityId missing - keeping regular token.');
                            setExpiresText(expiresInfo, 'Custom claims failed: referenceIdentityId not found in user search response. Using regular token.', '#cf222e');
                            return;
                        }

                        // === Step 3: custom claims token ===
                        var customUrl = AUTH_BASE_URL + CONFIG.customClaimTokenEndpoint.replace('{referenceIdentityId}', referenceIdentityId);
                        GM_xmlhttpRequest({
                            method:  'POST',
                            url:     customUrl,
                            headers: { 'X-Middleware-Authorization': 'Bearer ' + bearerToken, 'Content-Type': 'application/json' },
                            data:    JSON.stringify(claimsBody),
                            onload: function (r3) {
                                if (r3.status < 200 || r3.status >= 300) {
                                    restoreButton(getTokenBtn);
                                    log.warn('Custom claims step 3/3 (token) failed: status=%d, body=%s', r3.status, r3.responseText);
                                    setExpiresText(expiresInfo, 'Custom claims failed at token request - error ' + r3.status + (r3.statusText ? ' ' + r3.statusText : '') + responseSnippet(r3.responseText) + '. Using regular token.', '#cf222e');
                                    return;
                                }
                                var b3;
                                try { b3 = JSON.parse(r3.responseText); } catch (e) {
                                    restoreButton(getTokenBtn);
                                    log.warn('Custom claims step 3/3 (token): parse error - body=%s', r3.responseText);
                                    setExpiresText(expiresInfo, 'Custom claims failed: could not parse token response (status ' + r3.status + '). Using regular token.', '#cf222e');
                                    return;
                                }
                                var customToken = b3.accessToken;
                                var customExpires = b3.expiresIn;
                                if (!customToken) {
                                    restoreButton(getTokenBtn);
                                    log.warn('Custom claims step 3/3 (token): accessToken missing - keeping regular token.');
                                    setExpiresText(expiresInfo, 'Custom claims failed: accessToken missing in token response (status ' + r3.status + '). Using regular token.', '#cf222e');
                                    return;
                                }

                                // Set custom claims state BEFORE setAuthStatus so
                                // authorizedMessage() includes the claims note.
                                renewalState.useCustomClaims = true;
                                renewalState.referenceIdentityId = referenceIdentityId;
                                renewalState.customClaimsBody = claimsBody;

                                updateTokenDisplay(customToken);
                                authorizeBearer(customToken);
                                var customExpiry = customExpires != null ? new Date(Date.now() + customExpires * 1000) : expiryDate;
                                setAuthStatus(username, customExpiry);

                                // scheduleRenewal calls cancelRenewal() which clears
                                // useCustomClaims; restore them after.
                                scheduleRenewal(username, customExpires != null ? customExpires : expiresIn);
                                renewalState.useCustomClaims = true;
                                renewalState.referenceIdentityId = referenceIdentityId;
                                renewalState.customClaimsBody = claimsBody;
                                setExpiresText(expiresInfo, '', '');
                                restoreButton(getTokenBtn);
                                log.info('[Custom claims] Token applied for %s, expiresIn=%ds.', username, customExpires);
                            },
                            onerror:  function () { restoreButton(getTokenBtn); log.warn('Custom claims step 3/3 (token): network error - keeping regular token.'); setExpiresText(expiresInfo, 'Custom claims failed: network error on token request. Using regular token.', '#cf222e'); },
                            ontimeout: function () { restoreButton(getTokenBtn); log.warn('Custom claims step 3/3 (token): timeout - keeping regular token.'); setExpiresText(expiresInfo, 'Custom claims failed: token request timed out. Using regular token.', '#cf222e'); },
                            timeout: 15000,
                        });
                    },
                    onerror:  function () { restoreButton(getTokenBtn); log.warn('Custom claims step 2/3 (user search): network error - keeping regular token.'); setExpiresText(expiresInfo, 'Custom claims failed: network error on user search. Using regular token.', '#cf222e'); },
                    ontimeout: function () { restoreButton(getTokenBtn); log.warn('Custom claims step 2/3 (user search): timeout - keeping regular token.'); setExpiresText(expiresInfo, 'Custom claims failed: user search timed out. Using regular token.', '#cf222e'); },
                    timeout: 15000,
                });
            },
            onerror: function (error) {
                restoreButton(getTokenBtn);
                setExpiresText(expiresInfo, 'Network error - could not reach the auth server.', '#cf222e');
                log.error('Custom claims request network error for %s: %o', username, error);
            },
            ontimeout: function () {
                restoreButton(getTokenBtn);
                setExpiresText(expiresInfo, 'Request timed out.', '#cf222e');
                log.error('Custom claims request timed out for %s.', username);
            },
            timeout: 15000,
        });
    }

    // Silent renewal for custom claims = regular renewal step + extra custom token step.
    // We already have referenceIdentityId cached so user search is skipped.
    function silentRenewTokenWithCustomClaims(username) {
        var password = getPasswordForUser(username);
        var referenceIdentityId = renewalState.referenceIdentityId;
        var claimsBody = renewalState.customClaimsBody;

        if (!password || !referenceIdentityId || !claimsBody) {
            log.error('Custom claims renewal aborted: missing credentials or state for %s.', username);
            return;
        }

        GM_xmlhttpRequest({
            method:  'POST',
            url:     TOKEN_URL,
            headers: { 'Authorization': buildBasicAuthHeader(username, password), 'Content-Type': 'application/json' },
            onload: function (r1) {
                if (r1.status < 200 || r1.status >= 300) {
                    log.error('Custom claims renewal: bearer token error %d. Retrying in 30s.', r1.status);
                    renewalState.renewalTimer = setTimeout(function () { silentRenewTokenWithCustomClaims(username); }, 30000);
                    return;
                }
                var b1;
                try { b1 = JSON.parse(r1.responseText); } catch (e) {
                    log.error('Custom claims renewal: bearer token parse error.');
                    return;
                }
                if (!b1.accessToken) {
                    log.error('Custom claims renewal: no accessToken in bearer response.');
                    return;
                }

                var customUrl = AUTH_BASE_URL + CONFIG.customClaimTokenEndpoint.replace('{referenceIdentityId}', referenceIdentityId);
                GM_xmlhttpRequest({
                    method:  'POST',
                    url:     customUrl,
                    headers: { 'X-Middleware-Authorization': 'Bearer ' + b1.accessToken, 'Content-Type': 'application/json' },
                    data:    JSON.stringify(claimsBody),
                    onload: function (r2) {
                        if (r2.status < 200 || r2.status >= 300) {
                            log.error('Custom claims renewal: custom token error %d. Retrying in 30s.', r2.status);
                            renewalState.renewalTimer = setTimeout(function () { silentRenewTokenWithCustomClaims(username); }, 30000);
                            return;
                        }
                        var b2;
                        try { b2 = JSON.parse(r2.responseText); } catch (e) {
                            log.error('Custom claims renewal: custom token parse error.');
                            return;
                        }
                        var customToken = b2.accessToken;
                        var customExpires = b2.expiresIn;
                        if (!customToken) {
                            log.error('Custom claims renewal: no accessToken in custom token response.');
                            return;
                        }

                        var isFresh = (customExpires != null && customExpires >= CONFIG.fullTokenTtl);
                        log.info('[Custom claims] Renewal complete: expiresIn=%ds, isFresh=%s.', customExpires, isFresh);

                        authorizeBearer(customToken);

                        var expiresInfoEl = document.getElementById(IDS.expiresInfo);
                        if (expiresInfoEl) {
                            var renewExpiry = customExpires != null ? new Date(Date.now() + customExpires * 1000) : null;
                            setExpiresText(expiresInfoEl,
                                renewExpiry ? 'Token (custom claims) auto-renewed. Expires at ' + formatExpiryDate(renewExpiry) + '.' : 'Token (custom claims) auto-renewed.',
                                getThemeTokens().statusText);
                        }
                        updateTokenDisplay(customToken);

                        if (isFresh) {
                            var renewedExpiry = new Date(Date.now() + customExpires * 1000);
                            // useCustomClaims is already true; setAuthStatus shows the claims note.
                            setAuthStatus(username, renewedExpiry);
                            showSwaggerToast('[KEY] Custom claims token auto-renewed for ' + username + '.<br>Expires at ' + formatExpiryDate(renewedExpiry) + '.');
                        }
                        // scheduleRenewal/cancelRenewal clears custom claims state; restore after.
                        scheduleRenewal(username, customExpires != null ? customExpires : b1.expiresIn);
                        renewalState.useCustomClaims = true;
                        renewalState.referenceIdentityId = referenceIdentityId;
                        renewalState.customClaimsBody = claimsBody;
                    },
                    onerror: function () {
                        log.error('Custom claims renewal: network error on custom token. Retrying in 30s.');
                        renewalState.renewalTimer = setTimeout(function () { silentRenewTokenWithCustomClaims(username); }, 30000);
                    },
                    ontimeout: function () {
                        log.error('Custom claims renewal: custom token timed out. Retrying in 30s.');
                        renewalState.renewalTimer = setTimeout(function () { silentRenewTokenWithCustomClaims(username); }, 30000);
                    },
                    timeout: 15000,
                });
            },
            onerror: function () {
                log.error('Custom claims renewal: network error on bearer token. Retrying in 30s.');
                renewalState.renewalTimer = setTimeout(function () { silentRenewTokenWithCustomClaims(username); }, 30000);
            },
            ontimeout: function () {
                log.error('Custom claims renewal: bearer token timed out. Retrying in 30s.');
                renewalState.renewalTimer = setTimeout(function () { silentRenewTokenWithCustomClaims(username); }, 30000);
            },
            timeout: 15000,
        });
    }


        // Helpers
    function setNativeInputValue(input, value) {
        var setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
        setter.call(input, value);
        input.dispatchEvent(new Event('input', { bubbles: true }));
        input.dispatchEvent(new Event('change', { bubbles: true }));
    }

    var BEARER_PREFIX = 'Bearer ';

    // prefill "Bearer " for easy paste
    function prefillBearerInput(input) {
        if (!input) { return; }
        if ((input.value || '').trim() !== '') { return; }
        setNativeInputValue(input, BEARER_PREFIX);

        try {
            var end = BEARER_PREFIX.length;
            input.setSelectionRange(end, end);
        } catch (e) { /* unsupported input type */ }
    }

    // status line: errors only
    function setExpiresText(span, text, color) {
        span.textContent = text;
        span.style.color = color || getThemeTokens().subtext;
    }

    // Extracts a short, readable detail from a response body for error messages.
    // Prefers a JSON message/error field; falls back to a trimmed text snippet.
    function responseSnippet(responseText) {
        if (!responseText) { return ''; }
        var detail = '';
        try {
            var parsed = JSON.parse(responseText);
            var base = parsed.message || parsed.error || parsed.detail || parsed.title || '';
            // Append the `errors` field (object/array/string) when present.
            var errorsPart = '';
            if (parsed.errors != null) {
                if (typeof parsed.errors === 'string') {
                    errorsPart = parsed.errors;
                } else {
                    try { errorsPart = JSON.stringify(parsed.errors); } catch (e2) { errorsPart = String(parsed.errors); }
                }
            }
            detail = [base, errorsPart].filter(Boolean).join(' | ');
        } catch (e) {
            detail = String(responseText);
        }
        detail = (detail || '').replace(/\s+/g, ' ').trim();
        if (!detail) { return ''; }
        if (detail.length > 220) { detail = detail.slice(0, 220) + '...'; }
        return ' - ' + detail;
    }

    // Enable/disable ALL token controls (selector, checkbox, textarea, button)
    // during a fetch so the UI clearly reflects an in-progress operation.
    function setControlsBusy(busy) {
        var selector = document.getElementById(IDS.selector);
        var check = document.getElementById(IDS.customClaimsCheck);
        var claimsArea = document.getElementById(IDS.customClaimsArea);
        var btn = document.getElementById(IDS.getTokenBtn);
        if (selector) { selector.disabled = busy; }
        if (check) { check.disabled = busy; }
        if (claimsArea) { claimsArea.disabled = busy; }
        if (btn) {
            btn.disabled = busy;
            btn.style.opacity = busy ? '0.7' : '1';
            btn.style.cursor = busy ? 'progress' : 'pointer';
            if (busy) { btn.textContent = 'Fetching...'; }
        }
    }

    // Recompute the Get token button enabled-state from current controls (by ID).
    function refreshGetTokenBtn() {
        var selector = document.getElementById(IDS.selector);
        var check = document.getElementById(IDS.customClaimsCheck);
        var claimsArea = document.getElementById(IDS.customClaimsArea);
        var btn = document.getElementById(IDS.getTokenBtn);
        if (!btn) { return; }
        var hasUser = !!selector && selector.value !== '';
        var claimsOk = true;
        if (check && check.checked && claimsArea) {
            try { JSON.parse(claimsArea.value); } catch (e) { claimsOk = false; }
        }
        var ok = hasUser && claimsOk;
        btn.disabled = !ok;
        btn.style.opacity = ok ? '1' : '0.45';
        btn.style.cursor = ok ? 'pointer' : 'not-allowed';
    }

    // Restore the Get token button and re-enable all controls after a fetch.
    function restoreButton(btn) {
        if (btn) { btn.textContent = 'Get token'; }
        setControlsBusy(false);
        refreshGetTokenBtn();
    }

    function resetSelectorUI() {
        var selector = document.getElementById(IDS.selector);
        var getTokenBtn = document.getElementById(IDS.getTokenBtn);
        var expiresInfo = document.getElementById(IDS.expiresInfo);
        if (selector) { selector.value = ''; }
        if (getTokenBtn) {
            getTokenBtn.textContent = 'Get token'; getTokenBtn.disabled = true;
            getTokenBtn.style.opacity = '0.45'; getTokenBtn.style.cursor = 'not-allowed';
        }
        if (expiresInfo) { expiresInfo.textContent = ''; }
    }

    var injectDebounceTimer = null;
    var topbarDebounceTimer = null;
    var tryItTimers = {}, tryItNextId = 0;

    function scheduleTryItClick(btn) {
        if (!isEnabled()) { return; }
        var opblock = btn.closest('.opblock');
        if (opblock && isTryItOutActive(opblock)) { applyButtonVisibility(opblock); return; }
        if (!btn.getAttribute('data-mdt-id')) { btn.setAttribute('data-mdt-id', String(tryItNextId++)); }
        var id = btn.getAttribute('data-mdt-id');
        clearTimeout(tryItTimers[id]);
        tryItTimers[id] = setTimeout(function () {
            var ob = btn.closest('.opblock');
            if (document.body.contains(btn) && !(ob && isTryItOutActive(ob))) {
                btn.click();
                if (ob) { setTimeout(function () { applyButtonVisibility(ob); }, 50); }
            } else if (ob && isTryItOutActive(ob)) {
                applyButtonVisibility(ob);
            }
            delete tryItTimers[id];
        }, 120);
    }

    // Observe dialog/topbar/inputs/responses
    var observer = new MutationObserver(function (mutations) {
        for (var mi = 0; mi < mutations.length; mi++) {
            var addedNodes = mutations[mi].addedNodes;
            for (var ni = 0; ni < addedNodes.length; ni++) {
                var addedNode = addedNodes[ni];
                if (addedNode.nodeType !== Node.ELEMENT_NODE) { continue; }

                var isDialog = addedNode.classList && addedNode.classList.contains('dialog-ux');
                var hasDialog = addedNode.querySelector && addedNode.querySelector('.dialog-ux');
                if (isDialog || hasDialog) {
                    clearTimeout(injectDebounceTimer);
                    injectDebounceTimer = setTimeout(function () {
                        var stale = document.getElementById(IDS.container);
                        if (stale) { stale.remove(); }
                        injectHelperUI();
                        injectBasicAuthToggle();
                    }, 150);
                    continue;
                }

                var isTopbar = addedNode.classList && addedNode.classList.contains('topbar');
                var hasTopbar = addedNode.querySelector && addedNode.querySelector('.topbar-wrapper');
                if ((isTopbar || hasTopbar) && !document.getElementById(TOPBAR_TOGGLE_ID)) {
                    clearTimeout(topbarDebounceTimer);
                    topbarDebounceTimer = setTimeout(function () { injectTopbarToggle(); }, 200);
                    continue;
                }

                if (addedNode.classList && addedNode.classList.contains('try-out__btn')) {
                    scheduleTryItClick(addedNode);
                    continue;
                }

                if (isEnabled()) {
                    var keyInput = null;
                    if (addedNode.id === 'api_key_value') {
                        keyInput = addedNode;
                    } else if (addedNode.querySelector) {
                        keyInput = addedNode.querySelector('#api_key_value');
                    }
                    if (keyInput) {
                        setTimeout(function () {
                            var live = document.getElementById('api_key_value');
                            if (live) { prefillBearerInput(live); }
                        }, 50);
                    }
                }

                if (addedNode.querySelector) {
                    var innerBtns = addedNode.querySelectorAll('.try-out__btn');
                    for (var bi = 0; bi < innerBtns.length; bi++) { scheduleTryItClick(innerBtns[bi]); }
                }

                if (isEnabled()) {
                    var wrapper = addedNode.closest ? addedNode.closest('.responses-wrapper') : null;
                    if (!wrapper) { wrapper = addedNode.querySelector ? addedNode.querySelector('.responses-wrapper') : null; }
                    if (wrapper) {
                        var isLive = !!(
                            wrapper.querySelector('.live-responses-wrapper') ||
                            wrapper.querySelector('.request-url') ||
                            wrapper.querySelector('.curl-command') ||
                            wrapper.querySelector('.responses-inner .response')
                        );
                        if (isLive) {
                            wrapper.style.removeProperty('display');
                            wrapper.setAttribute('data-mdt-live', '1');
                        }
                    }
                }
            }
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // Logout: match real Logout btn (not Close/btn-done)
    function performFullLogout(reason) {
        log.info('Full logout (%s) - clearing tool state for %s.', reason, authStatus.username || '(none)');
        cancelRenewal();
        logoutAll();
        setAuthStatus(null, null, null);
        resetSelectorUI();
        var tdEl = document.getElementById(IDS.tokenDisplay);
        if (tdEl) { tdEl.value = ''; }
    }

    document.body.addEventListener('click', function (e) {
        var btn = e.target.closest('button');
        if (!btn) { return; }

        var inModal = !!btn.closest('.modal-ux-content');
        var isLogout = inModal &&
                       btn.classList.contains('auth') &&
                       !btn.classList.contains('btn-done') &&
                       btn.textContent.trim().toLowerCase() === 'logout';
        if (isLogout && authStatus.username) {
            setTimeout(function () { performFullLogout('native Logout button'); }, 100);
        }
    }, true);

    // Renew on focus if expired (bg timers throttle)
    document.addEventListener('visibilitychange', function () {
        if (document.visibilityState !== 'visible') { return; }
        if (!renewalState.activeUsername || renewalState.tokenExpiryTime === null) { return; }

        var now = Date.now();
        var msToExpiry = renewalState.tokenExpiryTime - now;

        if (msToExpiry <= CONFIG.renewalPostExpiryBufferSecs * 1000) {
            log.warn(
                'Tab visible; token expired/near-expiry (%ds to expiry). Renewing immediately for %s.',
                Math.round(msToExpiry / 1000), renewalState.activeUsername
            );
            clearTimeout(renewalState.renewalTimer);
            renewalState.renewalTimer = null;
            if (renewalState.useCustomClaims && renewalState.referenceIdentityId) {
                silentRenewTokenWithCustomClaims(renewalState.activeUsername);
            } else {
                silentRenewToken(renewalState.activeUsername);
            }
        }
    });

    // Init
    injectBasicAuthHideStyle();
    injectEndpointStyles();
    injectHelperUI();

    if (document.querySelector('.topbar-wrapper')) { injectTopbarToggle(); }

    // Tampermonkey menu entry (backup config trigger).
    try { GM_registerMenuCommand('Configure...', openConfigModal); } catch (e) { /* ignore */ }

    // First run with no stored config: open the config modal so users add creds.
    if (!hasStoredConfig()) {
        setTimeout(function () { openConfigModal(); }, 800);
    }

    if (isEnabled()) {
        document.body.classList.add(BODY_ACTIVE_CLASS);
        var initPollCount = 0;
        var initPollTimer = setInterval(function () {
            clickAllTryItOutButtons();
            initPollCount++;
            if (initPollCount >= 6) { clearInterval(initPollTimer); }
        }, 500);
    }

    try {
        var diagWin = pageWin();
        diagWin.mdtReduxDiagnostic = getReduxStoreDiagnostic;
        window.mdtReduxDiagnostic = getReduxStoreDiagnostic;
    } catch (e) {
        window.mdtReduxDiagnostic = getReduxStoreDiagnostic;
    }

    log.info('Swagger UI Middleware Dev Tools v3.1.5 initialised. Enabled: %s', isEnabled());

})();