commit a4a0abbef2aac426ab5162563020dfcf540d6a22
parent 666b227a62281f0ec872e0b848bbe87d1aa6535b
Author: SageAzakaela <106701693+SageAzakaela@users.noreply.github.com>
Date: Sun, 22 Feb 2026 17:31:07 -0700
started adding godot html5 export embed panel - not done yet though
it doesn't work yet *boowomp*
Diffstat:
9 files changed, 566 insertions(+), 13 deletions(-)
diff --git a/docs/PLUGINS.md b/docs/PLUGINS.md
@@ -14,6 +14,7 @@ Use this quick reference when packaging releases or deciding what to enable by d
| `library` | Shelf/Reader workflow, text + PDF style content management, community borrowing/return flow | Any instance | Best used as persistent knowledge + story archive. |
| `radio` | Community radio stations with uploaded tracks and shared listening | Any instance | Requires upload storage; tune in from panel UI. |
| `dice` | Shared dice roller (`XdY+Z`) broadcast to connected users | Any instance | Lightweight RP/TTRPG utility. |
+| `godot` | Template plugin for hosting a bundled Godot HTML5 export from `godotapp/` | Any instance | Put your export files in `plugins_dev/godot/godotapp/` before build. |
| `directory-server` | Public directory endpoint + moderation queue for listings (`/api/plugins/directory-server/list`) | **Directory host instance only** | Acts as the central directory service. |
| `directory-publisher` | Sends this instance announcement payloads to a directory server | Any instance that wants listing | Configure directory URL + instance URL/name, then publish. |
@@ -43,6 +44,10 @@ This repo includes starter plugins and zip builders:
- Dice: `plugins_dev/dice/`
- Build: `node scripts/build-dice-plugin.js`
- Upload: `dist/plugins/dice.zip`
+- Godot: `plugins_dev/godot/`
+ - Build: `node scripts/build-godot-plugin.js`
+ - Upload: `dist/plugins/godot.zip`
+ - Bundle app: place your Godot HTML5 export in `plugins_dev/godot/godotapp/` (entry file must be `index.html`)
- Directory Server (draft): `plugins_dev/directory-server/`
- Build: `node scripts/build-directory-server-plugin.js`
- Upload: `dist/plugins/directory-server.zip`
diff --git a/package.json b/package.json
@@ -19,6 +19,7 @@
"build:clean-install-zip": "node scripts/build-clean-install-zip.js",
"build:plugin:directory-server": "node scripts/build-directory-server-plugin.js",
"build:plugin:directory-publisher": "node scripts/build-directory-publisher-plugin.js",
+ "build:plugin:godot": "node scripts/build-godot-plugin.js",
"multi:init": "node scripts/multi-instance-init.js",
"multi:update": "node scripts/multi-instance-update.js",
"instances:scan": "node scripts/bzl-instances-update.js",
diff --git a/plugins_dev/godot/client.js b/plugins_dev/godot/client.js
@@ -0,0 +1,414 @@
+(() => {
+ if (!window?.BzlPluginHost?.register) return;
+
+ const BRIDGE_VERSION = "bzl.godot.v1";
+ const STORAGE_SOURCE_KEY = "source_path";
+ const FALLBACK_PLUGIN_ID = "godot";
+
+ function detectPluginIdFromScript() {
+ try {
+ const src = String(document.currentScript?.src || "");
+ if (!src) return FALLBACK_PLUGIN_ID;
+ const url = new URL(src, window.location.origin);
+ const m = url.pathname.match(/^\/plugins\/([a-z0-9][a-z0-9_.-]{0,31})\//i);
+ if (m && m[1]) return String(m[1]).toLowerCase();
+ } catch {
+ // ignore
+ }
+ return FALLBACK_PLUGIN_ID;
+ }
+
+ const PLUGIN_ID = detectPluginIdFromScript();
+
+ function defaultBundledPath(pluginId) {
+ const id = String(pluginId || FALLBACK_PLUGIN_ID).toLowerCase();
+ return `/plugins/${id}/godotapp/index.html`;
+ }
+
+ function esc(value) {
+ return String(value ?? "")
+ .replace(/&/g, "&")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ }
+
+ function ensureStyles() {
+ if (document.getElementById("bzlGodotPanelStyle")) return;
+ const style = document.createElement("style");
+ style.id = "bzlGodotPanelStyle";
+ style.textContent = `
+ .godotWrap { display:flex; flex-direction:column; gap:10px; min-height:0; height:100%; }
+ .godotControls { display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
+ .godotControls input[type="text"] { flex:1 1 340px; min-width:220px; }
+ .godotHint { font-size:12px; color: rgba(246,240,255,0.72); }
+ .godotStatus { font-size:12px; color: rgba(246,240,255,0.85); }
+ .godotStatus[data-kind="bad"] { color: var(--bad, #ff4d8a); }
+ .godotStatus[data-kind="good"] { color: var(--good, #3ddc97); }
+ .godotViewport {
+ position: relative;
+ min-height: 360px;
+ height: 100%;
+ flex: 1 1 auto;
+ border: 1px solid rgba(246,240,255,0.14);
+ border-radius: 14px;
+ background: linear-gradient(180deg, rgba(0,0,0,0.34), rgba(0,0,0,0.22));
+ overflow: hidden;
+ }
+ .godotFrame {
+ width: 100%;
+ height: 100%;
+ border: 0;
+ display: block;
+ }
+ .godotEmpty {
+ position:absolute;
+ inset:0;
+ display:flex;
+ align-items:center;
+ justify-content:center;
+ padding:16px;
+ text-align:center;
+ color: rgba(246,240,255,0.7);
+ font-size:13px;
+ }
+ .godotMeta { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
+ .godotChip {
+ border:1px solid rgba(246,240,255,0.16);
+ border-radius:999px;
+ padding:3px 9px;
+ font-size:11px;
+ color: rgba(246,240,255,0.78);
+ }
+ `;
+ document.head.appendChild(style);
+ }
+
+ function normalizeLocalPath(rawPath) {
+ const raw = String(rawPath || "").trim();
+ if (!raw) return { ok: false, error: "Enter a Godot export path." };
+ if (/^https?:\/\//i.test(raw)) {
+ return { ok: false, error: "Use a same-origin path (for example: /uploads/godot/my-game/index.html)." };
+ }
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/i.test(raw)) {
+ return { ok: false, error: "Only same-origin relative paths are allowed in MVP." };
+ }
+ const normalized = raw.startsWith("/") ? raw : `/${raw.replace(/^\/+/, "")}`;
+ let url;
+ try {
+ url = new URL(normalized, window.location.origin);
+ } catch {
+ return { ok: false, error: "Invalid path." };
+ }
+ if (url.origin !== window.location.origin) {
+ return { ok: false, error: "Only same-origin paths are allowed." };
+ }
+ return { ok: true, path: `${url.pathname}${url.search}${url.hash}` };
+ }
+
+ window.BzlPluginHost.register(PLUGIN_ID, (ctx) => {
+ ensureStyles();
+ const bundledPath = defaultBundledPath(ctx?.id || PLUGIN_ID);
+
+ let mountEl = null;
+ let panelEl = null;
+ let viewportEl = null;
+ let frameEl = null;
+ let sourceInputEl = null;
+ let statusEl = null;
+ let lastEventEl = null;
+ let lastVisibility = null;
+ let visibilityTimer = 0;
+ let visibilityObserver = null;
+ let resizeObserver = null;
+ let onVisibilityChange = null;
+ let onWindowResize = null;
+ let onMessage = null;
+ let loadId = 0;
+ let currentPath = "";
+ let lastBridgeEvent = "";
+
+ function setStatus(kind, message) {
+ if (!statusEl) return;
+ statusEl.dataset.kind = kind || "";
+ statusEl.textContent = String(message || "");
+ }
+
+ function setLastEvent(text) {
+ lastBridgeEvent = String(text || "");
+ if (!lastEventEl) return;
+ lastEventEl.textContent = lastBridgeEvent || "none";
+ }
+
+ function panelIsVisible() {
+ if (!(panelEl instanceof HTMLElement)) return false;
+ if (!panelEl.isConnected) return false;
+ if (panelEl.classList.contains("hidden")) return false;
+ if (document.visibilityState === "hidden") return false;
+ const style = window.getComputedStyle(panelEl);
+ if (style.display === "none" || style.visibility === "hidden") return false;
+ return true;
+ }
+
+ function postBridge(eventName, payload) {
+ if (!(frameEl instanceof HTMLIFrameElement)) return false;
+ if (!frameEl.contentWindow) return false;
+ const message = {
+ type: BRIDGE_VERSION,
+ event: String(eventName || ""),
+ payload: payload && typeof payload === "object" ? payload : {},
+ sentAt: Date.now()
+ };
+ frameEl.contentWindow.postMessage(message, "*");
+ setLastEvent(`host->game ${message.event}`);
+ return true;
+ }
+
+ function postResize() {
+ if (!(viewportEl instanceof HTMLElement)) return;
+ const rect = viewportEl.getBoundingClientRect();
+ postBridge("host:resize", {
+ width: Math.max(0, Math.round(rect.width)),
+ height: Math.max(0, Math.round(rect.height))
+ });
+ }
+
+ function syncLifecycle() {
+ const visible = panelIsVisible();
+ if (visible === lastVisibility) return;
+ lastVisibility = visible;
+ if (visible) {
+ postBridge("host:resume", { reason: "visible" });
+ setStatus("good", frameEl ? "Running." : "Ready.");
+ } else {
+ postBridge("host:pause", { reason: "hidden" });
+ setStatus("", frameEl ? "Paused while panel is hidden." : "Ready.");
+ }
+ }
+
+ function unloadFrame(reason) {
+ loadId += 1;
+ if (frameEl) {
+ try {
+ frameEl.remove();
+ } catch {
+ // ignore
+ }
+ }
+ frameEl = null;
+ if (viewportEl) {
+ viewportEl.innerHTML = `<div class="godotEmpty">No export loaded.</div>`;
+ }
+ if (reason) setStatus("", reason);
+ setLastEvent("none");
+ }
+
+ function loadFrame(path) {
+ const targetPath = String(path || "").trim();
+ if (!targetPath) {
+ setStatus("bad", "Provide a path first.");
+ return;
+ }
+ loadId += 1;
+ const id = loadId;
+ if (viewportEl) viewportEl.innerHTML = "";
+ const frame = document.createElement("iframe");
+ frame.className = "godotFrame";
+ frame.setAttribute("sandbox", "allow-scripts allow-same-origin allow-pointer-lock allow-downloads");
+ frame.setAttribute("allow", "fullscreen; gamepad");
+ frame.setAttribute("referrerpolicy", "no-referrer");
+ frame.src = targetPath;
+ frame.addEventListener("load", () => {
+ if (id !== loadId) return;
+ setStatus("good", `Loaded ${targetPath}`);
+ postBridge("host:ready", {
+ user: String(ctx.getUser?.() || ""),
+ role: String(ctx.getRole?.() || ""),
+ plugin: PLUGIN_ID,
+ bridge: BRIDGE_VERSION
+ });
+ postResize();
+ syncLifecycle();
+ });
+ frame.addEventListener("error", () => {
+ if (id !== loadId) return;
+ setStatus("bad", "Failed to load export.");
+ });
+ frameEl = frame;
+ viewportEl?.appendChild(frame);
+ setStatus("", `Loading ${targetPath} ...`);
+ }
+
+ function buildUi(api) {
+ const savedPath = String(api?.storage?.get(STORAGE_SOURCE_KEY) || "").trim();
+ currentPath = savedPath || bundledPath;
+
+ mountEl.innerHTML = `
+ <div class="godotWrap">
+ <div class="godotControls">
+ <input type="text" data-godot-source="1" maxlength="300" placeholder="${bundledPath}" value="${esc(currentPath)}" />
+ <button type="button" class="primary smallBtn" data-godot-loadbundled="1">Load Bundled App</button>
+ <button type="button" class="primary smallBtn" data-godot-load="1">Load</button>
+ <button type="button" class="ghost smallBtn" data-godot-reload="1">Reload</button>
+ <button type="button" class="ghost smallBtn" data-godot-unload="1">Unload</button>
+ </div>
+ <div class="godotHint">Template mode: put your Godot HTML5 export files in <code>godotapp/</code> inside this plugin zip. Default entry: <code>${bundledPath}</code></div>
+ <div class="godotMeta">
+ <div class="godotStatus" data-godot-status="1">Ready.</div>
+ <div class="godotChip">bridge: ${BRIDGE_VERSION}</div>
+ <div class="godotChip">last event: <span data-godot-last="1">none</span></div>
+ </div>
+ <div class="godotViewport" data-godot-viewport="1">
+ <div class="godotEmpty">No export loaded.</div>
+ </div>
+ </div>
+ `;
+
+ sourceInputEl = mountEl.querySelector("[data-godot-source='1']");
+ statusEl = mountEl.querySelector("[data-godot-status='1']");
+ lastEventEl = mountEl.querySelector("[data-godot-last='1']");
+ viewportEl = mountEl.querySelector("[data-godot-viewport='1']");
+ panelEl = mountEl.closest(".panel");
+
+ const loadNow = () => {
+ const normalized = normalizeLocalPath(sourceInputEl?.value);
+ if (!normalized.ok) {
+ setStatus("bad", normalized.error);
+ return;
+ }
+ currentPath = normalized.path;
+ api?.storage?.set(STORAGE_SOURCE_KEY, currentPath);
+ loadFrame(currentPath);
+ };
+
+ mountEl.querySelector("[data-godot-load='1']")?.addEventListener("click", loadNow);
+ mountEl.querySelector("[data-godot-loadbundled='1']")?.addEventListener("click", () => {
+ currentPath = bundledPath;
+ if (sourceInputEl) sourceInputEl.value = currentPath;
+ api?.storage?.set(STORAGE_SOURCE_KEY, currentPath);
+ loadFrame(currentPath);
+ });
+ mountEl.querySelector("[data-godot-reload='1']")?.addEventListener("click", () => {
+ if (!currentPath) {
+ setStatus("bad", "Load a source first.");
+ return;
+ }
+ loadFrame(currentPath);
+ });
+ mountEl.querySelector("[data-godot-unload='1']")?.addEventListener("click", () => {
+ unloadFrame("Unloaded.");
+ });
+ sourceInputEl?.addEventListener("keydown", (e) => {
+ if (e.key !== "Enter") return;
+ e.preventDefault();
+ loadNow();
+ });
+ }
+
+ function bindLifecycleObservers() {
+ if (!panelEl) return;
+ lastVisibility = null;
+ syncLifecycle();
+
+ visibilityObserver = new MutationObserver(() => syncLifecycle());
+ visibilityObserver.observe(panelEl, { attributes: true, attributeFilter: ["class", "style"] });
+
+ onVisibilityChange = () => syncLifecycle();
+ document.addEventListener("visibilitychange", onVisibilityChange);
+
+ onWindowResize = () => {
+ postResize();
+ syncLifecycle();
+ };
+ window.addEventListener("resize", onWindowResize);
+
+ if (window.ResizeObserver && viewportEl) {
+ resizeObserver = new ResizeObserver(() => postResize());
+ resizeObserver.observe(viewportEl);
+ }
+
+ visibilityTimer = window.setInterval(syncLifecycle, 900);
+ }
+
+ function bindMessageBridge() {
+ onMessage = (evt) => {
+ if (!(frameEl instanceof HTMLIFrameElement)) return;
+ if (evt.source !== frameEl.contentWindow) return;
+ const msg = evt.data;
+ if (!msg || typeof msg !== "object") return;
+ if (String(msg.type || "") !== BRIDGE_VERSION) return;
+ const ev = String(msg.event || "");
+ const payload = msg.payload && typeof msg.payload === "object" ? msg.payload : {};
+ setLastEvent(`game->host ${ev || "unknown"}`);
+
+ if (ev === "ready") {
+ setStatus("good", "Game reported ready.");
+ postResize();
+ return;
+ }
+ if (ev === "error") {
+ const detail = String(payload.message || payload.code || "unknown error");
+ setStatus("bad", `Game error: ${detail}`);
+ return;
+ }
+ if (ev === "state") {
+ const status = String(payload.status || "state");
+ setStatus("", `State: ${status}`);
+ return;
+ }
+ };
+ window.addEventListener("message", onMessage);
+ }
+
+ ctx.ui?.registerPanel?.({
+ id: "godot",
+ title: "Godot",
+ icon: "G",
+ defaultRack: "main",
+ role: "primary",
+ presetHints: {
+ defaultSocial: { place: "docked.bottom" },
+ mapsSession: { place: "docked.bottom" }
+ },
+ render(mount, api) {
+ mountEl = mount;
+ buildUi(api);
+ bindLifecycleObservers();
+ bindMessageBridge();
+
+ loadFrame(currentPath || bundledPath);
+
+ return () => {
+ if (visibilityTimer) window.clearInterval(visibilityTimer);
+ visibilityTimer = 0;
+ try {
+ visibilityObserver?.disconnect();
+ } catch {
+ // ignore
+ }
+ try {
+ resizeObserver?.disconnect();
+ } catch {
+ // ignore
+ }
+ if (onVisibilityChange) document.removeEventListener("visibilitychange", onVisibilityChange);
+ if (onWindowResize) window.removeEventListener("resize", onWindowResize);
+ if (onMessage) window.removeEventListener("message", onMessage);
+ unloadFrame("");
+ mountEl = null;
+ panelEl = null;
+ viewportEl = null;
+ sourceInputEl = null;
+ statusEl = null;
+ lastEventEl = null;
+ visibilityObserver = null;
+ resizeObserver = null;
+ onVisibilityChange = null;
+ onWindowResize = null;
+ onMessage = null;
+ };
+ }
+ });
+ });
+})();
diff --git a/plugins_dev/godot/godotapp/index.html b/plugins_dev/godot/godotapp/index.html
@@ -0,0 +1,58 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>Godot App Template</title>
+ <style>
+ html, body {
+ margin: 0;
+ width: 100%;
+ height: 100%;
+ font-family: system-ui, sans-serif;
+ background: #0b0d12;
+ color: #e8ecf6;
+ }
+ .wrap {
+ min-height: 100%;
+ display: grid;
+ place-items: center;
+ padding: 24px;
+ box-sizing: border-box;
+ text-align: center;
+ }
+ .card {
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ border-radius: 12px;
+ padding: 18px;
+ max-width: 760px;
+ background: rgba(255, 255, 255, 0.04);
+ }
+ code {
+ background: rgba(255, 255, 255, 0.08);
+ padding: 2px 6px;
+ border-radius: 6px;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="wrap">
+ <div class="card">
+ <h1>Godot Template Ready</h1>
+ <p>Replace this file with your Godot HTML5 export entry file.</p>
+ <p>Copy all export assets into this folder: <code>godotapp/</code></p>
+ <p>Expected entry path in Bzl: <code>/plugins/godot/godotapp/index.html</code></p>
+ </div>
+ </div>
+ <script>
+ window.parent?.postMessage?.(
+ {
+ type: "bzl.godot.v1",
+ event: "ready",
+ payload: { status: "template" }
+ },
+ "*"
+ );
+ </script>
+ </body>
+</html>
diff --git a/plugins_dev/godot/plugin.json b/plugins_dev/godot/plugin.json
@@ -0,0 +1,9 @@
+{
+ "id": "godot",
+ "name": "Godot Panel",
+ "version": "0.1.0",
+ "description": "Embeds a Godot HTML5 export in a sandboxed panel with basic host lifecycle messaging.",
+ "entryClient": "client.js",
+ "entryServer": "server.js",
+ "permissions": ["ui"]
+}
diff --git a/plugins_dev/godot/server.js b/plugins_dev/godot/server.js
@@ -0,0 +1,3 @@
+module.exports = function init(api) {
+ api.log("info", "godot:server:init", { plugin: "godot" });
+};
diff --git a/public/app.js b/public/app.js
@@ -10951,16 +10951,33 @@ modBodyEl?.addEventListener("click", (e) => {
renderModPanel();
return;
}
+ const maxZipBytes = 50 * 1024 * 1024;
+ const fileSize = Number(file.size || 0);
+ if (fileSize > maxZipBytes) {
+ pluginAdminStatus = `Plugin zip is too large (${Math.ceil(fileSize / (1024 * 1024))}MB). Max is 50MB.`;
+ renderModPanel();
+ return;
+ }
pluginAdminBusy = true;
- pluginAdminStatus = "Uploading plugin...";
+ pluginAdminStatus = `Uploading plugin (${Math.ceil(fileSize / 1024)} KB)...`;
renderModPanel();
(async () => {
+ const controller = new AbortController();
+ const timeoutMs = 2 * 60 * 1000;
+ const timeout = setTimeout(() => {
+ try {
+ controller.abort(new Error("UPLOAD_TIMEOUT"));
+ } catch {
+ // ignore
+ }
+ }, timeoutMs);
try {
const res = await fetch("/api/plugin-install", {
method: "POST",
headers: { "Content-Type": "application/zip", Authorization: `Bearer ${token}` },
body: file,
credentials: "same-origin",
+ signal: controller.signal,
});
const json = await res.json().catch(() => null);
if (!res.ok || !json || !json.ok) {
@@ -10976,8 +10993,17 @@ modBodyEl?.addEventListener("click", (e) => {
renderModPanel();
} catch (err) {
pluginAdminBusy = false;
- pluginAdminStatus = "Install failed.";
+ const message = String(err?.message || "");
+ if (message.includes("UPLOAD_TIMEOUT")) {
+ pluginAdminStatus = "Upload timed out after 2 minutes. Try a smaller zip or better network.";
+ } else if (String(err?.name || "") === "AbortError") {
+ pluginAdminStatus = "Upload was interrupted.";
+ } else {
+ pluginAdminStatus = `Install failed: ${message || "unknown error"}`;
+ }
renderModPanel();
+ } finally {
+ clearTimeout(timeout);
}
})();
return;
diff --git a/scripts/build-godot-plugin.js b/scripts/build-godot-plugin.js
@@ -0,0 +1,22 @@
+const fs = require("fs");
+const path = require("path");
+const AdmZip = require("adm-zip");
+
+const root = path.resolve(__dirname, "..");
+const pluginDir = path.join(root, "plugins_dev", "godot");
+const distDir = path.join(root, "dist", "plugins");
+const outZip = path.join(distDir, "godot.zip");
+
+function main() {
+ if (!fs.existsSync(pluginDir)) {
+ console.error("Missing plugin folder:", pluginDir);
+ process.exit(1);
+ }
+ fs.mkdirSync(distDir, { recursive: true });
+ const zip = new AdmZip();
+ zip.addLocalFolder(pluginDir, "");
+ zip.writeZip(outZip);
+ console.log("Built:", outZip);
+}
+
+main();
diff --git a/server.js b/server.js
@@ -3353,6 +3353,10 @@ function readRequestBodyToFile(req, filePath, maxBytes) {
out.end(() => finish(null, bytes));
});
+ req.on("aborted", () => finish(new Error("REQUEST_ABORTED")));
+ req.on("close", () => {
+ if (!done && !req.complete) finish(new Error("REQUEST_ABORTED"));
+ });
req.on("error", (e) => finish(e));
out.on("error", (e) => finish(e));
});
@@ -3828,17 +3832,28 @@ function servePluginFile(req, res, pathname) {
? "text/css; charset=utf-8"
: ext === ".js"
? "text/javascript; charset=utf-8"
- : ext === ".json"
- ? "application/json; charset=utf-8"
- : ext === ".png"
- ? "image/png"
- : ext === ".jpg" || ext === ".jpeg"
- ? "image/jpeg"
- : ext === ".gif"
- ? "image/gif"
- : ext === ".webp"
- ? "image/webp"
- : "application/octet-stream";
+ : ext === ".mjs"
+ ? "text/javascript; charset=utf-8"
+ : ext === ".json"
+ ? "application/json; charset=utf-8"
+ : ext === ".wasm"
+ ? "application/wasm"
+ : ext === ".svg"
+ ? "image/svg+xml"
+ : ext === ".png"
+ ? "image/png"
+ : ext === ".jpg" || ext === ".jpeg"
+ ? "image/jpeg"
+ : ext === ".gif"
+ ? "image/gif"
+ : ext === ".webp"
+ ? "image/webp"
+ : "application/octet-stream";
+
+ // Template app exports under /plugins/<id>/godotapp/* must be embeddable in-app.
+ if (rel.startsWith("godotapp/")) {
+ res.setHeader("X-Frame-Options", "SAMEORIGIN");
+ }
res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "no-store" });
fs.createReadStream(filePath).pipe(res);