Gdgt 2495 workaround unsafe eval near membrane requirement (#74717)
This commit is contained in:
committed by
GitHub
parent
3d893c1d75
commit
b94b50a122
10
bun.lock
10
bun.lock
@@ -33,7 +33,7 @@
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@locker/near-membrane-dom": "^0.15.0",
|
||||
"@locker/near-membrane-dom": "^0.16.0",
|
||||
"@loki/browser": "0.35.0",
|
||||
"@mantine/core": "8.3.18",
|
||||
"@mantine/dates": "8.3.18",
|
||||
@@ -1134,13 +1134,13 @@
|
||||
|
||||
"@lezer/python": ["@lezer/python@1.1.15", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-aVQ43m2zk4FZYedCqL0KHPEUsqZOrmAvRhkhHlVPnDD1HODDyyQv5BRIuod4DadkgBEZd53vQOtXTonNbEgjrQ=="],
|
||||
|
||||
"@locker/near-membrane-base": ["@locker/near-membrane-base@0.15.0", "", { "dependencies": { "@locker/near-membrane-shared": "0.15.0" } }, "sha512-ui7JZF4lG0TosGSl1ECGd4w0pwE9JXFw+JB0YYFA61qqw0ZmPk6yacPb6mawGqg1boa/yASZ1JIC5YRfk+jMCw=="],
|
||||
"@locker/near-membrane-base": ["@locker/near-membrane-base@0.16.0", "", { "dependencies": { "@locker/near-membrane-shared": "0.16.0" } }, "sha512-Ds6FQb3UMdA2L4zoGBtFwux6xD3Vi/4u6tgI+nCaAhCg0LcA138llFp5HasRpRoRkNu4CGdQu4nbC5/7ARybZQ=="],
|
||||
|
||||
"@locker/near-membrane-dom": ["@locker/near-membrane-dom@0.15.0", "", { "dependencies": { "@locker/near-membrane-base": "0.15.0", "@locker/near-membrane-shared": "0.15.0", "@locker/near-membrane-shared-dom": "0.15.0" } }, "sha512-sOo9rE7smymkBust2FzBhUAcbKksSTl9q/amroFE4P1Zlof6moTHCvVh3X/wnP4o7Md+yPWOEhNyrAA56kMIsg=="],
|
||||
"@locker/near-membrane-dom": ["@locker/near-membrane-dom@0.16.0", "", { "dependencies": { "@locker/near-membrane-base": "0.16.0", "@locker/near-membrane-shared": "0.16.0", "@locker/near-membrane-shared-dom": "0.16.0" } }, "sha512-h1FD6qPWwi9b20Yg5jFy8LWqewuJ5rXvj7dpUnYcY5gUAQdQSwOIhCUz8km/MpUIEVqgPKUyj97VwO6PjYpQBA=="],
|
||||
|
||||
"@locker/near-membrane-shared": ["@locker/near-membrane-shared@0.15.0", "", {}, "sha512-d6sJH7GrLmK9+k1ujobMCya7sIXIzAhjOCqRSxp03YGgnfurGR3ymq1R2oqdaGJldYIw3W4rD/Zc0n9q01CHfA=="],
|
||||
"@locker/near-membrane-shared": ["@locker/near-membrane-shared@0.16.0", "", {}, "sha512-b6B8n7Mgi+QOiuJox0pzbw8oTaZuyVGOc/w2Hxd8yg0C/s7eHbG80U6AtBtkvmpfh6Fug8f6zyUgbbkT93av8g=="],
|
||||
|
||||
"@locker/near-membrane-shared-dom": ["@locker/near-membrane-shared-dom@0.15.0", "", { "dependencies": { "@locker/near-membrane-shared": "0.15.0" } }, "sha512-Pe3xDLiWHk0NqRjwE3KSGqEXwiQ7pfZIwX8n/9JAfFeUV6AQPrn5+8X4kFQter3zY4Aj8rGg+C9S8BluFtxafg=="],
|
||||
"@locker/near-membrane-shared-dom": ["@locker/near-membrane-shared-dom@0.16.0", "", { "dependencies": { "@locker/near-membrane-shared": "0.16.0" } }, "sha512-0QFiJzT0cS5hwh4B0CZk4AkWFkl5IO2j0jWgNaZyC5odtvfNZMPKa5pe9MJod7shfXVSN9WaeLshMadgxki7DA=="],
|
||||
|
||||
"@loki/browser": ["@loki/browser@0.35.0", "", { "dependencies": { "@loki/integration-core": "^0.35.0" } }, "sha512-VYSd3bx9dOQ/Wo2C1gdNdE2mcYPleVtGInRcZmCOJ01va14L9VEGzG2zL1dMcmMUuJ2cT3Ri1GR59vEO3vKWvQ=="],
|
||||
|
||||
|
||||
@@ -261,6 +261,35 @@
|
||||
(finally
|
||||
(try (.delete tempfile) (catch Exception _))))))
|
||||
|
||||
(def ^:private sandbox-host-html
|
||||
"Minimal HTML doc that the patched `@locker/near-membrane-dom` loads as the iframe document
|
||||
so plugin code can be `eval`'d under a relaxed, per-iframe CSP."
|
||||
"<!doctype html><html><head><meta charset=\"utf-8\"></head><body></body></html>")
|
||||
|
||||
(def ^:private sandbox-host-csp
|
||||
"CSP applied ONLY to the sandbox iframe document.
|
||||
- `'unsafe-eval'` required by near-membrane to evaluate plugin code inside the realm.
|
||||
- `frame-ancestors 'self'` - so Metabase can embed this document."
|
||||
(str "default-src 'none'; "
|
||||
"script-src 'unsafe-eval'; "
|
||||
"frame-ancestors 'self';"))
|
||||
|
||||
(api.macros/defendpoint :get "/sandbox-host" :- :any
|
||||
"Serve a minimal HTML document used as the iframe `src` for the near-membrane custom-viz
|
||||
sandbox. The response carries a per-document `Content-Security-Policy` that permits
|
||||
`'unsafe-eval'` only inside this iframe, so the main Metabase document keeps its strict
|
||||
nonce-based CSP."
|
||||
[]
|
||||
{:status 200
|
||||
:headers {"Content-Type" "text/html; charset=utf-8"
|
||||
"Content-Security-Policy" sandbox-host-csp
|
||||
"X-Frame-Options" "SAMEORIGIN"
|
||||
"X-Content-Type-Options" "nosniff"
|
||||
"Cross-Origin-Resource-Policy" "same-origin"
|
||||
"Referrer-Policy" "no-referrer"
|
||||
"Cache-Control" "public, max-age=60"}
|
||||
:body sandbox-host-html})
|
||||
|
||||
(api.macros/defendpoint :get "/:id/bundle" :- :any
|
||||
"Serve the JS bundle for a plugin from the on-disk cache.
|
||||
Returns application/javascript with ETag and Cache-Control headers.
|
||||
|
||||
@@ -442,6 +442,44 @@
|
||||
(is (contains? identifiers "compat-viz"))
|
||||
(is (not (contains? identifiers "incompat-viz")))))))))
|
||||
|
||||
;;; ------------------------------------------------ Sandbox-host Endpoint ------------------------------------------------
|
||||
|
||||
(deftest sandbox-host-endpoint-test
|
||||
(testing "GET /sandbox-host returns minimal HTML with a per-document CSP"
|
||||
(mt/with-premium-features #{:custom-viz}
|
||||
(let [resp (mt/user-http-request-full-response
|
||||
:rasta :get 200 "ee/custom-viz-plugin/sandbox-host")
|
||||
headers (:headers resp)]
|
||||
(testing "returns a tiny HTML body"
|
||||
(is (string? (:body resp)))
|
||||
(is (str/includes? (:body resp) "<!doctype html>"))
|
||||
(is (str/includes? (:body resp) "<body></body>")))
|
||||
(testing "response is served as HTML"
|
||||
(is (str/starts-with? (get headers "Content-Type") "text/html")))
|
||||
(testing "Content-Security-Policy allows unsafe-eval only inside the sandbox doc"
|
||||
(let [csp (get headers "Content-Security-Policy")]
|
||||
(is (some? csp))
|
||||
(is (str/includes? csp "default-src 'none'"))
|
||||
(is (str/includes? csp "script-src 'unsafe-eval'"))
|
||||
(is (not (str/includes? csp "sandbox")))
|
||||
(is (str/includes? csp "frame-ancestors 'self'"))))
|
||||
(testing "framing is allowed for same-origin (overrides the global X-Frame-Options: DENY)"
|
||||
(is (= "SAMEORIGIN" (get headers "X-Frame-Options"))))
|
||||
(testing "hardening headers are present"
|
||||
(is (= "nosniff" (get headers "X-Content-Type-Options")))
|
||||
(is (= "no-referrer" (get headers "Referrer-Policy")))
|
||||
(is (= "same-origin" (get headers "Cross-Origin-Resource-Policy")))
|
||||
(is (= "public, max-age=60" (get headers "Cache-Control")))))))
|
||||
(testing "GET /sandbox-host requires the :custom-viz premium feature"
|
||||
(mt/with-premium-features #{}
|
||||
(mt/assert-has-premium-feature-error
|
||||
"Custom Visualizations"
|
||||
(mt/user-http-request :rasta :get 402 "ee/custom-viz-plugin/sandbox-host"))))
|
||||
(testing "GET /sandbox-host requires authentication"
|
||||
(mt/with-premium-features #{:custom-viz}
|
||||
(is (= "Unauthenticated"
|
||||
(client/client :get 401 "ee/custom-viz-plugin/sandbox-host"))))))
|
||||
|
||||
;;; ------------------------------------------------ Bundle Endpoint ------------------------------------------------
|
||||
|
||||
(deftest bundle-endpoint-test
|
||||
|
||||
@@ -39,6 +39,7 @@ import { isCustomVizDisplay } from "metabase-types/guards/visualization";
|
||||
import { trackCustomVizSelected } from "./analytics";
|
||||
import { applyDefaultVisualizationProps } from "./custom-viz-common";
|
||||
import { ensureVizApi } from "./custom-viz-globals";
|
||||
import type { SandboxMode } from "./sandbox";
|
||||
|
||||
// Track which plugins have already been loaded to avoid re-execution.
|
||||
// Maps plugin id → { identifier, hash } so we can detect when a re-uploaded
|
||||
@@ -105,7 +106,10 @@ function useCustomVizDevReload(
|
||||
);
|
||||
setLoading(true);
|
||||
try {
|
||||
await loadCustomVizPlugin(plugin, `?t=${Date.now()}`, onInfo);
|
||||
await loadCustomVizPlugin(plugin, {
|
||||
cacheBustSuffix: `?t=${Date.now()}`,
|
||||
onInfo,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -161,7 +165,7 @@ export function useAutoLoadCustomVizPlugin(display: string | undefined): {
|
||||
loadingRef.current = identifier;
|
||||
setLoading(true);
|
||||
try {
|
||||
await loadCustomVizPlugin(pluginToLoad, undefined, onInfo);
|
||||
await loadCustomVizPlugin(pluginToLoad, { onInfo });
|
||||
} finally {
|
||||
loadingRef.current = null;
|
||||
setLoading(false);
|
||||
@@ -246,6 +250,12 @@ export function useAutoLoadCustomVizPlugin(display: string | undefined): {
|
||||
return { loading: needsCustomViz && !isReady };
|
||||
}
|
||||
|
||||
export type LoadCustomVizPluginOptions = {
|
||||
cacheBustSuffix?: string;
|
||||
onInfo?: (message: string) => void;
|
||||
sandboxMode?: SandboxMode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dynamically load a custom viz plugin bundle, call its factory,
|
||||
* decompose the returned definition, and register it as a Metabase
|
||||
@@ -253,9 +263,9 @@ export function useAutoLoadCustomVizPlugin(display: string | undefined): {
|
||||
*/
|
||||
export async function loadCustomVizPlugin(
|
||||
plugin: CustomVizPluginRuntime,
|
||||
cacheBustSuffix?: string,
|
||||
onInfo?: (message: string) => void,
|
||||
options: LoadCustomVizPluginOptions = {},
|
||||
): Promise<string | null> {
|
||||
const { cacheBustSuffix, onInfo, sandboxMode = "hosted" } = options;
|
||||
const existing = loadedPlugins.get(plugin.id);
|
||||
const currentHash = plugin.bundle_hash ?? null;
|
||||
if (
|
||||
@@ -291,7 +301,7 @@ export async function loadCustomVizPlugin(
|
||||
// up in the static-viz bundle, which is evaluated by GraalVM and has no
|
||||
// DOM constructors.
|
||||
const { createPluginSandbox } = await import("./sandbox");
|
||||
const sandbox = createPluginSandbox(plugin.id);
|
||||
const sandbox = await createPluginSandbox(plugin.id, sandboxMode);
|
||||
const factory = sandbox.evaluate(text);
|
||||
|
||||
if (typeof factory !== "function") {
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
import createVirtualEnvironment from "@locker/near-membrane-dom";
|
||||
|
||||
import { getSubpathSafeUrl } from "metabase/urls";
|
||||
import type { CustomVizPluginId } from "metabase-types/api";
|
||||
|
||||
import { makeDistortionCallback } from "./distortions";
|
||||
|
||||
export type SandboxMode = "hosted" | "blank";
|
||||
|
||||
// Needed for React style declarations to be applied correctly.
|
||||
function isLiveTarget(target: object): boolean {
|
||||
return target instanceof CSSStyleDeclaration;
|
||||
}
|
||||
|
||||
export function createPluginSandbox(pluginId: CustomVizPluginId) {
|
||||
// Same-origin endpoint that serves a tiny HTML document with a permissive,
|
||||
// per-document CSP (allowing `'unsafe-eval'`).
|
||||
const SANDBOX_HOST_URL = "/api/ee/custom-viz-plugin/sandbox-host";
|
||||
|
||||
export async function createPluginSandbox(
|
||||
pluginId: CustomVizPluginId,
|
||||
mode: SandboxMode = "hosted",
|
||||
) {
|
||||
let capturedFactory: unknown;
|
||||
|
||||
const env = createVirtualEnvironment(window, {
|
||||
// "blank" skips the sandbox-host endpoint and lets the library fall back to
|
||||
// about:blank. Used in Storybook where no backend serves the endpoint.
|
||||
const iframeSrc =
|
||||
mode === "hosted" ? getSubpathSafeUrl(SANDBOX_HOST_URL) : undefined;
|
||||
|
||||
const env = await createVirtualEnvironment(window, {
|
||||
...(iframeSrc ? { iframeSrc } : {}),
|
||||
distortionCallback: makeDistortionCallback(pluginId),
|
||||
liveTargetCallback: isLiveTarget,
|
||||
endowments: Object.getOwnPropertyDescriptors({
|
||||
|
||||
@@ -33,8 +33,10 @@ const getDefaultPluginCustomViz = () => ({
|
||||
},
|
||||
loadCustomVizPlugin: async (
|
||||
_plugin: CustomVizPluginRuntime,
|
||||
_cacheBustSuffix?: string,
|
||||
_onInfo?: (message: string) => void,
|
||||
_options?: {
|
||||
cacheBustSuffix?: string;
|
||||
onInfo?: (message: string) => void;
|
||||
},
|
||||
) => null as string | null,
|
||||
getPluginAssetUrl: (_pluginId: number, _assetPath: string | null) =>
|
||||
undefined as string | undefined,
|
||||
|
||||
@@ -63,7 +63,7 @@ export const ChartTypeSidebar = ({
|
||||
let cancelled = false;
|
||||
Promise.all(
|
||||
customVizPlugins.map((plugin) =>
|
||||
PLUGIN_CUSTOM_VIZ.loadCustomVizPlugin(plugin, undefined, onInfo),
|
||||
PLUGIN_CUSTOM_VIZ.loadCustomVizPlugin(plugin, { onInfo }),
|
||||
),
|
||||
).then(() => {
|
||||
if (!cancelled) {
|
||||
|
||||
@@ -57,14 +57,21 @@ export function useHeatmapPlugin() {
|
||||
const [ready, setReady] = useState(false);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
loadCustomVizPlugin({
|
||||
id: 1,
|
||||
bundle_hash: "dev",
|
||||
bundle_url: `${PLUGIN_BASE_URL}/index.js`,
|
||||
display_name: "Calendar Heatmap",
|
||||
identifier: PLUGIN_IDENTIFIER,
|
||||
icon: `${PLUGIN_BASE_URL}/assets/calendar.svg`,
|
||||
}).then(() => {
|
||||
loadCustomVizPlugin(
|
||||
{
|
||||
id: 1,
|
||||
bundle_hash: "dev",
|
||||
bundle_url: `${PLUGIN_BASE_URL}/index.js`,
|
||||
display_name: "Calendar Heatmap",
|
||||
identifier: PLUGIN_IDENTIFIER,
|
||||
icon: `${PLUGIN_BASE_URL}/assets/calendar.svg`,
|
||||
},
|
||||
{
|
||||
// Storybook has no backend for the sandbox-host endpoint and no strict
|
||||
// parent CSP, so use about:blank instead of the iframe-src sandbox.
|
||||
sandboxMode: "blank",
|
||||
},
|
||||
).then(() => {
|
||||
if (!cancelled) {
|
||||
setReady(true);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@locker/near-membrane-dom": "^0.15.0",
|
||||
"@locker/near-membrane-dom": "^0.16.0",
|
||||
"@loki/browser": "0.35.0",
|
||||
"@mantine/core": "8.3.18",
|
||||
"@mantine/dates": "8.3.18",
|
||||
|
||||
140
patches/@locker+near-membrane-dom+0.16.0.patch
Normal file
140
patches/@locker+near-membrane-dom+0.16.0.patch
Normal file
@@ -0,0 +1,140 @@
|
||||
diff --git a/node_modules/@locker/near-membrane-dom/dist/index.mjs.js b/node_modules/@locker/near-membrane-dom/dist/index.mjs.js
|
||||
--- a/node_modules/@locker/near-membrane-dom/dist/index.mjs.js
|
||||
+++ b/node_modules/@locker/near-membrane-dom/dist/index.mjs.js
|
||||
@@ -107,7 +107,7 @@
|
||||
const IFRAME_SANDBOX_ATTRIBUTE_VALUE = 'allow-same-origin allow-scripts';
|
||||
const revoked = toSafeWeakSet(new WeakSetCtor());
|
||||
const blueCreateHooksCallbackCache = toSafeWeakMap(new WeakMapCtor());
|
||||
-function createDetachableIframe(doc) {
|
||||
+function createDetachableIframe(doc, iframeSrc) {
|
||||
var _ReflectApply;
|
||||
const iframe = ReflectApply(DocumentProtoCreateElement, doc, ['iframe']);
|
||||
// It is impossible to test whether the NodeProtoLastChildGetter branch is
|
||||
@@ -116,6 +116,13 @@
|
||||
const style = ReflectApply(HTMLElementProtoStyleGetter, iframe, []);
|
||||
style.display = 'none';
|
||||
ReflectApply(ElementProtoSetAttribute, iframe, ['sandbox', IFRAME_SANDBOX_ATTRIBUTE_VALUE]);
|
||||
+ // Metabase patch: when `iframeSrc` is provided, load the iframe from that URL so
|
||||
+ // its document carries the per-document CSP returned by the server (allowing
|
||||
+ // `'unsafe-eval'` only inside the sandbox), instead of inheriting the parent's
|
||||
+ // strict CSP via about:blank.
|
||||
+ if (iframeSrc) {
|
||||
+ ReflectApply(ElementProtoSetAttribute, iframe, ['src', iframeSrc]);
|
||||
+ }
|
||||
ReflectApply(NodeProtoAppendChild, parent, [iframe]);
|
||||
return iframe;
|
||||
}
|
||||
@@ -132,6 +139,9 @@
|
||||
defaultPolicy,
|
||||
endowments,
|
||||
globalObjectShape,
|
||||
+ // Metabase patch: when set, the iframe loads this URL instead of about:blank
|
||||
+ // so the sandbox document gets its own per-document CSP.
|
||||
+ iframeSrc,
|
||||
instrumentation,
|
||||
keepAlive = true,
|
||||
liveTargetCallback,
|
||||
@@ -142,7 +152,8 @@
|
||||
} = ObjectAssign({
|
||||
__proto__: null
|
||||
}, providedOptions);
|
||||
- const iframe = createDetachableIframe(blueRefs.document);
|
||||
+ const iframe = createDetachableIframe(blueRefs.document, iframeSrc);
|
||||
+ const finishSetup = () => {
|
||||
const redWindow = ReflectApply(HTMLIFrameElementProtoContentWindowGetter, iframe, []);
|
||||
const shouldUseDefaultGlobalOwnKeys = typeof globalObjectShape !== 'object' || globalObjectShape === null;
|
||||
const defaultGlobalOwnKeys = filterWindowKeys(getFilteredGlobalOwnKeys(redWindow, maxPerfMode));
|
||||
@@ -218,8 +229,14 @@
|
||||
// Revoke the proxies of the redDocument and redWindow to prevent access.
|
||||
revoked.add(redDocument);
|
||||
revoked.add(redWindow);
|
||||
- ReflectApply(DocumentProtoOpen, redDocument, []);
|
||||
- ReflectApply(DocumentProtoClose, redDocument, []);
|
||||
+ // Metabase patch: when an `iframeSrc` loaded a real document, skip the
|
||||
+ // open/close dance. Those calls would tear down the loaded document and
|
||||
+ // replace it with one whose CSP is inherited from the parent (about:blank
|
||||
+ // semantics), defeating the whole point of per-iframe CSP.
|
||||
+ if (!iframeSrc) {
|
||||
+ ReflectApply(DocumentProtoOpen, redDocument, []);
|
||||
+ ReflectApply(DocumentProtoClose, redDocument, []);
|
||||
+ }
|
||||
} else {
|
||||
// istanbul ignore next: coverage never runs in old Chromium (<v86)
|
||||
if (IS_OLD_CHROMIUM_BROWSER) {
|
||||
@@ -231,6 +248,22 @@
|
||||
ReflectApply(ElementProtoRemove, iframe, []);
|
||||
}
|
||||
return env;
|
||||
+ };
|
||||
+ // Metabase patch: when `iframeSrc` is set, wait for the iframe to load before
|
||||
+ // touching `contentWindow` so we read the realm of the loaded document (with
|
||||
+ // its server-sent CSP) rather than the initial about:blank one. Existing
|
||||
+ // callers without `iframeSrc` keep the synchronous return shape.
|
||||
+ if (iframeSrc) {
|
||||
+ return new Promise((resolve, reject) => {
|
||||
+ iframe.addEventListener('load', () => {
|
||||
+ try { resolve(finishSetup()); } catch (e) { reject(e); }
|
||||
+ }, { once: true });
|
||||
+ iframe.addEventListener('error', () => {
|
||||
+ reject(new Error('Failed to load sandbox iframe at ' + iframeSrc));
|
||||
+ }, { once: true });
|
||||
+ });
|
||||
+ }
|
||||
+ return finishSetup();
|
||||
}
|
||||
function revokedProxyCallback(value) {
|
||||
return revoked.has(value);
|
||||
diff --git a/node_modules/@locker/near-membrane-dom/dist/index.mjs.d.ts b/node_modules/@locker/near-membrane-dom/dist/index.mjs.d.ts
|
||||
--- a/node_modules/@locker/near-membrane-dom/dist/index.mjs.d.ts
|
||||
+++ b/node_modules/@locker/near-membrane-dom/dist/index.mjs.d.ts
|
||||
@@ -4,6 +4,15 @@
|
||||
defaultPolicy?: object;
|
||||
endowments?: PropertyDescriptorMap;
|
||||
globalObjectShape?: object;
|
||||
+ /**
|
||||
+ * Metabase patch: when set, the sandbox iframe loads this URL instead of
|
||||
+ * about:blank. The loaded document keeps its server-sent CSP, which lets
|
||||
+ * the sandbox carry a different (permissive) CSP from the parent page. When
|
||||
+ * this option is supplied, `createIframeVirtualEnvironment` returns a
|
||||
+ * Promise<VirtualEnvironment> so callers can await iframe `load` before
|
||||
+ * accessing the realm.
|
||||
+ */
|
||||
+ iframeSrc?: string;
|
||||
instrumentation?: Instrumentation;
|
||||
keepAlive?: boolean;
|
||||
liveTargetCallback?: LiveTargetCallback;
|
||||
@@ -11,6 +20,7 @@
|
||||
protectDistortions?: boolean;
|
||||
signSourceCallback?: SignSourceCallback;
|
||||
}
|
||||
+declare function createIframeVirtualEnvironment(globalObject: WindowProxy & typeof globalThis, providedOptions: BrowserEnvironmentOptions & { iframeSrc: string }): Promise<VirtualEnvironment>;
|
||||
declare function createIframeVirtualEnvironment(globalObject: WindowProxy & typeof globalThis, providedOptions?: BrowserEnvironmentOptions): VirtualEnvironment;
|
||||
export { createIframeVirtualEnvironment as default, BrowserEnvironmentOptions };
|
||||
export type { Connector, DistortionCallback, Instrumentation, LiveTargetCallback } from '@locker/near-membrane-base';
|
||||
diff --git a/node_modules/@locker/near-membrane-dom/dist/index.cjs.d.ts b/node_modules/@locker/near-membrane-dom/dist/index.cjs.d.ts
|
||||
--- a/node_modules/@locker/near-membrane-dom/dist/index.cjs.d.ts
|
||||
+++ b/node_modules/@locker/near-membrane-dom/dist/index.cjs.d.ts
|
||||
@@ -4,6 +4,15 @@
|
||||
defaultPolicy?: object;
|
||||
endowments?: PropertyDescriptorMap;
|
||||
globalObjectShape?: object;
|
||||
+ /**
|
||||
+ * Metabase patch: when set, the sandbox iframe loads this URL instead of
|
||||
+ * about:blank. The loaded document keeps its server-sent CSP, which lets
|
||||
+ * the sandbox carry a different (permissive) CSP from the parent page. When
|
||||
+ * this option is supplied, `createIframeVirtualEnvironment` returns a
|
||||
+ * Promise<VirtualEnvironment> so callers can await iframe `load` before
|
||||
+ * accessing the realm.
|
||||
+ */
|
||||
+ iframeSrc?: string;
|
||||
instrumentation?: Instrumentation;
|
||||
keepAlive?: boolean;
|
||||
liveTargetCallback?: LiveTargetCallback;
|
||||
@@ -11,6 +20,7 @@
|
||||
protectDistortions?: boolean;
|
||||
signSourceCallback?: SignSourceCallback;
|
||||
}
|
||||
+declare function createIframeVirtualEnvironment(globalObject: WindowProxy & typeof globalThis, providedOptions: BrowserEnvironmentOptions & { iframeSrc: string }): Promise<VirtualEnvironment>;
|
||||
declare function createIframeVirtualEnvironment(globalObject: WindowProxy & typeof globalThis, providedOptions?: BrowserEnvironmentOptions): VirtualEnvironment;
|
||||
export { createIframeVirtualEnvironment as default, BrowserEnvironmentOptions };
|
||||
export type { Connector, DistortionCallback, Instrumentation, LiveTargetCallback } from '@locker/near-membrane-base';
|
||||
Reference in New Issue
Block a user