electrobun 0.0.19-beta.8 → 0.0.19-beta.80

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 (39) hide show
  1. package/BUILD.md +90 -0
  2. package/bin/electrobun.cjs +165 -0
  3. package/debug.js +5 -0
  4. package/dist/api/browser/builtinrpcSchema.ts +19 -0
  5. package/dist/api/browser/index.ts +409 -0
  6. package/dist/api/browser/rpc/webview.ts +79 -0
  7. package/dist/api/browser/stylesAndElements.ts +3 -0
  8. package/dist/api/browser/webviewtag.ts +534 -0
  9. package/dist/api/bun/core/ApplicationMenu.ts +66 -0
  10. package/dist/api/bun/core/BrowserView.ts +349 -0
  11. package/dist/api/bun/core/BrowserWindow.ts +191 -0
  12. package/dist/api/bun/core/ContextMenu.ts +67 -0
  13. package/dist/api/bun/core/Paths.ts +5 -0
  14. package/dist/api/bun/core/Socket.ts +181 -0
  15. package/dist/api/bun/core/Tray.ts +107 -0
  16. package/dist/api/bun/core/Updater.ts +552 -0
  17. package/dist/api/bun/core/Utils.ts +48 -0
  18. package/dist/api/bun/events/ApplicationEvents.ts +14 -0
  19. package/dist/api/bun/events/event.ts +29 -0
  20. package/dist/api/bun/events/eventEmitter.ts +45 -0
  21. package/dist/api/bun/events/trayEvents.ts +9 -0
  22. package/dist/api/bun/events/webviewEvents.ts +16 -0
  23. package/dist/api/bun/events/windowEvents.ts +12 -0
  24. package/dist/api/bun/index.ts +45 -0
  25. package/dist/api/bun/proc/linux.md +43 -0
  26. package/dist/api/bun/proc/native.ts +1220 -0
  27. package/dist/api/shared/platform.ts +48 -0
  28. package/dist/main.js +53 -0
  29. package/package.json +15 -7
  30. package/src/cli/index.ts +1034 -210
  31. package/templates/hello-world/README.md +57 -0
  32. package/templates/hello-world/bun.lock +63 -0
  33. package/templates/hello-world/electrobun.config +18 -0
  34. package/templates/hello-world/package.json +16 -0
  35. package/templates/hello-world/src/bun/index.ts +15 -0
  36. package/templates/hello-world/src/mainview/index.css +124 -0
  37. package/templates/hello-world/src/mainview/index.html +47 -0
  38. package/templates/hello-world/src/mainview/index.ts +5 -0
  39. package/bin/electrobun +0 -0
@@ -0,0 +1,181 @@
1
+ import type { ServerWebSocket } from "bun";
2
+ import { BrowserView } from "./BrowserView";
3
+ import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
4
+
5
+ function base64ToUint8Array(base64: string) {
6
+ {
7
+ return new Uint8Array(
8
+ atob(base64)
9
+ .split("")
10
+ .map((char) => char.charCodeAt(0))
11
+ );
12
+ }
13
+ }
14
+
15
+ // Encrypt function
16
+ function encrypt(secretKey: Uint8Array, text: string) {
17
+ const iv = new Uint8Array(randomBytes(12)); // IV for AES-GCM
18
+ const cipher = createCipheriv("aes-256-gcm", secretKey, iv);
19
+ const encrypted = Buffer.concat([
20
+ new Uint8Array(cipher.update(text, "utf8")),
21
+ new Uint8Array(cipher.final()),
22
+ ]).toString("base64");
23
+ const tag = cipher.getAuthTag().toString("base64");
24
+ return { encrypted, iv: Buffer.from(iv).toString("base64"), tag };
25
+ }
26
+
27
+ // Decrypt function
28
+ function decrypt(
29
+ secretKey: Uint8Array,
30
+ encryptedData: Uint8Array,
31
+ iv: Uint8Array,
32
+ tag: Uint8Array
33
+ ) {
34
+ const decipher = createDecipheriv("aes-256-gcm", secretKey, iv);
35
+ decipher.setAuthTag(tag);
36
+ const decrypted = Buffer.concat([
37
+ new Uint8Array(decipher.update(encryptedData)),
38
+ new Uint8Array(decipher.final()),
39
+ ]);
40
+ return decrypted.toString("utf8");
41
+ }
42
+
43
+ export const socketMap: {
44
+ [webviewId: string]: {
45
+ socket: null | ServerWebSocket<unknown>;
46
+ queue: string[];
47
+ };
48
+ } = {};
49
+
50
+ const startRPCServer = () => {
51
+ const startPort = 50000;
52
+ const endPort = 65535;
53
+ const payloadLimit = 1024 * 1024 * 500; // 500MB
54
+ let port = startPort;
55
+ let server = null;
56
+
57
+ while (port <= endPort) {
58
+ try {
59
+ server = Bun.serve<{ webviewId: number }>({
60
+ port,
61
+ fetch(req, server) {
62
+ const url = new URL(req.url);
63
+ // const token = new URL(req.url).searchParams.get("token");
64
+ // if (token !== AUTH_TOKEN)
65
+ // return new Response("Unauthorized", { status: 401 });
66
+ // console.log("fetch!!", url.pathname);
67
+ if (url.pathname === "/socket") {
68
+ const webviewIdString = url.searchParams.get("webviewId");
69
+ if (!webviewIdString) {
70
+ return new Response("Missing webviewId", { status: 400 });
71
+ }
72
+ const webviewId = parseInt(webviewIdString, 10);
73
+ const success = server.upgrade(req, { data: { webviewId } });
74
+ return success
75
+ ? undefined
76
+ : new Response("Upgrade failed", { status: 500 });
77
+ }
78
+
79
+ console.log("unhandled RPC Server request", req.url);
80
+ },
81
+ websocket: {
82
+ idleTimeout: 960,
83
+ // 500MB max payload should be plenty
84
+ maxPayloadLength: payloadLimit,
85
+ // Anything beyond the backpressure limit will be dropped
86
+ backpressureLimit: payloadLimit * 2,
87
+ open(ws) {
88
+ const { webviewId } = ws.data;
89
+
90
+ if (!socketMap[webviewId]) {
91
+ socketMap[webviewId] = { socket: ws, queue: [] };
92
+ } else {
93
+ socketMap[webviewId].socket = ws;
94
+ }
95
+ },
96
+ close(ws, code, reason) {
97
+ const { webviewId } = ws.data;
98
+ console.log("Closed:", webviewId, code, reason);
99
+ socketMap[webviewId].socket = null;
100
+ },
101
+
102
+ message(ws, message) {
103
+ const { webviewId } = ws.data;
104
+ const browserView = BrowserView.getById(webviewId);
105
+
106
+ if (browserView.rpcHandler) {
107
+ if (typeof message === "string") {
108
+ try {
109
+ const encryptedPacket = JSON.parse(message);
110
+ const decrypted = decrypt(
111
+ browserView.secretKey,
112
+ base64ToUint8Array(encryptedPacket.encryptedData),
113
+ base64ToUint8Array(encryptedPacket.iv),
114
+ base64ToUint8Array(encryptedPacket.tag)
115
+ );
116
+
117
+ // Note: At this point the secretKey for the webview id would
118
+ // have had to match the encrypted packet data, so we can trust
119
+ // that this message can be passed to this browserview's rpc
120
+ // methods.
121
+ browserView.rpcHandler(JSON.parse(decrypted));
122
+ } catch (error) {
123
+ console.log("Error handling message:", error);
124
+ }
125
+ } else if (message instanceof ArrayBuffer) {
126
+ console.log("TODO: Received ArrayBuffer message:", message);
127
+ }
128
+ }
129
+ },
130
+ },
131
+ });
132
+
133
+ break;
134
+ } catch (error: any) {
135
+ if (error.code === "EADDRINUSE") {
136
+ console.log(`Port ${port} in use, trying next port...`);
137
+ port++;
138
+ } else {
139
+ throw error;
140
+ }
141
+ }
142
+ }
143
+
144
+ return { rpcServer: server, rpcPort: port };
145
+ };
146
+
147
+ export const { rpcServer, rpcPort } = startRPCServer();
148
+
149
+ // Will return true if message was sent over websocket
150
+ // false if it was not (caller should fallback to postMessage/evaluateJS rpc)
151
+ export const sendMessageToWebviewViaSocket = (
152
+ webviewId: number,
153
+ message: any
154
+ ): boolean => {
155
+ const rpc = socketMap[webviewId];
156
+ const browserView = BrowserView.getById(webviewId);
157
+
158
+ if (rpc?.socket?.readyState === WebSocket.OPEN) {
159
+ try {
160
+ const unencryptedString = JSON.stringify(message);
161
+ const encrypted = encrypt(browserView.secretKey, unencryptedString);
162
+
163
+ const encryptedPacket = {
164
+ encryptedData: encrypted.encrypted,
165
+ iv: encrypted.iv,
166
+ tag: encrypted.tag,
167
+ };
168
+
169
+ const encryptedPacketString = JSON.stringify(encryptedPacket);
170
+
171
+ rpc.socket.send(encryptedPacketString);
172
+ return true;
173
+ } catch (error) {
174
+ console.error("Error sending message to webview via socket:", error);
175
+ }
176
+ }
177
+
178
+ return false;
179
+ };
180
+
181
+ console.log("Server started at", rpcServer?.url.origin);
@@ -0,0 +1,107 @@
1
+ import { ffi, type MenuItemConfig } from "../proc/native";
2
+ import electrobunEventEmitter from "../events/eventEmitter";
3
+ import { VIEWS_FOLDER } from "./Paths";
4
+ import { join } from "path";
5
+ import {FFIType} from 'bun:ffi';
6
+
7
+ let nextTrayId = 1;
8
+ const TrayMap = {};
9
+
10
+ type ConstructorOptions = {
11
+ title?: string;
12
+ image?: string;
13
+ template?: boolean;
14
+ width?: number;
15
+ height?: number;
16
+ };
17
+
18
+ export class Tray {
19
+ id: number = nextTrayId++;
20
+ ptr: FFIType.ptr;
21
+
22
+ constructor({
23
+ title = "",
24
+ image = "",
25
+ template = true,
26
+ width = 16,
27
+ height = 16,
28
+ }: ConstructorOptions = {}) {
29
+ console.log("img", image);
30
+ console.log("img", this.resolveImagePath(image));
31
+ this.ptr = ffi.request.createTray({
32
+ id: this.id,
33
+ title,
34
+ image: this.resolveImagePath(image),
35
+ template,
36
+ width,
37
+ height,
38
+ });
39
+
40
+ TrayMap[this.id] = this;
41
+ }
42
+
43
+ resolveImagePath(imgPath: string) {
44
+ if (imgPath.startsWith("views://")) {
45
+ return join(VIEWS_FOLDER, imgPath.replace("views://", ""));
46
+ } else {
47
+ // can specify any file path here
48
+ return imgPath;
49
+ }
50
+ }
51
+
52
+ setTitle(title: string) {
53
+ ffi.request.setTrayTitle({ id: this.id, title });
54
+ }
55
+
56
+ setImage(imgPath: string) {
57
+ ffi.request.setTrayImage({
58
+ id: this.id,
59
+ image: this.resolveImagePath(imgPath),
60
+ });
61
+ }
62
+
63
+ setMenu(menu: Array<MenuItemConfig>) {
64
+ const menuWithDefaults = menuConfigWithDefaults(menu);
65
+ ffi.request.setTrayMenu({
66
+ id: this.id,
67
+ menuConfig: JSON.stringify(menuWithDefaults),
68
+ });
69
+ }
70
+
71
+ on(name: "tray-clicked", handler) {
72
+ const specificName = `${name}-${this.id}`;
73
+ electrobunEventEmitter.on(specificName, handler);
74
+ }
75
+
76
+ static getById(id: number) {
77
+ return TrayMap[id];
78
+ }
79
+
80
+ static getAll() {
81
+ return Object.values(TrayMap);
82
+ }
83
+ }
84
+
85
+ const menuConfigWithDefaults = (
86
+ menu: Array<MenuItemConfig>
87
+ ): Array<MenuItemConfig> => {
88
+ return menu.map((item) => {
89
+ if (item.type === "divider" || item.type === "separator") {
90
+ return { type: "divider" };
91
+ } else {
92
+ return {
93
+ label: item.label || "",
94
+ type: item.type || "normal",
95
+ action: item.action || "",
96
+ // default enabled to true unless explicitly set to false
97
+ enabled: item.enabled === false ? false : true,
98
+ checked: Boolean(item.checked),
99
+ hidden: Boolean(item.hidden),
100
+ tooltip: item.tooltip || undefined,
101
+ ...(item.submenu
102
+ ? { submenu: menuConfigWithDefaults(item.submenu) }
103
+ : {}),
104
+ };
105
+ }
106
+ });
107
+ };