@dimina-kit/devtools 0.3.1-dev.20260513123738 → 0.3.1-dev.20260515105504

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +103 -41
  2. package/dist/main/api.d.ts +6 -2
  3. package/dist/main/api.js +5 -2
  4. package/dist/main/app/app.d.ts +1 -1
  5. package/dist/main/app/app.js +71 -36
  6. package/dist/main/app/launch.d.ts +2 -2
  7. package/dist/main/app/launch.js +6 -3
  8. package/dist/main/index.bundle.js +1074 -289
  9. package/dist/main/ipc/app.d.ts +2 -1
  10. package/dist/main/ipc/app.js +5 -4
  11. package/dist/main/ipc/index.d.ts +5 -4
  12. package/dist/main/ipc/index.js +5 -4
  13. package/dist/main/ipc/panels.d.ts +2 -1
  14. package/dist/main/ipc/panels.js +14 -9
  15. package/dist/main/ipc/popover.d.ts +4 -1
  16. package/dist/main/ipc/popover.js +15 -5
  17. package/dist/main/ipc/projects.d.ts +4 -1
  18. package/dist/main/ipc/projects.js +42 -5
  19. package/dist/main/ipc/session.d.ts +4 -1
  20. package/dist/main/ipc/session.js +26 -6
  21. package/dist/main/ipc/settings.d.ts +4 -1
  22. package/dist/main/ipc/settings.js +30 -17
  23. package/dist/main/ipc/simulator-module.d.ts +8 -0
  24. package/dist/main/ipc/simulator-module.js +19 -0
  25. package/dist/main/ipc/simulator.d.ts +2 -1
  26. package/dist/main/ipc/simulator.js +29 -15
  27. package/dist/main/ipc/toolbar.d.ts +2 -1
  28. package/dist/main/ipc/toolbar.js +3 -2
  29. package/dist/main/services/automation/index.js +88 -13
  30. package/dist/main/services/mcp/server.d.ts +2 -1
  31. package/dist/main/services/mcp/server.js +5 -0
  32. package/dist/main/services/module.d.ts +18 -0
  33. package/dist/main/services/module.js +2 -0
  34. package/dist/main/services/notifications/renderer-notifier.d.ts +2 -0
  35. package/dist/main/services/projects/project-repository.d.ts +1 -0
  36. package/dist/main/services/projects/project-repository.js +3 -0
  37. package/dist/main/services/settings/index.d.ts +4 -0
  38. package/dist/main/services/settings/index.js +12 -1
  39. package/dist/main/services/simulator/custom-apis.d.ts +10 -0
  40. package/dist/main/services/simulator/custom-apis.js +26 -0
  41. package/dist/main/services/simulator-storage/index.d.ts +22 -1
  42. package/dist/main/services/simulator-storage/index.js +365 -98
  43. package/dist/main/services/update/update-manager.d.ts +16 -1
  44. package/dist/main/services/update/update-manager.js +18 -15
  45. package/dist/main/services/views/view-manager.d.ts +4 -0
  46. package/dist/main/services/views/view-manager.js +33 -2
  47. package/dist/main/services/window-service.d.ts +24 -0
  48. package/dist/main/services/window-service.js +26 -0
  49. package/dist/main/services/workbench-context.d.ts +15 -0
  50. package/dist/main/services/workbench-context.js +6 -0
  51. package/dist/main/services/workspace/workspace-service.d.ts +1 -0
  52. package/dist/main/services/workspace/workspace-service.js +7 -3
  53. package/dist/main/simulator-apis.d.ts +23 -0
  54. package/dist/main/simulator-apis.js +24 -0
  55. package/dist/main/utils/disposable.d.ts +13 -0
  56. package/dist/main/utils/disposable.js +60 -0
  57. package/dist/main/utils/ipc-registry.d.ts +39 -0
  58. package/dist/main/utils/ipc-registry.js +109 -0
  59. package/dist/main/utils/ipc-schema.d.ts +28 -0
  60. package/dist/main/utils/ipc-schema.js +38 -0
  61. package/dist/main/utils/paths.d.ts +6 -0
  62. package/dist/main/utils/paths.js +6 -0
  63. package/dist/main/utils/sender-policy.d.ts +23 -0
  64. package/dist/main/utils/sender-policy.js +42 -0
  65. package/dist/main/windows/main-window/create.d.ts +6 -0
  66. package/dist/main/windows/main-window/create.js +65 -9
  67. package/dist/main/windows/main-window/events.d.ts +2 -1
  68. package/dist/main/windows/main-window/events.js +25 -2
  69. package/dist/main/windows/navigation-hardening.d.ts +24 -0
  70. package/dist/main/windows/navigation-hardening.js +55 -0
  71. package/dist/main/windows/settings-window/create.js +11 -1
  72. package/dist/preload/index.d.ts +17 -0
  73. package/dist/preload/index.js +17 -0
  74. package/dist/preload/instrumentation/app-data.js +178 -19
  75. package/dist/preload/runtime/custom-apis.d.ts +6 -0
  76. package/dist/preload/runtime/custom-apis.js +18 -0
  77. package/dist/preload/runtime/host.d.ts +1 -0
  78. package/dist/preload/runtime/host.js +13 -0
  79. package/dist/preload/windows/main.cjs +47 -0
  80. package/dist/preload/windows/main.cjs.map +7 -0
  81. package/dist/preload/windows/simulator.js +174 -18
  82. package/dist/renderer/assets/index-DW_0thr-.js +46 -0
  83. package/dist/renderer/assets/{input-DxfYnzX_.js → input-C5M5Wxn6.js} +2 -2
  84. package/dist/renderer/assets/{ipc-transport-ClIvDcYY.js → ipc-transport-CUsl-fS2.js} +2 -2
  85. package/dist/renderer/assets/ipc-transport-CltRuvhi.css +1 -0
  86. package/dist/renderer/assets/{popover-CtXcBJtM.js → popover-BSBPVO0o.js} +2 -2
  87. package/dist/renderer/assets/{select-DMfp49f1.js → select-B1tF3UAc.js} +2 -2
  88. package/dist/renderer/assets/{settings-DteGraEU.js → settings-7w54H6p-.js} +2 -2
  89. package/dist/renderer/assets/settings-api-BjJalghB.js +2 -0
  90. package/dist/renderer/assets/workbenchSettings-q2_-nc8t.js +8 -0
  91. package/dist/renderer/entries/main/index.html +12 -5
  92. package/dist/renderer/entries/popover/index.html +7 -5
  93. package/dist/renderer/entries/settings/index.html +7 -5
  94. package/dist/renderer/entries/workbench-settings/index.html +6 -4
  95. package/dist/shared/ipc-channels.d.ts +22 -0
  96. package/dist/shared/ipc-channels.js +23 -0
  97. package/dist/shared/ipc-schemas.d.ts +113 -0
  98. package/dist/shared/ipc-schemas.js +128 -0
  99. package/dist/shared/types.d.ts +2 -0
  100. package/dist/simulator/assets/simulator-DL6kOCq-.js +9 -0
  101. package/dist/simulator/simulator.html +1 -1
  102. package/package.json +11 -27
  103. package/dist/preload/windows/main.d.ts +0 -6
  104. package/dist/preload/windows/main.js +0 -6
  105. package/dist/renderer/assets/index-D1p6crtf.js +0 -46
  106. package/dist/renderer/assets/ipc-transport-EiWxu_hG.css +0 -1
  107. package/dist/renderer/assets/settings-api-CY84UMpN.js +0 -2
  108. package/dist/renderer/assets/workbenchSettings-DjELLnSJ.js +0 -8
  109. package/dist/simulator/assets/simulator-C1eHXnuq.js +0 -9
@@ -1,5 +1,7 @@
1
1
  import { WebContentsView, webContents } from 'electron';
2
2
  import path from 'path';
3
+ import { mainPreloadPath } from '../../utils/paths.js';
4
+ import { applyNavigationHardening } from '../../windows/navigation-hardening.js';
3
5
  import * as layout from '../layout/index.js';
4
6
  import { getDefaultTab } from '../workbench-context.js';
5
7
  /**
@@ -143,8 +145,16 @@ export function createViewManager(ctx) {
143
145
  async function showSettings() {
144
146
  if (!settingsView) {
145
147
  settingsView = new WebContentsView({
146
- webPreferences: { nodeIntegration: true, contextIsolation: false },
148
+ webPreferences: {
149
+ nodeIntegration: false,
150
+ contextIsolation: true,
151
+ sandbox: false,
152
+ preload: mainPreloadPath,
153
+ },
147
154
  });
155
+ // Overlay loads mainPreloadPath, so the same navigation rules as the
156
+ // main window apply — see navigation-hardening.ts.
157
+ applyNavigationHardening(settingsView.webContents, ctx.rendererDir);
148
158
  await settingsView.webContents.loadFile(path.join(ctx.rendererDir, 'entries/settings/index.html'));
149
159
  }
150
160
  if (!settingsViewAdded) {
@@ -165,8 +175,15 @@ export function createViewManager(ctx) {
165
175
  function showPopover(data) {
166
176
  hidePopover();
167
177
  const popover = new WebContentsView({
168
- webPreferences: { nodeIntegration: true, contextIsolation: false },
178
+ webPreferences: {
179
+ nodeIntegration: false,
180
+ contextIsolation: true,
181
+ sandbox: false,
182
+ preload: mainPreloadPath,
183
+ },
169
184
  });
185
+ // Popover overlay loads mainPreloadPath — same navigation rules apply.
186
+ applyNavigationHardening(popover.webContents, ctx.rendererDir);
170
187
  popoverView = popover;
171
188
  popover.setBackgroundColor('#00000000');
172
189
  ctx.mainWindow.contentView.addChildView(popover);
@@ -247,6 +264,20 @@ export function createViewManager(ctx) {
247
264
  return null;
248
265
  return settingsView.webContents;
249
266
  },
267
+ getSettingsWebContentsId: () => {
268
+ if (!settingsView)
269
+ return null;
270
+ if (settingsView.webContents.isDestroyed())
271
+ return null;
272
+ return settingsView.webContents.id;
273
+ },
274
+ getPopoverWebContentsId: () => {
275
+ if (!popoverView)
276
+ return null;
277
+ if (popoverView.webContents.isDestroyed())
278
+ return null;
279
+ return popoverView.webContents.id;
280
+ },
250
281
  resize,
251
282
  setVisible,
252
283
  };
@@ -0,0 +1,24 @@
1
+ import type { BrowserWindow } from 'electron';
2
+ /**
3
+ * Owns every top-level BrowserWindow the workbench is responsible for.
4
+ *
5
+ * Callers should prefer this service over reading bare `mainWindow` /
6
+ * `workbenchSettingsWindow` fields off `WorkbenchContext` — the latter
7
+ * remain as @deprecated synchronized mirrors during the migration window.
8
+ */
9
+ export interface WindowService {
10
+ /** The primary devtools window. Created during app bootstrap. */
11
+ readonly mainWindow: BrowserWindow;
12
+ /** Standalone workbench-settings BrowserWindow, or null when closed. */
13
+ readonly settingsWindow: BrowserWindow | null;
14
+ /** Update the tracked settings window (null when closed/destroyed). */
15
+ setSettingsWindow(win: BrowserWindow | null): void;
16
+ /** Close the settings window if it is open and clear the reference. */
17
+ closeSettingsWindow(): void;
18
+ /** True when `webContentsId` belongs to the (alive) main window renderer. */
19
+ isMainSender(webContentsId: number): boolean;
20
+ /** True when `webContentsId` belongs to the (alive) settings window renderer. */
21
+ isSettingsWindowSender(webContentsId: number): boolean;
22
+ }
23
+ export declare function createWindowService(mainWindow: BrowserWindow): WindowService;
24
+ //# sourceMappingURL=window-service.d.ts.map
@@ -0,0 +1,26 @@
1
+ export function createWindowService(mainWindow) {
2
+ let settingsWin = null;
3
+ return {
4
+ get mainWindow() {
5
+ return mainWindow;
6
+ },
7
+ get settingsWindow() {
8
+ return settingsWin;
9
+ },
10
+ setSettingsWindow(win) {
11
+ settingsWin = win;
12
+ },
13
+ closeSettingsWindow() {
14
+ if (settingsWin && !settingsWin.isDestroyed())
15
+ settingsWin.close();
16
+ settingsWin = null;
17
+ },
18
+ isMainSender(id) {
19
+ return !mainWindow.isDestroyed() && mainWindow.webContents.id === id;
20
+ },
21
+ isSettingsWindowSender(id) {
22
+ return !!settingsWin && !settingsWin.isDestroyed() && settingsWin.webContents.id === id;
23
+ },
24
+ };
25
+ }
26
+ //# sourceMappingURL=window-service.js.map
@@ -1,19 +1,24 @@
1
1
  import type { BrowserWindow } from 'electron';
2
2
  import type { CompilationAdapter } from '../../shared/types.js';
3
+ import { DisposableRegistry } from '../utils/disposable.js';
4
+ import type { SenderPolicy } from '../utils/ipc-registry.js';
3
5
  import { type RendererNotifier } from './notifications/renderer-notifier.js';
4
6
  import { type ViewManager } from './views/view-manager.js';
7
+ import { type WindowService } from './window-service.js';
5
8
  import { type WorkspaceService } from './workspace/workspace-service.js';
6
9
  /**
7
10
  * Shared mutable state for the workbench application.
8
11
  * Passed to each IPC module so they can read/write shared state without closures.
9
12
  */
10
13
  export interface WorkbenchContext {
14
+ /** @deprecated use ctx.windows.mainWindow */
11
15
  mainWindow: BrowserWindow;
12
16
  adapter: CompilationAdapter;
13
17
  /** Absolute path to the preload script loaded into the simulator webview */
14
18
  preloadPath: string;
15
19
  /** Absolute path to the renderer dist directory */
16
20
  rendererDir: string;
21
+ /** @deprecated use ctx.windows.settingsWindow / setSettingsWindow / closeSettingsWindow */
17
22
  workbenchSettingsWindow: BrowserWindow | null;
18
23
  /** Built-in panel IDs to display (default: all) */
19
24
  panels: string[];
@@ -37,10 +42,20 @@ export interface WorkbenchContext {
37
42
  };
38
43
  /** Unified lifecycle manager for all overlay WebContentsViews */
39
44
  views: ViewManager;
45
+ /** Owns BrowserWindow instances (main + settings) and their sender-id checks. */
46
+ windows: WindowService;
40
47
  /** Unified main → renderer event dispatcher */
41
48
  notify: RendererNotifier;
42
49
  /** Single source of truth for project + session + per-project settings */
43
50
  workspace: WorkspaceService;
51
+ /**
52
+ * Trust predicate consulted by `IpcRegistry` for every incoming IPC.
53
+ * Resolves the currently-trusted senders (main renderer + overlays)
54
+ * lazily so it stays correct as windows/views come and go.
55
+ */
56
+ senderPolicy: SenderPolicy;
57
+ /** Aggregates dispose handlers for every IPC handler, listener, watcher, and CDP session registered by the workbench. */
58
+ registry: DisposableRegistry;
44
59
  }
45
60
  export interface CreateContextOptions {
46
61
  mainWindow: BrowserWindow;
@@ -1,6 +1,9 @@
1
+ import { DisposableRegistry } from '../utils/disposable.js';
2
+ import { createWorkbenchSenderPolicy } from '../utils/sender-policy.js';
1
3
  import { defaultAdapter } from './default-adapter.js';
2
4
  import { createRendererNotifier, } from './notifications/renderer-notifier.js';
3
5
  import { createViewManager } from './views/view-manager.js';
6
+ import { createWindowService } from './window-service.js';
4
7
  import { createWorkspaceService, } from './workspace/workspace-service.js';
5
8
  export function hasBuiltinPanel(ctx, panelId) {
6
9
  return ctx.panels.includes(panelId);
@@ -25,9 +28,12 @@ export function createWorkbenchContext(opts) {
25
28
  brandingProvider: opts.brandingProvider,
26
29
  workbenchSettingsWindow: null,
27
30
  };
31
+ ctx.registry = new DisposableRegistry();
32
+ ctx.windows = createWindowService(ctx.mainWindow);
28
33
  ctx.views = createViewManager(ctx);
29
34
  ctx.notify = createRendererNotifier(ctx);
30
35
  ctx.workspace = createWorkspaceService(ctx);
36
+ ctx.senderPolicy = createWorkbenchSenderPolicy(ctx);
31
37
  return ctx;
32
38
  }
33
39
  //# sourceMappingURL=workbench-context.js.map
@@ -28,6 +28,7 @@ export interface WorkspaceService {
28
28
  listProjects(): Project[];
29
29
  addProject(dirPath: string): Project;
30
30
  removeProject(dirPath: string): void;
31
+ hasProject(dirPath: string): boolean;
31
32
  validateProjectDir(dirPath: string): string | null;
32
33
  openProject(projectPath: string): Promise<OpenProjectResult>;
33
34
  closeProject(): Promise<void>;
@@ -1,11 +1,12 @@
1
1
  import * as repo from '../projects/project-repository.js';
2
2
  import { clearSimulatorServicewechatReferer, setSimulatorServicewechatReferer, } from '../simulator/referer.js';
3
+ import { loadWorkbenchSettings } from '../settings/index.js';
3
4
  /** Build a workspace service bound to the given workbench context. */
4
5
  export function createWorkspaceService(ctx) {
5
6
  let currentSession = null;
6
7
  let currentProjectPath = '';
7
- function sendStatus(status, message) {
8
- ctx.notify.projectStatus({ status, message });
8
+ function sendStatus(status, message, hotReload) {
9
+ ctx.notify.projectStatus(hotReload ? { status, message, hotReload: true } : { status, message });
9
10
  }
10
11
  function bestEffort(label, fn) {
11
12
  try {
@@ -40,6 +41,7 @@ export function createWorkspaceService(ctx) {
40
41
  listProjects: () => repo.listProjects(),
41
42
  addProject: (dirPath) => repo.addProject(dirPath),
42
43
  removeProject: (dirPath) => repo.removeProject(dirPath),
44
+ hasProject: (dirPath) => repo.hasProject(dirPath),
43
45
  validateProjectDir: (dirPath) => repo.validateProjectDir(dirPath),
44
46
  async openProject(projectPath) {
45
47
  clearSimulatorServicewechatReferer();
@@ -58,12 +60,14 @@ export function createWorkspaceService(ctx) {
58
60
  return { success: false, error: dirError };
59
61
  }
60
62
  sendStatus('compiling', '正在编译...');
63
+ const { compile } = loadWorkbenchSettings();
61
64
  let session;
62
65
  try {
63
66
  session = await ctx.adapter.openProject({
64
67
  projectPath,
65
68
  sourcemap: true,
66
- onRebuild: () => sendStatus('ready', '编译完成,已热更新'),
69
+ watch: compile.watch,
70
+ onRebuild: () => sendStatus('ready', '编译完成,已热更新', true),
67
71
  onBuildError: (err) => sendStatus('error', String(err)),
68
72
  });
69
73
  }
@@ -0,0 +1,23 @@
1
+ import { type SimulatorApiHandler } from './services/simulator/custom-apis.js';
2
+ export type { SimulatorApiHandler } from './services/simulator/custom-apis.js';
3
+ /**
4
+ * Register a handler for a simulator API callable from mini-program code.
5
+ *
6
+ * The handler runs in the Electron main process (Node.js context) and may
7
+ * use any main-process capability (filesystem, network, native deps). When
8
+ * the mini-program invokes `wx.<name>(params)` inside the simulator, the
9
+ * simulator forwards the call over IPC and resolves to the handler's return
10
+ * value. Errors thrown by the handler propagate back as a rejection.
11
+ *
12
+ * Re-registering the same name silently replaces the previous handler.
13
+ * The returned disposer removes only the registration it created — if the
14
+ * name was overwritten by a later `registerSimulatorApi`, calling the
15
+ * earlier disposer is a no-op.
16
+ *
17
+ * @example
18
+ * registerSimulatorApi('myCompany.login', async ({ username }) => {
19
+ * return await callInternalAuth(username)
20
+ * })
21
+ */
22
+ export declare function registerSimulatorApi(name: string, handler: SimulatorApiHandler): () => void;
23
+ //# sourceMappingURL=simulator-apis.d.ts.map
@@ -0,0 +1,24 @@
1
+ import { simulatorApiRegistry } from './services/simulator/custom-apis.js';
2
+ /**
3
+ * Register a handler for a simulator API callable from mini-program code.
4
+ *
5
+ * The handler runs in the Electron main process (Node.js context) and may
6
+ * use any main-process capability (filesystem, network, native deps). When
7
+ * the mini-program invokes `wx.<name>(params)` inside the simulator, the
8
+ * simulator forwards the call over IPC and resolves to the handler's return
9
+ * value. Errors thrown by the handler propagate back as a rejection.
10
+ *
11
+ * Re-registering the same name silently replaces the previous handler.
12
+ * The returned disposer removes only the registration it created — if the
13
+ * name was overwritten by a later `registerSimulatorApi`, calling the
14
+ * earlier disposer is a no-op.
15
+ *
16
+ * @example
17
+ * registerSimulatorApi('myCompany.login', async ({ username }) => {
18
+ * return await callInternalAuth(username)
19
+ * })
20
+ */
21
+ export function registerSimulatorApi(name, handler) {
22
+ return simulatorApiRegistry.register(name, handler);
23
+ }
24
+ //# sourceMappingURL=simulator-apis.js.map
@@ -0,0 +1,13 @@
1
+ export interface Disposable {
2
+ dispose(): void | Promise<void>;
3
+ }
4
+ export type DisposeFn = () => void | Promise<void>;
5
+ export declare function toDisposable(fn: DisposeFn): Disposable;
6
+ export declare class DisposableRegistry implements Disposable {
7
+ private entries;
8
+ private _disposed;
9
+ add(d: Disposable | DisposeFn): Disposable;
10
+ disposeAll(): Promise<void>;
11
+ dispose(): Promise<void>;
12
+ }
13
+ //# sourceMappingURL=disposable.d.ts.map
@@ -0,0 +1,60 @@
1
+ export function toDisposable(fn) {
2
+ let done = false;
3
+ return {
4
+ dispose() {
5
+ if (done)
6
+ return;
7
+ done = true;
8
+ return fn();
9
+ },
10
+ };
11
+ }
12
+ export class DisposableRegistry {
13
+ entries = [];
14
+ _disposed = false;
15
+ add(d) {
16
+ if (this._disposed) {
17
+ throw new Error('cannot add to disposed registry');
18
+ }
19
+ const fn = typeof d === 'function' ? d : () => d.dispose();
20
+ const entry = { fn, released: false };
21
+ this.entries.push(entry);
22
+ return {
23
+ dispose: () => {
24
+ if (entry.released)
25
+ return;
26
+ entry.released = true;
27
+ const i = this.entries.indexOf(entry);
28
+ if (i >= 0)
29
+ this.entries.splice(i, 1);
30
+ return fn();
31
+ },
32
+ };
33
+ }
34
+ async disposeAll() {
35
+ if (this._disposed)
36
+ return;
37
+ this._disposed = true;
38
+ const items = this.entries.slice().reverse();
39
+ this.entries = [];
40
+ const errors = [];
41
+ for (const entry of items) {
42
+ if (entry.released)
43
+ continue;
44
+ entry.released = true;
45
+ try {
46
+ await entry.fn();
47
+ }
48
+ catch (e) {
49
+ errors.push(e);
50
+ }
51
+ }
52
+ if (errors.length > 0) {
53
+ throw new AggregateError(errors, 'DisposableRegistry encountered errors during disposeAll');
54
+ }
55
+ }
56
+ dispose() {
57
+ return this.disposeAll();
58
+ }
59
+ }
60
+ //# sourceMappingURL=disposable.js.map
@@ -0,0 +1,39 @@
1
+ import type { WebContents } from 'electron';
2
+ import { ipcMain } from 'electron';
3
+ import { type Disposable } from './disposable.js';
4
+ type HandleFn = Parameters<typeof ipcMain.handle>[1];
5
+ type ListenerFn = Parameters<typeof ipcMain.on>[1];
6
+ /**
7
+ * Trust predicate used to gate IPC delivery on the main process.
8
+ *
9
+ * Returns true when the calling WebContents is allowed to invoke / emit on
10
+ * the channel, false otherwise.
11
+ */
12
+ export type SenderPolicy = (sender: WebContents) => boolean;
13
+ /**
14
+ * Tiny fluent helper that wraps every `ipcMain.handle` / `ipcMain.on` with a
15
+ * matching removeHandler/removeListener registered into an internal registry.
16
+ *
17
+ * Each `register*Ipc(ctx)` returns one of these as a Disposable so the
18
+ * workbench-level registry can dispose all built-in handlers in one shot.
19
+ *
20
+ * If a {@link SenderPolicy} is provided, every incoming invocation is gated:
21
+ * - `handle`: rejected senders cause the invoke promise to reject with
22
+ * `Error('IPC sender rejected for channel <channel>')`.
23
+ * - `on`: rejected senders are silently dropped (the original listener is
24
+ * never called).
25
+ * In both cases a single `console.warn` is emitted with the channel name
26
+ * and a short sender summary. Without a policy the wrapper is a no-op,
27
+ * preserving backwards compatibility for unit tests and callers that
28
+ * opted out.
29
+ */
30
+ export declare class IpcRegistry implements Disposable {
31
+ private policy?;
32
+ private registry;
33
+ constructor(policy?: SenderPolicy | undefined);
34
+ handle(channel: string, fn: HandleFn): this;
35
+ on(channel: string, fn: ListenerFn): this;
36
+ dispose(): Promise<void>;
37
+ }
38
+ export {};
39
+ //# sourceMappingURL=ipc-registry.d.ts.map
@@ -0,0 +1,109 @@
1
+ import { ipcMain } from 'electron';
2
+ import { DisposableRegistry } from './disposable.js';
3
+ import { IpcValidationError } from './ipc-schema.js';
4
+ import { createLogger } from './logger.js';
5
+ const log = createLogger('ipc');
6
+ /**
7
+ * Funnels errors thrown by an `on()` listener into the logger so they don't
8
+ * escape into Electron's event loop. Validation errors get a compact `warn`
9
+ * (channel + zod paths), other errors get a full `error` with stack.
10
+ */
11
+ function reportListenerError(channel, err) {
12
+ if (err instanceof IpcValidationError) {
13
+ log.warn(`schema reject on '${channel}' at [${err.paths.join(', ')}]`);
14
+ return;
15
+ }
16
+ if (err instanceof Error) {
17
+ log.error(`listener threw on '${channel}': ${err.message}`, err.stack);
18
+ return;
19
+ }
20
+ log.error(`listener threw on '${channel}'`, err);
21
+ }
22
+ function isThenable(v) {
23
+ return (v != null &&
24
+ (typeof v === 'object' || typeof v === 'function') &&
25
+ typeof v.then === 'function');
26
+ }
27
+ function summarizeSender(sender) {
28
+ if (sender.isDestroyed())
29
+ return '<destroyed>';
30
+ const url = sender.getURL();
31
+ return `id=${sender.id} ${url.slice(0, 120)}`;
32
+ }
33
+ /**
34
+ * Tiny fluent helper that wraps every `ipcMain.handle` / `ipcMain.on` with a
35
+ * matching removeHandler/removeListener registered into an internal registry.
36
+ *
37
+ * Each `register*Ipc(ctx)` returns one of these as a Disposable so the
38
+ * workbench-level registry can dispose all built-in handlers in one shot.
39
+ *
40
+ * If a {@link SenderPolicy} is provided, every incoming invocation is gated:
41
+ * - `handle`: rejected senders cause the invoke promise to reject with
42
+ * `Error('IPC sender rejected for channel <channel>')`.
43
+ * - `on`: rejected senders are silently dropped (the original listener is
44
+ * never called).
45
+ * In both cases a single `console.warn` is emitted with the channel name
46
+ * and a short sender summary. Without a policy the wrapper is a no-op,
47
+ * preserving backwards compatibility for unit tests and callers that
48
+ * opted out.
49
+ */
50
+ export class IpcRegistry {
51
+ policy;
52
+ registry = new DisposableRegistry();
53
+ constructor(policy) {
54
+ this.policy = policy;
55
+ }
56
+ handle(channel, fn) {
57
+ const policy = this.policy;
58
+ const guarded = policy
59
+ ? (event, ...args) => {
60
+ const sender = event.sender;
61
+ if (!policy(sender)) {
62
+ console.warn(`[ipc] sender rejected for channel '${channel}' (${summarizeSender(sender)})`);
63
+ throw new Error(`IPC sender rejected for channel ${channel}`);
64
+ }
65
+ return fn(event, ...args);
66
+ }
67
+ : fn;
68
+ ipcMain.handle(channel, guarded);
69
+ this.registry.add(() => ipcMain.removeHandler(channel));
70
+ return this;
71
+ }
72
+ on(channel, fn) {
73
+ const policy = this.policy;
74
+ const raw = fn;
75
+ const safeInvoke = (event, args) => {
76
+ try {
77
+ const ret = raw(event, ...args);
78
+ if (isThenable(ret)) {
79
+ // Async listeners would otherwise leak rejections into Electron's
80
+ // event loop as `UnhandledPromiseRejection`. Funnel them into the
81
+ // same logger path as sync throws.
82
+ Promise.resolve(ret).catch((err) => reportListenerError(channel, err));
83
+ }
84
+ }
85
+ catch (err) {
86
+ reportListenerError(channel, err);
87
+ }
88
+ };
89
+ const guarded = policy
90
+ ? (event, ...args) => {
91
+ const sender = event.sender;
92
+ if (!policy(sender)) {
93
+ console.warn(`[ipc] sender rejected for channel '${channel}' (${summarizeSender(sender)})`);
94
+ return;
95
+ }
96
+ safeInvoke(event, args);
97
+ }
98
+ : (event, ...args) => safeInvoke(event, args);
99
+ ipcMain.on(channel, guarded);
100
+ this.registry.add(() => {
101
+ ipcMain.removeListener(channel, guarded);
102
+ });
103
+ return this;
104
+ }
105
+ dispose() {
106
+ return this.registry.dispose();
107
+ }
108
+ }
109
+ //# sourceMappingURL=ipc-registry.js.map
@@ -0,0 +1,28 @@
1
+ import type { z } from 'zod';
2
+ /**
3
+ * Thrown by {@link validate} when an IPC payload fails its zod schema.
4
+ *
5
+ * Carries the channel name plus a compact list of failing zod paths so
6
+ * the IpcRegistry wrapper can log a short, actionable summary without
7
+ * dumping the full payload.
8
+ */
9
+ export declare class IpcValidationError extends Error {
10
+ readonly channel: string;
11
+ readonly paths: string[];
12
+ constructor(channel: string, issues: ReadonlyArray<{
13
+ path: ReadonlyArray<PropertyKey>;
14
+ }>);
15
+ }
16
+ /**
17
+ * Parse an IPC handler's argument tuple against a zod schema.
18
+ *
19
+ * On failure: warns to console with the channel name + issue list, then throws
20
+ * an {@link IpcValidationError} so that:
21
+ * - `ipcMain.handle` invocations reject the renderer's invoke promise
22
+ * without leaking schema internals.
23
+ * - `ipcMain.on` listeners (wrapped by IpcRegistry.on) can recognise the
24
+ * error type and log a structured warning instead of letting the throw
25
+ * escape into Electron's event loop.
26
+ */
27
+ export declare function validate<T extends z.ZodTypeAny>(channel: string, schema: T, args: unknown[]): z.infer<T>;
28
+ //# sourceMappingURL=ipc-schema.d.ts.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Thrown by {@link validate} when an IPC payload fails its zod schema.
3
+ *
4
+ * Carries the channel name plus a compact list of failing zod paths so
5
+ * the IpcRegistry wrapper can log a short, actionable summary without
6
+ * dumping the full payload.
7
+ */
8
+ export class IpcValidationError extends Error {
9
+ channel;
10
+ paths;
11
+ constructor(channel, issues) {
12
+ const paths = issues.map((i) => i.path.map((p) => String(p)).join('.') || '<root>');
13
+ super(`Invalid IPC arguments for ${channel}`);
14
+ this.name = 'IpcValidationError';
15
+ this.channel = channel;
16
+ this.paths = paths;
17
+ }
18
+ }
19
+ /**
20
+ * Parse an IPC handler's argument tuple against a zod schema.
21
+ *
22
+ * On failure: warns to console with the channel name + issue list, then throws
23
+ * an {@link IpcValidationError} so that:
24
+ * - `ipcMain.handle` invocations reject the renderer's invoke promise
25
+ * without leaking schema internals.
26
+ * - `ipcMain.on` listeners (wrapped by IpcRegistry.on) can recognise the
27
+ * error type and log a structured warning instead of letting the throw
28
+ * escape into Electron's event loop.
29
+ */
30
+ export function validate(channel, schema, args) {
31
+ const r = schema.safeParse(args);
32
+ if (!r.success) {
33
+ console.warn('[ipc] schema reject', channel, r.error.issues);
34
+ throw new IpcValidationError(channel, r.error.issues);
35
+ }
36
+ return r.data;
37
+ }
38
+ //# sourceMappingURL=ipc-schema.js.map
@@ -1,6 +1,12 @@
1
1
  export declare const devtoolsPackageRoot: string;
2
2
  export declare const rendererDir: string;
3
3
  export declare const defaultPreloadPath: string;
4
+ /**
5
+ * Preload bundle for the main window, settings window, and the settings /
6
+ * popover overlay WebContentsViews. Exposes `window.devtools.ipc` via
7
+ * contextBridge so the renderer never needs `window.require('electron')`.
8
+ */
9
+ export declare const mainPreloadPath: string;
4
10
  export declare const simulatorDir: string;
5
11
  export declare function getRendererDir(): string;
6
12
  export declare function getPreloadDir(): string;
@@ -25,6 +25,12 @@ function resolveDevtoolsPackageRoot() {
25
25
  export const devtoolsPackageRoot = resolveDevtoolsPackageRoot();
26
26
  export const rendererDir = path.join(devtoolsPackageRoot, 'dist/renderer');
27
27
  export const defaultPreloadPath = path.join(devtoolsPackageRoot, 'dist/preload/windows/simulator.js');
28
+ /**
29
+ * Preload bundle for the main window, settings window, and the settings /
30
+ * popover overlay WebContentsViews. Exposes `window.devtools.ipc` via
31
+ * contextBridge so the renderer never needs `window.require('electron')`.
32
+ */
33
+ export const mainPreloadPath = path.join(devtoolsPackageRoot, 'dist/preload/windows/main.cjs');
28
34
  export const simulatorDir = path.join(devtoolsPackageRoot, 'dist/simulator');
29
35
  export function getRendererDir() {
30
36
  return rendererDir;
@@ -0,0 +1,23 @@
1
+ import type { WorkbenchContext } from '../services/workbench-context.js';
2
+ import type { SenderPolicy } from './ipc-registry.js';
3
+ /**
4
+ * Default workbench sender white-list.
5
+ *
6
+ * An IPC message is accepted only when its `event.sender` matches one of
7
+ * the WebContents the main process itself created and trusts:
8
+ * - the main window's renderer
9
+ * - the optional workbench settings BrowserWindow's renderer (when open)
10
+ * - the settings overlay view (when open)
11
+ * - the popover overlay view (when open)
12
+ *
13
+ * The simulator webview is intentionally NOT on this list: its preload
14
+ * (`src/preload/windows/simulator.ts`) uses `ipcRenderer.sendToHost`
15
+ * exclusively, never `invoke`/`send`, so it can't legitimately reach an
16
+ * ipcMain handler. Including it would only widen the attack surface if
17
+ * the guest were ever compromised.
18
+ *
19
+ * Any other sender — including a stale/destroyed sender or an unknown
20
+ * iframe — is rejected.
21
+ */
22
+ export declare function createWorkbenchSenderPolicy(ctx: Pick<WorkbenchContext, 'windows' | 'views'>): SenderPolicy;
23
+ //# sourceMappingURL=sender-policy.d.ts.map