/* Forge chat launcher - a self-contained, dependency-free floating chat bubble that embeds the * Forge widget (/embed) in an iframe. Drop one * * The host site must be in the project's allowed_origins (the /embed page sets a * frame-ancestors CSP) - the launcher cannot bypass that. The publishable key is safe to ship; * the optional session_token must be minted server-side and injected per request. */ (function () { "use strict"; var me = document.currentScript || document.querySelector("script[data-forge-key]"); if (!me) return; var ds = me.dataset; var KEY = ds.forgeKey; var TOKEN = ds.forgeToken || ""; var TITLE = ds.forgeTitle || "Chat"; var COLOR = ds.forgeColor || "#4f46e5"; var ORIGIN = ds.forgeOrigin || new URL(me.src, location.href).origin; if (!KEY) { console.warn("[forge] launcher: missing data-forge-key"); return; } if (window.__forgeLauncher) return; // idempotent window.__forgeLauncher = true; // Pass our (the host page's) origin so the widget can validate the postMessage channel // strictly, without depending on the referrer (which strict Referrer-Policy can strip). var src = ORIGIN + "/embed?key=" + encodeURIComponent(KEY) + "&host=" + encodeURIComponent(location.origin); if (TOKEN) src += "&session_token=" + encodeURIComponent(TOKEN); var REDUCED = (window.matchMedia && matchMedia("(prefers-reduced-motion: reduce)").matches); var css = ".forge-fab{position:fixed;bottom:20px;right:20px;width:56px;height:56px;border-radius:50%;" + "border:0;cursor:pointer;z-index:2147483000;background:" + COLOR + ";color:#fff;" + "box-shadow:0 4px 14px rgba(0,0,0,.25);display:flex;align-items:center;justify-content:center;" + "padding:0;transition:" + (REDUCED ? "none" : "transform .15s ease") + "}" + ".forge-fab:hover{transform:" + (REDUCED ? "none" : "scale(1.05)") + "}" + ".forge-fab:focus-visible{outline:3px solid rgba(0,0,0,.35);outline-offset:2px}" + ".forge-fab svg{width:26px;height:26px;display:block}" + ".forge-panel{position:fixed;bottom:88px;right:20px;width:400px;height:600px;" + "max-height:calc(100vh - 108px);border:0;border-radius:16px;overflow:hidden;z-index:2147483000;" + "background:#fff;box-shadow:0 12px 40px rgba(0,0,0,.28);display:none}" + ".forge-panel.forge-open{display:block}" + ".forge-panel iframe{width:100%;height:100%;border:0;display:block}" + "@media (max-width:480px){.forge-panel{bottom:0;right:0;left:0;top:0;width:100%;height:100%;" + "max-height:100%;border-radius:0}.forge-fab.forge-hidden{display:none}}"; var style = document.createElement("style"); style.textContent = css; document.head.appendChild(style); var openIcon = ''; var closeIcon = ''; var fab = document.createElement("button"); fab.type = "button"; fab.className = "forge-fab"; fab.setAttribute("aria-haspopup", "dialog"); fab.setAttribute("aria-expanded", "false"); fab.setAttribute("aria-label", "Open " + TITLE); fab.innerHTML = openIcon; var panel = document.createElement("div"); panel.className = "forge-panel"; panel.setAttribute("role", "dialog"); panel.setAttribute("aria-label", TITLE); var iframe = document.createElement("iframe"); iframe.title = TITLE; iframe.setAttribute("allow", "clipboard-write"); panel.appendChild(iframe); var open = false, loaded = false; function setOpen(next) { open = next; if (open && !loaded) { iframe.src = src; loaded = true; } // lazy-load on first open panel.classList.toggle("forge-open", open); fab.classList.toggle("forge-hidden", open); fab.setAttribute("aria-expanded", String(open)); fab.setAttribute("aria-label", (open ? "Close " : "Open ") + TITLE); fab.innerHTML = open ? closeIcon : openIcon; if (open) { try { iframe.focus(); } catch (e) {} } else { try { fab.focus(); } catch (e) {} } } fab.addEventListener("click", function () { setOpen(!open); }); document.addEventListener("keydown", function (e) { if (e.key === "Escape" && open) setOpen(false); }); document.body.appendChild(panel); document.body.appendChild(fab); // postMessage: accept ONLY forge:-namespaced messages from OUR iframe at the Forge ORIGIN. window.addEventListener("message", function (e) { if (e.origin !== ORIGIN) return; if (e.source !== iframe.contentWindow) return; var m = e.data; if (!m || typeof m !== "object" || typeof m.type !== "string" || m.type.indexOf("forge:") !== 0) return; if (m.type === "forge:ready") { iframe.contentWindow.postMessage({ type: "forge:host", host: location.origin, title: TITLE, color: COLOR }, ORIGIN); } else if (m.type === "forge:close") { setOpen(false); } else if (m.type === "forge:open") { setOpen(true); } }); })();