@workbench-kit/electron-shell 0.0.2-prototype.0.2.10 → 0.0.2-prototype.0.2.13
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.
- package/README.md +93 -0
- package/package.json +4 -3
- package/src/preload/allowlisted-ipc.ts +58 -0
- package/src/preload/create-preload-api.ts +119 -0
- package/src/preload/expose-preload-api.ts +27 -0
- package/src/preload/index.ts +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# `@workbench-kit/electron-shell`
|
|
2
|
+
|
|
3
|
+
Electron **main-process** helpers (window controls, asset protocol, secret vault)
|
|
4
|
+
and a typed **preload** scaffold. The package stays Electron-free — hosts inject
|
|
5
|
+
narrow `ipcMain` / `ipcRenderer` / `contextBridge` surfaces.
|
|
6
|
+
|
|
7
|
+
Published on npm with the **`prototype`** dist tag.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```powershell
|
|
12
|
+
pnpm add @workbench-kit/electron-shell@prototype
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Main entry
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import {
|
|
19
|
+
registerWindowControlIpc,
|
|
20
|
+
createEncryptedSecretVault,
|
|
21
|
+
requireOwnedWindowForSender,
|
|
22
|
+
} from '@workbench-kit/electron-shell';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Typed preload scaffold (`./preload`)
|
|
26
|
+
|
|
27
|
+
Secure renderer↔main pattern: allowlisted invoke/subscribe + `contextBridge`
|
|
28
|
+
expose. **Never** put `ipcRenderer` on `window`.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
// preload.ts (host)
|
|
32
|
+
import {
|
|
33
|
+
createWorkbenchKitPreloadApi,
|
|
34
|
+
exposeWorkbenchKitPreload,
|
|
35
|
+
} from '@workbench-kit/electron-shell/preload';
|
|
36
|
+
import { contextBridge, ipcRenderer } from 'electron';
|
|
37
|
+
|
|
38
|
+
const windowChannels = {
|
|
39
|
+
minimize: 'wk:window:minimize',
|
|
40
|
+
toggleMaximized: 'wk:window:toggleMaximized',
|
|
41
|
+
close: 'wk:window:close',
|
|
42
|
+
isMaximized: 'wk:window:isMaximized',
|
|
43
|
+
maximizedChanged: 'wk:window:maximizedChanged',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const api = createWorkbenchKitPreloadApi({
|
|
47
|
+
windowChannels,
|
|
48
|
+
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
|
|
49
|
+
subscribe: (channel, listener) => {
|
|
50
|
+
const wrapped = (_event: unknown, ...args: unknown[]) => listener(...args);
|
|
51
|
+
ipcRenderer.on(channel, wrapped);
|
|
52
|
+
return () => ipcRenderer.removeListener(channel, wrapped);
|
|
53
|
+
},
|
|
54
|
+
// optional:
|
|
55
|
+
// vaultChannels: { get, set, delete },
|
|
56
|
+
// openExternalLinkChannel: 'wk:openExternal',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
exposeWorkbenchKitPreload({
|
|
60
|
+
api,
|
|
61
|
+
exposeInMainWorld: (key, value) => contextBridge.exposeInMainWorld(key, value),
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Renderer TypeScript:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import type { WorkbenchKitPreloadApi } from '@workbench-kit/electron-shell/preload';
|
|
69
|
+
|
|
70
|
+
declare global {
|
|
71
|
+
interface Window {
|
|
72
|
+
workbenchKit: WorkbenchKitPreloadApi;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await window.workbenchKit.window.minimize();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Enablement checklist
|
|
80
|
+
|
|
81
|
+
1. `contextIsolation: true`, `nodeIntegration: false` on BrowserWindow
|
|
82
|
+
2. Register main IPC with `registerWindowControlIpc` + `requireOwnedWindowForSender`
|
|
83
|
+
3. Build preload with `createWorkbenchKitPreloadApi` + `exposeWorkbenchKitPreload`
|
|
84
|
+
4. Do **not** expose `ipcRenderer`, `require`, or Node builtins to the page
|
|
85
|
+
5. Keep channel names host-owned; only allowlist channels the scaffold will call
|
|
86
|
+
6. Pair dynamic click-through windows with platform residency + renderer hit-testing
|
|
87
|
+
|
|
88
|
+
See [security-boundary.md](../../docs/architecture/security-boundary.md).
|
|
89
|
+
|
|
90
|
+
## Related
|
|
91
|
+
|
|
92
|
+
- `@workbench-kit/platform` — `applyWindowResidencyPolicy` for secondary windows
|
|
93
|
+
- Issue #102 — CJS leaf consumption for Electron main (complementary packaging)
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workbench-kit/electron-shell",
|
|
3
|
-
"version": "0.0.2-prototype.0.2.
|
|
3
|
+
"version": "0.0.2-prototype.0.2.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
|
-
".": "./src/index.ts"
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./preload": "./src/preload/index.ts"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"src",
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
"!src/**/*.stories.ts",
|
|
14
15
|
"!src/**/*.stories.tsx"
|
|
15
16
|
],
|
|
16
|
-
"description": "Electron main-process helpers for Workbench Kit hosts (window controls, asset protocol, secret vault).",
|
|
17
|
+
"description": "Electron main-process helpers for Workbench Kit hosts (window controls, asset protocol, secret vault) plus a typed preload scaffold.",
|
|
17
18
|
"publishConfig": {
|
|
18
19
|
"access": "public",
|
|
19
20
|
"tag": "prototype",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allowlisted IPC helpers for preload scripts.
|
|
3
|
+
* Never forward raw ipcRenderer to the page — only these wrappers leave preload.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class DisallowedIpcChannelError extends Error {
|
|
7
|
+
readonly channel: string;
|
|
8
|
+
|
|
9
|
+
constructor(channel: string) {
|
|
10
|
+
super(`IPC channel "${channel}" is not allowlisted for the preload bridge.`);
|
|
11
|
+
this.name = 'DisallowedIpcChannelError';
|
|
12
|
+
this.channel = channel;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type PreloadInvoke = (channel: string, ...args: unknown[]) => Promise<unknown>;
|
|
17
|
+
export type PreloadSubscribe = (
|
|
18
|
+
channel: string,
|
|
19
|
+
listener: (...args: unknown[]) => void,
|
|
20
|
+
) => () => void;
|
|
21
|
+
|
|
22
|
+
export interface CreateAllowlistedInvokeOptions {
|
|
23
|
+
readonly allowedChannels: ReadonlySet<string> | readonly string[];
|
|
24
|
+
readonly invoke: PreloadInvoke;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CreateAllowlistedSubscribeOptions {
|
|
28
|
+
readonly allowedChannels: ReadonlySet<string> | readonly string[];
|
|
29
|
+
readonly subscribe: PreloadSubscribe;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toChannelSet(channels: ReadonlySet<string> | readonly string[]): ReadonlySet<string> {
|
|
33
|
+
return channels instanceof Set ? channels : new Set(channels);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Wrap invoke so only allowlisted channels can be called. */
|
|
37
|
+
export function createAllowlistedInvoke(options: CreateAllowlistedInvokeOptions): PreloadInvoke {
|
|
38
|
+
const allowed = toChannelSet(options.allowedChannels);
|
|
39
|
+
return async (channel, ...args) => {
|
|
40
|
+
if (!allowed.has(channel)) {
|
|
41
|
+
throw new DisallowedIpcChannelError(channel);
|
|
42
|
+
}
|
|
43
|
+
return options.invoke(channel, ...args);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Wrap subscribe so only allowlisted push channels can be observed. */
|
|
48
|
+
export function createAllowlistedSubscribe(
|
|
49
|
+
options: CreateAllowlistedSubscribeOptions,
|
|
50
|
+
): PreloadSubscribe {
|
|
51
|
+
const allowed = toChannelSet(options.allowedChannels);
|
|
52
|
+
return (channel, listener) => {
|
|
53
|
+
if (!allowed.has(channel)) {
|
|
54
|
+
throw new DisallowedIpcChannelError(channel);
|
|
55
|
+
}
|
|
56
|
+
return options.subscribe(channel, listener);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createWindowControlsBridge,
|
|
3
|
+
type WindowControlIpcChannels,
|
|
4
|
+
type WindowControlsBridge,
|
|
5
|
+
} from '../window/window-controls.js';
|
|
6
|
+
import {
|
|
7
|
+
createAllowlistedInvoke,
|
|
8
|
+
createAllowlistedSubscribe,
|
|
9
|
+
type PreloadInvoke,
|
|
10
|
+
type PreloadSubscribe,
|
|
11
|
+
} from './allowlisted-ipc.js';
|
|
12
|
+
|
|
13
|
+
/** Optional vault subset exposed to the renderer (hosts implement main handlers). */
|
|
14
|
+
export interface WorkbenchKitPreloadVaultApi {
|
|
15
|
+
getSecret(key: string): Promise<string | null>;
|
|
16
|
+
setSecret(key: string, value: string): Promise<void>;
|
|
17
|
+
deleteSecret(key: string): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface WorkbenchKitPreloadVaultChannels {
|
|
21
|
+
readonly get: string;
|
|
22
|
+
readonly set: string;
|
|
23
|
+
readonly delete: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Typed API exposed via contextBridge (never includes ipcRenderer).
|
|
28
|
+
* Window controls are required; vault / openExternalLink are optional host subsets.
|
|
29
|
+
*/
|
|
30
|
+
export interface WorkbenchKitPreloadApi {
|
|
31
|
+
readonly window: WindowControlsBridge;
|
|
32
|
+
readonly vault?: WorkbenchKitPreloadVaultApi;
|
|
33
|
+
readonly openExternalLink?: (linkId: string) => Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CreateWorkbenchKitPreloadApiOptions {
|
|
37
|
+
readonly windowChannels: WindowControlIpcChannels;
|
|
38
|
+
readonly invoke: PreloadInvoke;
|
|
39
|
+
readonly subscribe: PreloadSubscribe;
|
|
40
|
+
readonly vaultChannels?: WorkbenchKitPreloadVaultChannels;
|
|
41
|
+
readonly openExternalLinkChannel?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Extra allowlisted invoke channels beyond window/vault/openExternal.
|
|
44
|
+
* Prefer declaring every channel the scaffold will call.
|
|
45
|
+
*/
|
|
46
|
+
readonly additionalInvokeChannels?: readonly string[];
|
|
47
|
+
readonly additionalSubscribeChannels?: readonly string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function collectInvokeChannels(options: CreateWorkbenchKitPreloadApiOptions): string[] {
|
|
51
|
+
const { windowChannels, vaultChannels, openExternalLinkChannel, additionalInvokeChannels } =
|
|
52
|
+
options;
|
|
53
|
+
return [
|
|
54
|
+
windowChannels.minimize,
|
|
55
|
+
windowChannels.toggleMaximized,
|
|
56
|
+
windowChannels.close,
|
|
57
|
+
windowChannels.isMaximized,
|
|
58
|
+
...(vaultChannels ? [vaultChannels.get, vaultChannels.set, vaultChannels.delete] : []),
|
|
59
|
+
...(openExternalLinkChannel ? [openExternalLinkChannel] : []),
|
|
60
|
+
...(additionalInvokeChannels ?? []),
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function collectSubscribeChannels(options: CreateWorkbenchKitPreloadApiOptions): string[] {
|
|
65
|
+
return [options.windowChannels.maximizedChanged, ...(options.additionalSubscribeChannels ?? [])];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build a typed preload API over allowlisted invoke/subscribe.
|
|
70
|
+
* Hosts inject Electron ipcRenderer.invoke / .on wrappers — kit stays Electron-free.
|
|
71
|
+
*/
|
|
72
|
+
export function createWorkbenchKitPreloadApi(
|
|
73
|
+
options: CreateWorkbenchKitPreloadApiOptions,
|
|
74
|
+
): WorkbenchKitPreloadApi {
|
|
75
|
+
const invoke = createAllowlistedInvoke({
|
|
76
|
+
allowedChannels: collectInvokeChannels(options),
|
|
77
|
+
invoke: options.invoke,
|
|
78
|
+
});
|
|
79
|
+
const subscribe = createAllowlistedSubscribe({
|
|
80
|
+
allowedChannels: collectSubscribeChannels(options),
|
|
81
|
+
subscribe: options.subscribe,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const api: WorkbenchKitPreloadApi = {
|
|
85
|
+
window: createWindowControlsBridge({
|
|
86
|
+
channels: options.windowChannels,
|
|
87
|
+
invoke,
|
|
88
|
+
subscribe,
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (options.vaultChannels) {
|
|
93
|
+
const vaultChannels = options.vaultChannels;
|
|
94
|
+
const vault: WorkbenchKitPreloadVaultApi = {
|
|
95
|
+
getSecret: async (key) => {
|
|
96
|
+
const value = await invoke(vaultChannels.get, key);
|
|
97
|
+
return typeof value === 'string' ? value : null;
|
|
98
|
+
},
|
|
99
|
+
setSecret: async (key, value) => {
|
|
100
|
+
await invoke(vaultChannels.set, key, value);
|
|
101
|
+
},
|
|
102
|
+
deleteSecret: async (key) => {
|
|
103
|
+
await invoke(vaultChannels.delete, key);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
Object.assign(api, { vault });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (options.openExternalLinkChannel) {
|
|
110
|
+
const channel = options.openExternalLinkChannel;
|
|
111
|
+
Object.assign(api, {
|
|
112
|
+
openExternalLink: async (linkId: string) => {
|
|
113
|
+
await invoke(channel, linkId);
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return api;
|
|
119
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { WorkbenchKitPreloadApi } from './create-preload-api.js';
|
|
2
|
+
|
|
3
|
+
/** Narrow contextBridge surface — hosts inject Electron `contextBridge.exposeInMainWorld`. */
|
|
4
|
+
export type ExposeInMainWorld = (apiKey: string, api: WorkbenchKitPreloadApi) => void;
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_WORKBENCH_KIT_PRELOAD_KEY = 'workbenchKit' as const;
|
|
7
|
+
|
|
8
|
+
export interface ExposeWorkbenchKitPreloadOptions {
|
|
9
|
+
readonly api: WorkbenchKitPreloadApi;
|
|
10
|
+
readonly exposeInMainWorld: ExposeInMainWorld;
|
|
11
|
+
/** Defaults to {@link DEFAULT_WORKBENCH_KIT_PRELOAD_KEY}. */
|
|
12
|
+
readonly apiKey?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Expose a typed preload API on `window[apiKey]` without leaking ipcRenderer.
|
|
17
|
+
*/
|
|
18
|
+
export function exposeWorkbenchKitPreload(options: ExposeWorkbenchKitPreloadOptions): string {
|
|
19
|
+
const apiKey = options.apiKey ?? DEFAULT_WORKBENCH_KIT_PRELOAD_KEY;
|
|
20
|
+
options.exposeInMainWorld(apiKey, options.api);
|
|
21
|
+
return apiKey;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Optional Window augmentation helper for host TypeScript projects. */
|
|
25
|
+
export interface WorkbenchKitPreloadWindow {
|
|
26
|
+
readonly workbenchKit: WorkbenchKitPreloadApi;
|
|
27
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createAllowlistedInvoke,
|
|
3
|
+
createAllowlistedSubscribe,
|
|
4
|
+
DisallowedIpcChannelError,
|
|
5
|
+
type CreateAllowlistedInvokeOptions,
|
|
6
|
+
type CreateAllowlistedSubscribeOptions,
|
|
7
|
+
type PreloadInvoke,
|
|
8
|
+
type PreloadSubscribe,
|
|
9
|
+
} from './allowlisted-ipc.js';
|
|
10
|
+
export {
|
|
11
|
+
createWorkbenchKitPreloadApi,
|
|
12
|
+
type CreateWorkbenchKitPreloadApiOptions,
|
|
13
|
+
type WorkbenchKitPreloadApi,
|
|
14
|
+
type WorkbenchKitPreloadVaultApi,
|
|
15
|
+
type WorkbenchKitPreloadVaultChannels,
|
|
16
|
+
} from './create-preload-api.js';
|
|
17
|
+
export {
|
|
18
|
+
DEFAULT_WORKBENCH_KIT_PRELOAD_KEY,
|
|
19
|
+
exposeWorkbenchKitPreload,
|
|
20
|
+
type ExposeInMainWorld,
|
|
21
|
+
type ExposeWorkbenchKitPreloadOptions,
|
|
22
|
+
type WorkbenchKitPreloadWindow,
|
|
23
|
+
} from './expose-preload-api.js';
|