"use strict"; const fs = require("node:fs/promises"); const { randomUUID } = require("node:crypto"); const { isPluginDevelopmentEnabled } = require("./constants.cjs"); const { raceWithAbort } = require("./rpcRouter.cjs"); const { INLINE_SYNC_OBJECT_SAFE_BYTES, MAX_SYNC_OBJECT_BYTES, STREAM_WINDOW_BYTES, } = require("./extensionProviderService.cjs"); const { assertSecretKey } = require("./secretStore.cjs"); const CHANNELS = Object.freeze({ status: "netcatty:plugins:status", list: "netcatty:plugins:list", install: "netcatty:plugins:install", setEnabled: "netcatty:plugins:set-enabled", restart: "netcatty:plugins:restart", uninstall: "netcatty:plugins:uninstall", contributions: "netcatty:plugins:contributions", contributionIcon: "netcatty:plugins:contribution-icon", contributionsChanged: "netcatty:plugins:contributions-changed", executeCommand: "netcatty:plugins:execute-command", updateSetting: "netcatty:plugins:update-setting", resetSetting: "netcatty:plugins:reset-setting", setEnvironment: "netcatty:plugins:set-environment", terminalProviders: "netcatty:plugins:terminal-providers", terminalProvide: "netcatty:plugins:terminal-provide", terminalCancel: "netcatty:plugins:terminal-cancel", terminalSessionEvent: "netcatty:plugins:terminal-session-event", extensionProviders: "netcatty:plugins:extension-providers", extensionInvoke: "netcatty:plugins:extension-invoke", extensionCancel: "netcatty:plugins:extension-cancel", syncConnect: "netcatty:plugins:sync-connect", syncDisconnect: "netcatty:plugins:sync-disconnect", syncGetAccount: "netcatty:plugins:sync-get-account", syncGetCapabilities: "netcatty:plugins:sync-get-capabilities", syncReadObject: "netcatty:plugins:sync-read-object", syncReadChunk: "netcatty:plugins:sync-read-chunk", syncWriteObject: "netcatty:plugins:sync-write-object", syncWriteBegin: "netcatty:plugins:sync-write-begin", syncWriteChunk: "netcatty:plugins:sync-write-chunk", syncWriteCommit: "netcatty:plugins:sync-write-commit", syncDeleteObject: "netcatty:plugins:sync-delete-object", syncPutSecret: "netcatty:plugins:sync-put-secret", syncDeleteSecrets: "netcatty:plugins:sync-delete-secrets", syncRestoreSecrets: "netcatty:plugins:sync-restore-secrets", syncSidecarsCollect: "netcatty:plugins:sync-sidecars-collect", syncSidecarsApply: "netcatty:plugins:sync-sidecars-apply", hostAvailableSync: "netcatty:plugins:host-available-sync", connectionStart: "netcatty:plugins:connection-start", connectionWrite: "netcatty:plugins:connection-write", connectionControl: "netcatty:plugins:connection-control", connectionData: "netcatty:plugins:connection-data", connectionClosed: "netcatty:plugins:connection-closed", credentialCatalogUpdate: "netcatty:plugins:credential-catalog-update", authenticationChallenge: "netcatty:plugins:authentication-challenge", authenticationRespond: "netcatty:plugins:authentication-respond", importerDetect: "netcatty:plugins:importer-detect", importerSelectFile: "netcatty:plugins:importer-select-file", importerReleaseFile: "netcatty:plugins:importer-release-file", importerParseFile: "netcatty:plugins:importer-parse-file", importerProgress: "netcatty:plugins:importer-progress", openView: "netcatty:plugins:open-view", closeView: "netcatty:plugins:close-view", setViewBounds: "netcatty:plugins:set-view-bounds", setViewVisibility: "netcatty:plugins:set-view-visibility", viewMessage: "netcatty:plugins:view-message", viewMessagePosted: "netcatty:plugins:view-message-posted", viewClosed: "netcatty:plugins:view-closed", getScopeCatalog: "netcatty:plugins:get-scope-catalog", setScopeCatalog: "netcatty:plugins:set-scope-catalog", scopeCatalogChanged: "netcatty:plugins:scope-catalog-changed", }); const SCOPE_KINDS = Object.freeze(["workspace", "host", "session", "device"]); const MAX_ACTIVE_TERMINAL_REQUESTS_PER_SENDER = 64; const MAX_SYNC_TRANSFERS_PER_SENDER = 8; const SYNC_TRANSFER_TTL_MS = 5 * 60_000; const MAX_IMPORT_FILE_BYTES = 64 * 1024 * 1024; const MAX_IMPORT_SELECTIONS_PER_SENDER = 8; const IMPORT_SELECTION_TTL_MS = 5 * 60_000; const DEFAULT_CONNECTION_STATUS_POLL_MS = 500; function boundedErrorMessage(value, fallback = "Plugin connection failed") { const message = typeof value === "string" && value ? value : value && typeof value === "object" && typeof value.message === "string" && value.message ? value.message : fallback; return String(message).slice(0, 2048); } function connectionOutputCloseDetails(reason) { if (reason === "end") return { reason: "exited", exitCode: 0 }; if (typeof reason === "string") return { reason: "closed" }; if (reason && typeof reason === "object") { return { reason: "error", error: boundedErrorMessage(reason, "Plugin connection output stream failed"), }; } return { reason: "closed" }; } function boundedConnectionDiagnostics(value) { if (!Array.isArray(value)) return []; return value.slice(0, 256).flatMap((issue) => { if (!issue || typeof issue !== "object" || Array.isArray(issue)) return []; const severity = issue.severity === "error" ? "error" : "warning"; const message = typeof issue.message === "string" && issue.message ? issue.message.slice(0, 2048) : ""; if (!message) return []; const code = typeof issue.code === "string" && issue.code ? issue.code.slice(0, 128) : undefined; const path = typeof issue.path === "string" && issue.path ? issue.path.slice(0, 1024) : undefined; return [Object.freeze({ severity, message, ...(code ? { code } : {}), ...(path ? { path } : {}), })]; }); } function connectionStatusCloseDetails(status) { const diagnostics = boundedConnectionDiagnostics(status?.diagnostics); return { reason: status?.status === "error" ? "error" : "closed", ...(status?.message ? { error: boundedErrorMessage(status.message) } : {}), ...(diagnostics.length > 0 ? { diagnostics } : {}), }; } function normalizePluginScopeCatalog(value) { const source = value && typeof value === "object" ? value : {}; const result = {}; let total = 0; for (const kind of SCOPE_KINDS) { const entries = Array.isArray(source[kind]) ? source[kind] : []; const seen = new Set(); result[kind] = []; for (const entry of entries) { const id = typeof entry?.id === "string" ? entry.id.trim() : ""; const label = typeof entry?.label === "string" ? entry.label.trim() : ""; if (!id || id.length > 256 || id.includes("\0") || !label || label.length > 512 || seen.has(id)) continue; if (++total > 4096) throw new TypeError("Plugin setting scope catalog is too large"); seen.add(id); result[kind].push(Object.freeze({ id, label })); } Object.freeze(result[kind]); } return Object.freeze(result); } function mergePluginScopeCatalogs(catalogs) { const result = {}; let total = 0; for (const kind of SCOPE_KINDS) { const seen = new Set(); result[kind] = []; for (const catalog of catalogs) { for (const entry of catalog?.[kind] ?? []) { if (seen.has(entry.id) || total >= 4096) continue; seen.add(entry.id); total += 1; result[kind].push(entry); } } Object.freeze(result[kind]); } return Object.freeze(result); } function createTrustedPluginBridgeSender(options = {}) { const devServerOrigin = options.devServerUrl ? new URL(options.devServerUrl).origin : null; return (event) => { const senderUrl = event?.senderFrame?.url || event?.sender?.getURL?.() || ""; try { const url = new URL(senderUrl); if (url.protocol === "app:" && url.hostname === "netcatty") return true; return Boolean(devServerOrigin && url.origin === devServerOrigin); } catch { return false; } }; } function registerPluginBridge(ipcMain, options) { const manager = options.manager; const contributionService = options.contributionService; const terminalProviderService = options.terminalProviderService; const terminalDataPipelineService = options.terminalDataPipelineService; const extensionProviderService = options.extensionProviderService; const syncSidecarService = options.syncSidecarService; const credentialResolver = options.credentialResolver; const secretStore = options.secretStore; const connectionStatusPollMs = Number.isSafeInteger(options.connectionStatusPollMs) && options.connectionStatusPollMs >= 0 && options.connectionStatusPollMs <= 60_000 ? options.connectionStatusPollMs : DEFAULT_CONNECTION_STATUS_POLL_MS; const getTerminalWorkerManager = options.getTerminalWorkerManager ?? (() => null); const selectImporterFile = options.selectImporterFile; const viewHost = options.viewHost; const env = options.env ?? process.env; const isTrustedSender = options.isTrustedSender; const defaultScopeCatalog = normalizePluginScopeCatalog({ device: [{ id: "device", label: "This device" }] }); const scopeCatalogs = new Map(); const scopeCatalogOwners = new Map(); const observedScopeCatalogSenders = new WeakSet(); const terminalRequestsBySender = new WeakMap(); const observedTerminalRequestSenders = new WeakSet(); const extensionRequestsBySender = new WeakMap(); const observedExtensionRequestSenders = new WeakSet(); const connectionSessionsBySender = new WeakMap(); const connectionMonitorsBySender = new WeakMap(); const observedConnectionSenders = new WeakSet(); const authenticationChallengesBySender = new WeakMap(); const observedAuthenticationSenders = new WeakSet(); const importerSelectionsBySender = new WeakMap(); const observedImporterSelectionSenders = new WeakSet(); const syncTransfersBySender = new WeakMap(); const observedSyncTransferSenders = new WeakSet(); const scopeCatalogSenderKey = (event) => { const id = event?.sender?.id; return Number.isSafeInteger(id) && id > 0 ? id : "default"; }; const currentScopeCatalog = () => mergePluginScopeCatalogs([ ...scopeCatalogs.values(), defaultScopeCatalog, ]); const publishScopeCatalog = (event) => { const catalog = currentScopeCatalog(); if (typeof options.broadcast === "function") options.broadcast(CHANNELS.scopeCatalogChanged, catalog); else event?.sender?.send?.(CHANNELS.scopeCatalogChanged, catalog); }; const observeScopeCatalogSender = (event, key) => { const sender = event?.sender; if (!sender || typeof sender !== "object" || observedScopeCatalogSenders.has(sender)) return; observedScopeCatalogSenders.add(sender); sender.once?.("destroyed", () => { if (scopeCatalogOwners.get(key) !== sender) return; scopeCatalogOwners.delete(key); if (scopeCatalogs.delete(key)) publishScopeCatalog(); }); }; const configured = isPluginDevelopmentEnabled(env) && Boolean(manager) && typeof manager.initialize === "function"; const resolveManager = async () => { if (!configured) throw new Error("Plugin development runtime is disabled or unavailable"); try { await manager.initialize(); } catch (cause) { throw new Error("Plugin development runtime is disabled or unavailable", { cause }); } return manager; }; const handle = (channel, callback) => { ipcMain.handle(channel, async (event, payload) => { if (!isTrustedSender(event)) throw new Error("Untrusted plugin management sender"); const activeManager = await resolveManager(); return callback(activeManager, payload, event); }); }; // These channels are called by ordinary host/terminal lifecycles even when // the experimental plugin runtime is disabled. Treat the disabled runtime as // an empty provider set instead of throwing (and making Electron print a // stack trace) on every terminal mount, Vault refresh, and scope update. const handlePassive = (channel, fallback, callback) => { ipcMain.handle(channel, async (event, payload) => { if (!isTrustedSender(event)) throw new Error("Untrusted plugin management sender"); let activeManager; try { activeManager = await resolveManager(); } catch { return typeof fallback === "function" ? fallback(payload, event) : fallback; } return callback(activeManager, payload, event); }); }; const terminalRequestMap = (sender) => { if (!sender || typeof sender !== "object") throw new Error("Plugin terminal request sender is unavailable"); let requests = terminalRequestsBySender.get(sender); if (!requests) { requests = new Map(); terminalRequestsBySender.set(sender, requests); } if (!observedTerminalRequestSenders.has(sender)) { observedTerminalRequestSenders.add(sender); sender.once?.("destroyed", () => { for (const controller of requests.values()) controller.abort(); requests.clear(); }); } return requests; }; const extensionRequestMap = (sender) => { if (!sender || typeof sender !== "object") throw new Error("Plugin extension request sender is unavailable"); let requests = extensionRequestsBySender.get(sender); if (!requests) { requests = new Map(); extensionRequestsBySender.set(sender, requests); } if (!observedExtensionRequestSenders.has(sender)) { observedExtensionRequestSenders.add(sender); sender.once?.("destroyed", () => { for (const controller of requests.values()) controller.abort(); requests.clear(); }); } return requests; }; const runExtensionRequest = async (event, payload, callback) => { if (!isTrustedSender(event)) throw new Error("Untrusted plugin management sender"); const requestId = payload?.requestId; if (typeof requestId !== "string" || requestId.length < 1 || requestId.length > 128 || requestId.includes("\0")) { throw new TypeError("Plugin extension request ID is invalid"); } const requests = extensionRequestMap(event.sender); if (requests.has(requestId)) throw new Error("Plugin extension request ID is already active"); if (requests.size >= MAX_ACTIVE_TERMINAL_REQUESTS_PER_SENDER) { throw new Error("Too many active Plugin extension requests"); } const controller = new AbortController(); requests.set(requestId, controller); try { await raceWithAbort(resolveManager(), controller.signal); return await callback(controller.signal); } finally { if (requests.get(requestId) === controller) requests.delete(requestId); } }; const connectionSessionMap = (sender) => { if (!sender || typeof sender !== "object") throw new Error("Plugin connection sender is unavailable"); let sessions = connectionSessionsBySender.get(sender); if (!sessions) { sessions = new Map(); connectionSessionsBySender.set(sender, sessions); } if (!observedConnectionSenders.has(sender)) { observedConnectionSenders.add(sender); sender.once?.("destroyed", () => { const monitors = connectionMonitorsBySender.get(sender); for (const monitor of monitors?.values?.() ?? []) monitor.controller.abort(); monitors?.clear?.(); const ownedSessions = [...sessions]; sessions.clear(); for (const [sessionId, sessionOwner] of ownedSessions) { void (async () => { try { await extensionProviderService?.control?.(sessionId, "close", {}, { sessionOwner }); } catch { extensionProviderService?.closeSessionLocal?.(sessionId, undefined, sessionOwner); } try { await getTerminalWorkerManager()?.finishExternalSession?.(sessionId, { reason: "closed" }, sessionOwner); } catch {} })(); } }); } return sessions; }; const connectionMonitorMap = (sender) => { if (!sender || typeof sender !== "object") throw new Error("Plugin connection sender is unavailable"); let monitors = connectionMonitorsBySender.get(sender); if (!monitors) { monitors = new Map(); connectionMonitorsBySender.set(sender, monitors); } return monitors; }; const authenticationChallengeMap = (sender) => { if (!sender || typeof sender !== "object") throw new Error("Plugin authentication sender is unavailable"); let challenges = authenticationChallengesBySender.get(sender); if (!challenges) { challenges = new Map(); authenticationChallengesBySender.set(sender, challenges); } if (!observedAuthenticationSenders.has(sender)) { observedAuthenticationSenders.add(sender); sender.once?.("destroyed", () => { for (const pending of challenges.values()) pending.reject(new Error("Plugin authentication window closed")); challenges.clear(); }); } return challenges; }; const requestAuthenticationChallenge = (event, requestId, challenge, signal) => { const challenges = authenticationChallengeMap(event.sender); const challengeRequestId = randomUUID(); return new Promise((resolve, reject) => { const sendCancellation = () => { try { event.sender.send(CHANNELS.authenticationChallenge, { requestId, challengeRequestId, challengeId: challenge.id, cancelled: true, }); } catch {} }; const onAbort = () => { if (!challenges.delete(challengeRequestId)) return; signal?.removeEventListener?.("abort", onAbort); sendCancellation(); reject(signal.reason ?? new DOMException("Aborted", "AbortError")); }; const finish = (callback, value) => { signal?.removeEventListener?.("abort", onAbort); callback(value); }; challenges.set(challengeRequestId, { requestId, challengeId: challenge.id, resolve: (value) => finish(resolve, value), reject: (error) => finish(reject, error), }); if (signal?.aborted) { onAbort(); return; } signal?.addEventListener?.("abort", onAbort, { once: true }); try { event.sender.send(CHANNELS.authenticationChallenge, { requestId, challengeRequestId, challenge, }); } catch (error) { challenges.delete(challengeRequestId); finish(reject, error); } }); }; const closeImporterSelection = (selection) => { if (!selection?.handle) return; void selection.handle.close().catch(() => {}); }; const waitForConnectionStatusPoll = (signal) => new Promise((resolve, reject) => { if (signal.aborted) { reject(signal.reason ?? new DOMException("Plugin connection monitoring was cancelled", "AbortError")); return; } let timer; const onAbort = () => { clearTimeout(timer); reject(signal.reason ?? new DOMException("Plugin connection monitoring was cancelled", "AbortError")); }; timer = setTimeout(() => { signal.removeEventListener("abort", onAbort); resolve(); }, connectionStatusPollMs); // A zero-delay poll is used by deterministic tests and should retain the // event loop until its immediate callback runs. Longer production polls // remain unrefed so monitoring cannot keep the Electron process alive. if (connectionStatusPollMs > 0) timer.unref?.(); signal.addEventListener("abort", onAbort, { once: true }); }); const pluginConnectionReadyMeta = Object.freeze({ pluginPipelineIngressBytes: 0, pluginConnectionReady: true }); const monitorPluginConnection = async ({ sessionId, sessionOwner, sessions, monitors, controller, terminalWorkerManager, readyPublished = false, }) => { if (typeof extensionProviderService?.control !== "function") return; let hasPublishedReady = readyPublished; let reconnectAttempted = false; try { while (!controller.signal.aborted && sessions.get(sessionId) === sessionOwner) { await waitForConnectionStatusPoll(controller.signal); if (controller.signal.aborted || sessions.get(sessionId) !== sessionOwner) return; const status = await extensionProviderService.control( sessionId, "getStatus", {}, { signal: controller.signal, sessionOwner }, ); if (status.status === "connected") { reconnectAttempted = false; // A zero-byte terminal delivery transitions silent protocols out of // the connecting UI without inventing visible terminal output. if (!hasPublishedReady) { await terminalWorkerManager.pushExternalOutput(sessionId, "", pluginConnectionReadyMeta, sessionOwner); hasPublishedReady = true; } continue; } if (status.status === "closed" || status.status === "error") { if (status.retryable === true && !reconnectAttempted) { reconnectAttempted = true; await extensionProviderService.control( sessionId, "reconnect", {}, { signal: controller.signal, sessionOwner }, ); continue; } if (sessions.get(sessionId) === sessionOwner) sessions.delete(sessionId); try { await terminalWorkerManager.finishExternalSession( sessionId, connectionStatusCloseDetails(status), sessionOwner, ); } finally { extensionProviderService.closeSessionLocal(sessionId, undefined, sessionOwner); } return; } } } catch (error) { if (controller.signal.aborted) return; if (sessions.get(sessionId) === sessionOwner) sessions.delete(sessionId); try { await terminalWorkerManager.finishExternalSession(sessionId, { reason: "error", error: boundedErrorMessage(error), }, sessionOwner); } finally { extensionProviderService.closeSessionLocal(sessionId, undefined, sessionOwner); } } finally { if (monitors.get(sessionId)?.sessionOwner === sessionOwner) monitors.delete(sessionId); } }; const importerSelectionMap = (sender) => { if (!sender || typeof sender !== "object") throw new Error("Plugin importer sender is unavailable"); let selections = importerSelectionsBySender.get(sender); if (!selections) { selections = new Map(); importerSelectionsBySender.set(sender, selections); } if (!observedImporterSelectionSenders.has(sender)) { observedImporterSelectionSenders.add(sender); sender.once?.("destroyed", () => { for (const selection of selections.values()) closeImporterSelection(selection); selections.clear(); }); } const now = Date.now(); for (const [token, selection] of selections) { if (selection.expiresAt <= now) { selections.delete(token); closeImporterSelection(selection); } } return selections; }; const syncTransferMap = (sender) => { if (!sender || typeof sender !== "object") throw new Error("Plugin sync transfer sender is unavailable"); let transfers = syncTransfersBySender.get(sender); if (!transfers) { transfers = new Map(); syncTransfersBySender.set(sender, transfers); } if (!observedSyncTransferSenders.has(sender)) { observedSyncTransferSenders.add(sender); sender.once?.("destroyed", () => { for (const [transferId, transfer] of [...transfers.entries()]) { try { transfer.controller?.abort(); if (transfer.requestId) { extensionRequestMap(sender).delete(transfer.requestId); } } catch { /* ignore */ } transfers.delete(transferId); } }); } const now = Date.now(); for (const [transferId, transfer] of [...transfers.entries()]) { if (typeof transfer.expiresAt === "number" && transfer.expiresAt <= now) { try { transfer.controller?.abort(); if (transfer.requestId) extensionRequestMap(sender).delete(transfer.requestId); } catch { /* ignore */ } transfers.delete(transferId); } } return transfers; }; const releaseSyncTransfer = (sender, transferId, transfer) => { const transfers = syncTransfersBySender.get(sender); if (transfers && transferId && transfers.get(transferId) === transfer) { transfers.delete(transferId); } if (transfer?.requestId) { const requests = extensionRequestMap(sender); if (requests.get(transfer.requestId) === transfer.controller || transfer.controller == null) { requests.delete(transfer.requestId); } } }; const assertSyncTransferBudget = (transfers) => { if (transfers.size >= MAX_SYNC_TRANSFERS_PER_SENDER) { throw new Error("Too many active Plugin sync transfers"); } }; const coerceIpcBytes = (value, label) => { if (value instanceof Uint8Array) return value; if (Buffer.isBuffer(value)) return value; if (ArrayBuffer.isView(value)) { return Buffer.from(value.buffer, value.byteOffset, value.byteLength); } if (value && typeof value === "object" && value.type === "Buffer" && Array.isArray(value.data)) { return Buffer.from(value.data); } throw new TypeError(`${label} must be binary bytes`); }; const registerStreamingWriteRequest = (event, requestId) => { if (typeof requestId !== "string" || requestId.length < 1 || requestId.length > 128 || requestId.includes("\0")) { throw new TypeError("Plugin extension request ID is invalid"); } const requests = extensionRequestMap(event.sender); if (requests.has(requestId)) throw new Error("Plugin extension request ID is already active"); if (requests.size >= MAX_ACTIVE_TERMINAL_REQUESTS_PER_SENDER) { throw new Error("Too many active Plugin extension requests"); } const controller = new AbortController(); requests.set(requestId, controller); return controller; }; ipcMain.handle(CHANNELS.status, async (event) => { if (!isTrustedSender(event)) throw new Error("Untrusted plugin management sender"); let available = false; try { await resolveManager(); available = true; } catch {} return { available, experimental: true }; }); handle(CHANNELS.list, async (activeManager) => activeManager.list()); handle(CHANNELS.install, async (activeManager, payload) => activeManager.install( payload?.archivePath, { enable: payload?.enable === true }, )); handle(CHANNELS.setEnabled, async (activeManager, payload) => activeManager.setEnabled( payload?.pluginId, payload?.enabled === true, )); handle(CHANNELS.restart, async (activeManager, payload) => activeManager.restart(payload?.pluginId)); handle(CHANNELS.uninstall, async (activeManager, payload) => activeManager.uninstall(payload?.pluginId)); handle(CHANNELS.contributions, async (_activeManager, payload) => { if (!contributionService) throw new Error("Plugin contributions are unavailable"); return contributionService.snapshot(payload ?? {}); }); handle(CHANNELS.contributionIcon, async (_activeManager, payload) => { if (!options.resolveContributionIcon) throw new Error("Plugin contribution icons are unavailable"); return options.resolveContributionIcon(payload); }); handle(CHANNELS.executeCommand, async (_activeManager, payload) => { if (!contributionService) throw new Error("Plugin contributions are unavailable"); return contributionService.executeCommand(payload?.command, payload?.args, { source: "renderer", context: payload?.context, }); }); handle(CHANNELS.updateSetting, async (_activeManager, payload) => { if (!contributionService) throw new Error("Plugin contributions are unavailable"); return contributionService.updateSetting( payload?.pluginId, payload?.settingId, payload?.value, payload?.scopeId, { source: "host" }, ); }); handle(CHANNELS.resetSetting, async (_activeManager, payload) => { if (!contributionService) throw new Error("Plugin contributions are unavailable"); return contributionService.resetSetting(payload?.pluginId, payload?.settingId, payload?.scopeId); }); handle(CHANNELS.setEnvironment, async (_activeManager, payload) => { if (!contributionService) throw new Error("Plugin contributions are unavailable"); await contributionService.setEnvironment(payload ?? {}); viewHost?.setEnvironment?.(payload ?? {}); return null; }); handlePassive(CHANNELS.terminalProviders, [], async (_activeManager, payload) => { if (!terminalProviderService) throw new Error("Plugin Terminal Providers are unavailable"); return terminalProviderService.listProviders(payload ?? {}); }); ipcMain.handle(CHANNELS.terminalProvide, async (event, payload) => { if (!isTrustedSender(event)) throw new Error("Untrusted plugin management sender"); if (!terminalProviderService) throw new Error("Plugin Terminal Providers are unavailable"); const requestId = payload?.requestId; if (typeof requestId !== "string" || requestId.length < 1 || requestId.length > 128 || requestId.includes("\0")) { throw new TypeError("Plugin terminal request ID is invalid"); } const requests = terminalRequestMap(event.sender); if (requests.has(requestId)) throw new Error("Plugin terminal request ID is already active"); if (requests.size >= MAX_ACTIVE_TERMINAL_REQUESTS_PER_SENDER) { throw new Error("Too many active Plugin terminal requests"); } const controller = new AbortController(); requests.set(requestId, controller); try { await raceWithAbort(resolveManager(), controller.signal); const { requestId: _requestId, ...providerRequest } = payload; return await terminalProviderService.provide(providerRequest, { signal: controller.signal }); } finally { if (requests.get(requestId) === controller) requests.delete(requestId); } }); ipcMain.handle(CHANNELS.terminalCancel, async (event, payload) => { if (!isTrustedSender(event)) throw new Error("Untrusted plugin management sender"); const requestId = payload?.requestId; if (typeof requestId !== "string" || requestId.length < 1 || requestId.length > 128) { throw new TypeError("Plugin terminal request ID is invalid"); } const requests = terminalRequestMap(event.sender); const controller = requests.get(requestId); controller?.abort(); return controller != null; }); handlePassive(CHANNELS.terminalSessionEvent, [], async (_activeManager, payload, event) => { if (!terminalProviderService) throw new Error("Plugin Terminal Providers are unavailable"); if (terminalDataPipelineService?.acceptsSessionEvent && !terminalDataPipelineService.acceptsSessionEvent(payload, event?.sender?.id)) { return []; } const [providers] = await Promise.all([ terminalProviderService.publishSessionEvent(payload), terminalDataPipelineService?.handleSessionEvent?.(payload, { webContentsId: event?.sender?.id, }) ?? [], ]); return providers; }); handlePassive(CHANNELS.extensionProviders, [], async (_activeManager, payload) => { if (!extensionProviderService) throw new Error("Plugin extension Providers are unavailable"); return extensionProviderService.listProviders(payload ?? {}); }); // Synchronous readiness probe for renderer restore paths (no await). ipcMain.on(CHANNELS.hostAvailableSync, (event) => { event.returnValue = Boolean(syncSidecarService); }); // Fallback null = host gated off / manager unavailable. Renderer must treat // null as "unavailable" (last-known) rather than an authoritative empty set. handlePassive(CHANNELS.syncSidecarsCollect, null, async () => { if (!syncSidecarService) return null; return syncSidecarService.collectForSync(); }); handlePassive(CHANNELS.syncSidecarsApply, null, async (_activeManager, payload) => { if (!syncSidecarService) return { applied: false }; const entries = await syncSidecarService.applyFromSync(payload ?? { version: 1, entries: [] }); // Include merged entries so the renderer can refresh last-known without a // follow-up collect (preserves missing-plugin rows remote omitted). return { applied: true, count: entries.length, entries }; }); const runSyncProvider = (channel, methodName) => { ipcMain.handle(channel, async (event, payload) => runExtensionRequest(event, payload, async (signal) => { if (!extensionProviderService) throw new Error("Plugin sync Providers are unavailable"); const method = extensionProviderService[methodName]; if (typeof method !== "function") throw new Error(`Plugin sync method missing: ${methodName}`); return method.call(extensionProviderService, payload ?? {}, { signal }); })); }; runSyncProvider(CHANNELS.syncConnect, "connectSync"); runSyncProvider(CHANNELS.syncDisconnect, "disconnectSync"); runSyncProvider(CHANNELS.syncGetAccount, "getSyncAccount"); runSyncProvider(CHANNELS.syncGetCapabilities, "getSyncCapabilities"); runSyncProvider(CHANNELS.syncDeleteObject, "deleteSyncObject"); ipcMain.handle(CHANNELS.syncReadObject, async (event, payload) => { if (!isTrustedSender(event)) throw new Error("Untrusted plugin management sender"); if (!extensionProviderService) throw new Error("Plugin sync Providers are unavailable"); const requestId = payload?.requestId; const controller = registerStreamingWriteRequest(event, requestId); try { await raceWithAbort(resolveManager(), controller.signal); const result = await extensionProviderService.readSyncObject(payload ?? {}, { signal: controller.signal, }); if (!result?.found || !result.bytes) { extensionRequestMap(event.sender).delete(requestId); return { found: false, key: payload?.key, data: null }; } const bytes = result.bytes instanceof Uint8Array ? result.bytes : new Uint8Array(result.bytes); const preferStream = payload?.preferStream === true; if (!preferStream && bytes.byteLength <= INLINE_SYNC_OBJECT_SAFE_BYTES) { extensionRequestMap(event.sender).delete(requestId); return { found: true, key: result.key, data: bytes, revision: result.revision, contentType: result.contentType, }; } const transfers = syncTransferMap(event.sender); assertSyncTransferBudget(transfers); const transferId = randomUUID(); const transfer = { kind: "read", sender: event.sender, requestId, controller, bytes, offset: 0, expiresAt: Date.now() + SYNC_TRANSFER_TTL_MS, }; transfers.set(transferId, transfer); const onAbort = () => releaseSyncTransfer(event.sender, transferId, transfer); controller.signal.addEventListener("abort", onAbort, { once: true }); transfer.onAbort = onAbort; return { found: true, key: result.key, streamed: true, transferId, byteLength: bytes.byteLength, revision: result.revision, contentType: result.contentType, }; } catch (error) { extensionRequestMap(event.sender).delete(requestId); throw error; } }); ipcMain.handle(CHANNELS.syncReadChunk, async (event, payload) => { if (!isTrustedSender(event)) throw new Error("Untrusted plugin management sender"); const transferId = payload?.transferId; if (typeof transferId !== "string" || transferId.length < 1) { throw new TypeError("Sync read transfer ID is invalid"); } const transfers = syncTransferMap(event.sender); const transfer = transfers.get(transferId); if (!transfer || transfer.kind !== "read") { throw new Error("Sync read transfer is not owned by this window"); } if (payload?.requestId !== transfer.requestId) { throw new Error("Sync read transfer request ID mismatch"); } if (transfer.controller?.signal?.aborted) { releaseSyncTransfer(event.sender, transferId, transfer); throw new DOMException("Aborted", "AbortError"); } transfer.expiresAt = Date.now() + SYNC_TRANSFER_TTL_MS; const maxBytes = Number.isSafeInteger(payload?.maxBytes) && payload.maxBytes > 0 ? Math.min(payload.maxBytes, STREAM_WINDOW_BYTES) : STREAM_WINDOW_BYTES; const start = transfer.offset; const end = Math.min(transfer.bytes.byteLength, start + maxBytes); const chunk = transfer.bytes.subarray(start, end); transfer.offset = end; const done = end >= transfer.bytes.byteLength; if (done) releaseSyncTransfer(event.sender, transferId, transfer); return { chunk, done }; }); ipcMain.handle(CHANNELS.syncWriteObject, async (event, payload) => runExtensionRequest(event, payload, async (signal) => { if (!extensionProviderService) throw new Error("Plugin sync Providers are unavailable"); const data = coerceIpcBytes(payload?.data ?? payload?.bytes, "Sync writeObject data"); if (data.byteLength > INLINE_SYNC_OBJECT_SAFE_BYTES) { throw new Error("Sync writeObject payload requires the streamed write path"); } if (data.byteLength > MAX_SYNC_OBJECT_BYTES) { throw new Error("Sync writeObject payload exceeds the size limit"); } return extensionProviderService.writeSyncObject({ ...payload, bytes: data instanceof Uint8Array ? data : new Uint8Array(data), preferStream: false, }, { signal }); })); ipcMain.handle(CHANNELS.syncWriteBegin, async (event, payload) => { if (!isTrustedSender(event)) throw new Error("Untrusted plugin management sender"); if (!extensionProviderService) throw new Error("Plugin sync Providers are unavailable"); const requestId = payload?.requestId; const controller = registerStreamingWriteRequest(event, requestId); const failBegin = (error) => { controller.abort(); extensionRequestMap(event.sender).delete(requestId); throw error; }; const byteLength = payload?.byteLength; if (!Number.isSafeInteger(byteLength) || byteLength < 1 || byteLength > MAX_SYNC_OBJECT_BYTES) { failBegin(new TypeError("Sync writeObject byteLength is invalid")); } const providerId = payload?.providerId; const key = payload?.key; if (typeof providerId !== "string" || providerId.length < 1) { failBegin(new TypeError("Sync writeObject provider ID is invalid")); } if (typeof key !== "string" || key.length < 1) { failBegin(new TypeError("Sync writeObject key is invalid")); } const transfers = syncTransferMap(event.sender); try { assertSyncTransferBudget(transfers); } catch (error) { failBegin(error); } const transferId = randomUUID(); const transfer = { kind: "write", sender: event.sender, requestId, controller, providerId, key, expectedRevision: payload?.expectedRevision, deadlineMs: payload?.deadlineMs, byteLength, received: 0, sequence: -1, chunks: [], signal: controller.signal, expiresAt: Date.now() + SYNC_TRANSFER_TTL_MS, }; transfers.set(transferId, transfer); const onAbort = () => releaseSyncTransfer(event.sender, transferId, transfer); controller.signal.addEventListener("abort", onAbort, { once: true }); transfer.onAbort = onAbort; return { transferId, windowBytes: STREAM_WINDOW_BYTES }; }); ipcMain.handle(CHANNELS.syncWriteChunk, async (event, payload) => { if (!isTrustedSender(event)) throw new Error("Untrusted plugin management sender"); const transferId = payload?.transferId; if (typeof transferId !== "string" || transferId.length < 1) { throw new TypeError("Sync write transfer ID is invalid"); } const transfers = syncTransferMap(event.sender); const transfer = transfers.get(transferId); if (!transfer || transfer.kind !== "write") { throw new Error("Sync write transfer is not owned by this window"); } const failChunk = (error) => { try { transfer.controller?.abort(); } catch { /* ignore */ } releaseSyncTransfer(event.sender, transferId, transfer); throw error; }; if (payload?.requestId !== transfer.requestId) { failChunk(new Error("Sync write transfer request ID mismatch")); } if (transfer.signal.aborted) { failChunk(new DOMException("Aborted", "AbortError")); } const sequence = payload?.sequence; if (!Number.isSafeInteger(sequence) || sequence !== transfer.sequence + 1) { failChunk(new TypeError("Sync write chunk sequence is invalid")); } let chunk; try { chunk = coerceIpcBytes(payload?.chunk, "Sync write chunk"); } catch (error) { failChunk(error); } if (chunk.byteLength < 1 || chunk.byteLength > STREAM_WINDOW_BYTES) { failChunk(new TypeError("Sync write chunk size is invalid")); } if (transfer.received + chunk.byteLength > transfer.byteLength) { failChunk(new Error("Sync write exceeded declared byteLength")); } transfer.chunks.push(Buffer.from(chunk)); transfer.received += chunk.byteLength; transfer.sequence = sequence; transfer.expiresAt = Date.now() + SYNC_TRANSFER_TTL_MS; return { accepted: chunk.byteLength }; }); ipcMain.handle(CHANNELS.syncWriteCommit, async (event, payload) => { if (!isTrustedSender(event)) throw new Error("Untrusted plugin management sender"); if (!extensionProviderService) throw new Error("Plugin sync Providers are unavailable"); const transferId = payload?.transferId; if (typeof transferId !== "string" || transferId.length < 1) { throw new TypeError("Sync write transfer ID is invalid"); } const transfers = syncTransferMap(event.sender); const transfer = transfers.get(transferId); if (!transfer || transfer.kind !== "write") { throw new Error("Sync write transfer is not owned by this window"); } const failCommit = (error) => { try { transfer.controller?.abort(); } catch { /* ignore */ } releaseSyncTransfer(event.sender, transferId, transfer); throw error; }; if (payload?.requestId !== transfer.requestId) { failCommit(new Error("Sync write transfer request ID mismatch")); } if (transfer.received !== transfer.byteLength) { failCommit(new Error("Sync write commit size does not match byteLength")); } const bytes = Buffer.concat(transfer.chunks); transfers.delete(transferId); try { if (transfer.signal.aborted) { throw new DOMException("Aborted", "AbortError"); } await raceWithAbort(resolveManager(), transfer.signal); return await extensionProviderService.writeSyncObject({ providerId: transfer.providerId, key: transfer.key, bytes: new Uint8Array(bytes), expectedRevision: transfer.expectedRevision, preferStream: true, deadlineMs: transfer.deadlineMs, }, { signal: transfer.signal }); } catch (error) { throw error; } finally { if (extensionRequestMap(event.sender).get(transfer.requestId) === transfer.controller || extensionRequestMap(event.sender).has(transfer.requestId)) { extensionRequestMap(event.sender).delete(transfer.requestId); } } }); ipcMain.handle(CHANNELS.syncPutSecret, async (event, payload) => { if (!isTrustedSender(event)) throw new Error("Untrusted plugin management sender"); if (!secretStore || typeof secretStore.set !== "function") { throw new Error("Plugin sync secret storage is unavailable"); } if (!extensionProviderService) throw new Error("Plugin sync Providers are unavailable"); const providerId = payload?.providerId; if (typeof providerId !== "string" || providerId.length < 1) { throw new TypeError("Sync provider ID is invalid"); } const key = assertSecretKey(typeof payload?.key === "string" && payload.key ? payload.key : "sync-credential"); const value = payload?.value; if (typeof value !== "string" || value.length < 1) { throw new TypeError("Sync secret value is invalid"); } const providers = extensionProviderService.listProviders({ kind: "sync" }); const match = Array.isArray(providers) ? providers.find((entry) => entry?.provider?.id === providerId || entry?.id === providerId) : null; if (!match || typeof match.pluginId !== "string") { throw new Error(`Plugin sync provider is unavailable: ${providerId}`); } const existing = typeof secretStore.getReference === "function" ? secretStore.getReference(match.pluginId, key) : null; const stored = typeof secretStore.set === "function" ? secretStore.set(match.pluginId, key, value, { stashPrevious: true }) : null; if (!stored) { throw new Error("Plugin sync secret storage is unavailable"); } // Remember which plugin owns this provider so disconnect can clean secrets // after the contribution is gone (disabled/uninstalled). if (typeof secretStore.bindSyncProviderPlugin === "function") { try { secretStore.bindSyncProviderPlugin(match.pluginId, providerId); } catch { /* binding is best-effort; credential write already succeeded */ } } return Object.freeze({ ...stored, created: !existing }); }); ipcMain.handle(CHANNELS.syncDeleteSecrets, async (event, payload) => { if (!isTrustedSender(event)) throw new Error("Untrusted plugin management sender"); if (!secretStore || typeof secretStore.delete !== "function") { throw new Error("Plugin sync secret storage is unavailable"); } if (!extensionProviderService) throw new Error("Plugin sync Providers are unavailable"); // Wait for package recovery + binding seed before resolving ownership so a // schema-2 disconnect during startup cannot miss retained bindings (Codex P2). try { await resolveManager(); } catch { // Runtime disabled: continue with whatever secretStore already has. } const providerId = payload?.providerId; if (typeof providerId !== "string" || providerId.length < 1) { throw new TypeError("Sync provider ID is invalid"); } const providers = extensionProviderService.listProviders({ kind: "sync" }); const match = Array.isArray(providers) ? providers.find((entry) => entry?.provider?.id === providerId || entry?.id === providerId) : null; let pluginId = match && typeof match.pluginId === "string" ? match.pluginId : null; if (!pluginId && typeof secretStore.resolveSyncProviderPlugin === "function") { pluginId = secretStore.resolveSyncProviderPlugin(providerId) ?? null; } if (!pluginId) { // No live contribution and no retained binding — nothing to delete. return { deleted: 0 }; } const keys = Array.isArray(payload?.keys) && payload.keys.length > 0 ? payload.keys : null; if (!keys) { // Wipe every sync-credential* key (well-known + schema writeOnly fields). let deleted = 0; if (typeof secretStore.deleteByKeyPrefix === "function") { deleted = Number(secretStore.deleteByKeyPrefix(pluginId, "sync-credential")) || 0; } if (typeof secretStore.unbindSyncProviderPlugin === "function") { secretStore.unbindSyncProviderPlugin(pluginId, providerId); } return { deleted }; } let deleted = 0; for (const rawKey of keys ?? [ "sync-credential", "sync-credential:token", "sync-credential:secret", "sync-credential:apiKey", "sync-credential:accessToken", ]) { if (typeof rawKey !== "string" || rawKey.length < 1) continue; const key = assertSecretKey(rawKey); try { secretStore.delete(pluginId, key); deleted += 1; } catch { /* ignore missing / unavailable keys */ } } return { deleted }; }); ipcMain.handle(CHANNELS.syncRestoreSecrets, async (event, payload) => { if (!isTrustedSender(event)) throw new Error("Untrusted plugin management sender"); if (!secretStore) { throw new Error("Plugin sync secret restore is unavailable"); } if (!extensionProviderService) throw new Error("Plugin sync Providers are unavailable"); const providerId = payload?.providerId; if (typeof providerId !== "string" || providerId.length < 1) { throw new TypeError("Sync provider ID is invalid"); } const keys = Array.isArray(payload?.keys) ? payload.keys : []; if (keys.length < 1) return { restored: 0, discarded: 0 }; const providers = extensionProviderService.listProviders({ kind: "sync" }); const match = Array.isArray(providers) ? providers.find((entry) => entry?.provider?.id === providerId || entry?.id === providerId) : null; let pluginId = match && typeof match.pluginId === "string" ? match.pluginId : null; if (!pluginId && typeof secretStore.resolveSyncProviderPlugin === "function") { pluginId = secretStore.resolveSyncProviderPlugin(providerId) ?? null; } if (!pluginId) return { restored: 0, discarded: 0 }; const discard = payload?.discard === true; let restored = 0; let discarded = 0; for (const rawKey of keys) { if (typeof rawKey !== "string" || rawKey.length < 1) continue; const key = assertSecretKey(rawKey); try { if (discard) { if (typeof secretStore.clearOverwriteStash === "function") { secretStore.clearOverwriteStash(pluginId, key); discarded += 1; } } else if (typeof secretStore.restoreOverwrite === "function" && secretStore.restoreOverwrite(pluginId, key)) { restored += 1; } } catch { /* best-effort restore/discard */ } } return { restored, discarded }; }); handlePassive(CHANNELS.credentialCatalogUpdate, 0, async (_activeManager, payload) => { if (!credentialResolver || typeof credentialResolver.update !== "function") { throw new Error("Plugin Vault credential catalog is unavailable"); } return credentialResolver.update(payload?.entries); }); ipcMain.handle(CHANNELS.extensionInvoke, async (event, payload) => runExtensionRequest(event, payload, async (signal) => { if (!extensionProviderService) throw new Error("Plugin extension Providers are unavailable"); if ((payload?.kind === "connection" && payload?.operation !== "validateConfiguration" && payload?.operation !== "probe") || payload?.kind === "authentication" || (payload?.kind === "importer" && payload?.operation === "parse")) { throw new TypeError("Stateful Provider operations require their dedicated host workflow"); } return extensionProviderService.invoke(payload ?? {}, { signal }); })); ipcMain.handle(CHANNELS.extensionCancel, async (event, payload) => { if (!isTrustedSender(event)) throw new Error("Untrusted plugin management sender"); const requestId = payload?.requestId; if (typeof requestId !== "string" || requestId.length < 1 || requestId.length > 128) { throw new TypeError("Plugin extension request ID is invalid"); } const controller = extensionRequestMap(event.sender).get(requestId); controller?.abort(); return controller != null; }); ipcMain.handle(CHANNELS.connectionStart, async (event, payload) => runExtensionRequest(event, payload, async (signal) => { if (!extensionProviderService) throw new Error("Plugin connection Providers are unavailable"); const terminalWorkerManager = getTerminalWorkerManager(); if (!terminalWorkerManager?.startExternalSession) { throw new Error("Host terminal pipeline is unavailable for plugin connections"); } const sessions = connectionSessionMap(event.sender); const monitors = connectionMonitorMap(event.sender); const sessionId = payload?.sessionId; const sessionOwner = Symbol(`plugin-connection:${sessionId}`); const outputDecoder = new TextDecoder("utf-8"); let credential = payload?.credential; let closedDuringStart = false; let providerOpened = false; let providerCloseStarted = false; let acceptProviderOutput = true; const connectionController = new AbortController(); const abortConnection = () => connectionController.abort( signal.reason ?? new DOMException("Plugin connection request was cancelled", "AbortError"), ); if (signal.aborted) abortConnection(); else signal.addEventListener("abort", abortConnection, { once: true }); try { if (connectionController.signal.aborted) throw connectionController.signal.reason; await terminalWorkerManager.startExternalSession({ sessionId, ownerToken: sessionOwner, webContentsId: event.sender.id, columns: payload?.columns, rows: payload?.rows, protocol: payload?.protocol, hostLabel: payload?.hostLabel, hostname: payload?.hostname, sessionLog: payload?.sessionLog, onInput: (data) => (providerOpened ? extensionProviderService.write(sessionId, data, sessionOwner) : undefined), onResize: ({ columns, rows }) => extensionProviderService.control( sessionId, "resize", { columns, rows }, { sessionOwner }, ), onClose: async () => { acceptProviderOutput = false; if (sessions.get(sessionId) === sessionOwner) sessions.delete(sessionId); if (monitors.get(sessionId)?.sessionOwner === sessionOwner) monitors.delete(sessionId); connectionController.abort(new DOMException("Terminal session closed", "AbortError")); providerCloseStarted = true; if (providerOpened) { try { await extensionProviderService.control(sessionId, "close", {}, { sessionOwner }); } catch { extensionProviderService.closeSessionLocal(sessionId, undefined, sessionOwner); } } else { extensionProviderService.closeSessionLocal(sessionId, undefined, sessionOwner); } }, }); if (payload?.authenticationProviderId) { const authentication = await extensionProviderService.authenticate({ providerId: payload.authenticationProviderId, connectionProviderId: payload?.providerId, configuration: payload?.configuration, ...(credential === undefined ? {} : { credential }), }, (challenge) => requestAuthenticationChallenge( event, payload.requestId, challenge, connectionController.signal, ), { signal: connectionController.signal }); if (authentication.status !== "authenticated") { throw new Error(authentication.message || "Plugin authentication did not complete"); } credential = authentication.credential; } const opened = await extensionProviderService.openConnection({ ...payload, ...(credential === undefined ? {} : { credential }), }, { signal: connectionController.signal, sessionOwner, onData: async (bytes) => { if (!acceptProviderOutput || (providerOpened && sessions.get(sessionId) !== sessionOwner)) return; const data = outputDecoder.decode(bytes, { stream: true }); if (!data) return; try { await terminalWorkerManager.pushExternalOutput(sessionId, data, undefined, sessionOwner); } catch (error) { if (!acceptProviderOutput || connectionController.signal.aborted || (providerOpened && sessions.get(sessionId) !== sessionOwner)) return; throw error; } }, onOutputClose: async (reason) => { const shouldFlushOutput = acceptProviderOutput && !connectionController.signal.aborted && (!providerOpened || sessions.get(sessionId) === sessionOwner); acceptProviderOutput = false; closedDuringStart = true; if (sessions.get(sessionId) === sessionOwner) sessions.delete(sessionId); if (monitors.get(sessionId)?.sessionOwner === sessionOwner) monitors.delete(sessionId); connectionController.abort(new DOMException("Plugin connection output closed", "AbortError")); const finalData = outputDecoder.decode(); let finalOutputError; if (shouldFlushOutput && finalData) { try { await terminalWorkerManager.pushExternalOutput(sessionId, finalData, undefined, sessionOwner); } catch (error) { finalOutputError = error; } } await terminalWorkerManager.finishExternalSession( sessionId, finalOutputError ? { reason: "error", error: boundedErrorMessage(finalOutputError, "Plugin connection final output failed") } : connectionOutputCloseDetails(reason), sessionOwner, ); }, }); providerOpened = true; if (!closedDuringStart) { sessions.set(opened.sessionId, sessionOwner); monitors.set(opened.sessionId, { sessionOwner, controller: connectionController }); if (opened.status === "connected") { // The terminal renderer treats the first delivery as the connection // readiness boundary. Preserve that boundary for silent protocols // whose Provider completes open before producing terminal bytes. await terminalWorkerManager.pushExternalOutput( opened.sessionId, "", pluginConnectionReadyMeta, sessionOwner, ); } void monitorPluginConnection({ sessionId: opened.sessionId, sessionOwner, sessions, monitors, controller: connectionController, terminalWorkerManager, readyPublished: opened.status === "connected", }); } return opened; } catch (error) { acceptProviderOutput = false; if (sessions.get(sessionId) === sessionOwner) sessions.delete(sessionId); if (monitors.get(sessionId)?.sessionOwner === sessionOwner) monitors.delete(sessionId); connectionController.abort(error); if (!providerCloseStarted && providerOpened) { try { await extensionProviderService.control(sessionId, "close", {}, { sessionOwner }); } catch { extensionProviderService.closeSessionLocal(sessionId, undefined, sessionOwner); } } else if (!providerCloseStarted && !closedDuringStart) { extensionProviderService.closeSessionLocal(sessionId, undefined, sessionOwner); } await terminalWorkerManager.finishExternalSession(sessionId, { reason: "error", error: boundedErrorMessage(error), }, sessionOwner); throw error; } finally { signal.removeEventListener("abort", abortConnection); } })); ipcMain.handle(CHANNELS.authenticationRespond, async (event, payload) => { if (!isTrustedSender(event)) throw new Error("Untrusted plugin management sender"); const challengeRequestId = payload?.challengeRequestId; if (typeof challengeRequestId !== "string" || challengeRequestId.length < 1 || challengeRequestId.length > 128) { throw new TypeError("Plugin authentication challenge request ID is invalid"); } const challenges = authenticationChallengeMap(event.sender); const pending = challenges.get(challengeRequestId); if (!pending || pending.requestId !== payload?.requestId || pending.challengeId !== payload?.challengeId) { throw new Error("Plugin authentication challenge is not owned by this window"); } challenges.delete(challengeRequestId); if (payload?.cancelled === true) pending.reject(new DOMException("Cancelled", "AbortError")); else pending.resolve(payload?.response); return null; }); handle(CHANNELS.connectionWrite, async (_activeManager, payload, event) => { if (!extensionProviderService) throw new Error("Plugin connection Providers are unavailable"); const sessions = connectionSessionMap(event.sender); const sessionOwner = sessions.get(payload?.sessionId); if (sessionOwner === undefined) throw new Error("Plugin connection session is not owned by this window"); await extensionProviderService.write(payload.sessionId, payload.data, sessionOwner); return null; }); handle(CHANNELS.connectionControl, async (_activeManager, payload, event) => { if (!extensionProviderService) throw new Error("Plugin connection Providers are unavailable"); const sessions = connectionSessionMap(event.sender); const monitors = connectionMonitorMap(event.sender); const terminalWorkerManager = getTerminalWorkerManager(); const sessionOwner = sessions.get(payload?.sessionId); if (sessionOwner === undefined) throw new Error("Plugin connection session is not owned by this window"); const isClose = payload.operation === "close"; if (isClose) { if (sessions.get(payload.sessionId) === sessionOwner) sessions.delete(payload.sessionId); const monitor = monitors.get(payload.sessionId); if (monitor?.sessionOwner === sessionOwner) { monitors.delete(payload.sessionId); monitor.controller.abort(new DOMException("Plugin connection closed", "AbortError")); } } const controlResult = Promise.resolve(extensionProviderService.control( payload.sessionId, payload.operation, payload.payload ?? {}, { sessionOwner }, )); if (!isClose) return controlResult; const settledControl = controlResult.then( (value) => ({ status: "fulfilled", value }), (reason) => ({ status: "rejected", reason }), ); try { await terminalWorkerManager?.finishExternalSession?.( payload.sessionId, { reason: "closed" }, sessionOwner, ); } catch {} const outcome = await settledControl; if (outcome.status === "rejected") throw outcome.reason; return outcome.value; }); ipcMain.handle(CHANNELS.importerDetect, async (event, payload) => runExtensionRequest(event, payload, async (signal) => { if (!extensionProviderService) throw new Error("Plugin importer Providers are unavailable"); return extensionProviderService.detectImporter({ ...payload, sample: payload?.sample }, { signal }); })); handle(CHANNELS.importerSelectFile, async (_activeManager, _payload, event) => { if (typeof selectImporterFile !== "function") throw new Error("Plugin importer file selection is unavailable"); const filePath = await selectImporterFile(event); if (!filePath) return null; const handle = await fs.open(filePath, "r"); let retained = false; try { const stat = await handle.stat(); if (!stat.isFile() || stat.size < 1 || stat.size > MAX_IMPORT_FILE_BYTES) { throw new TypeError("Plugin importer file is not a supported regular file"); } const sample = Buffer.allocUnsafe(Math.min(stat.size, 128 * 1024)); const { bytesRead } = await handle.read(sample, 0, sample.byteLength, 0); if (bytesRead !== sample.byteLength) throw new Error("Plugin importer file changed while sampling"); const selections = importerSelectionMap(event.sender); if (selections.size >= MAX_IMPORT_SELECTIONS_PER_SENDER) { throw new Error("Too many pending Plugin importer file selections"); } const selectionToken = randomUUID(); selections.set(selectionToken, { handle, filePath, fileName: filePath.split(/[\\/]/u).at(-1) || "import", size: stat.size, dev: stat.dev, ino: stat.ino, mtimeMs: stat.mtimeMs, expiresAt: Date.now() + IMPORT_SELECTION_TTL_MS, }); retained = true; return { selectionToken, fileName: selections.get(selectionToken).fileName, sample: sample.subarray(0, bytesRead) }; } finally { if (!retained) await handle.close(); } }); handle(CHANNELS.importerReleaseFile, async (_activeManager, payload, event) => { const selectionToken = payload?.selectionToken; if (typeof selectionToken !== "string" || selectionToken.length < 1 || selectionToken.length > 128) { throw new TypeError("Plugin importer selection token is invalid"); } const selections = importerSelectionMap(event.sender); const selection = selections.get(selectionToken); const deleted = selections.delete(selectionToken); closeImporterSelection(selection); return deleted; }); ipcMain.handle(CHANNELS.importerParseFile, async (event, payload) => runExtensionRequest(event, payload, async (signal) => { if (!extensionProviderService) throw new Error("Plugin importer Providers are unavailable"); const selectionToken = payload?.selectionToken; if (typeof selectionToken !== "string" || selectionToken.length < 1 || selectionToken.length > 128) { throw new TypeError("Plugin importer selection token is invalid"); } const selections = importerSelectionMap(event.sender); const selection = selections.get(selectionToken); selections.delete(selectionToken); if (!selection || selection.expiresAt <= Date.now()) throw new Error("Plugin importer file selection expired"); const handle = selection.handle; try { const stat = await handle.stat(); if (!stat.isFile() || stat.size !== selection.size || stat.dev !== selection.dev || stat.ino !== selection.ino || stat.mtimeMs !== selection.mtimeMs) { throw new Error("Plugin importer file changed after selection"); } return await extensionProviderService.parseImporter({ ...payload, fileName: selection.fileName, source: handle.createReadStream({ autoClose: false, signal, start: 0 }), sourceByteLength: stat.size, }, { signal, onProgress: (progress) => { if (!event.sender.isDestroyed?.()) { event.sender.send(CHANNELS.importerProgress, { requestId: payload.requestId, providerId: payload.providerId, progress, }); } }, }); } finally { await handle.close(); } })); handle(CHANNELS.openView, async (_activeManager, payload, event) => { if (!viewHost) throw new Error("Plugin views are unavailable"); return viewHost.open(payload, event.sender); }); handle(CHANNELS.closeView, async (_activeManager, payload, event) => { if (!viewHost) throw new Error("Plugin views are unavailable"); await viewHost.close(payload?.instanceId, event.sender); return null; }); handle(CHANNELS.setViewBounds, async (_activeManager, payload, event) => { if (!viewHost) throw new Error("Plugin views are unavailable"); viewHost.setBounds(payload?.instanceId, payload?.bounds, event.sender); return null; }); handle(CHANNELS.setViewVisibility, async (_activeManager, payload, event) => { if (!viewHost) throw new Error("Plugin views are unavailable"); viewHost.setVisible(payload?.instanceId, payload?.visible, event.sender); return null; }); handle(CHANNELS.viewMessage, async (_activeManager, payload, event) => { if (!viewHost) throw new Error("Plugin views are unavailable"); await viewHost.postMessage(payload?.instanceId, payload?.message, event.sender); return null; }); handlePassive(CHANNELS.getScopeCatalog, () => currentScopeCatalog(), async () => currentScopeCatalog()); handlePassive(CHANNELS.setScopeCatalog, null, async (_activeManager, payload, event) => { const key = scopeCatalogSenderKey(event); const scopeCatalog = normalizePluginScopeCatalog(payload); scopeCatalogs.set(key, scopeCatalog); scopeCatalogOwners.set(key, event?.sender); observeScopeCatalogSender(event, key); publishScopeCatalog(event); return null; }); contributionService?.onDidChange?.((event) => options.broadcast?.(CHANNELS.contributionsChanged, event)); contributionService?.onDidPostViewMessage?.((event) => options.broadcast?.(CHANNELS.viewMessagePosted, event)); viewHost?.onDidClose?.((event) => options.broadcast?.(CHANNELS.viewClosed, event)); } module.exports = { CHANNELS, createTrustedPluginBridgeSender, mergePluginScopeCatalogs, normalizePluginScopeCatalog, registerPluginBridge, };