(function () { 'use strict'; var d = window.__ginaData; if (!d || !d.user) return; var env = d.user.environment || {}; var data = d.user.data || {}; var bundle = env.bundle || '?'; var envName = env.env || '?'; var dot = data.error ? '#e74c3c' : '#2ecc71'; // ── Inspector secret redaction (#R7) ────────────────────────────────── // Matches the server-side rules in lib/inspector-redact. Strips secret- // looking fields from anything passed to ginaToolbar.update() before it // touches window.__ginaData or localStorage. Patterns/types/replacement // come from window.__ginaData.gina.inspectorRedact (server-injected). var _rdc = (d.gina && d.gina.inspectorRedact) || {}; var _rdcRepl = (typeof _rdc.replacement === 'string') ? _rdc.replacement : '[redacted]'; var _rdcTypes = {}; if (Array.isArray(_rdc.types)) { for (var _ti = 0; _ti < _rdc.types.length; _ti++) { _rdcTypes[String(_rdc.types[_ti]).toLowerCase()] = true; } } else { _rdcTypes['password'] = true; } // Patterns are anchored (`^(?:...)$`) and tested against tokens, not raw // substrings. So `apikey` covers `apiKey` / `api_key` / `apikey` while // `companyName` no longer false-positives on `pan`. Mirrors the server-side // DEFAULT_PATTERNS in lib/inspector-redact — keep both in sync. var _rdcRe = []; var _rdcSrc = Array.isArray(_rdc.patterns) ? _rdc.patterns : ['password','passwd','pwd','secret','token','apikey','cvv','cvc','ccv','pan','ssn','authorization','credentials','privatekey']; for (var _ri = 0; _ri < _rdcSrc.length; _ri++) { try { _rdcRe.push(new RegExp('^(?:' + _rdcSrc[_ri] + ')$', 'i')); } catch (e) { /* skip */ } } // Anchored both ends — applied against the LAST token, not the raw key. // Keys whose final token is a metadata word (rule/policy/config/etc.) // describe validation, not user input. Mirrors the server-side // NON_SECRET_SUFFIX regex in lib/inspector-redact. var _rdcNonSecret = /^(rule|rules|policy|policies|validator|config|configuration|settings|setting|meta|metadata|format|requirements|strength|constraint|constraints|options|option|schema|definition|definitions|spec|specs)$/i; function _rdcTokenize(k) { if (typeof k !== 'string' || k.length === 0) return []; var spaced = k .replace(/([a-z0-9])([A-Z])/g, '$1 $2') .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2'); var parts = spaced.split(/[\s_\-.\[\]:\/]+/); var out = []; for (var i = 0; i < parts.length; i++) { if (parts[i].length > 0) out.push(parts[i].toLowerCase()); } return out; } function _rdcKeyHit(k) { if (typeof k !== 'string') return false; var tokens = _rdcTokenize(k); if (tokens.length === 0) return false; if (_rdcNonSecret.test(tokens[tokens.length - 1])) return false; var joined = tokens.join(''); for (var i = 0; i < _rdcRe.length; i++) { var re = _rdcRe[i]; if (re.test(joined)) return true; for (var t = 0; t < tokens.length; t++) { if (re.test(tokens[t])) return true; } } return false; } // Build a Set of secret field names from the form DOM (input[type=password] // etc.). Lets us redact by actual input type, not just by field-name regex. function _rdcDomSecrets(formId) { var hits = {}; try { var doc = (window.opener && window.opener.document) || document; var form = formId ? doc.getElementById(formId) : null; var els = form ? form.querySelectorAll('input,select,textarea') : doc.querySelectorAll('input'); for (var i = 0; i < els.length; i++) { var t = (els[i].type || '').toLowerCase(); var n = els[i].name || els[i].id || ''; if (n && _rdcTypes[t]) hits[n] = true; } } catch (e) { /* cross-origin or detached opener */ } return hits; } function _rdcWalk(v, depth, seen, domHits) { if (depth > 50) return v; if (v === null || typeof v !== 'object') return v; if (seen.indexOf(v) !== -1) return '[circular]'; seen.push(v); if (Array.isArray(v)) { var arr = new Array(v.length); for (var i = 0; i < v.length; i++) arr[i] = _rdcWalk(v[i], depth + 1, seen, domHits); return arr; } var out = {}; var ks = Object.keys(v); for (var k = 0; k < ks.length; k++) { var key = ks[k]; var val = v[key]; if (_rdcKeyHit(key) || (domHits && domHits[key])) { // Only primitive leaf values are redacted. Object/array values // under a secret-like key are metadata (validation rules, error // maps, validator specs keyed by form field name like // `account[password]`) — walk into them so nested primitive // secrets still get caught, but the rule structure itself // passes through. if (val === null || typeof val === 'undefined') { out[key] = val; } else if (typeof val === 'object') { out[key] = _rdcWalk(val, depth + 1, seen, domHits); } else { out[key] = _rdcRepl; } } else { out[key] = _rdcWalk(val, depth + 1, seen, domHits); } } return out; } function _rdc_redact(value, formId) { var domHits = formId ? _rdcDomSecrets(formId) : null; return _rdcWalk(value, 0, [], domHits); } // ── ginaToolbar shim ────────────────────────────────────────────────── // gina.min.js is a stale build that creates its own window.ginaToolbar // with a loadData function that accesses removed DOM elements // (#gina-toolbar-json). We use Object.defineProperty to lock our shim // so gina.min.js cannot overwrite it on DOMContentLoaded. var _origData = null; var _shim = { update: function (section, sectionData) { if (!window.__ginaData) return; var u = window.__ginaData.user; var g = window.__ginaData.gina; if (!u || !g) return; // Save original state for restore (once) if (!_origData) { try { _origData = JSON.parse(JSON.stringify({ data: u.data, view: u.view })); } catch (e) {} } if (section === 'data-xhr' || section === 'view-xhr') { if (sectionData) { var _safe = _rdc_redact(sectionData, null); u[section] = _safe; g[section] = _safe; } else { delete u[section]; delete g[section]; } } else if (section === 'el-xhr') { u['el-xhr'] = _rdc_redact(sectionData, null); } else if (section === 'forms') { if (typeof sectionData === 'object' && sectionData !== null && sectionData.id) { if (!u.forms) u.forms = {}; if (!u.forms[sectionData.id]) u.forms[sectionData.id] = {}; var fd = u.forms[sectionData.id]; var _safeForm = _rdc_redact(sectionData, sectionData.id); var fk = Object.keys(_safeForm); for (var i = 0; i < fk.length; i++) { if (fk[i] === 'id') continue; fd[fk[i]] = _safeForm[fk[i]]; } } } // Sync localStorage for the Inspector's fallback channel try { localStorage.setItem('__ginaData', JSON.stringify(window.__ginaData)); } catch (e) {} }, restore: function () { if (!window.__ginaData) return; var u = window.__ginaData.user; if (!u) return; delete u['data-xhr']; delete u['view-xhr']; delete u['el-xhr']; if (_origData) { try { u.data = JSON.parse(JSON.stringify(_origData.data)); u.view = JSON.parse(JSON.stringify(_origData.view)); } catch (e) {} } try { localStorage.setItem('__ginaData', JSON.stringify(window.__ginaData)); } catch (e) {} }, hasParsedUrls: false }; // Lock with defineProperty — gina.min.js DOMContentLoaded init will // try window.ginaToolbar = new GinaToolbar() which triggers our setter // that silently absorbs the assignment while preserving our update/restore. try { Object.defineProperty(window, 'ginaToolbar', { get: function () { return _shim; }, set: function (v) { // Absorb gina.min.js's assignment without overwriting shim if (v && typeof v === 'object') { var keys = Object.keys(v); for (var i = 0; i < keys.length; i++) { var k = keys[i]; if (k !== 'update' && k !== 'restore') { _shim[k] = v[k]; } } } }, configurable: true, enumerable: true }); } catch (e) { window.ginaToolbar = _shim; } // Publish data for Inspector via localStorage (works regardless of opener) try { localStorage.setItem('__ginaData', JSON.stringify(d)); } catch (e) {} var webroot = env.webroot || '/'; if (webroot.charAt(webroot.length - 1) !== '/') webroot += '/'; var el = document.createElement('div'); el.id = '__gina-statusbar'; var sr = el.attachShadow({ mode: 'open' }); sr.innerHTML = '' + '
' + '' + '' + bundle + '@' + envName + '' + 'Inspector ↗' + '
'; // Standalone Inspector URL — set in bundle settings.json as // `inspector.url` (e.g. "http://localhost:4200/inspector/"). When present, // the statusbar link opens the standalone SPA in a new tab with // ?target=. When absent, falls back to the embedded // same-origin popup at {webroot}/_gina/inspector/ (legacy behaviour). var _standaloneUrl = (window.__ginaData && window.__ginaData.gina && window.__ginaData.gina.inspectorUrl) ? window.__ginaData.gina.inspectorUrl : null; var _embeddedUrl = webroot + '_gina/inspector/'; function inject() { if (document.body) document.body.appendChild(el); var link = sr.getElementById('insp'); if (link) link.addEventListener('click', function (ev) { ev.preventDefault(); if (_standaloneUrl) { var _base = _standaloneUrl.replace(/\/+$/, '') + '/'; var _target = location.origin + webroot.replace(/\/$/, ''); window.open(_base + '?target=' + encodeURIComponent(_target), '_blank'); return; } var w = Math.round(screen.availWidth / 3); var h = screen.availHeight; var left = screen.availWidth - w; var top = 0; try { var geo = JSON.parse(localStorage.getItem('__gina_inspector_geometry')); if (geo && geo.w > 100 && geo.h > 100) { w = geo.w; h = geo.h; left = geo.x; top = geo.y; } } catch (e) {} window.open( _embeddedUrl, 'gina-inspector', 'width=' + w + ',height=' + h + ',left=' + left + ',top=' + top + ',menubar=no,toolbar=no,location=no,status=no,resizable=yes,scrollbars=yes' ); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', inject); } else { inject(); } }());