{
  "version": 3,
  "sources": ["../src/modules/security.js"],
  "sourcesContent": ["// zelvior-runtime/security -- client-side hardening helpers for pages and\n// web apps that embed this runtime. ESM source of truth; bundled by\n// build.mjs. Zero dependency on core zelvior.js.\n//\n// IMPORTANT SCOPE NOTE: nothing here is a substitute for server-side\n// input validation, output encoding at the templating layer, a real\n// Content-Security-Policy header, or an actual security review. This\n// module covers a narrow, real slice of client-side risk that's\n// meaningful to reduce even when you don't control the server: XSS\n// vectors at the specific point where untrusted strings get written into\n// the DOM or into href/src, clickjacking via framing, and casual\n// prototype-pollution-shaped bugs. Treat it as defense in depth, not a\n// perimeter.\n\n// --- HTML sanitization ------------------------------------------------\n// Allowlist-based, not blocklist-based (blocklists are how XSS filters\n// keep getting bypassed). Strips everything except a small set of plain\n// formatting tags and removes all attributes except a safe subset on\n// those tags. This is deliberately conservative: it is fine for\n// sanitizing untrusted text destined for display (comments, chat\n// messages, user bios), not intended for scenarios needing rich HTML\n// (that needs a real library like DOMPurify with a wider, audited\n// allowlist and more edge-case coverage than this module aims for).\nvar ALLOWED_TAGS = { B: 1, I: 1, EM: 1, STRONG: 1, U: 1, BR: 1, P: 1, SPAN: 1 };\nvar ALLOWED_ATTRS = { SPAN: { class: 1 } };\n\nfunction sanitizeNode(node, doc) {\n  if (node.nodeType === 3) return node.cloneNode(); // text node, always safe\n  if (node.nodeType !== 1) return null; // drop comments, processing instructions, etc.\n  var tag = node.tagName;\n  if (!ALLOWED_TAGS[tag]) {\n    // Not an allowed element -- keep its text content (so \"some <script>\n    // text</script>\" degrades to \"some  text\" rather than vanishing\n    // silently or, worse, being interpreted as markup), but drop the\n    // element and all its attributes/event handlers.\n    var frag = doc.createDocumentFragment();\n    for (var i = 0; i < node.childNodes.length; i++) {\n      var child = sanitizeNode(node.childNodes[i], doc);\n      if (child) frag.appendChild(child);\n    }\n    return frag;\n  }\n  var clean = doc.createElement(tag);\n  var allowedAttrs = ALLOWED_ATTRS[tag];\n  if (allowedAttrs) {\n    for (var j = 0; j < node.attributes.length; j++) {\n      var attr = node.attributes[j];\n      if (allowedAttrs[attr.name]) clean.setAttribute(attr.name, attr.value);\n    }\n  }\n  for (var k = 0; k < node.childNodes.length; k++) {\n    var c = sanitizeNode(node.childNodes[k], doc);\n    if (c) clean.appendChild(c);\n  }\n  return clean;\n}\n\n/**\n * Parse `html` in a detached document (never the live page document, so\n * nothing in it can execute) and return a sanitized HTML string safe to\n * assign to `.innerHTML` on the real page. Everything outside\n * `ALLOWED_TAGS`/`ALLOWED_ATTRS` is stripped; text content of removed\n * elements is preserved.\n */\nexport function sanitizeHTML(html) {\n  if (typeof html !== 'string') return '';\n  if (typeof DOMParser === 'undefined') {\n    // No DOMParser (very old engine, or a non-browser environment) --\n    // fail closed: strip all tags rather than risk passing something\n    // through unsanitized.\n    return String(html).replace(/<[^>]*>/g, '');\n  }\n  var parser = new DOMParser();\n  var parsed = parser.parseFromString('<div>' + html + '</div>', 'text/html');\n  var root = parsed.body && parsed.body.firstChild;\n  if (!root) return '';\n  var out = document.implementation.createHTMLDocument('');\n  var clean = sanitizeNode(root, out);\n  var holder = out.createElement('div');\n  if (clean) holder.appendChild(clean);\n  return holder.innerHTML;\n}\n\n// --- URL safety ---------------------------------------------------------\n// The classic `href=\"javascript:...\"` / `src=\"data:text/html,...\"` XSS\n// vector -- anywhere a URL comes from user input (a profile link, a\n// redirect target, a stored \"website\" field) and gets assigned to\n// href/src/action, this is the check that belongs in front of it.\nvar UNSAFE_SCHEMES = ['javascript:', 'vbscript:', 'data:text/html', 'data:application'];\n\n/**\n * Returns true if `url` is safe to assign to href/src/action -- i.e. not\n * a script-executing pseudo-scheme. Relative URLs and ordinary\n * http(s)/mailto/tel schemes pass. Does not validate that the URL points\n * anywhere sensible, only that it can't execute script directly.\n */\nexport function isSafeURL(url) {\n  if (typeof url !== 'string') return false;\n  // Deliberate: browsers ignore embedded tabs/newlines/control chars when\n  // parsing a URL scheme, so \"java\\tscript:alert(1)\" is a real,\n  // browser-parseable bypass of a naive scheme check. Stripping them\n  // before comparison is the fix, not an accident.\n  // eslint-disable-next-line no-control-regex\n  var trimmed = url.replace(/[\\s\\u0000-\\u001f]+/g, '').toLowerCase();\n  for (var i = 0; i < UNSAFE_SCHEMES.length; i++) {\n    if (trimmed.indexOf(UNSAFE_SCHEMES[i]) === 0) return false;\n  }\n  return true;\n}\n\n// --- Clickjacking ---------------------------------------------------------\n\n/**\n * Returns true if the current page is being rendered inside another\n * origin's frame (the precondition for a clickjacking attack). Reading\n * `window.top.location` across origins throws by design (same-origin\n * policy) -- that throw is itself the signal, not an error to route\n * around.\n */\nexport function isFramed() {\n  try {\n    return window.top !== window.self;\n  } catch (e) {\n    return true; // cross-origin frame access threw -- definitely framed\n  }\n}\n\n/**\n * If the page is framed by a *different* origin, replace the framed\n * page's content with a top-level navigation to itself, breaking out of\n * the frame. Same-origin framing (your own site framing itself, e.g. in\n * a preview pane) is left alone. This is a real, if blunt, clickjacking\n * mitigation for pages that can't set an `X-Frame-Options` /\n * `frame-ancestors` CSP header (e.g. static hosting with no control over\n * response headers) -- setting that header server-side is the correct\n * primary defense; this is a client-side fallback, not a replacement.\n */\nexport function preventClickjacking(opts) {\n  opts = opts || {};\n  if (!isFramed()) return false;\n  if (opts.onDetected) {\n    try {\n      opts.onDetected();\n    } catch (e) {}\n  }\n  if (opts.breakout === false) return true;\n  try {\n    if (window.top) window.top.location = window.self.location.href;\n  } catch (e) {\n    // Cross-origin write also throws in some engines/policies -- last\n    // resort: blank the page so at least the clickjack target is inert.\n    try {\n      document.documentElement.style.display = 'none';\n    } catch (e2) {}\n  }\n  return true;\n}\n\n// --- Prototype pollution guard ------------------------------------------\n\n/**\n * Freezes Object.prototype, Array.prototype, and Function.prototype.\n * Blunts the common `JSON.parse` + `for...in`/`Object.assign`-based\n * prototype pollution gadget chains (e.g. an attacker-controlled\n * `{\"__proto__\": {\"isAdmin\": true}}` merged into a config object) by\n * making the prototypes themselves immutable, without touching your own\n * objects at all.\n *\n * REAL TRADE-OFF, not hidden: any code on the page (yours or a\n * third-party script) that legitimately extends a built-in prototype\n * (a polyfill, an older utility library patching `Array.prototype`) will\n * silently fail after this runs, since `Object.freeze` makes property\n * assignment a no-op in non-strict code and a throw in strict code. Call\n * this after your own polyfills/libraries have loaded, not before, and\n * test your specific dependency set before relying on it in production.\n */\nexport function freezePrototypes() {\n  var targets = [Object.prototype, Array.prototype, Function.prototype, String.prototype];\n  for (var i = 0; i < targets.length; i++) {\n    try {\n      Object.freeze(targets[i]);\n    } catch (e) {}\n  }\n}\n\n// --- Lightweight CSRF token helper ---------------------------------------\n// For same-origin form submissions where you don't have a server-side\n// session framework generating tokens for you (a static site posting to\n// a serverless function, for example). Uses crypto.getRandomValues, which\n// is a real CSPRNG, not Math.random. The server side still has to\n// actually check the token matches what it issued -- this only generates\n// and locally verifies one.\n\nfunction randomToken(bytes) {\n  var arr = new Uint8Array(bytes || 32);\n  if (typeof crypto !== 'undefined' && crypto.getRandomValues) {\n    crypto.getRandomValues(arr);\n  } else {\n    // No Web Crypto (very old engine) -- fail loudly rather than\n    // silently degrading to a non-cryptographic RNG for a security token.\n    throw new Error('zelvior-runtime/security: crypto.getRandomValues unavailable, cannot generate a secure token');\n  }\n  var hex = '';\n  for (var i = 0; i < arr.length; i++) hex += (arr[i] < 16 ? '0' : '') + arr[i].toString(16);\n  return hex;\n}\n\n/** Generate a fresh CSRF token and store it (sessionStorage, tab-scoped) under `key`. Returns the token. */\nexport function generateCSRFToken(key) {\n  var token = randomToken(32);\n  try {\n    sessionStorage.setItem('zelvior-csrf:' + (key || 'default'), token);\n  } catch (e) {}\n  return token;\n}\n\n/** Verify `token` matches the one generated for `key` in this tab's session. */\nexport function verifyCSRFToken(token, key) {\n  try {\n    var stored = sessionStorage.getItem('zelvior-csrf:' + (key || 'default'));\n    return !!stored && !!token && stored === token;\n  } catch (e) {\n    return false;\n  }\n}\n"],
  "mappings": ";4ZAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,sBAAAE,EAAA,sBAAAC,EAAA,aAAAC,EAAA,cAAAC,EAAA,wBAAAC,EAAA,iBAAAC,EAAA,oBAAAC,IAAA,eAAAC,EAAAT,GAuBA,IAAIU,EAAe,CAAE,EAAG,EAAG,EAAG,EAAG,GAAI,EAAG,OAAQ,EAAG,EAAG,EAAG,GAAI,EAAG,EAAG,EAAG,KAAM,CAAE,EAC1EC,EAAgB,CAAE,KAAM,CAAE,MAAO,CAAE,CAAE,EAEzC,SAASC,EAAaC,EAAMC,EAAK,CAC/B,GAAID,EAAK,WAAa,EAAG,OAAOA,EAAK,UAAU,EAC/C,GAAIA,EAAK,WAAa,EAAG,OAAO,KAChC,IAAIE,EAAMF,EAAK,QACf,GAAI,CAACH,EAAaK,CAAG,EAAG,CAMtB,QADIC,EAAOF,EAAI,uBAAuB,EAC7BG,EAAI,EAAGA,EAAIJ,EAAK,WAAW,OAAQI,IAAK,CAC/C,IAAIC,EAAQN,EAAaC,EAAK,WAAWI,CAAC,EAAGH,CAAG,EAC5CI,GAAOF,EAAK,YAAYE,CAAK,CACnC,CACA,OAAOF,CACT,CACA,IAAIG,EAAQL,EAAI,cAAcC,CAAG,EAC7BK,EAAeT,EAAcI,CAAG,EACpC,GAAIK,EACF,QAASC,EAAI,EAAGA,EAAIR,EAAK,WAAW,OAAQQ,IAAK,CAC/C,IAAIC,EAAOT,EAAK,WAAWQ,CAAC,EACxBD,EAAaE,EAAK,IAAI,GAAGH,EAAM,aAAaG,EAAK,KAAMA,EAAK,KAAK,CACvE,CAEF,QAASC,EAAI,EAAGA,EAAIV,EAAK,WAAW,OAAQU,IAAK,CAC/C,IAAIC,EAAIZ,EAAaC,EAAK,WAAWU,CAAC,EAAGT,CAAG,EACxCU,GAAGL,EAAM,YAAYK,CAAC,CAC5B,CACA,OAAOL,CACT,CASO,SAASZ,EAAakB,EAAM,CACjC,GAAI,OAAOA,GAAS,SAAU,MAAO,GACrC,GAAI,OAAO,WAAc,YAIvB,OAAO,OAAOA,CAAI,EAAE,QAAQ,WAAY,EAAE,EAE5C,IAAIC,EAAS,IAAI,UACbC,EAASD,EAAO,gBAAgB,QAAUD,EAAO,SAAU,WAAW,EACtEG,EAAOD,EAAO,MAAQA,EAAO,KAAK,WACtC,GAAI,CAACC,EAAM,MAAO,GAClB,IAAIC,EAAM,SAAS,eAAe,mBAAmB,EAAE,EACnDV,EAAQP,EAAagB,EAAMC,CAAG,EAC9BC,EAASD,EAAI,cAAc,KAAK,EACpC,OAAIV,GAAOW,EAAO,YAAYX,CAAK,EAC5BW,EAAO,SAChB,CAOA,IAAIC,EAAiB,CAAC,cAAe,YAAa,iBAAkB,kBAAkB,EAQ/E,SAAS1B,EAAU2B,EAAK,CAC7B,GAAI,OAAOA,GAAQ,SAAU,MAAO,GAOpC,QADIC,EAAUD,EAAI,QAAQ,sBAAuB,EAAE,EAAE,YAAY,EACxDf,EAAI,EAAGA,EAAIc,EAAe,OAAQd,IACzC,GAAIgB,EAAQ,QAAQF,EAAed,CAAC,CAAC,IAAM,EAAG,MAAO,GAEvD,MAAO,EACT,CAWO,SAASb,GAAW,CACzB,GAAI,CACF,OAAO,OAAO,MAAQ,OAAO,IAC/B,OAAS,EAAG,CACV,MAAO,EACT,CACF,CAYO,SAASE,EAAoB4B,EAAM,CAExC,GADAA,EAAOA,GAAQ,CAAC,EACZ,CAAC9B,EAAS,EAAG,MAAO,GACxB,GAAI8B,EAAK,WACP,GAAI,CACFA,EAAK,WAAW,CAClB,OAASC,EAAG,CAAC,CAEf,GAAID,EAAK,WAAa,GAAO,MAAO,GACpC,GAAI,CACE,OAAO,MAAK,OAAO,IAAI,SAAW,OAAO,KAAK,SAAS,KAC7D,OAASC,EAAG,CAGV,GAAI,CACF,SAAS,gBAAgB,MAAM,QAAU,MAC3C,OAASC,EAAI,CAAC,CAChB,CACA,MAAO,EACT,CAoBO,SAASlC,GAAmB,CAEjC,QADImC,EAAU,CAAC,OAAO,UAAW,MAAM,UAAW,SAAS,UAAW,OAAO,SAAS,EAC7EpB,EAAI,EAAGA,EAAIoB,EAAQ,OAAQpB,IAClC,GAAI,CACF,OAAO,OAAOoB,EAAQpB,CAAC,CAAC,CAC1B,OAASkB,EAAG,CAAC,CAEjB,CAUA,SAASG,EAAYC,EAAO,CAC1B,IAAIC,EAAM,IAAI,WAAWD,GAAS,EAAE,EACpC,GAAI,OAAO,QAAW,aAAe,OAAO,gBAC1C,OAAO,gBAAgBC,CAAG,MAI1B,OAAM,IAAI,MAAM,8FAA8F,EAGhH,QADIC,EAAM,GACDxB,EAAI,EAAGA,EAAIuB,EAAI,OAAQvB,IAAKwB,IAAQD,EAAIvB,CAAC,EAAI,GAAK,IAAM,IAAMuB,EAAIvB,CAAC,EAAE,SAAS,EAAE,EACzF,OAAOwB,CACT,CAGO,SAAStC,EAAkBuC,EAAK,CACrC,IAAIC,EAAQL,EAAY,EAAE,EAC1B,GAAI,CACF,eAAe,QAAQ,iBAAmBI,GAAO,WAAYC,CAAK,CACpE,OAASR,EAAG,CAAC,CACb,OAAOQ,CACT,CAGO,SAASnC,EAAgBmC,EAAOD,EAAK,CAC1C,GAAI,CACF,IAAIE,EAAS,eAAe,QAAQ,iBAAmBF,GAAO,UAAU,EACxE,MAAO,CAAC,CAACE,GAAU,CAAC,CAACD,GAASC,IAAWD,CAC3C,OAASR,EAAG,CACV,MAAO,EACT,CACF",
  "names": ["security_exports", "__export", "freezePrototypes", "generateCSRFToken", "isFramed", "isSafeURL", "preventClickjacking", "sanitizeHTML", "verifyCSRFToken", "__toCommonJS", "ALLOWED_TAGS", "ALLOWED_ATTRS", "sanitizeNode", "node", "doc", "tag", "frag", "i", "child", "clean", "allowedAttrs", "j", "attr", "k", "c", "html", "parser", "parsed", "root", "out", "holder", "UNSAFE_SCHEMES", "url", "trimmed", "opts", "e", "e2", "targets", "randomToken", "bytes", "arr", "hex", "key", "token", "stored"]
}
