mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Add Initial implementation for frontend plugins sandboxing (#68889)
This commit is contained in:
parent
6c7b17b59f
commit
1ed4c0382b
@ -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"],
|
||||
|
@ -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
|
||||
|
||||
|
@ -29,6 +29,8 @@ module.exports = {
|
||||
'\\.svg': '<rootDir>/public/test/mocks/svg.ts',
|
||||
'\\.css': '<rootDir>/public/test/mocks/style.ts',
|
||||
'monaco-editor/esm/vs/editor/editor.api': '<rootDir>/public/test/mocks/monaco.ts',
|
||||
// near-membrane-dom won't work in a nodejs environment.
|
||||
'@locker/near-membrane-dom': '<rootDir>/public/test/mocks/nearMembraneDom.ts',
|
||||
},
|
||||
// Log the test results with dynamic Loki tags. Drone CI only
|
||||
reporters: ['default', ['<rootDir>/public/test/log-reporter.js', { enable: process.env.DRONE === 'true' }]],
|
||||
|
@ -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",
|
||||
|
@ -98,4 +98,5 @@ export interface FeatureToggles {
|
||||
dataSourcePageHeader?: boolean;
|
||||
extraThemes?: boolean;
|
||||
lokiPredefinedOperations?: boolean;
|
||||
pluginsFrontendSandbox?: boolean;
|
||||
}
|
||||
|
@ -81,6 +81,7 @@ export interface PluginMeta<T extends KeyValue = {}> {
|
||||
signatureType?: PluginSignatureType;
|
||||
signatureOrg?: string;
|
||||
live?: boolean;
|
||||
angularDetected?: boolean;
|
||||
}
|
||||
|
||||
interface PluginDependencyInfo {
|
||||
|
@ -29,6 +29,7 @@ export type AppPluginConfig = {
|
||||
path: string;
|
||||
version: string;
|
||||
preload: boolean;
|
||||
angularDetected?: boolean;
|
||||
};
|
||||
|
||||
export class GrafanaBootConfig implements GrafanaConfig {
|
||||
|
@ -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 {
|
||||
|
@ -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() {
|
||||
|
@ -542,5 +542,12 @@ var (
|
||||
State: FeatureStateAlpha,
|
||||
Owner: grafanaObservabilityLogsSquad,
|
||||
},
|
||||
{
|
||||
Name: "pluginsFrontendSandbox",
|
||||
Description: "Enables the plugins frontend sandbox",
|
||||
State: FeatureStateAlpha,
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaPluginsPlatformSquad,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
@ -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
|
||||
|
|
@ -326,4 +326,8 @@ const (
|
||||
// FlagLokiPredefinedOperations
|
||||
// Adds predefined query operations to Loki query editor
|
||||
FlagLokiPredefinedOperations = "lokiPredefinedOperations"
|
||||
|
||||
// FlagPluginsFrontendSandbox
|
||||
// Enables the plugins frontend sandbox
|
||||
FlagPluginsFrontendSandbox = "pluginsFrontendSandbox"
|
||||
)
|
||||
|
@ -37,7 +37,12 @@ export function syncGetPanelPlugin(id: string): PanelPlugin | undefined {
|
||||
}
|
||||
|
||||
function getPanelPlugin(meta: PanelPluginMeta): Promise<PanelPlugin> {
|
||||
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;
|
||||
|
@ -22,7 +22,12 @@ async function preload(config: AppPluginConfig): Promise<PluginPreloadResult> {
|
||||
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) {
|
||||
|
@ -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<any> {
|
||||
export async function importPluginModule({
|
||||
path,
|
||||
version,
|
||||
isAngular,
|
||||
pluginId,
|
||||
}: {
|
||||
path: string;
|
||||
pluginId: string;
|
||||
version?: string;
|
||||
isAngular?: boolean;
|
||||
}): Promise<any> {
|
||||
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<GenericDataSourcePlugin> {
|
||||
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<grafanaData.AppPlugin> {
|
||||
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;
|
||||
|
73
public/app/features/plugins/sandbox/distortion_map.ts
Normal file
73
public/app/features/plugins/sandbox/distortion_map.ts
Normal file
@ -0,0 +1,73 @@
|
||||
type DistortionMap = Map<unknown, unknown>;
|
||||
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;
|
||||
}
|
@ -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<string, unknown>([]);
|
175
public/app/features/plugins/sandbox/sandbox_plugin_loader.ts
Normal file
175
public/app/features/plugins/sandbox/sandbox_plugin_loader.ts
Normal file
@ -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<string, Promise<unknown>>();
|
||||
|
||||
export async function importPluginModuleInSandbox({ pluginId }: { pluginId: string }): Promise<unknown> {
|
||||
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<unknown> {
|
||||
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;
|
||||
}
|
5
public/test/mocks/nearMembraneDom.ts
Normal file
5
public/test/mocks/nearMembraneDom.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default function () {
|
||||
return {
|
||||
evaluate: () => {},
|
||||
};
|
||||
}
|
38
yarn.lock
38
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
|
||||
|
Loading…
Reference in New Issue
Block a user