import type { Penpot } from '@penpot/plugin-types'; import type { createPluginManager } from './plugin-manager'; import { createApi } from './api'; import { ses } from './ses.js'; /** * WeakMap used to track errors originating from plugin code. * Using a WeakMap is safer than extending error objects because: * 1. It works even if the error object is frozen (SES hardened environment) * 2. It doesn't require modifying the error object * 3. It allows garbage collection of error objects when no longer referenced */ const pluginErrors = new WeakMap(); /** * Checks if an error originated from plugin code. */ export function isPluginError(error: unknown): boolean { if (error !== null && typeof error === 'object') { return pluginErrors.has(error as object); } return false; } /** * Marks an error as originating from plugin code. * Uses a WeakMap so it works even if the error object is frozen. */ export function markPluginError(error: unknown): void { if (error !== null && typeof error === 'object') { pluginErrors.set(error as object, true); } } /** * Wraps a handler function to mark any thrown errors as plugin errors. * Errors are marked and re-thrown so they propagate to the global error handler, * where they can be identified and handled appropriately. */ function wrapHandler unknown>( handler: T, ): (...args: Parameters) => ReturnType { return function (...args: Parameters) { try { const result = handler(...args); // Handle async functions - mark errors in the returned promise if (result instanceof Promise) { return result.catch((error: unknown) => { markPluginError(error); throw error; }) as ReturnType; } return result as ReturnType; } catch (error) { markPluginError(error); throw error; } }; } export function createSandbox( plugin: Awaited>, apiExtensions?: object, ) { ses.hardenIntrinsics(); const pluginApi = createApi(plugin); const safeHandler = { get(target: Penpot, prop: string, receiver: unknown) { const originalValue = Reflect.get(target, prop, receiver); if (typeof originalValue === 'function') { return function (...args: unknown[]) { const result = originalValue.apply(target, args); return ses.safeReturn(result); }; } return ses.safeReturn(originalValue); }, }; const proxyApi = new Proxy(pluginApi.penpot, safeHandler); const safeFetch = (url: string, options: RequestInit) => { const sanitizedOptions: RequestInit = { ...options, credentials: 'omit', headers: { ...options?.headers, Authorization: '', }, }; return fetch(url, sanitizedOptions).then((response) => { const safeResponse = { ok: response.ok, status: response.status, statusText: response.statusText, url: response.url, text: response.text.bind(response), json: response.json.bind(response), }; return ses.safeReturn(safeResponse); }); }; let publicPluginApi = { penpot: proxyApi, fetch: ses.harden(safeFetch), setTimeout: ses.harden( (...[handler, timeout]: Parameters) => { const wrappedHandler = wrapHandler( typeof handler === 'function' ? handler : () => {}, ); const timeoutId = setTimeout(wrappedHandler, timeout); plugin.timeouts.add(timeoutId); return ses.safeReturn(timeoutId); }, ) as typeof setTimeout, clearTimeout: ses.harden((id: ReturnType) => { clearTimeout(id); plugin.timeouts.delete(id); }), setInterval: ses.harden( (...[handler, interval]: Parameters) => { const wrappedHandler = wrapHandler( typeof handler === 'function' ? handler : () => {}, ); const intervalId = setInterval(wrappedHandler, interval); plugin.intervals.add(intervalId); return ses.safeReturn(intervalId); }, ) as typeof setInterval, clearInterval: ses.harden((id: ReturnType) => { clearInterval(id); plugin.intervals.delete(id); }), /** * GLOBAL FUNCTIONS ACCESIBLE TO PLUGINS **/ isFinite: ses.harden(isFinite), isNaN: ses.harden(isNaN), parseFloat: ses.harden(parseFloat), parseInt: ses.harden(parseInt), decodeURI: ses.harden(decodeURI), decodeURIComponent: ses.harden(decodeURIComponent), encodeURI: ses.harden(encodeURI), encodeURIComponent: ses.harden(encodeURIComponent), Object: ses.harden(Object), Boolean: ses.harden(Boolean), Symbol: ses.harden(Symbol), Number: ses.harden(Number), BigInt: ses.harden(BigInt), Math: ses.harden(Math), Date: ses.harden(Date), String: ses.harden(String), RegExp: ses.harden(RegExp), Array: ses.harden(Array), Int8Array: ses.harden(Int8Array), Uint8Array: ses.harden(Uint8Array), Uint8ClampedArray: ses.harden(Uint8ClampedArray), Int16Array: ses.harden(Int16Array), Uint16Array: ses.harden(Uint16Array), Int32Array: ses.harden(Int32Array), Uint32Array: ses.harden(Uint32Array), BigInt64Array: ses.harden(BigInt64Array), BigUint64Array: ses.harden(BigUint64Array), Float32Array: ses.harden(Float32Array), Float64Array: ses.harden(Float64Array), Map: ses.harden(Map), Set: ses.harden(Set), WeakMap: ses.harden(WeakMap), WeakSet: ses.harden(WeakSet), ArrayBuffer: ses.harden(ArrayBuffer), DataView: ses.harden(DataView), Atomics: ses.harden(Atomics), JSON: ses.harden(JSON), Promise: ses.harden(Promise), Proxy: ses.harden(Proxy), Intl: ses.harden(Intl), // Window properties console: ses.harden(window.console), devicePixelRatio: window.devicePixelRatio, atob: ses.harden(window.atob.bind(null)), btoa: ses.harden(window.btoa.bind(null)), structuredClone: ses.harden(window.structuredClone), }; if (apiExtensions) { publicPluginApi = Object.assign(publicPluginApi, apiExtensions); } const compartment = ses.createCompartment(publicPluginApi); return { evaluate: () => { compartment.evaluate(plugin.code); }, cleanGlobalThis: () => { Object.keys(publicPluginApi).forEach((key) => { delete compartment.globalThis[key]; }); }, compartment, }; }