NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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());
})();