electrobun 0.0.15 → 0.0.16

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.
@@ -18,6 +18,7 @@ interface ElectrobunWebviewRPCSChema {
18
18
  }
19
19
 
20
20
  const WEBVIEW_ID = window.__electrobunWebviewId;
21
+ const RPC_SOCKET_PORT = window.__electrobunRpcSocketPort;
21
22
 
22
23
  // todo (yoav): move this stuff to browser/rpc/webview.ts
23
24
  type ZigWebviewHandlers = RPCSchema<{
@@ -94,6 +95,7 @@ type WebviewTagHandlers = RPCSchema<{
94
95
  }>;
95
96
 
96
97
  class Electroview<T> {
98
+ bunSocket?: WebSocket;
97
99
  // user's custom rpc browser <-> bun
98
100
  rpc?: T;
99
101
  rpcHandler?: (msg: any) => void;
@@ -114,6 +116,7 @@ class Electroview<T> {
114
116
  // todo (yoav): should init webviewTag by default when src is local
115
117
  // and have a setting that forces it enabled or disabled
116
118
  this.initZigRpc();
119
+ this.initSocketToBun();
117
120
  // Note:
118
121
  // syncRPC messages doesn't need to be defined since there's no need for sync 1-way message
119
122
  // just use non-blocking async rpc for that, we just need sync requests
@@ -156,6 +159,50 @@ class Electroview<T> {
156
159
  });
157
160
  }
158
161
 
162
+ initSocketToBun() {
163
+ // todo: upgrade to tls
164
+ const socket = new WebSocket(
165
+ `ws://localhost:${RPC_SOCKET_PORT}/socket?webviewId=${WEBVIEW_ID}`
166
+ );
167
+
168
+ this.bunSocket = socket;
169
+
170
+ socket.addEventListener("open", () => {
171
+ // this.bunSocket?.send("Hello from webview " + WEBVIEW_ID);
172
+ });
173
+
174
+ socket.addEventListener("message", async (event) => {
175
+ const message = event.data;
176
+ if (typeof message === "string") {
177
+ try {
178
+ const encryptedPacket = JSON.parse(message);
179
+
180
+ const decrypted = await window.__electrobun_decrypt(
181
+ encryptedPacket.encryptedData,
182
+ encryptedPacket.iv,
183
+ encryptedPacket.tag
184
+ );
185
+
186
+ this.rpcHandler?.(JSON.parse(decrypted));
187
+ } catch (err) {
188
+ console.error("Error parsing bun message:", err);
189
+ }
190
+ } else if (message instanceof Blob) {
191
+ // Handle binary data (e.g., convert Blob to ArrayBuffer if needed)
192
+ } else {
193
+ console.error("UNKNOWN DATA TYPE RECEIVED:", event.data);
194
+ }
195
+ });
196
+
197
+ socket.addEventListener("error", (event) => {
198
+ console.error("Socket error:", event);
199
+ });
200
+
201
+ socket.addEventListener("close", (event) => {
202
+ // console.log("Socket closed:", event);
203
+ });
204
+ }
205
+
159
206
  // This will be attached to the global object, zig can rpc reply by executingJavascript
160
207
  // of that global reference to the function
161
208
  receiveMessageFromZig(msg: any) {
@@ -220,6 +267,7 @@ class Electroview<T> {
220
267
 
221
268
  // call any of your bun syncrpc methods in a way that appears synchronous from the browser context
222
269
  bunBridgeSync(msg: string) {
270
+ console.warn("DEPRECATED: use async rpc if possible");
223
271
  var xhr = new XMLHttpRequest();
224
272
  // Note: setting false here makes the xhr request blocking. This completely
225
273
  // blocks the main thread which is terrible. You can use this safely from a webworker.
@@ -240,7 +288,28 @@ class Electroview<T> {
240
288
  }
241
289
  }
242
290
 
243
- bunBridge(msg: string) {
291
+ async bunBridge(msg: string) {
292
+ if (this.bunSocket?.readyState === WebSocket.OPEN) {
293
+ try {
294
+ const { encryptedData, iv, tag } = await window.__electrobun_encrypt(
295
+ msg
296
+ );
297
+
298
+ const encryptedPacket = {
299
+ encryptedData: encryptedData,
300
+ iv: iv,
301
+ tag: tag,
302
+ };
303
+ const encryptedPacketString = JSON.stringify(encryptedPacket);
304
+ this.bunSocket.send(encryptedPacketString);
305
+ return;
306
+ } catch (error) {
307
+ console.error("Error sending message to bun via socket:", error);
308
+ }
309
+ }
310
+
311
+ // if socket's are unavailable, fallback to postMessage
312
+
244
313
  // Note: messageHandlers seem to freeze when sending large messages
245
314
  // but xhr to views://rpc can run into CORS issues on non views://
246
315
  // loaded content (eg: when writing extensions/preload scripts for
@@ -252,6 +321,7 @@ class Electroview<T> {
252
321
 
253
322
  // TEMP: disable the fallback for now. for some reason suddenly can't
254
323
  // repro now that other places are chunking messages and laptop restart
324
+
255
325
  if (true || msg.length < 8 * 1024) {
256
326
  window.webkit.messageHandlers.bunBridge.postMessage(msg);
257
327
  } else {
@@ -12,8 +12,12 @@ import {
12
12
  } from "rpc-anywhere";
13
13
  import { Updater } from "./Updater";
14
14
  import type { BuiltinBunToWebviewSchema } from "../../browser/builtinrpcSchema";
15
+ import { rpcPort, sendMessageToWebviewViaSocket } from "./Socket";
16
+ import { randomBytes } from "crypto";
15
17
 
16
- const BrowserViewMap = {};
18
+ const BrowserViewMap: {
19
+ [id: number]: BrowserView<any>;
20
+ } = {};
17
21
  let nextWebviewId = 1;
18
22
 
19
23
  const CHUNK_SIZE = 1024 * 4; // 4KB
@@ -40,7 +44,7 @@ interface ElectrobunWebviewRPCSChema {
40
44
  webview: RPCSchema;
41
45
  }
42
46
 
43
- const defaultOptions: BrowserViewOptions = {
47
+ const defaultOptions: Partial<BrowserViewOptions> = {
44
48
  url: "https://electrobun.dev",
45
49
  html: null,
46
50
  preload: null,
@@ -61,7 +65,7 @@ const internalSyncRpcHandlers = {
61
65
  preload,
62
66
  partition,
63
67
  frame,
64
- }) => {
68
+ }: BrowserViewOptions & { windowId: number }) => {
65
69
  const webviewForTag = new BrowserView({
66
70
  url,
67
71
  html,
@@ -121,17 +125,20 @@ export class BrowserView<T> {
121
125
  pipePrefix: string;
122
126
  inStream: fs.WriteStream;
123
127
  outStream: ReadableStream<Uint8Array>;
128
+ secretKey: Uint8Array;
124
129
  rpc?: T;
125
130
  syncRpc?: { [method: string]: (params: any) => any };
131
+ rpcHandler?: (msg: any) => void;
126
132
 
127
133
  constructor(options: Partial<BrowserViewOptions<T>> = defaultOptions) {
128
- this.url = options.url || defaultOptions.url;
129
- this.html = options.html || defaultOptions.html;
130
- this.preload = options.preload || defaultOptions.preload;
134
+ this.url = options.url || defaultOptions.url || null;
135
+ this.html = options.html || defaultOptions.html || null;
136
+ this.preload = options.preload || defaultOptions.preload || null;
131
137
  this.frame = options.frame
132
138
  ? { ...defaultOptions.frame, ...options.frame }
133
139
  : { ...defaultOptions.frame };
134
140
  this.rpc = options.rpc;
141
+ this.secretKey = new Uint8Array(randomBytes(32));
135
142
  this.syncRpc = { ...(options.syncRpc || {}), ...internalSyncRpcHandlers };
136
143
  this.partition = options.partition || null;
137
144
  // todo (yoav): since collisions can crash the app add a function that checks if the
@@ -147,6 +154,9 @@ export class BrowserView<T> {
147
154
  // TODO: add a then to this that fires an onReady event
148
155
  zigRPC.request.createWebview({
149
156
  id: this.id,
157
+ rpcPort: rpcPort,
158
+ // todo: consider sending secretKey as base64
159
+ secretKey: this.secretKey.toString(),
150
160
  hostWebviewId: this.hostWebviewId || null,
151
161
  pipePrefix: this.pipePrefix,
152
162
  partition: this.partition,
@@ -205,7 +215,7 @@ export class BrowserView<T> {
205
215
  }
206
216
  }
207
217
 
208
- sendMessageToWebview(jsonMessage) {
218
+ sendMessageToWebviewViaExecute(jsonMessage) {
209
219
  const stringifiedMessage =
210
220
  typeof jsonMessage === "string"
211
221
  ? jsonMessage
@@ -261,16 +271,21 @@ export class BrowserView<T> {
261
271
  const that = this;
262
272
 
263
273
  return {
264
- send(message) {
265
- // todo (yoav): note: this is the same as the zig transport
266
- try {
267
- const messageString = JSON.stringify(message);
268
- that.sendMessageToWebview(messageString);
269
- } catch (error) {
270
- console.error("bun: failed to serialize message to webview", error);
274
+ send(message: any) {
275
+ const sentOverSocket = sendMessageToWebviewViaSocket(that.id, message);
276
+
277
+ if (!sentOverSocket) {
278
+ try {
279
+ const messageString = JSON.stringify(message);
280
+ that.sendMessageToWebviewViaExecute(messageString);
281
+ } catch (error) {
282
+ console.error("bun: failed to serialize message to webview", error);
283
+ }
271
284
  }
272
285
  },
273
286
  registerHandler(handler) {
287
+ that.rpcHandler = handler;
288
+
274
289
  async function readFromPipe(
275
290
  reader: ReadableStreamDefaultReader<Uint8Array>
276
291
  ) {
@@ -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);
@@ -9,6 +9,7 @@ import * as Utils from "./core/Utils";
9
9
  import { type RPCSchema, createRPC } from "rpc-anywhere";
10
10
  import type ElectrobunEvent from "./events/event";
11
11
  import * as PATHS from "./core/Paths";
12
+ import * as Socket from "./core/Socket";
12
13
 
13
14
  // Named Exports
14
15
  export {
@@ -23,6 +24,7 @@ export {
23
24
  ApplicationMenu,
24
25
  ContextMenu,
25
26
  PATHS,
27
+ Socket,
26
28
  };
27
29
 
28
30
  // Default Export
@@ -36,6 +38,7 @@ const Electrobun = {
36
38
  ContextMenu,
37
39
  events: electobunEventEmmitter,
38
40
  PATHS,
41
+ Socket,
39
42
  };
40
43
 
41
44
  // Electrobun
@@ -188,6 +188,8 @@ type ZigHandlers = RPCSchema<{
188
188
  createWebview: {
189
189
  params: {
190
190
  id: number;
191
+ rpcPort: number;
192
+ secretKey: string;
191
193
  hostWebviewId: number | null;
192
194
  pipePrefix: string;
193
195
  url: string | null;
@@ -458,7 +460,7 @@ const zigRPC = createRPC<BunHandlers, ZigHandlers>({
458
460
  console.log(err);
459
461
  return { payload: err };
460
462
  }
461
-
463
+ console.warn("DEPRECATED: use async rpc if possible", method);
462
464
  const handler = webview.syncRpc[method];
463
465
  var response;
464
466
  try {
package/dist/bsdiff CHANGED
Binary file
package/dist/bspatch CHANGED
Binary file
package/dist/electrobun CHANGED
Binary file
package/dist/extractor CHANGED
Binary file
package/dist/launcher CHANGED
Binary file
package/dist/webview CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrobun",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "Build ultra fast, tiny, and cross-platform desktop apps with Typescript.",
5
5
  "license": "MIT",
6
6
  "author": "Blackboard Technologies Inc.",
@@ -32,14 +32,14 @@
32
32
  "build:zig:release": "cd src/zig && ../../vendors/zig/zig build -Doptimize=ReleaseFast",
33
33
  "build:zig:trdiff:release": "cd src/bsdiff && ../../vendors/zig/zig build -Doptimize=ReleaseFast",
34
34
  "build:launcher:release": "cd src/launcher && ../../vendors/zig/zig build -Doptimize=ReleaseSmall",
35
- "build:extractor:release": "cd src/extractor && ../../vendors/zig/zig build -Doptimize=ReleaseSmall",
35
+ "build:extractor:release": "cd src/extractor && ../../vendors/zig/zig build -Doptimize=ReleaseSmall",
36
36
  "build:cli": "bun build src/cli/index.ts --compile --outfile src/cli/build/electrobun",
37
37
  "build:debug": "npm install && bun build:zig:trdiff && bun build:objc && bun build:zig && bun build:launcher && bun build:extractor && bun build:cli",
38
38
  "build:release": "bun build:objc && bun build:zig:trdiff:release && bun build:zig:release && bun build:launcher:release && bun build:extractor:release && bun build:cli",
39
39
  "build:package": "bun build:release && bun ./scripts/copy-to-dist.ts",
40
40
  "build:dev": "bun build:debug && bun ./scripts/copy-to-dist.ts",
41
41
  "build:electrobun": "bun build:objc && bun build:zig && bun build:bun",
42
- "dev:playground": "bun build:dev && cd playground && npm install && bun build:dev && bun start",
42
+ "dev:playground": "bun build:dev && cd playground && npm install && bun build:dev && bun start",
43
43
  "dev:playground:rerun": "cd playground && bun start",
44
44
  "dev:playground:canary": "bun build:package && cd playground && npm install && bun build:canary && bun start:canary",
45
45
  "dev:docs": "cd documentation && bun start",
@@ -51,8 +51,8 @@
51
51
  "bun": "1.1.29"
52
52
  },
53
53
  "dependencies": {
54
- "@oneidentity/zstd-js": "^1.0.3",
55
- "rpc-anywhere": "1.5.0",
56
- "tar": "^6.2.1"
54
+ "@oneidentity/zstd-js": "^1.0.3",
55
+ "rpc-anywhere": "1.5.0",
56
+ "tar": "^6.2.1"
57
57
  }
58
58
  }