From 3c276de0a001895456fdfa5330a551e9885ea69e Mon Sep 17 00:00:00 2001 From: CR0CKER <6056387+CR0CKER@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:18:29 +0200 Subject: [PATCH] feat(electron): upgrade to Electron 43 and restore plugin loading and HTTP 41.x reached end of life on 2026-08-25; 43.x is supported until 2027-01-05. Restores the two things the newer Chromium breaks, and changes nothing about what a plugin is allowed to reach. 43 rather than 44 because 44.0.0 was two days old when this was prepared and 43.4.1 is a matured patch line. better-sqlite3 12.10.0 no longer compiles against Electron 43's V8 -- its SetNativeDataProperty call is ambiguous against the three overloads now declared in v8-template.h. 13.0.3 fixes that and ships N-API prebuilds, so it no longer needs an electron-rebuild pass per Electron major. node-abi bumped to 4.33.0, and the root yarn.lock regenerated so it agrees with the bumped devDependency (net -255 lines: @electron/get 5 drops transitives that version 2 pulled in). Note for packagers: from Electron 42 the npm package no longer fetches its binary in a postinstall script -- it downloads on first use, so offline or sandboxed builds need to prime the cache explicitly. PLUGIN LOADING. Electron 40 tightened opaque-origin rules. Loading the renderer from file:// makes the parent origin opaque, which breaks the postMessage handshake plugin iframes rely on, so no plugin loads at all. The renderer is served over the privileged lsp:// scheme instead. That means lsp:// can no longer serve static app files from one flat root, so the routes are namespaced into /plugins/ (dot-root installs) and /external// (plugins installed elsewhere), with everything else still resolving against __dirname. The bare legacy form lsp://logseq.io//... is still accepted: themes register under it and their URLs are persisted in preferences.json and localStorage, so rejecting it silently breaks every installed theme on upgrade. Ported from logseq/logseq#12741. PLUGIN HTTP. Serving the renderer over lsp:// gives plugin frames a real tuple origin, so Chromium began enforcing CORS on requests they make; under file:// none was applied and the same calls worked. Most endpoints a plugin talks to send no Access-Control-Allow-Origin at all -- ordinary web pages, local APIs like Zotero or Syncthing -- or omit the plugin's custom client header from Access-Control-Allow-Headers, so the request fails before it is sent. Observed: Readwise sync, url-title-tagger, and Syncthing/Zotero-backed plugins (logseq/logseq#12736). Two halves. The SDK installs a fetch bridge in plugin frames that routes http(s) fetch through the main process, sidestepping the browser's CORS layer rather than relaxing it; :httpRequest gains includeResponse so a real Response can be rebuilt with its status, headers and url. Anything the bridge cannot carry faithfully keeps the native implementation rather than being silently altered: non-http(s) URLs, credentials:'include' (a main-process request has no cookies), and a non-string body such as FormData/Blob/ArrayBuffer, which the handler would JSON-serialise into "{}". AbortSignal is wired to the host's own abort path, and an abort is never retried natively. A string body is also no longer double-encoded by JSON.stringify, which broke the common fetch(url, {body: JSON.stringify(x)}) shape. The other half is a permissive CORS response for the requests the bridge cannot see -- XHR, and anything issued before the bridge installs. It publishes a WILDCARD origin, deliberately, not the echoed request origin: the browser rejects "*" for credentialed requests, so cookie-bearing cross-origin reads stay blocked. Echoing the origin would let a plugin read another site's authenticated responses -- a real escalation. Access-Control-Expose-Headers is published too, since under file:// a plugin could read every response header and the CORS-safelisted six are not enough for the Link/ETag/X-RateLimit headers real APIs answer with. Otherwise this grants no new capability: any plugin can already issue unrestricted HTTP via logseq.Request -> exper_request -> node-fetch in the main process, with no CORS at all. Unlike webSecurity:false, same-origin DOM access and mixed-content blocking are untouched. The relaxation is scoped and fails closed. The main renderer is ALSO lsp://logseq.com (electron.html), so matching on scheme alone would relax the app's own requests -- plugin paths are matched specifically, and only xhr/other resource types. Identity is resolved in onBeforeRequest, where the frame is still alive, and recorded against the webRequest id; Electron documents details.frame as nullable once a frame has navigated or been destroyed, which is exactly its state at onHeadersReceived time. Anything not positively attributed is left untouched. The onBeforeRequest listener never cancels a request -- it only records attribution -- so plugin network reach is exactly what it was before. It is filtered to http(s): an unfiltered listener also sees the renderer's own lsp:// asset loads and stalls them, and the app never finishes starting. logseq.Request also now initiates over the postMessage caller rather than Experiments.invokeExperMethod, which does a synchronous window.top.logseq read -- that throws for a plugin iframe on a different origin than the host, i.e. every :effect false plugin, so requests never started there. Ported from logseq/logseq#12753. PATH CONTAINMENT. The new /external/ route takes the directory to serve from out of the URL, so containment alone cannot decide whether that directory may be read at all: resolveWithin faithfully confirms that id_ed25519 is inside ~/.ssh. A URL may therefore only name a root the app already knows about -- preferences.json's "externals", plus /tmp, where the SDK generates the entry document for a plugin whose package main is a .js file. That second root is not optional: such a plugin addresses its generated entry with its own directory as the root, which is never an "external", so leaving it out refuses the entry and the plugin does not load at all. It is app-controlled rather than named by the URL, so trusting it adds no reach. The list is seeded at startup and re-read when the handler meets an unrecognised root (throttled to once a second, so a stream of bogus roots cannot hammer the disk). Re-reading is not enough for a FIRST install, though, and load order is why: PluginLocal#load() mounts the plugin's frame -- which fetches its entry document and then the plugin's own scripts over lsp:// -- BEFORE LSPluginCore registers it and writes preferences.json. At that moment the file is not yet a record of anything, so nothing on disk can authorise the plugin being installed. The authority there is the directory the user picked, so the load-unpacked-plugin flow gets its own IPC (:openPluginDirDialog) that opens the same dialog and allows the chosen directory for the rest of the session. It is deliberately not a renderer-callable "trust this path" API: the only path it ever accepts is one the main process watched the user choose in a file dialog. From the next launch preferences.json covers it. Every branch of the handler additionally resolves through resolveWithin, which also closes the pre-existing traversal in the plugin branch. RELEASE-BUILD TRAPS, all invisible to a dev compile. utils.js is Closure-compiled under :advanced in a release build, which renames any path.* PROPERTY it has no extern for: path.sep and path.relative compile to mangled keys that are undefined on Node's real path object, so a containment check written with path.sep returns null for EVERYTHING -- including the app's own electron.html, giving a blank window. Containment is done with plain string ops instead. externs.js gains onBeforeRequest/resourceType/frame for the same reason: without them the call fails with "$onBeforeRequest$ is not a function", which aborts app setup before the 'main' IPC channel registers and leaves the renderer dead on arrival. Registration is wrapped so a failure degrades to "no attribution" rather than taking the app down. The same renaming bites a property read on PARSED DATA, where there is nothing to write an extern against: preferences.json's `externals`, read as prefs.externals, compiled to a mangled key that is undefined on the real object -- so only the tmp root was seeded and every plugin installed outside the dot-root was refused. It is read as prefs['externals']; a string literal cannot be renamed. Caught by running the packaged app, not by the suite, so there is now a guard over the compiled output for it too. Note the trap inside the trap: a pseudo-named build spells the BROKEN form `.$externals$`, which still contains the word, so grepping the bundle for "externals" proves nothing -- the guard asserts the absence of the mangled form. Adds test/electron-js (46 cases, bare `node --test`, no Electron or build step) covering containment, the external-root allowlist and its re-seed path, and which requests are relaxed plus the header rewrite itself. It lives outside the deps.edn :paths so it stays off the shadow-cljs compile surface, is chained into `npm test`, and runs as its own step in the build workflow, whose test job invokes yarn cljs:test directly rather than yarn test. Two of those cases are guards over the COMPILED output, since neither trap above is observable any other way. They read static/electron.js when it exists and was built with pseudo-names (`--debug`); a plain release renames the symbols away entirely, leaving nothing to match on, so that case is skipped explicitly. When the build IS observable, a missing marker FAILS rather than skipping -- a guard whose marker has gone stale silently protects nothing. The marker anchors on resolveWithin's DEFINITION rather than its export line, because Closure emits the export as a bare alias once the function has a second caller inside the module. Verified against a real advanced build (`clojure -M:cljs release electron --debug`): resolveWithin compiles to plain string comparisons with path.resolve/path.join intact and no path.sep, and onBeforeRequest / onHeadersReceived / responseHeaders / resourceType all survive unrenamed. Fixes #32. Co-Authored-By: Claude Opus 5 --- .github/workflows/build.yml | 3 + externs.js | 7 + libs/src/LSPlugin.core.ts | 78 ++++- libs/src/LSPlugin.user.ts | 148 ++++++++ libs/src/helpers.ts | 5 +- libs/src/modules/LSPlugin.Request.ts | 9 +- package.json | 5 +- resources/js/lsplugin.core.js | 2 +- resources/js/lsplugin.user.js | 2 +- resources/package.json | 12 +- src/electron/electron/core.cljs | 104 +++++- src/electron/electron/handler.cljs | 57 ++- src/electron/electron/utils.js | 362 +++++++++++++++++++ src/electron/electron/window.cljs | 7 +- src/main/frontend/handler/plugin.cljs | 5 +- static/yarn.lock | 220 +++--------- test/electron-js/plugin-cors.test.mjs | 480 ++++++++++++++++++++++++++ yarn.lock | 331 ++---------------- 18 files changed, 1327 insertions(+), 510 deletions(-) create mode 100644 test/electron-js/plugin-cors.test.mjs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bd9c56b5bf..dbc2673ecc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -82,6 +82,9 @@ jobs: yarn cljs:test node static/tests.js + - name: Run Electron main-process tests + run: yarn test:electron-js + lint: runs-on: ubuntu-22.04 diff --git a/externs.js b/externs.js index 23bc4f1d4e..16c31605fe 100644 --- a/externs.js +++ b/externs.js @@ -122,6 +122,13 @@ dummy.commit = function() {}; dummy.raw = function() {}; dummy.onHeadersReceived = function() {}; dummy.responseHeaders = function() {}; +// Electron webRequest names used by the plugin CORS policy in +// src/electron/electron/utils.js. Without these, advanced-mode property renaming +// mangles them and the call fails at runtime with "$onBeforeRequest$ is not a +// function" -- which aborts app setup before the 'main' IPC handler registers. +dummy.onBeforeRequest = function() {}; +dummy.resourceType = function() {}; +dummy.frame = function() {}; dummy.velocityDecay = function() {}; dummy.velocityDecay = function() {}; dummy.updatePosition = function() {}; diff --git a/libs/src/LSPlugin.core.ts b/libs/src/LSPlugin.core.ts index 92f57ba6eb..6bdadad1c1 100644 --- a/libs/src/LSPlugin.core.ts +++ b/libs/src/LSPlugin.core.ts @@ -11,6 +11,9 @@ import { getSDKPathRoot, PROTOCOL_FILE, URL_LSP, + URL_LSP_EXTERNAL, + URL_LSP_HOST, + URL_LSP_HOST_EXTERNAL, safetyPathJoin, path, safetyPathNormalize, @@ -371,16 +374,43 @@ function initApiProxyHandlers(pluginLocal: PluginLocal) { }) } -function convertToLSPResource(fullUrl: string, dotPluginRoot: string) { - if (dotPluginRoot && fullUrl.startsWith(PROTOCOL_FILE + dotPluginRoot)) { +function convertToLSPResource( + fullUrl: string, + localRoot: string, + lspRoot = URL_LSP +) { + if (localRoot && fullUrl.startsWith(PROTOCOL_FILE + localRoot)) { fullUrl = safetyPathJoin( - URL_LSP, - fullUrl.substr(PROTOCOL_FILE.length + dotPluginRoot.length) + lspRoot, + fullUrl.substr(PROTOCOL_FILE.length + localRoot.length) ) } return fullUrl } +function getPluginLSPRoot(effect?: boolean) { + return effect ? URL_LSP_HOST : URL_LSP +} + +function getExternalLSPRoot(localRoot: string, effect?: boolean) { + return safetyPathJoin( + effect === false ? URL_LSP_EXTERNAL : URL_LSP_HOST_EXTERNAL, + encodeURIComponent(localRoot) + ) +} + +function convertToExternalLSPResource( + fullUrl: string, + localRoot: string, + effect?: boolean +) { + return convertToLSPResource( + fullUrl, + localRoot, + getExternalLSPRoot(localRoot, effect) + ) +} + class IllegalPluginPackageError extends Error { constructor(message: string) { super(message) @@ -507,9 +537,19 @@ class PluginLocal extends EventEmitter< const url = path.join(localRoot, filePath) filePath = reg.test(url) ? url : PROTOCOL_FILE + url } - return !this.options.effect && this.isInstalledInDotRoot - ? convertToLSPResource(filePath, this.dotPluginsRoot) - : filePath + if (this.isInstalledInDotRoot) { + return convertToLSPResource( + filePath, + this.dotPluginsRoot, + getPluginLSPRoot(this.options.effect) + ) + } + + return convertToExternalLSPResource( + filePath, + localRoot, + this.options.effect + ) } async _preparePackageConfigs() { @@ -619,7 +659,10 @@ class PluginLocal extends EventEmitter< devEntry = devEntry || settings?.get('_devEntry') if (devEntry) { - this._options.entry = devEntry + this._options.entry = this._resolveResourceFullUrl( + devEntry, + this._localRoot + ) return } @@ -657,10 +700,21 @@ class PluginLocal extends EventEmitter< dirPathInstalled ) - entry = convertToLSPResource( - withFileProtocol(path.normalize(entryPath)), - this.dotPluginsRoot - ) + entry = withFileProtocol(path.normalize(entryPath)) + + if (this.isInstalledInDotRoot) { + entry = convertToLSPResource( + entry, + this.dotPluginsRoot, + getPluginLSPRoot(this.options.effect) + ) + } else { + entry = convertToExternalLSPResource( + entry, + path.dirname(entryPath), + this.options.effect + ) + } this._options.entry = entry } diff --git a/libs/src/LSPlugin.user.ts b/libs/src/LSPlugin.user.ts index 99922371a9..e9f00fa59f 100644 --- a/libs/src/LSPlugin.user.ts +++ b/libs/src/LSPlugin.user.ts @@ -526,6 +526,154 @@ export class LSPluginUser actor?.reject(e) } }) + + this._installFetchBridge() + } + + /** + * Route plugin `fetch` calls for http(s) URLs through the host process. + * + * Plugin iframes are served over the privileged `lsp://` scheme, which is a real + * origin, so Chromium enforces CORS on requests they make. Most endpoints a plugin + * talks to either send no `Access-Control-Allow-Origin` at all (ordinary web pages, + * local APIs such as Zotero or Syncthing) or omit the plugin's custom client header + * from `Access-Control-Allow-Headers`, so the request fails before it is sent. Under + * the old `file://` renderer Chromium applied no CORS and the same calls worked. + * + * Performing the request in the main process sidesteps the browser's CORS layer + * instead of disabling it: nothing in the renderer is relaxed, and the scope is + * exactly plugin frames. Non-http(s) URLs -- the plugin's own `lsp://` assets, + * `data:`, `blob:`, relative paths -- keep the native implementation. + */ + private _installFetchBridge () { + const g = globalThis as any + if (g.__lspFetchBridged) return + + const nativeFetch = typeof g.fetch === 'function' ? g.fetch.bind(g) : null + if (!nativeFetch) return + + const toHeaderRecord = (h: any): Record => { + const out: Record = {} + if (!h) return out + if (typeof h.forEach === 'function') h.forEach((v: any, k: any) => { out[String(k)] = String(v) }) + else if (Array.isArray(h)) for (const [k, v] of h) out[String(k)] = String(v) + else Object.assign(out, h) + return out + } + + const abortError = () => + typeof DOMException !== 'undefined' + ? new DOMException('The operation was aborted.', 'AbortError') + : Object.assign(new Error('The operation was aborted.'), { name: 'AbortError' }) + + g.fetch = async (input: any, init?: any) => { + const url = typeof input === 'string' + ? input + : (typeof URL !== 'undefined' && input instanceof URL ? input.href : input?.url) + + if (typeof url !== 'string' || !/^https?:\/\//i.test(url)) { + return nativeFetch(input, init) + } + + const req = input && typeof input === 'object' && 'url' in input ? input : null + const signal: AbortSignal | undefined = init?.signal ?? req?.signal + const credentials = init?.credentials ?? req?.credentials + let body = init?.body ?? undefined + + // The host performs the request outside the browser, so it carries no + // cookies. A caller that explicitly asked for them is better served by the + // native path, where the browser attaches them, than by a bridged request + // that silently goes out unauthenticated. + if (credentials === 'include') { + return nativeFetch(input, init) + } + + if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) { + body = body.toString() + } + + // The host serialises a non-string body with JSON.stringify, which turns a + // FormData/Blob/ArrayBuffer upload into "{}" and sends it without + // complaint. Silent corruption is worse than CORS, so those keep the + // native path. + if (body != null && typeof body !== 'string') { + return nativeFetch(input, init) + } + + if (signal?.aborted) throw abortError() + + try { + const options: any = { + url, + method: String(init?.method ?? req?.method ?? 'GET').toUpperCase(), + headers: { ...toHeaderRecord(req?.headers), ...toHeaderRecord(init?.headers) }, + data: body, + returnType: 'base64', + includeResponse: true + } + + let res: any + if (signal) { + // `abortable` makes _request resolve a task rather than the payload, + // which is the only handle the host gives us onto an in-flight request. + const task: any = await this.Request._request({ ...options, abortable: true }) + const onAbort = () => task.abort?.() + signal.addEventListener('abort', onAbort, { once: true }) + try { + res = await Promise.race([ + task.promise, + new Promise((_, reject) => { + signal.addEventListener('abort', () => reject(abortError()), { once: true }) + }) + ]) + } finally { + signal.removeEventListener('abort', onAbort) + } + } else { + res = await this.Request._request(options) + } + + // A host without includeResponse support resolves the bare body; there is + // no status or headers to rebuild from, so let the native path handle it. + if (!res || typeof res !== 'object' || typeof res.status !== 'number') { + return nativeFetch(input, init) + } + + let payload: Uint8Array | null = null + if (res.body) { + const bin = atob(res.body) + payload = Uint8Array.from(bin, (c: string) => c.charCodeAt(0)) + } + + // 204/205/304 are forbidden from carrying a body + const nullBody = res.status === 204 || res.status === 205 || res.status === 304 + + const response = new Response(nullBody ? null : payload, { + status: res.status, + statusText: res.statusText || '', + headers: res.headers || {} + }) + + // Response.url is read-only and empty on a constructed Response; plugins + // read it after a redirect, so publish the URL we actually requested. + try { + Object.defineProperty(response, 'url', { value: url, configurable: true }) + } catch (e) { + // non-fatal: the response is still usable without it + } + + return response + } catch (e) { + // An abort is the caller's own decision -- never retry it on the native + // path, which would send the request a second time. + if (signal?.aborted || (e as any)?.name === 'AbortError') throw e + // Otherwise never turn a request the native path could have served into a + // hard failure -- fall back rather than propagating a bridge-side error. + return nativeFetch(input, init) + } + } + + g.__lspFetchBridged = true } // Life related diff --git a/libs/src/helpers.ts b/libs/src/helpers.ts index 63397c1bf6..b9d6529497 100644 --- a/libs/src/helpers.ts +++ b/libs/src/helpers.ts @@ -19,7 +19,10 @@ export const path = export const IS_DEV = process.env.NODE_ENV === 'development' export const PROTOCOL_FILE = 'file://' export const PROTOCOL_LSP = 'lsp://' -export const URL_LSP = PROTOCOL_LSP + 'logseq.io/' +export const URL_LSP = PROTOCOL_LSP + 'logseq.io/plugins/' +export const URL_LSP_EXTERNAL = PROTOCOL_LSP + 'logseq.io/external/' +export const URL_LSP_HOST = PROTOCOL_LSP + 'logseq.com/plugins/' +export const URL_LSP_HOST_EXTERNAL = PROTOCOL_LSP + 'logseq.com/external/' let _appPathRoot diff --git a/libs/src/modules/LSPlugin.Request.ts b/libs/src/modules/LSPlugin.Request.ts index b730f9c0bf..4f85a5c566 100644 --- a/libs/src/modules/LSPlugin.Request.ts +++ b/libs/src/modules/LSPlugin.Request.ts @@ -118,8 +118,13 @@ export class LSPluginRequest extends EventEmitter { > { const pid = this.ctx.baseInfo.id const { success, fail, final, ...requestOptions } = options - const reqID = this.ctx.Experiments.invokeExperMethod( - 'request', + // Initiate over the postMessage caller rather than Experiments.invokeExperMethod. + // The latter does a synchronous `window.top.logseq` read, which throws for a + // plugin iframe on a different origin than the host (any `:effect false` + // plugin, served from lsp://logseq.io), so requests never start there. + // The abort path already goes through the caller. Ported from logseq/logseq#12753. + const reqID = await this.ctx._execCallableAPIAsync( + 'exper_request', pid, requestOptions ) diff --git a/package.json b/package.json index f4131c41c4..e1301f5290 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "run-android-release": "yarn clean && yarn release-app && rm -rf ./public/static && rm -rf ./static/js/*.map && mv static ./public && npx cap sync android && npx cap run android", "run-ios-release": "yarn clean && yarn release-app && rm -rf ./public/static && rm -rf ./static/js/*.map && mv static ./public && npx cap sync ios && npx cap run ios", "clean": "gulp clean", - "test": "run-s cljs:test cljs:run-test", + "test": "run-s cljs:test cljs:run-test test:electron-js", + "test:electron-js": "node --test \"test/electron-js/**/*.test.mjs\"", "report": "run-s cljs:report", "style:lint": "stylelint \"src/**/*.css\"", "gulp:watch": "gulp watch", @@ -113,7 +114,7 @@ "d3-force": "3.0.0", "diff": "5.0.0", "dompurify": "2.4.0", - "electron": "41.7.1", + "electron": "43.4.1", "electron-dl": "^4.0.0", "fs": "0.0.1-security", "fs-extra": "9.1.0", diff --git a/resources/js/lsplugin.core.js b/resources/js/lsplugin.core.js index 0cf415bf39..6956865b5b 100644 --- a/resources/js/lsplugin.core.js +++ b/resources/js/lsplugin.core.js @@ -1,2 +1,2 @@ /*! For license information please see lsplugin.core.js.LICENSE.txt */ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.LSPlugin=t():e.LSPlugin=t()}(self,(()=>(()=>{var e={227:(e,t,n)=>{var r=n(155);t.formatArgs=function(t){if(t[0]=(this.useColors?"%c":"")+this.namespace+(this.useColors?" %c":" ")+t[0]+(this.useColors?"%c ":" ")+"+"+e.exports.humanize(this.diff),!this.useColors)return;const n="color: "+this.color;t.splice(1,0,n,"color: inherit");let r=0,i=0;t[0].replace(/%[a-zA-Z%]/g,(e=>{"%%"!==e&&(r++,"%c"===e&&(i=r))})),t.splice(i,0,n)},t.save=function(e){try{e?t.storage.setItem("debug",e):t.storage.removeItem("debug")}catch(e){}},t.load=function(){let e;try{e=t.storage.getItem("debug")}catch(e){}return!e&&void 0!==r&&"env"in r&&(e=r.env.DEBUG),e},t.useColors=function(){return!("undefined"==typeof window||!window.process||"renderer"!==window.process.type&&!window.process.__nwjs)||("undefined"==typeof navigator||!navigator.userAgent||!navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))&&("undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&parseInt(RegExp.$1,10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))},t.storage=function(){try{return localStorage}catch(e){}}(),t.destroy=(()=>{let e=!1;return()=>{e||(e=!0,console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."))}})(),t.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],t.log=console.debug||console.log||(()=>{}),e.exports=n(447)(t);const{formatters:i}=e.exports;i.j=function(e){try{return JSON.stringify(e)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}}},447:(e,t,n)=>{e.exports=function(e){function t(e){let n,i,o,s=null;function a(...e){if(!a.enabled)return;const r=a,i=Number(new Date),o=i-(n||i);r.diff=o,r.prev=n,r.curr=i,n=i,e[0]=t.coerce(e[0]),"string"!=typeof e[0]&&e.unshift("%O");let s=0;e[0]=e[0].replace(/%([a-zA-Z%])/g,((n,i)=>{if("%%"===n)return"%";s++;const o=t.formatters[i];if("function"==typeof o){const t=e[s];n=o.call(r,t),e.splice(s,1),s--}return n})),t.formatArgs.call(r,e),(r.log||t.log).apply(r,e)}return a.namespace=e,a.useColors=t.useColors(),a.color=t.selectColor(e),a.extend=r,a.destroy=t.destroy,Object.defineProperty(a,"enabled",{enumerable:!0,configurable:!1,get:()=>null!==s?s:(i!==t.namespaces&&(i=t.namespaces,o=t.enabled(e)),o),set:e=>{s=e}}),"function"==typeof t.init&&t.init(a),a}function r(e,n){const r=t(this.namespace+(void 0===n?":":n)+e);return r.log=this.log,r}function i(e){return e.toString().substring(2,e.toString().length-2).replace(/\.\*\?$/,"*")}return t.debug=t,t.default=t,t.coerce=function(e){return e instanceof Error?e.stack||e.message:e},t.disable=function(){const e=[...t.names.map(i),...t.skips.map(i).map((e=>"-"+e))].join(",");return t.enable(""),e},t.enable=function(e){let n;t.save(e),t.namespaces=e,t.names=[],t.skips=[];const r=("string"==typeof e?e:"").split(/[\s,]+/),i=r.length;for(n=0;n{t[n]=e[n]})),t.names=[],t.skips=[],t.formatters={},t.selectColor=function(e){let n=0;for(let t=0;t{"use strict";var t=function(e){return function(e){return!!e&&"object"==typeof e}(e)&&!function(e){var t=Object.prototype.toString.call(e);return"[object RegExp]"===t||"[object Date]"===t||function(e){return e.$$typeof===n}(e)}(e)},n="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function r(e,t){return!1!==t.clone&&t.isMergeableObject(e)?a((n=e,Array.isArray(n)?[]:{}),e,t):e;var n}function i(e,t,n){return e.concat(t).map((function(e){return r(e,n)}))}function o(e){return Object.keys(e).concat(function(e){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(e).filter((function(t){return Object.propertyIsEnumerable.call(e,t)})):[]}(e))}function s(e,t){try{return t in e}catch(e){return!1}}function a(e,n,c){(c=c||{}).arrayMerge=c.arrayMerge||i,c.isMergeableObject=c.isMergeableObject||t,c.cloneUnlessOtherwiseSpecified=r;var l=Array.isArray(n);return l===Array.isArray(e)?l?c.arrayMerge(e,n,c):function(e,t,n){var i={};return n.isMergeableObject(e)&&o(e).forEach((function(t){i[t]=r(e[t],n)})),o(t).forEach((function(o){(function(e,t){return s(e,t)&&!(Object.hasOwnProperty.call(e,t)&&Object.propertyIsEnumerable.call(e,t))})(e,o)||(s(e,o)&&n.isMergeableObject(t[o])?i[o]=function(e,t){if(!t.customMerge)return a;var n=t.customMerge(e);return"function"==typeof n?n:a}(o,n)(e[o],t[o],n):i[o]=r(t[o],n))})),i}(e,n,c):r(n,c)}a.all=function(e,t){if(!Array.isArray(e))throw new Error("first argument should be an array");return e.reduce((function(e,n){return a(e,n,t)}),{})};var c=a;e.exports=c},856:function(e){e.exports=function(){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}function t(e,n){return t=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},t(e,n)}function n(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}function r(e,i,o){return r=n()?Reflect.construct:function(e,n,r){var i=[null];i.push.apply(i,n);var o=new(Function.bind.apply(e,i));return r&&t(o,r.prototype),o},r.apply(null,arguments)}function i(e){return function(e){if(Array.isArray(e))return o(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(e){if("string"==typeof e)return o(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?o(e,t):void 0}}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function o(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n1?n-1:0),i=1;i/gm),W=d(/^data-[\-\w.\u00B7-\uFFFF]/),G=d(/^aria-[\-\w]+$/),J=d(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),V=d(/^(?:\w+script|data):/i),K=d(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),Z=d(/^html$/i),Y=function(){return"undefined"==typeof window?null:window},Q=function(t,n){if("object"!==e(t)||"function"!=typeof t.createPolicy)return null;var r=null,i="data-tt-policy-suffix";n.currentScript&&n.currentScript.hasAttribute(i)&&(r=n.currentScript.getAttribute(i));var o="dompurify"+(r?"#"+r:"");try{return t.createPolicy(o,{createHTML:function(e){return e}})}catch(e){return console.warn("TrustedTypes policy "+o+" could not be created."),null}};return function t(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:Y(),r=function(e){return t(e)};if(r.version="2.3.8",r.removed=[],!n||!n.document||9!==n.document.nodeType)return r.isSupported=!1,r;var o=n.document,s=n.document,a=n.DocumentFragment,c=n.HTMLTemplateElement,l=n.Node,u=n.Element,d=n.NodeFilter,p=n.NamedNodeMap,f=void 0===p?n.NamedNodeMap||n.MozNamedAttrMap:p,g=n.HTMLFormElement,m=n.DOMParser,y=n.trustedTypes,k=u.prototype,X=I(k,"cloneNode"),ee=I(k,"nextSibling"),te=I(k,"childNodes"),ne=I(k,"parentNode");if("function"==typeof c){var re=s.createElement("template");re.content&&re.content.ownerDocument&&(s=re.content.ownerDocument)}var ie=Q(y,o),oe=ie?ie.createHTML(""):"",se=s,ae=se.implementation,ce=se.createNodeIterator,le=se.createDocumentFragment,ue=se.getElementsByTagName,he=o.importNode,de={};try{de=T(s).documentMode?s.documentMode:{}}catch(e){}var pe={};r.isSupported="function"==typeof ne&&ae&&void 0!==ae.createHTMLDocument&&9!==de;var fe,ge,me=q,ye=B,_e=W,be=G,ve=V,we=K,xe=J,Ce=null,Se=j({},[].concat(i(L),i(M),i(P),i(R),i(D))),Oe=null,Ee=j({},[].concat(i(U),i($),i(z),i(H))),Ae=Object.seal(Object.create(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),ke=null,je=null,Te=!0,Ie=!0,Le=!1,Me=!1,Pe=!1,Ne=!1,Re=!1,Fe=!1,De=!1,Ue=!1,$e=!0,ze=!0,He=!1,qe={},Be=null,We=j({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),Ge=null,Je=j({},["audio","video","img","source","image","track"]),Ve=null,Ke=j({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Ze="http://www.w3.org/1998/Math/MathML",Ye="http://www.w3.org/2000/svg",Qe="http://www.w3.org/1999/xhtml",Xe=Qe,et=!1,tt=["application/xhtml+xml","text/html"],nt="text/html",rt=null,it=s.createElement("form"),ot=function(e){return e instanceof RegExp||e instanceof Function},st=function(t){rt&&rt===t||(t&&"object"===e(t)||(t={}),t=T(t),Ce="ALLOWED_TAGS"in t?j({},t.ALLOWED_TAGS):Se,Oe="ALLOWED_ATTR"in t?j({},t.ALLOWED_ATTR):Ee,Ve="ADD_URI_SAFE_ATTR"in t?j(T(Ke),t.ADD_URI_SAFE_ATTR):Ke,Ge="ADD_DATA_URI_TAGS"in t?j(T(Je),t.ADD_DATA_URI_TAGS):Je,Be="FORBID_CONTENTS"in t?j({},t.FORBID_CONTENTS):We,ke="FORBID_TAGS"in t?j({},t.FORBID_TAGS):{},je="FORBID_ATTR"in t?j({},t.FORBID_ATTR):{},qe="USE_PROFILES"in t&&t.USE_PROFILES,Te=!1!==t.ALLOW_ARIA_ATTR,Ie=!1!==t.ALLOW_DATA_ATTR,Le=t.ALLOW_UNKNOWN_PROTOCOLS||!1,Me=t.SAFE_FOR_TEMPLATES||!1,Pe=t.WHOLE_DOCUMENT||!1,Fe=t.RETURN_DOM||!1,De=t.RETURN_DOM_FRAGMENT||!1,Ue=t.RETURN_TRUSTED_TYPE||!1,Re=t.FORCE_BODY||!1,$e=!1!==t.SANITIZE_DOM,ze=!1!==t.KEEP_CONTENT,He=t.IN_PLACE||!1,xe=t.ALLOWED_URI_REGEXP||xe,Xe=t.NAMESPACE||Qe,t.CUSTOM_ELEMENT_HANDLING&&ot(t.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Ae.tagNameCheck=t.CUSTOM_ELEMENT_HANDLING.tagNameCheck),t.CUSTOM_ELEMENT_HANDLING&&ot(t.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Ae.attributeNameCheck=t.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),t.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof t.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(Ae.allowCustomizedBuiltInElements=t.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),fe=fe=-1===tt.indexOf(t.PARSER_MEDIA_TYPE)?nt:t.PARSER_MEDIA_TYPE,ge="application/xhtml+xml"===fe?function(e){return e}:w,Me&&(Ie=!1),De&&(Fe=!0),qe&&(Ce=j({},i(D)),Oe=[],!0===qe.html&&(j(Ce,L),j(Oe,U)),!0===qe.svg&&(j(Ce,M),j(Oe,$),j(Oe,H)),!0===qe.svgFilters&&(j(Ce,P),j(Oe,$),j(Oe,H)),!0===qe.mathMl&&(j(Ce,R),j(Oe,z),j(Oe,H))),t.ADD_TAGS&&(Ce===Se&&(Ce=T(Ce)),j(Ce,t.ADD_TAGS)),t.ADD_ATTR&&(Oe===Ee&&(Oe=T(Oe)),j(Oe,t.ADD_ATTR)),t.ADD_URI_SAFE_ATTR&&j(Ve,t.ADD_URI_SAFE_ATTR),t.FORBID_CONTENTS&&(Be===We&&(Be=T(Be)),j(Be,t.FORBID_CONTENTS)),ze&&(Ce["#text"]=!0),Pe&&j(Ce,["html","head","body"]),Ce.table&&(j(Ce,["tbody"]),delete ke.tbody),h&&h(t),rt=t)},at=j({},["mi","mo","mn","ms","mtext"]),ct=j({},["foreignobject","desc","title","annotation-xml"]),lt=j({},["title","style","font","a","script"]),ut=j({},M);j(ut,P),j(ut,N);var ht=j({},R);j(ht,F);var dt=function(e){var t=ne(e);t&&t.tagName||(t={namespaceURI:Qe,tagName:"template"});var n=w(e.tagName),r=w(t.tagName);return e.namespaceURI===Ye?t.namespaceURI===Qe?"svg"===n:t.namespaceURI===Ze?"svg"===n&&("annotation-xml"===r||at[r]):Boolean(ut[n]):e.namespaceURI===Ze?t.namespaceURI===Qe?"math"===n:t.namespaceURI===Ye?"math"===n&&ct[r]:Boolean(ht[n]):e.namespaceURI===Qe&&!(t.namespaceURI===Ye&&!ct[r])&&!(t.namespaceURI===Ze&&!at[r])&&!ht[n]&&(lt[n]||!ut[n])},pt=function(e){v(r.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){try{e.outerHTML=oe}catch(t){e.remove()}}},ft=function(e,t){try{v(r.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){v(r.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Oe[e])if(Fe||De)try{pt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},gt=function(e){var t,n;if(Re)e=""+e;else{var r=x(e,/^[\r\n\t ]+/);n=r&&r[0]}"application/xhtml+xml"===fe&&(e=''+e+"");var i=ie?ie.createHTML(e):e;if(Xe===Qe)try{t=(new m).parseFromString(i,fe)}catch(e){}if(!t||!t.documentElement){t=ae.createDocument(Xe,"template",null);try{t.documentElement.innerHTML=et?"":i}catch(e){}}var o=t.body||t.documentElement;return e&&n&&o.insertBefore(s.createTextNode(n),o.childNodes[0]||null),Xe===Qe?ue.call(t,Pe?"html":"body")[0]:Pe?t.documentElement:o},mt=function(e){return ce.call(e.ownerDocument||e,e,d.SHOW_ELEMENT|d.SHOW_COMMENT|d.SHOW_TEXT,null,!1)},yt=function(e){return e instanceof g&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof f)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore)},_t=function(t){return"object"===e(l)?t instanceof l:t&&"object"===e(t)&&"number"==typeof t.nodeType&&"string"==typeof t.nodeName},bt=function(e,t,n){pe[e]&&_(pe[e],(function(e){e.call(r,t,n,rt)}))},vt=function(e){var t;if(bt("beforeSanitizeElements",e,null),yt(e))return pt(e),!0;if(E(/[\u0080-\uFFFF]/,e.nodeName))return pt(e),!0;var n=ge(e.nodeName);if(bt("uponSanitizeElement",e,{tagName:n,allowedTags:Ce}),e.hasChildNodes()&&!_t(e.firstElementChild)&&(!_t(e.content)||!_t(e.content.firstElementChild))&&E(/<[/\w]/g,e.innerHTML)&&E(/<[/\w]/g,e.textContent))return pt(e),!0;if("select"===n&&E(/