From 1ed4c0382b3812eecf9722596d8a3ac922f3543d Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Mon, 5 Jun 2023 10:51:36 +0200 Subject: [PATCH] Plugins: Add Initial implementation for frontend plugins sandboxing (#68889) --- .betterer.results | 4 + .../feature-toggles/index.md | 1 + jest.config.js | 2 + package.json | 2 + .../src/types/featureToggles.gen.ts | 1 + packages/grafana-data/src/types/plugin.ts | 1 + packages/grafana-runtime/src/config.ts | 1 + pkg/api/dtos/plugins.go | 13 +- pkg/api/plugins.go | 1 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + .../app/features/plugins/importPanelPlugin.ts | 7 +- .../app/features/plugins/pluginPreloader.ts | 7 +- public/app/features/plugins/plugin_loader.ts | 43 ++++- .../plugins/sandbox/distortion_map.ts | 73 ++++++++ .../plugins/sandbox/plugin_dependencies.ts | 5 + .../plugins/sandbox/sandbox_plugin_loader.ts | 175 ++++++++++++++++++ public/test/mocks/nearMembraneDom.ts | 5 + yarn.lock | 38 ++++ 20 files changed, 380 insertions(+), 11 deletions(-) create mode 100644 public/app/features/plugins/sandbox/distortion_map.ts create mode 100644 public/app/features/plugins/sandbox/plugin_dependencies.ts create mode 100644 public/app/features/plugins/sandbox/sandbox_plugin_loader.ts create mode 100644 public/test/mocks/nearMembraneDom.ts diff --git a/.betterer.results b/.betterer.results index b1d0b44e1e1..3cc54cfff84 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2988,6 +2988,10 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "7"], [0, 0, 0, "Do not use any type assertions.", "8"] ], + "public/app/features/plugins/sandbox/sandbox_plugin_loader.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"] + ], "public/app/features/plugins/sql/components/visual-query-builder/AwesomeQueryBuilder.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index f439d728ea6..349e7de80d6 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -111,6 +111,7 @@ Alpha features might be changed or removed without prior notice. | `pluginsAPIManifestKey` | Use grafana.com API to retrieve the public manifest key | | `extraThemes` | Enables extra themes | | `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor | +| `pluginsFrontendSandbox` | Enables the plugins frontend sandbox | ## Development feature toggles diff --git a/jest.config.js b/jest.config.js index cc8725cf2e5..6db1df5886c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -29,6 +29,8 @@ module.exports = { '\\.svg': '/public/test/mocks/svg.ts', '\\.css': '/public/test/mocks/style.ts', 'monaco-editor/esm/vs/editor/editor.api': '/public/test/mocks/monaco.ts', + // near-membrane-dom won't work in a nodejs environment. + '@locker/near-membrane-dom': '/public/test/mocks/nearMembraneDom.ts', }, // Log the test results with dynamic Loki tags. Drone CI only reporters: ['default', ['/public/test/log-reporter.js', { enable: process.env.DRONE === 'true' }]], diff --git a/package.json b/package.json index 2e2433dba86..67722d81a56 100644 --- a/package.json +++ b/package.json @@ -269,6 +269,8 @@ "@lezer/common": "1.0.2", "@lezer/highlight": "1.1.3", "@lezer/lr": "1.3.3", + "@locker/near-membrane-dom": "^0.12.14", + "@locker/near-membrane-shared": "^0.12.14", "@opentelemetry/api": "1.4.0", "@opentelemetry/exporter-collector": "0.25.0", "@opentelemetry/semantic-conventions": "1.9.1", diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 8780d28c1b9..1acece10928 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -98,4 +98,5 @@ export interface FeatureToggles { dataSourcePageHeader?: boolean; extraThemes?: boolean; lokiPredefinedOperations?: boolean; + pluginsFrontendSandbox?: boolean; } diff --git a/packages/grafana-data/src/types/plugin.ts b/packages/grafana-data/src/types/plugin.ts index 06aaf8a4a4c..42f18275064 100644 --- a/packages/grafana-data/src/types/plugin.ts +++ b/packages/grafana-data/src/types/plugin.ts @@ -81,6 +81,7 @@ export interface PluginMeta { signatureType?: PluginSignatureType; signatureOrg?: string; live?: boolean; + angularDetected?: boolean; } interface PluginDependencyInfo { diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 5fefab38caf..d1cf3406009 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -29,6 +29,7 @@ export type AppPluginConfig = { path: string; version: string; preload: boolean; + angularDetected?: boolean; }; export class GrafanaBootConfig implements GrafanaConfig { diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index 6ed45388327..b12aa87b647 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -20,12 +20,13 @@ type PluginSetting struct { SecureJsonFields map[string]bool `json:"secureJsonFields"` DefaultNavUrl string `json:"defaultNavUrl"` - LatestVersion string `json:"latestVersion"` - HasUpdate bool `json:"hasUpdate"` - State plugins.ReleaseState `json:"state"` - Signature plugins.SignatureStatus `json:"signature"` - SignatureType plugins.SignatureType `json:"signatureType"` - SignatureOrg string `json:"signatureOrg"` + LatestVersion string `json:"latestVersion"` + HasUpdate bool `json:"hasUpdate"` + State plugins.ReleaseState `json:"state"` + Signature plugins.SignatureStatus `json:"signature"` + SignatureType plugins.SignatureType `json:"signatureType"` + SignatureOrg string `json:"signatureOrg"` + AngularDetected bool `json:"angularDetected"` } type PluginListItem struct { diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 6d1fd5af37b..6451af12b78 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -196,6 +196,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *contextmodel.ReqContext) response. SignatureType: plugin.SignatureType, SignatureOrg: plugin.SignatureOrg, SecureJsonFields: map[string]bool{}, + AngularDetected: plugin.AngularDetected, } if plugin.IsApp() { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index bf0d55d69e3..4738005065f 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -542,5 +542,12 @@ var ( State: FeatureStateAlpha, Owner: grafanaObservabilityLogsSquad, }, + { + Name: "pluginsFrontendSandbox", + Description: "Enables the plugins frontend sandbox", + State: FeatureStateAlpha, + FrontendOnly: true, + Owner: grafanaPluginsPlatformSquad, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index e0a84dbf47c..b7ea1e85c78 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -79,3 +79,4 @@ enableDatagridEditing,beta,@grafana/grafana-bi-squad,false,false,false,true dataSourcePageHeader,beta,@grafana/enterprise-datasources,false,false,false,true extraThemes,alpha,@grafana/user-essentials,false,false,false,true lokiPredefinedOperations,alpha,@grafana/observability-logs,false,false,false,true +pluginsFrontendSandbox,alpha,@grafana/plugins-platform-backend,false,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 7cc3435dc17..dcbe72b95a3 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -326,4 +326,8 @@ const ( // FlagLokiPredefinedOperations // Adds predefined query operations to Loki query editor FlagLokiPredefinedOperations = "lokiPredefinedOperations" + + // FlagPluginsFrontendSandbox + // Enables the plugins frontend sandbox + FlagPluginsFrontendSandbox = "pluginsFrontendSandbox" ) diff --git a/public/app/features/plugins/importPanelPlugin.ts b/public/app/features/plugins/importPanelPlugin.ts index 43dcd57821e..e6a708789c2 100644 --- a/public/app/features/plugins/importPanelPlugin.ts +++ b/public/app/features/plugins/importPanelPlugin.ts @@ -37,7 +37,12 @@ export function syncGetPanelPlugin(id: string): PanelPlugin | undefined { } function getPanelPlugin(meta: PanelPluginMeta): Promise { - return importPluginModule(meta.module, meta.info?.version) + return importPluginModule({ + path: meta.module, + version: meta.info?.version, + isAngular: meta.angularDetected, + pluginId: meta.id, + }) .then((pluginExports) => { if (pluginExports.plugin) { return pluginExports.plugin as PanelPlugin; diff --git a/public/app/features/plugins/pluginPreloader.ts b/public/app/features/plugins/pluginPreloader.ts index 78a98f49f77..205a359eaab 100644 --- a/public/app/features/plugins/pluginPreloader.ts +++ b/public/app/features/plugins/pluginPreloader.ts @@ -22,7 +22,12 @@ async function preload(config: AppPluginConfig): Promise { const { path, version, id: pluginId } = config; try { startMeasure(`frontend_plugin_preload_${pluginId}`); - const { plugin } = await pluginLoader.importPluginModule(path, version); + const { plugin } = await pluginLoader.importPluginModule({ + path, + version, + isAngular: config.angularDetected, + pluginId, + }); const { extensionConfigs = [] } = plugin; return { pluginId, extensionConfigs }; } catch (error) { diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index 6c1981f245e..c0e7885c7d9 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -33,6 +33,8 @@ import * as ticks from 'app/core/utils/ticks'; import { GenericDataSourcePlugin } from '../datasources/types'; import builtInPlugins from './built_in_plugins'; +import { sandboxPluginDependencies } from './sandbox/plugin_dependencies'; +import { importPluginModuleInSandbox } from './sandbox/sandbox_plugin_loader'; import { locateFromCDN, translateForCDN } from './systemjsPlugins/pluginCDN'; import { fetchCSS, locateCSS } from './systemjsPlugins/pluginCSS'; import { locateWithCache, registerPluginInCache } from './systemjsPlugins/pluginCacheBuster'; @@ -88,6 +90,11 @@ export function exposeToPlugin(name: string, component: any) { grafanaRuntime.SystemJS.registerDynamic(name, [], true, (require: any, exports: any, module: { exports: any }) => { module.exports = component; }); + + // exposes this dependency to sandboxed plugins too. + // the following sandboxPluginDependencies don't depend or interact + // with SystemJS in any way. + sandboxPluginDependencies.set(name, component); } exposeToPlugin('@grafana/data', grafanaData); @@ -186,7 +193,17 @@ for (const flotDep of flotDeps) { exposeToPlugin(flotDep, { fakeDep: 1 }); } -export async function importPluginModule(path: string, version?: string): Promise { +export async function importPluginModule({ + path, + version, + isAngular, + pluginId, +}: { + path: string; + pluginId: string; + version?: string; + isAngular?: boolean; +}): Promise { if (version) { registerPluginInCache({ path, version }); } @@ -200,11 +217,26 @@ export async function importPluginModule(path: string, version?: string): Promis return builtIn; } } + + // the sandboxing environment code cannot work in nodejs and requires a real browser + if (isFrontendSandboxSupported(isAngular)) { + return importPluginModuleInSandbox({ pluginId }); + } + return grafanaRuntime.SystemJS.import(path); } +function isFrontendSandboxSupported(isAngular?: boolean): boolean { + return !isAngular && Boolean(config.featureToggles.pluginsFrontendSandbox) && process.env.NODE_ENV !== 'test'; +} + export function importDataSourcePlugin(meta: grafanaData.DataSourcePluginMeta): Promise { - return importPluginModule(meta.module, meta.info?.version).then((pluginExports) => { + return importPluginModule({ + path: meta.module, + version: meta.info?.version, + isAngular: meta.angularDetected, + pluginId: meta.id, + }).then((pluginExports) => { if (pluginExports.plugin) { const dsPlugin = pluginExports.plugin as GenericDataSourcePlugin; dsPlugin.meta = meta; @@ -227,7 +259,12 @@ export function importDataSourcePlugin(meta: grafanaData.DataSourcePluginMeta): } export function importAppPlugin(meta: grafanaData.PluginMeta): Promise { - return importPluginModule(meta.module, meta.info?.version).then((pluginExports) => { + return importPluginModule({ + path: meta.module, + version: meta.info?.version, + isAngular: meta.angularDetected, + pluginId: meta.id, + }).then((pluginExports) => { const plugin = pluginExports.plugin ? (pluginExports.plugin as grafanaData.AppPlugin) : new grafanaData.AppPlugin(); plugin.init(meta); plugin.meta = meta; diff --git a/public/app/features/plugins/sandbox/distortion_map.ts b/public/app/features/plugins/sandbox/distortion_map.ts new file mode 100644 index 00000000000..b62495ff655 --- /dev/null +++ b/public/app/features/plugins/sandbox/distortion_map.ts @@ -0,0 +1,73 @@ +type DistortionMap = Map; +const generalDistortionMap: DistortionMap = new Map(); + +function failToSet() { + throw new Error('Plugins are not allowed to set sandboxed properties'); +} + +// sets distortion to protect iframe elements +function distortIframeAttributes(distortions: DistortionMap) { + const iframeHtmlForbiddenProperties = ['contentDocument', 'contentWindow', 'src', 'srcdoc', 'srcObject', 'srcset']; + + for (const property of iframeHtmlForbiddenProperties) { + const descriptor = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, property); + if (descriptor) { + function fail() { + throw new Error('iframe.' + property + ' is not allowed in sandboxed plugins'); + } + if (descriptor.value) { + distortions.set(descriptor.value, fail); + } + if (descriptor.set) { + distortions.set(descriptor.set, fail); + } + if (descriptor.get) { + distortions.set(descriptor.get, fail); + } + } + } +} + +function distortConsole(distortions: DistortionMap) { + // distorts window.console to prefix it + const descriptor = Object.getOwnPropertyDescriptor(window, 'console'); + if (descriptor?.value) { + function sandboxLog(...args: unknown[]) { + console.log(`[plugin]`, ...args); + } + const sandboxConsole = { + log: sandboxLog, + warn: sandboxLog, + error: sandboxLog, + info: sandboxLog, + debug: sandboxLog, + }; + + distortions.set(descriptor.value, sandboxConsole); + } + if (descriptor?.set) { + distortions.set(descriptor.set, failToSet); + } +} + +function distortAlert(distortions: DistortionMap) { + const descriptor = Object.getOwnPropertyDescriptor(window, 'alert'); + if (descriptor?.value) { + function sandboxAlert(...args: unknown[]) { + console.log(`[plugin]`, ...args); + } + distortions.set(descriptor.value, sandboxAlert); + } + if (descriptor?.set) { + distortions.set(descriptor.set, failToSet); + } +} + +export function getGeneralSandboxDistortionMap() { + if (generalDistortionMap.size === 0) { + distortIframeAttributes(generalDistortionMap); + distortConsole(generalDistortionMap); + distortAlert(generalDistortionMap); + } + return generalDistortionMap; +} diff --git a/public/app/features/plugins/sandbox/plugin_dependencies.ts b/public/app/features/plugins/sandbox/plugin_dependencies.ts new file mode 100644 index 00000000000..eab8cd0418d --- /dev/null +++ b/public/app/features/plugins/sandbox/plugin_dependencies.ts @@ -0,0 +1,5 @@ +/** + * Map with all dependencies that are exposed to plugins sandbox + * e.g.: @grafana/ui, @grafana/data, etc... + */ +export const sandboxPluginDependencies = new Map([]); diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts b/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts new file mode 100644 index 00000000000..80d5e86ff1f --- /dev/null +++ b/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts @@ -0,0 +1,175 @@ +import createVirtualEnvironment from '@locker/near-membrane-dom'; +import { ProxyTarget } from '@locker/near-membrane-shared'; + +import { GrafanaPlugin, PluginMeta } from '@grafana/data'; + +import { getPluginSettings } from '../pluginSettings'; + +import { getGeneralSandboxDistortionMap } from './distortion_map'; +import { sandboxPluginDependencies } from './plugin_dependencies'; + +type CompartmentDependencyModule = unknown; +type PluginFactoryFunction = (...args: CompartmentDependencyModule[]) => { + plugin: GrafanaPlugin; +}; + +const pluginImportCache = new Map>(); + +export async function importPluginModuleInSandbox({ pluginId }: { pluginId: string }): Promise { + try { + const pluginMeta = await getPluginSettings(pluginId); + if (!pluginImportCache.has(pluginId)) { + pluginImportCache.set(pluginId, doImportPluginModuleInSandbox(pluginMeta)); + } + return pluginImportCache.get(pluginId); + } catch (e) { + throw new Error(`Could not import plugin ${pluginId} inside sandbox: ` + e); + } +} + +async function doImportPluginModuleInSandbox(meta: PluginMeta): Promise { + const generalDistortionMap = getGeneralSandboxDistortionMap(); + + function distortionCallback(v: ProxyTarget): ProxyTarget { + return generalDistortionMap.get(v) ?? v; + } + + return new Promise(async (resolve, reject) => { + // each plugin has its own sandbox + const sandboxEnvironment = createVirtualEnvironment(window, { + // distortions are interceptors to modify the behavior of objects when + // the code inside the sandbox tries to access them + distortionCallback, + // endowments are custom variables we make available to plugins in their window object + endowments: Object.getOwnPropertyDescriptors({ + // Plugins builds use the AMD module system. Their code consists + // of a single function call to `define()` that internally contains all the plugin code. + // This is that `define` function the plugin will call. + // More info about how AMD works https://github.com/amdjs/amdjs-api/blob/master/AMD.md + // Plugins code normally use the "anonymous module" signature: define(depencies, factoryFunction) + define( + idOrDependencies: string | string[], + maybeDependencies: string[] | PluginFactoryFunction, + maybeFactory?: PluginFactoryFunction + ): void { + let dependencies: string[]; + let factory: PluginFactoryFunction; + if (Array.isArray(idOrDependencies)) { + dependencies = idOrDependencies; + factory = maybeDependencies as PluginFactoryFunction; + } else { + dependencies = maybeDependencies as string[]; + factory = maybeFactory!; + } + + try { + const resolvedDeps = resolvePluginDependencies(dependencies); + // execute the plugin's code + const pluginExports: { plugin: GrafanaPlugin } = factory.apply(null, resolvedDeps); + // only after the plugin has been executed + // we can return the plugin exports. + // This is what grafana effectively gets. + resolve(pluginExports); + } catch (e) { + reject(new Error(`Could not execute plugin ${meta.id}: ` + e)); + } + }, + }), + // This improves the error message output for plugins + // because errors thrown inside of the sandbox have a stack + // trace that is difficult to read due to all the sandboxing + // layers. + instrumentation: { + // near-membrane concept of "activity" is something that happens inside + // the plugin instrumentation + startActivity() { + return { + stop: () => {}, + error: getActivityErrorHandler(meta.id), + }; + }, + log: () => {}, + error: () => {}, + }, + }); + + // fetch and evalute the plugin code inside the sandbox + try { + let pluginCode = await getPluginCode(meta.module); + pluginCode = patchPluginSourceMap(meta, pluginCode); + + // runs the code inside the sandbox environment + // this evaluate will eventually run the `define` function inside + // of endowments. + sandboxEnvironment.evaluate(pluginCode); + } catch (e) { + reject(new Error(`Could not execute plugin ${meta.id}: ` + e)); + } + }); +} + +async function getPluginCode(modulePath: string) { + const response = await fetch('public/' + modulePath + '.js'); + return await response.text(); +} + +function getActivityErrorHandler(pluginId: string) { + return async function error(proxyError?: Error & { sandboxError?: boolean }) { + if (!proxyError) { + return; + } + // flag this error as a sandbox error + proxyError.sandboxError = true; + + // create a new error to unwrap it from the proxy + const newError = new Error(proxyError.message.toString()); + newError.name = proxyError.name.toString(); + newError.stack = proxyError.stack || ''; + + // If you are seeing this is because + // the plugin is throwing an error + // and it is not being caught by the plugin code + // This is a sandbox wrapper error. + // and not the real error + console.log(`[sandbox] Error from plugin ${pluginId}`); + console.error(newError); + }; +} + +/** + * Patches the plugin's module.js source code references to sourcemaps to include the full url + * of the module.js file instead of the regular relative reference. + * + * Because the plugin module.js code is loaded via fetch and then "eval" as a string + * it can't find the references to the module.js.map directly and we need to patch it + * to point to the correct location + */ +function patchPluginSourceMap(meta: PluginMeta, pluginCode: string): string { + // skips inlined and files without source maps + if (pluginCode.includes('//# sourceMappingURL=module.js.map')) { + let replaceWith = ''; + // make sure we don't add the sourceURL twice + if (!pluginCode.includes('//# sourceURL') || !pluginCode.includes('//@ sourceUrl')) { + replaceWith += `//# sourceURL=module.js\n`; + } + // modify the source map url to point to the correct location + const sourceCodeMapUrl = `/public/${meta.module}.js.map`; + replaceWith += `//# sourceMappingURL=${sourceCodeMapUrl}`; + + return pluginCode.replace('//# sourceMappingURL=module.js.map', replaceWith); + } + return pluginCode; +} + +function resolvePluginDependencies(deps: string[]) { + // resolve dependencies + const resolvedDeps: CompartmentDependencyModule[] = []; + for (const dep of deps) { + const resolvedDep = sandboxPluginDependencies.get(dep); + if (!resolvedDep) { + throw new Error(`[sandbox] Could not resolve dependency ${dep}`); + } + resolvedDeps.push(resolvedDep); + } + return resolvedDeps; +} diff --git a/public/test/mocks/nearMembraneDom.ts b/public/test/mocks/nearMembraneDom.ts new file mode 100644 index 00000000000..1ae37537109 --- /dev/null +++ b/public/test/mocks/nearMembraneDom.ts @@ -0,0 +1,5 @@ +export default function () { + return { + evaluate: () => {}, + }; +} diff --git a/yarn.lock b/yarn.lock index c044ee8089f..e3903a327f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5195,6 +5195,42 @@ __metadata: languageName: node linkType: hard +"@locker/near-membrane-base@npm:0.12.14": + version: 0.12.14 + resolution: "@locker/near-membrane-base@npm:0.12.14" + dependencies: + "@locker/near-membrane-shared": 0.12.14 + checksum: f6878bbb59bf5241632e610c521be9138cc039d7983d2f9b3aeb670483446364a1b92d407f93fc1ae2834093d033f4223752cb9fb012a48f4b2eb985974a425c + languageName: node + linkType: hard + +"@locker/near-membrane-dom@npm:^0.12.14": + version: 0.12.14 + resolution: "@locker/near-membrane-dom@npm:0.12.14" + dependencies: + "@locker/near-membrane-base": 0.12.14 + "@locker/near-membrane-shared": 0.12.14 + "@locker/near-membrane-shared-dom": 0.12.14 + checksum: fa8178feaa691fcd5c18405387b1296945da7b13ee8fade2efcbd1bbcd57dd17e2015ca70fe41807ae0c2892aa542286a5c07ac4c37c59032333642b7ae4628b + languageName: node + linkType: hard + +"@locker/near-membrane-shared-dom@npm:0.12.14": + version: 0.12.14 + resolution: "@locker/near-membrane-shared-dom@npm:0.12.14" + dependencies: + "@locker/near-membrane-shared": 0.12.14 + checksum: 7e3e6352b0f4aa3306e1b1f49f11b5ddc0c9820dd84d8317b68545d949fa583b2f7b3ab0c56446769e7d993431d1baf3245fd27ff255ee3e1c2c95d3e5c1876c + languageName: node + linkType: hard + +"@locker/near-membrane-shared@npm:0.12.14, @locker/near-membrane-shared@npm:^0.12.14": + version: 0.12.14 + resolution: "@locker/near-membrane-shared@npm:0.12.14" + checksum: f5e75ae422b5369ba5323a72e7cdb979ccc2178c7202a6e85b7e9adaf9971a367de413503cd1e743672798afec1f53836bdbe849acadfb640e41d93ee0125194 + languageName: node + linkType: hard + "@mapbox/jsonlint-lines-primitives@npm:~2.0.2": version: 2.0.2 resolution: "@mapbox/jsonlint-lines-primitives@npm:2.0.2" @@ -18162,6 +18198,8 @@ __metadata: "@lezer/common": 1.0.2 "@lezer/highlight": 1.1.3 "@lezer/lr": 1.3.3 + "@locker/near-membrane-dom": ^0.12.14 + "@locker/near-membrane-shared": ^0.12.14 "@opentelemetry/api": 1.4.0 "@opentelemetry/exporter-collector": 0.25.0 "@opentelemetry/semantic-conventions": 1.9.1