bunite-core 0.0.1
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/package.json +24 -0
- package/src/bun/core/App.ts +142 -0
- package/src/bun/core/BrowserView.ts +262 -0
- package/src/bun/core/BrowserWindow.ts +322 -0
- package/src/bun/core/Socket.ts +186 -0
- package/src/bun/core/Utils.ts +72 -0
- package/src/bun/core/windowIds.ts +7 -0
- package/src/bun/events/appEvents.ts +7 -0
- package/src/bun/events/event.ts +20 -0
- package/src/bun/events/eventEmitter.ts +28 -0
- package/src/bun/events/webviewEvents.ts +13 -0
- package/src/bun/events/windowEvents.ts +19 -0
- package/src/bun/index.ts +41 -0
- package/src/bun/preload/index.ts +73 -0
- package/src/bun/preload/inline.ts +87 -0
- package/src/bun/proc/native.ts +666 -0
- package/src/native/shared/callbacks.h +6 -0
- package/src/native/shared/cef_response_filter.h +116 -0
- package/src/native/shared/ffi_exports.h +119 -0
- package/src/native/shared/webview_storage.h +89 -0
- package/src/native/win/native_host.cpp +2453 -0
- package/src/native/win/process_helper_win.cpp +26 -0
- package/src/preload/runtime.built.js +1 -0
- package/src/preload/runtime.ts +215 -0
- package/src/preload/tsconfig.json +13 -0
- package/src/shared/paths.ts +133 -0
- package/src/shared/platform.ts +9 -0
- package/src/shared/rpc.ts +399 -0
- package/src/shared/rpcWire.ts +54 -0
- package/src/shared/rpcWireConstants.ts +3 -0
- package/src/types/config.ts +29 -0
- package/src/view/index.ts +159 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import type { Pointer } from "bun:ffi";
|
|
2
|
+
import { BuniteEvent } from "../events/event";
|
|
3
|
+
import { buniteEventEmitter } from "../events/eventEmitter";
|
|
4
|
+
import { ensureNativeRuntime, getNativeLibrary, toCString } from "../proc/native";
|
|
5
|
+
import { BrowserView, type BrowserViewOptions } from "./BrowserView";
|
|
6
|
+
import type { RPCWithTransport } from "../../shared/rpc";
|
|
7
|
+
import { getNextWindowId } from "./windowIds";
|
|
8
|
+
import { resolveDefaultViewsRoot } from "../../shared/paths";
|
|
9
|
+
|
|
10
|
+
export type WindowOptionsType<T = undefined> = {
|
|
11
|
+
title: string;
|
|
12
|
+
frame: {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
maximized?: boolean;
|
|
18
|
+
minimized?: boolean;
|
|
19
|
+
};
|
|
20
|
+
url: string | null;
|
|
21
|
+
html: string | null;
|
|
22
|
+
preload: string | null;
|
|
23
|
+
viewsRoot: string | null;
|
|
24
|
+
rpc?: T;
|
|
25
|
+
titleBarStyle: "hidden" | "hiddenInset" | "default";
|
|
26
|
+
transparent: boolean;
|
|
27
|
+
hidden?: boolean;
|
|
28
|
+
navigationRules: string[] | null;
|
|
29
|
+
sandbox: boolean;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const defaultOptions: WindowOptionsType = {
|
|
33
|
+
title: "bunite",
|
|
34
|
+
frame: {
|
|
35
|
+
x: 80,
|
|
36
|
+
y: 80,
|
|
37
|
+
width: 1280,
|
|
38
|
+
height: 900
|
|
39
|
+
},
|
|
40
|
+
url: null,
|
|
41
|
+
html: null,
|
|
42
|
+
preload: null,
|
|
43
|
+
viewsRoot: null,
|
|
44
|
+
titleBarStyle: "default",
|
|
45
|
+
transparent: false,
|
|
46
|
+
hidden: false,
|
|
47
|
+
navigationRules: null,
|
|
48
|
+
sandbox: false
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const BrowserWindowMap: Record<number, BrowserWindow<any>> = {};
|
|
52
|
+
|
|
53
|
+
export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
|
|
54
|
+
id = getNextWindowId();
|
|
55
|
+
ptr: Pointer | null = null;
|
|
56
|
+
title: string;
|
|
57
|
+
frame: WindowOptionsType["frame"];
|
|
58
|
+
url: string | null;
|
|
59
|
+
html: string | null;
|
|
60
|
+
preload: string | null;
|
|
61
|
+
viewsRoot: string | null;
|
|
62
|
+
titleBarStyle: WindowOptionsType["titleBarStyle"];
|
|
63
|
+
transparent: boolean;
|
|
64
|
+
hidden: boolean;
|
|
65
|
+
navigationRules: string[] | null;
|
|
66
|
+
sandbox: boolean;
|
|
67
|
+
webviewId: number;
|
|
68
|
+
private closed = false;
|
|
69
|
+
private restoreMaximizedAfterMinimize = false;
|
|
70
|
+
private readonly handleNativeMove = (event: unknown) => {
|
|
71
|
+
const data = (event as {
|
|
72
|
+
data?: { x?: number; y?: number; maximized?: boolean; minimized?: boolean };
|
|
73
|
+
}).data;
|
|
74
|
+
if (!data) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.frame = {
|
|
79
|
+
...this.frame,
|
|
80
|
+
x: data.x ?? this.frame.x,
|
|
81
|
+
y: data.y ?? this.frame.y,
|
|
82
|
+
maximized: data.maximized ?? this.frame.maximized,
|
|
83
|
+
minimized: data.minimized ?? this.frame.minimized
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
private readonly handleNativeResize = (event: unknown) => {
|
|
87
|
+
const data = (event as {
|
|
88
|
+
data?: { x?: number; y?: number; width?: number; height?: number; maximized?: boolean; minimized?: boolean };
|
|
89
|
+
}).data;
|
|
90
|
+
if (!data) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.frame = {
|
|
95
|
+
...this.frame,
|
|
96
|
+
x: data.x ?? this.frame.x,
|
|
97
|
+
y: data.y ?? this.frame.y,
|
|
98
|
+
width: data.width ?? this.frame.width,
|
|
99
|
+
height: data.height ?? this.frame.height,
|
|
100
|
+
maximized: data.maximized ?? this.frame.maximized,
|
|
101
|
+
minimized: data.minimized ?? this.frame.minimized
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
private readonly handleNativeClose = () => {
|
|
105
|
+
if (this.closed) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this.closed = true;
|
|
109
|
+
this.ptr = null;
|
|
110
|
+
BrowserView.getById(this.webviewId)?.detachFromNative();
|
|
111
|
+
delete BrowserWindowMap[this.id];
|
|
112
|
+
buniteEventEmitter.off(`move-${this.id}`, this.handleNativeMove);
|
|
113
|
+
buniteEventEmitter.off(`resize-${this.id}`, this.handleNativeResize);
|
|
114
|
+
buniteEventEmitter.off(`close-${this.id}`, this.handleNativeClose);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
constructor(options: Partial<WindowOptionsType<T>> = {}) {
|
|
118
|
+
ensureNativeRuntime();
|
|
119
|
+
|
|
120
|
+
this.title = options.title ?? defaultOptions.title;
|
|
121
|
+
this.frame = { ...defaultOptions.frame, ...options.frame };
|
|
122
|
+
this.url = options.url ?? defaultOptions.url;
|
|
123
|
+
this.html = options.html ?? defaultOptions.html;
|
|
124
|
+
this.preload = options.preload ?? defaultOptions.preload;
|
|
125
|
+
this.viewsRoot = options.viewsRoot ?? defaultOptions.viewsRoot ?? resolveDefaultViewsRoot();
|
|
126
|
+
this.titleBarStyle = options.titleBarStyle ?? defaultOptions.titleBarStyle;
|
|
127
|
+
this.transparent = options.transparent ?? defaultOptions.transparent;
|
|
128
|
+
this.hidden = options.hidden ?? defaultOptions.hidden!;
|
|
129
|
+
this.navigationRules = options.navigationRules ?? defaultOptions.navigationRules;
|
|
130
|
+
this.sandbox = options.sandbox ?? defaultOptions.sandbox;
|
|
131
|
+
|
|
132
|
+
const native = getNativeLibrary();
|
|
133
|
+
this.ptr =
|
|
134
|
+
native?.symbols.bunite_window_create(
|
|
135
|
+
this.id,
|
|
136
|
+
this.frame.x,
|
|
137
|
+
this.frame.y,
|
|
138
|
+
this.frame.width,
|
|
139
|
+
this.frame.height,
|
|
140
|
+
toCString(this.title),
|
|
141
|
+
toCString(this.titleBarStyle),
|
|
142
|
+
this.transparent,
|
|
143
|
+
this.hidden,
|
|
144
|
+
Boolean(this.frame.minimized),
|
|
145
|
+
Boolean(this.frame.maximized)
|
|
146
|
+
) ?? null;
|
|
147
|
+
|
|
148
|
+
BrowserWindowMap[this.id] = this;
|
|
149
|
+
buniteEventEmitter.on(`move-${this.id}`, this.handleNativeMove);
|
|
150
|
+
buniteEventEmitter.on(`resize-${this.id}`, this.handleNativeResize);
|
|
151
|
+
buniteEventEmitter.on(`close-${this.id}`, this.handleNativeClose);
|
|
152
|
+
|
|
153
|
+
const webview = new BrowserView({
|
|
154
|
+
url: this.url,
|
|
155
|
+
html: this.html,
|
|
156
|
+
preload: this.preload,
|
|
157
|
+
viewsRoot: this.viewsRoot,
|
|
158
|
+
windowPtr: this.ptr,
|
|
159
|
+
frame: {
|
|
160
|
+
x: 0,
|
|
161
|
+
y: 0,
|
|
162
|
+
width: this.frame.width,
|
|
163
|
+
height: this.frame.height
|
|
164
|
+
},
|
|
165
|
+
rpc: options.rpc as BrowserViewOptions<T>["rpc"],
|
|
166
|
+
windowId: this.id,
|
|
167
|
+
navigationRules: this.navigationRules,
|
|
168
|
+
sandbox: this.sandbox
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
this.webviewId = webview.id;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
static getById(id: number) {
|
|
175
|
+
return BrowserWindowMap[id];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
get webview() {
|
|
179
|
+
return BrowserView.getById(this.webviewId) as BrowserView<T>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
show() {
|
|
183
|
+
this.hidden = false;
|
|
184
|
+
if (this.ptr) {
|
|
185
|
+
getNativeLibrary()?.symbols.bunite_window_show(this.ptr);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
close() {
|
|
190
|
+
if (this.closed) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.closed = true;
|
|
194
|
+
const hadNativePtr = Boolean(this.ptr);
|
|
195
|
+
if (this.ptr) {
|
|
196
|
+
getNativeLibrary()?.symbols.bunite_window_close(this.ptr);
|
|
197
|
+
this.ptr = null;
|
|
198
|
+
} else {
|
|
199
|
+
BrowserView.getById(this.webviewId)?.remove();
|
|
200
|
+
}
|
|
201
|
+
delete BrowserWindowMap[this.id];
|
|
202
|
+
buniteEventEmitter.off(`move-${this.id}`, this.handleNativeMove);
|
|
203
|
+
buniteEventEmitter.off(`resize-${this.id}`, this.handleNativeResize);
|
|
204
|
+
buniteEventEmitter.off(`close-${this.id}`, this.handleNativeClose);
|
|
205
|
+
if (!hadNativePtr) {
|
|
206
|
+
buniteEventEmitter.emitEvent(buniteEventEmitter.events.window.close({ id: this.id }), this.id);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
maximize() {
|
|
211
|
+
if (!this.ptr) {
|
|
212
|
+
this.frame.maximized = true;
|
|
213
|
+
this.frame.minimized = false;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const native = getNativeLibrary();
|
|
218
|
+
if (!native) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
native.symbols.bunite_window_maximize(this.ptr);
|
|
223
|
+
this.frame.minimized = native.symbols.bunite_window_is_minimized(this.ptr);
|
|
224
|
+
this.frame.maximized = native.symbols.bunite_window_is_maximized(this.ptr);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
unmaximize() {
|
|
228
|
+
if (!this.ptr) {
|
|
229
|
+
this.frame.maximized = false;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const native = getNativeLibrary();
|
|
234
|
+
if (!native) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
native.symbols.bunite_window_unmaximize(this.ptr);
|
|
239
|
+
this.frame.minimized = native.symbols.bunite_window_is_minimized(this.ptr);
|
|
240
|
+
this.frame.maximized = native.symbols.bunite_window_is_maximized(this.ptr);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
isMaximized() {
|
|
244
|
+
if (!this.ptr) {
|
|
245
|
+
return Boolean(this.frame.maximized);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const maximized = getNativeLibrary()?.symbols.bunite_window_is_maximized(this.ptr) ?? false;
|
|
249
|
+
this.frame.maximized = maximized;
|
|
250
|
+
return maximized;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
minimize() {
|
|
254
|
+
if (!this.ptr) {
|
|
255
|
+
this.restoreMaximizedAfterMinimize = Boolean(this.frame.maximized);
|
|
256
|
+
this.frame.minimized = true;
|
|
257
|
+
this.frame.maximized = false;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const native = getNativeLibrary();
|
|
262
|
+
if (!native) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
native.symbols.bunite_window_minimize(this.ptr);
|
|
267
|
+
this.frame.minimized = native.symbols.bunite_window_is_minimized(this.ptr);
|
|
268
|
+
this.frame.maximized = native.symbols.bunite_window_is_maximized(this.ptr);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
unminimize() {
|
|
272
|
+
if (!this.ptr) {
|
|
273
|
+
this.frame.minimized = false;
|
|
274
|
+
this.frame.maximized = this.restoreMaximizedAfterMinimize;
|
|
275
|
+
this.restoreMaximizedAfterMinimize = false;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const native = getNativeLibrary();
|
|
280
|
+
if (!native) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
native.symbols.bunite_window_unminimize(this.ptr);
|
|
285
|
+
this.frame.minimized = native.symbols.bunite_window_is_minimized(this.ptr);
|
|
286
|
+
this.frame.maximized = native.symbols.bunite_window_is_maximized(this.ptr);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
isMinimized() {
|
|
290
|
+
if (!this.ptr) {
|
|
291
|
+
return Boolean(this.frame.minimized);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const minimized = getNativeLibrary()?.symbols.bunite_window_is_minimized(this.ptr) ?? false;
|
|
295
|
+
this.frame.minimized = minimized;
|
|
296
|
+
return minimized;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
setTitle(title: string) {
|
|
300
|
+
this.title = title;
|
|
301
|
+
if (this.ptr) {
|
|
302
|
+
getNativeLibrary()?.symbols.bunite_window_set_title(this.ptr, toCString(title));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
setFrame(x: number, y: number, width: number, height: number) {
|
|
307
|
+
this.frame = { ...this.frame, x, y, width, height };
|
|
308
|
+
if (this.ptr) {
|
|
309
|
+
getNativeLibrary()?.symbols.bunite_window_set_frame(this.ptr, x, y, width, height);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
getFrame() {
|
|
314
|
+
return this.frame;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
on(name: "close" | "focus" | "blur" | "move" | "resize", handler: (event: unknown) => void) {
|
|
318
|
+
const specificName = `${name}-${this.id}`;
|
|
319
|
+
buniteEventEmitter.on(specificName, handler);
|
|
320
|
+
return () => buniteEventEmitter.off(specificName, handler);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { Server, ServerWebSocket } from "bun";
|
|
2
|
+
import type { BrowserView } from "./BrowserView";
|
|
3
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
4
|
+
import type { RPCPacket, RPCRequestPacket } from "../../shared/rpc";
|
|
5
|
+
import type { GlobalIPCHandler } from "./App";
|
|
6
|
+
import {
|
|
7
|
+
asUint8Array,
|
|
8
|
+
createEncryptedRPCFrame,
|
|
9
|
+
decodeRPCPacket,
|
|
10
|
+
encodeRPCPacket,
|
|
11
|
+
parseEncryptedRPCFrame
|
|
12
|
+
} from "../../shared/rpcWire";
|
|
13
|
+
import { RPC_AUTH_TAG_LENGTH } from "../../shared/rpcWireConstants";
|
|
14
|
+
|
|
15
|
+
type ViewRegistry = {
|
|
16
|
+
getById(id: number): BrowserView | undefined;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type WebSocketData = {
|
|
20
|
+
webviewId: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let rpcServer: Server<WebSocketData> | null = null;
|
|
24
|
+
let rpcPort = 0;
|
|
25
|
+
|
|
26
|
+
const socketMap: Record<number, ServerWebSocket<WebSocketData> | null> = {};
|
|
27
|
+
let registry: ViewRegistry | null = null;
|
|
28
|
+
let globalIPCResolver: ((channel: string) => GlobalIPCHandler | undefined) | null = null;
|
|
29
|
+
|
|
30
|
+
export function attachGlobalIPCResolver(resolver: (channel: string) => GlobalIPCHandler | undefined) {
|
|
31
|
+
globalIPCResolver = resolver;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function encrypt(secretKey: Uint8Array, payload: Uint8Array) {
|
|
35
|
+
const iv = new Uint8Array(randomBytes(12));
|
|
36
|
+
const cipher = createCipheriv("aes-256-gcm", secretKey, iv);
|
|
37
|
+
const encrypted = Buffer.concat([cipher.update(payload), cipher.final(), cipher.getAuthTag()]);
|
|
38
|
+
return createEncryptedRPCFrame(iv, new Uint8Array(encrypted));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function decrypt(secretKey: Uint8Array, frame: Uint8Array) {
|
|
42
|
+
const { iv, encryptedPayload } = parseEncryptedRPCFrame(frame);
|
|
43
|
+
const ciphertext = encryptedPayload.subarray(0, encryptedPayload.byteLength - RPC_AUTH_TAG_LENGTH);
|
|
44
|
+
const tag = encryptedPayload.subarray(encryptedPayload.byteLength - RPC_AUTH_TAG_LENGTH);
|
|
45
|
+
const decipher = createDecipheriv("aes-256-gcm", secretKey, iv);
|
|
46
|
+
decipher.setAuthTag(tag);
|
|
47
|
+
return new Uint8Array(Buffer.concat([decipher.update(ciphertext), decipher.final()]));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeIncomingBinaryMessage(
|
|
51
|
+
message: string | ArrayBuffer | Uint8Array | Buffer
|
|
52
|
+
): Uint8Array | null {
|
|
53
|
+
if (typeof message === "string") {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return asUint8Array(message);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function attachBrowserViewRegistry(nextRegistry: ViewRegistry) {
|
|
60
|
+
registry = nextRegistry;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function ensureRPCServer() {
|
|
64
|
+
if (rpcServer) {
|
|
65
|
+
return { rpcServer, rpcPort };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let port = 45000;
|
|
69
|
+
while (port <= 65535) {
|
|
70
|
+
try {
|
|
71
|
+
rpcServer = Bun.serve<WebSocketData>({
|
|
72
|
+
hostname: "127.0.0.1",
|
|
73
|
+
port,
|
|
74
|
+
fetch(req, server) {
|
|
75
|
+
const url = new URL(req.url);
|
|
76
|
+
if (url.pathname !== "/socket") {
|
|
77
|
+
return new Response("Not found", { status: 404 });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const webviewId = Number(url.searchParams.get("webviewId"));
|
|
81
|
+
if (!Number.isFinite(webviewId)) {
|
|
82
|
+
return new Response("Missing webviewId", { status: 400 });
|
|
83
|
+
}
|
|
84
|
+
if (!registry?.getById(webviewId)) {
|
|
85
|
+
return new Response("Unknown webviewId", { status: 403 });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const upgraded = server.upgrade(req, {
|
|
89
|
+
data: { webviewId }
|
|
90
|
+
});
|
|
91
|
+
return upgraded ? undefined : new Response("Upgrade failed", { status: 500 });
|
|
92
|
+
},
|
|
93
|
+
websocket: {
|
|
94
|
+
open(ws) {
|
|
95
|
+
socketMap[ws.data.webviewId] = ws;
|
|
96
|
+
},
|
|
97
|
+
close(ws) {
|
|
98
|
+
socketMap[ws.data.webviewId] = null;
|
|
99
|
+
},
|
|
100
|
+
message(ws, message) {
|
|
101
|
+
const view = registry?.getById(ws.data.webviewId);
|
|
102
|
+
const binaryMessage = normalizeIncomingBinaryMessage(message);
|
|
103
|
+
if (!view || !binaryMessage) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const decryptedMessage = decrypt(view.secretKey, binaryMessage);
|
|
108
|
+
const packet = decodeRPCPacket(decryptedMessage);
|
|
109
|
+
|
|
110
|
+
if (packet.type === "request" && (packet as RPCRequestPacket).scope === "global") {
|
|
111
|
+
void handleGlobalIPC(packet as RPCRequestPacket, ws.data.webviewId);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
view.handleIncomingRPC(packet);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error("[bunite] Failed to parse RPC payload", error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
rpcPort = port;
|
|
123
|
+
break;
|
|
124
|
+
} catch (error: any) {
|
|
125
|
+
if (error?.code === "EADDRINUSE") {
|
|
126
|
+
port += 1;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!rpcServer) {
|
|
134
|
+
throw new Error("Could not start bunite RPC server.");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { rpcServer, rpcPort };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function getRPCPort(): number {
|
|
141
|
+
return rpcPort;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function handleGlobalIPC(packet: RPCRequestPacket, viewId: number) {
|
|
145
|
+
const handler = globalIPCResolver?.(packet.method);
|
|
146
|
+
if (!handler) {
|
|
147
|
+
sendMessageToView(viewId, {
|
|
148
|
+
type: "response",
|
|
149
|
+
id: packet.id,
|
|
150
|
+
success: false,
|
|
151
|
+
error: `No handler registered for: ${packet.method}`,
|
|
152
|
+
scope: "global"
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const result = await handler(packet.params, { viewId });
|
|
158
|
+
sendMessageToView(viewId, {
|
|
159
|
+
type: "response",
|
|
160
|
+
id: packet.id,
|
|
161
|
+
success: true,
|
|
162
|
+
payload: result,
|
|
163
|
+
scope: "global"
|
|
164
|
+
});
|
|
165
|
+
} catch (error) {
|
|
166
|
+
sendMessageToView(viewId, {
|
|
167
|
+
type: "response",
|
|
168
|
+
id: packet.id,
|
|
169
|
+
success: false,
|
|
170
|
+
error: error instanceof Error ? error.message : String(error),
|
|
171
|
+
scope: "global"
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function sendMessageToView(viewId: number, message: RPCPacket): boolean {
|
|
177
|
+
const socket = socketMap[viewId];
|
|
178
|
+
const view = registry?.getById(viewId);
|
|
179
|
+
if (!socket || socket.readyState !== WebSocket.OPEN || !view) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const encrypted = encrypt(view.secretKey, encodeRPCPacket(message));
|
|
184
|
+
socket.send(encrypted);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { buniteEventEmitter } from "../events/eventEmitter";
|
|
2
|
+
import {
|
|
3
|
+
cancelBrowserMessageBoxRequest,
|
|
4
|
+
ensureNativeRuntime,
|
|
5
|
+
getNativeLibrary,
|
|
6
|
+
requestBrowserMessageBox,
|
|
7
|
+
showNativeMessageBox
|
|
8
|
+
} from "../proc/native";
|
|
9
|
+
|
|
10
|
+
export type MessageBoxOptions = {
|
|
11
|
+
type?: "none" | "info" | "warning" | "error" | "question";
|
|
12
|
+
title?: string;
|
|
13
|
+
message?: string;
|
|
14
|
+
detail?: string;
|
|
15
|
+
buttons?: string[];
|
|
16
|
+
defaultId?: number;
|
|
17
|
+
cancelId?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type MessageBoxResponse = {
|
|
21
|
+
response: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export async function showMessageBox(
|
|
25
|
+
options: MessageBoxOptions = {}
|
|
26
|
+
): Promise<MessageBoxResponse> {
|
|
27
|
+
ensureNativeRuntime();
|
|
28
|
+
|
|
29
|
+
if (!getNativeLibrary()) {
|
|
30
|
+
console.warn(
|
|
31
|
+
"[bunite] Utils.showMessageBox() requires the native runtime. Returning a stub response."
|
|
32
|
+
);
|
|
33
|
+
return {
|
|
34
|
+
response: options.cancelId ?? options.defaultId ?? 0
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const requestId = requestBrowserMessageBox(options);
|
|
39
|
+
if (requestId > 0) {
|
|
40
|
+
const response = await new Promise<number>((resolve) => {
|
|
41
|
+
const fallbackResponse = options.cancelId ?? options.defaultId ?? 0;
|
|
42
|
+
const handleResponse = (event: unknown) => {
|
|
43
|
+
const data = (event as { data?: { requestId?: number; response?: number } }).data;
|
|
44
|
+
if (!data || data.requestId !== requestId) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
clearTimeout(timeoutId);
|
|
49
|
+
buniteEventEmitter.off("message-box-response", handleResponse);
|
|
50
|
+
resolve(
|
|
51
|
+
typeof data.response === "number" && data.response >= 0
|
|
52
|
+
? data.response
|
|
53
|
+
: fallbackResponse
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const timeoutId = setTimeout(() => {
|
|
58
|
+
buniteEventEmitter.off("message-box-response", handleResponse);
|
|
59
|
+
cancelBrowserMessageBoxRequest(requestId);
|
|
60
|
+
resolve(fallbackResponse);
|
|
61
|
+
}, 15_000);
|
|
62
|
+
|
|
63
|
+
buniteEventEmitter.on("message-box-response", handleResponse);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return { response };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
response: showNativeMessageBox(options)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { BuniteEvent } from "./event";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
ready: (data: Record<string, unknown>) => new BuniteEvent("ready", data),
|
|
5
|
+
beforeQuit: (data: Record<string, unknown>) =>
|
|
6
|
+
new BuniteEvent<Record<string, unknown>, { allow?: boolean }>("before-quit", data)
|
|
7
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class BuniteEvent<Data = unknown, Response = unknown> {
|
|
2
|
+
name: string;
|
|
3
|
+
data: Data;
|
|
4
|
+
private _response: Response | undefined;
|
|
5
|
+
responseWasSet = false;
|
|
6
|
+
|
|
7
|
+
constructor(name: string, data: Data) {
|
|
8
|
+
this.name = name;
|
|
9
|
+
this.data = data;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get response(): Response | undefined {
|
|
13
|
+
return this._response;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
set response(value: Response) {
|
|
17
|
+
this._response = value;
|
|
18
|
+
this.responseWasSet = true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import EventEmitter from "node:events";
|
|
2
|
+
import { BuniteEvent } from "./event";
|
|
3
|
+
import appEvents from "./appEvents";
|
|
4
|
+
import windowEvents from "./windowEvents";
|
|
5
|
+
import webviewEvents from "./webviewEvents";
|
|
6
|
+
|
|
7
|
+
class BuniteEventEmitter extends EventEmitter {
|
|
8
|
+
emitEvent(event: BuniteEvent, specifier?: string | number) {
|
|
9
|
+
if (specifier !== undefined) {
|
|
10
|
+
this.emit(`${event.name}-${specifier}`, event);
|
|
11
|
+
}
|
|
12
|
+
this.emit(event.name, event);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
events = {
|
|
16
|
+
app: {
|
|
17
|
+
...appEvents
|
|
18
|
+
},
|
|
19
|
+
window: {
|
|
20
|
+
...windowEvents
|
|
21
|
+
},
|
|
22
|
+
webview: {
|
|
23
|
+
...webviewEvents
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const buniteEventEmitter = new BuniteEventEmitter();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BuniteEvent } from "./event";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
willNavigate: (data: { detail: string }) => new BuniteEvent("will-navigate", data),
|
|
5
|
+
didNavigate: (data: { detail: string }) => new BuniteEvent("did-navigate", data),
|
|
6
|
+
domReady: (data: { detail: string }) => new BuniteEvent("dom-ready", data),
|
|
7
|
+
newWindowOpen: (data: { detail: string | { url: string } }) =>
|
|
8
|
+
new BuniteEvent("new-window-open", data),
|
|
9
|
+
permissionRequested: (data: { requestId: number; kind: number; url?: string }) =>
|
|
10
|
+
new BuniteEvent("permission-requested", data),
|
|
11
|
+
messageBoxResponse: (data: { requestId: number; response: number }) =>
|
|
12
|
+
new BuniteEvent("message-box-response", data)
|
|
13
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BuniteEvent } from "./event";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
close: (data: { id: number }) => new BuniteEvent("close", data),
|
|
5
|
+
focus: (data: { id: number }) => new BuniteEvent("focus", data),
|
|
6
|
+
blur: (data: { id: number }) => new BuniteEvent("blur", data),
|
|
7
|
+
move: (data: { id: number; x: number; y: number; maximized: boolean; minimized: boolean }) =>
|
|
8
|
+
new BuniteEvent("move", data),
|
|
9
|
+
resize: (data: {
|
|
10
|
+
id: number;
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
maximized: boolean;
|
|
16
|
+
minimized: boolean;
|
|
17
|
+
}) =>
|
|
18
|
+
new BuniteEvent("resize", data)
|
|
19
|
+
};
|