@swf2es/desktop 0.1.14 → 0.1.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.
Files changed (52) hide show
  1. package/README.md +48 -12
  2. package/dist/launch/launch.js +4 -1
  3. package/dist/launch/launch.js.map +1 -1
  4. package/dist/main/main.js +102 -14
  5. package/dist/main/main.js.map +1 -1
  6. package/dist/main/netlog.d.ts +21 -0
  7. package/dist/main/netlog.d.ts.map +1 -0
  8. package/dist/main/netlog.js +90 -0
  9. package/dist/main/netlog.js.map +1 -0
  10. package/dist/main/sockets.d.ts +4 -2
  11. package/dist/main/sockets.d.ts.map +1 -1
  12. package/dist/main/sockets.js +15 -2
  13. package/dist/main/sockets.js.map +1 -1
  14. package/dist/main/websocket-connections.d.ts +62 -0
  15. package/dist/main/websocket-connections.d.ts.map +1 -0
  16. package/dist/main/websocket-connections.js +198 -0
  17. package/dist/main/websocket-connections.js.map +1 -0
  18. package/dist/main/websockets.d.ts +11 -0
  19. package/dist/main/websockets.d.ts.map +1 -0
  20. package/dist/main/websockets.js +93 -0
  21. package/dist/main/websockets.js.map +1 -0
  22. package/dist/preload/preload.cjs +20 -0
  23. package/dist/preload/preload.cjs.map +1 -1
  24. package/dist/renderer/air.d.ts.map +1 -1
  25. package/dist/renderer/air.js +1 -0
  26. package/dist/renderer/air.js.map +1 -1
  27. package/dist/renderer/full-screen.d.ts +4 -0
  28. package/dist/renderer/full-screen.d.ts.map +1 -0
  29. package/dist/renderer/full-screen.js +32 -0
  30. package/dist/renderer/full-screen.js.map +1 -0
  31. package/dist/renderer/index.d.ts.map +1 -1
  32. package/dist/renderer/index.js +4 -0
  33. package/dist/renderer/index.js.map +1 -1
  34. package/dist/renderer/websockets.d.ts +4 -0
  35. package/dist/renderer/websockets.d.ts.map +1 -0
  36. package/dist/renderer/websockets.js +36 -0
  37. package/dist/renderer/websockets.js.map +1 -0
  38. package/dist/shared/api.d.ts +53 -0
  39. package/dist/shared/api.d.ts.map +1 -1
  40. package/package.json +7 -7
  41. package/src/launch/launch.ts +5 -2
  42. package/src/main/main.ts +128 -15
  43. package/src/main/netlog.ts +108 -0
  44. package/src/main/sockets.ts +17 -1
  45. package/src/main/websocket-connections.ts +264 -0
  46. package/src/main/websockets.ts +115 -0
  47. package/src/preload/preload.cts +23 -0
  48. package/src/renderer/air.ts +1 -0
  49. package/src/renderer/full-screen.ts +44 -0
  50. package/src/renderer/index.ts +4 -0
  51. package/src/renderer/websockets.ts +43 -0
  52. package/src/shared/api.ts +40 -0
package/src/main/main.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  // electron apps/desktop [--trace] [file.swf | http(s)://host/movie.swf]
6
6
  // electron apps/desktop [--trace] app.xml [root-dir] [-- arguments...]
7
7
  //
8
- // --ignore-certificate-errors[=host,...] leaves HTTPS certificates unchecked.
8
+ // --ignore-certificate-errors[=host,...] leaves HTTPS and WSS certificates unchecked.
9
9
  //
10
10
  // The second runs an AIR application by its descriptor, as adl does.
11
11
  import { statSync } from "node:fs";
@@ -23,11 +23,13 @@ import {
23
23
  Menu,
24
24
  type MenuItemConstructorOptions,
25
25
  safeStorage,
26
+ screen,
26
27
  session,
27
28
  shell,
28
29
  type WebContents,
29
30
  } from "electron";
30
31
  import type {
32
+ AirScreenState,
31
33
  AirWindowState,
32
34
  LibraryName,
33
35
  NetworkResponse,
@@ -48,11 +50,14 @@ import {
48
50
  writeOf,
49
51
  } from "./air.ts";
50
52
  import { bridgeFiles } from "./files.ts";
53
+ import { failureLine, requestLine } from "./netlog.ts";
51
54
  import { type Movie, Network, Refused, unverifiedHosts } from "./network.ts";
52
55
  import { APP_ORIGIN, registerScheme, serve } from "./protocol.ts";
53
56
  import { Sandbox } from "./sandbox.ts";
54
57
  import { SettingsFile } from "./settings.ts";
55
58
  import { bridgeSockets } from "./sockets.ts";
59
+ import { hasWebSocket, unverifiedDispatcher, webSocketOpener } from "./websocket-connections.ts";
60
+ import { bridgeWebSockets } from "./websockets.ts";
56
61
 
57
62
  const preload = fileURLToPath(new URL("../preload/preload.cjs", import.meta.url));
58
63
  const promptPreload = fileURLToPath(new URL("../preload/prompt.cjs", import.meta.url));
@@ -77,6 +82,11 @@ app.enableSandbox();
77
82
 
78
83
  /** --trace: what the page logs, a SWF's trace() among it, goes to the terminal. */
79
84
  const trace = process.argv.includes("--trace");
85
+ // Each request and socket of the SWF's said on stderr: the page's DevTools show none of them.
86
+ // --log-sockets says what each socket sends and receives as well, and so the rest too.
87
+ const logSockets = process.argv.includes("--log-sockets");
88
+ const logNetwork = logSockets || process.argv.includes("--log-network");
89
+ const netLog = logNetwork ? (line: string) => process.stderr.write(`${line}\n`) : null;
80
90
  const sandbox = new Sandbox();
81
91
  /** The SWF's http and https requests, judged as Flash's sandboxes had them. */
82
92
  /**
@@ -89,15 +99,23 @@ const unverified = unverifiedHosts(process.argv.slice(1));
89
99
  if (unverified) {
90
100
  process.stderr.write(
91
101
  unverified.all
92
- ? "swf2es: --ignore-certificate-errors: no HTTPS server's certificate is verified\n"
102
+ ? "swf2es: --ignore-certificate-errors: no HTTPS or WSS server's certificate is verified\n"
93
103
  : `swf2es: --ignore-certificate-errors: certificates are not verified for ${unverified.hosts.join(", ") || "no host"}\n`,
94
104
  );
95
105
  }
96
106
 
97
- const network = new Network({
98
- unverified: (host) =>
99
- !!unverified && (unverified.all || unverified.hosts.includes(host.toLowerCase())),
100
- });
107
+ const unverifiedHost = (host: string) =>
108
+ !!unverified && (unverified.all || unverified.hosts.includes(host.toLowerCase()));
109
+ const network = new Network({ unverified: unverifiedHost });
110
+ // What opens an AIR application's WebSockets: the global's client, a wss: server's certificate
111
+ // unchecked where the switch names its host.
112
+ const insecureWebSockets = unverified ? unverifiedDispatcher() : null;
113
+ if (unverified && !insecureWebSockets) {
114
+ process.stderr.write(
115
+ "swf2es: --ignore-certificate-errors: a WebSocket's certificate is verified all the same\n",
116
+ );
117
+ }
118
+
101
119
  /** What the page's requests under way abort with, by the preload's number. */
102
120
  const requests = new Map<number, AbortController>();
103
121
  let settings: SettingsFile;
@@ -376,6 +394,37 @@ async function openUrl(text: string): Promise<void> {
376
394
  show(url.href, { url: url.href, name, sandbox: "remote" }, { kind: "remote", url: url.href });
377
395
  }
378
396
 
397
+ /**
398
+ * Open the file the user chose in the dialog, dropped on the window or
399
+ * opened with the app: an AIR application's descriptor (`.xml`) runs the
400
+ * application, anything else is played as a SWF.
401
+ */
402
+ async function openChosen(path: string): Promise<void> {
403
+ if (!/\.xml$/i.test(path)) {
404
+ return open(path);
405
+ }
406
+
407
+ // An application has an installed program's reach, which a file chosen in
408
+ // passing should not get unasked: asked once, until it leaves the recent list.
409
+ if (window && !settings.settings.recent.includes(path)) {
410
+ const { response } = await dialog.showMessageBox(window, {
411
+ type: "question",
412
+ message: `Run ${basename(path)} as an AIR application?`,
413
+ detail:
414
+ "An AIR application reads and writes your files and reaches the network unasked, as an installed program does. Run only one you trust.",
415
+ buttons: ["Run", "Cancel"],
416
+ defaultId: 1,
417
+ cancelId: 1,
418
+ });
419
+ if (response !== 0) {
420
+ return;
421
+ }
422
+ }
423
+
424
+ // Run from its own directory, with no arguments, as one started by its icon is.
425
+ return openAir({ descriptor: path, root: null, arguments: [] }, dirname(path));
426
+ }
427
+
379
428
  /** Play the SWF at `path` in the window, in place of what plays, in its sandbox. */
380
429
  async function open(path: string): Promise<void> {
381
430
  const name = basename(path);
@@ -529,21 +578,38 @@ async function askForSocket(name: string, swf: string, endpoint: string): Promis
529
578
  }
530
579
  }
531
580
 
581
+ /**
582
+ * Whether what plays may open a WebSocket to `url`: an AIR application,
583
+ * unasked and anywhere, as AIR's application sandbox connects and as its
584
+ * sockets do here. air.net.WebSocket is AIR's alone, so nothing else has one.
585
+ */
586
+ function permitWebSocket(url: string): boolean {
587
+ if (sandbox.application && hasWebSocket()) {
588
+ return true;
589
+ }
590
+
591
+ const why = sandbox.application
592
+ ? "this Electron has no WebSocket client"
593
+ : "only an AIR application has one";
594
+ process.stderr.write(`swf2es: no WebSocket to ${url.slice(0, 2048)}: ${why}\n`);
595
+ return false;
596
+ }
597
+
532
598
  async function showOpenDialog(): Promise<void> {
533
599
  if (!window) {
534
600
  return;
535
601
  }
536
602
 
537
603
  const result = await dialog.showOpenDialog(window, {
538
- title: "Open a SWF",
604
+ title: "Open a SWF or an AIR application",
539
605
  properties: ["openFile"],
540
606
  filters: [
541
- { name: "SWF", extensions: ["swf"] },
607
+ { name: "SWF and AIR application descriptors", extensions: ["swf", "xml"] },
542
608
  { name: "All files", extensions: ["*"] },
543
609
  ],
544
610
  });
545
611
  if (!result.canceled && result.filePaths[0]) {
546
- await open(result.filePaths[0]);
612
+ await openChosen(result.filePaths[0]);
547
613
  }
548
614
  }
549
615
 
@@ -769,6 +835,9 @@ function createWindow(): void {
769
835
  window.on("closed", () => {
770
836
  window = null;
771
837
  });
838
+ // The page hears of full screen however it came, a SWF's Stage.displayState or the menu's F11.
839
+ window.on("enter-full-screen", () => window?.webContents.send("desktop:full-screen", true));
840
+ window.on("leave-full-screen", () => window?.webContents.send("desktop:full-screen", false));
772
841
  guard(window.webContents);
773
842
  void window.loadURL(`${APP_ORIGIN}/`);
774
843
  }
@@ -800,6 +869,12 @@ function listen(): void {
800
869
  build,
801
870
  };
802
871
  });
872
+ // Stage.displayState of a SWF the user opened: the window's full screen, asked with no gesture.
873
+ ipcMain.on("desktop:full-screen", (event, fullScreen: unknown) => {
874
+ if (trusted(event) && current && window) {
875
+ window.setFullScreen(fullScreen === true);
876
+ }
877
+ });
803
878
  ipcMain.on("desktop:open-dialog", (event) => {
804
879
  if (trusted(event)) {
805
880
  void showOpenDialog();
@@ -830,7 +905,7 @@ function listen(): void {
830
905
  });
831
906
  ipcMain.on("desktop:open-path", (event, path: unknown) => {
832
907
  if (trusted(event) && typeof path === "string" && isAbsolute(path)) {
833
- void open(path);
908
+ void openChosen(path);
834
909
  }
835
910
  });
836
911
  ipcMain.on("desktop:choose-library", (event, name: unknown) => {
@@ -838,7 +913,14 @@ function listen(): void {
838
913
  void chooseLibrary(name);
839
914
  }
840
915
  });
841
- bridgeSockets(trusted, permitSocket);
916
+ bridgeSockets(trusted, permitSocket, netLog, logSockets);
917
+ bridgeWebSockets(
918
+ trusted,
919
+ permitWebSocket,
920
+ insecureWebSockets ? webSocketOpener(unverifiedHost, insecureWebSockets) : undefined,
921
+ netLog,
922
+ logSockets,
923
+ );
842
924
  ipcMain.handle("net:fetch", (event, id: unknown, request: unknown) => {
843
925
  if (!trusted(event) || typeof id !== "number") {
844
926
  throw new Error("not the app's page");
@@ -886,6 +968,17 @@ function windowState(w: BrowserWindow): AirWindowState {
886
968
  };
887
969
  }
888
970
 
971
+ /** The displays as an AIR application's Screen reads them: the work area is past the system's bars. */
972
+ function screenStates(): AirScreenState[] {
973
+ const primary = screen.getPrimaryDisplay().id;
974
+ return screen.getAllDisplays().map((display) => ({
975
+ bounds: display.bounds,
976
+ visibleBounds: display.workArea,
977
+ colorDepth: display.colorDepth,
978
+ main: display.id === primary,
979
+ }));
980
+ }
981
+
889
982
  /**
890
983
  * An AIR application's reach, shared/api.ts DesktopAir: the file system
891
984
  * and the window, only while one the user opened plays and only for its
@@ -968,6 +1061,9 @@ function listenForAir(): void {
968
1061
  ipcMain.on("air:window", (event) => {
969
1062
  event.returnValue = playing(event) && window ? windowState(window) : null;
970
1063
  });
1064
+ ipcMain.on("air:screens", (event) => {
1065
+ event.returnValue = playing(event) ? screenStates() : [];
1066
+ });
971
1067
  ipcMain.on("air:set-bounds", (event, bounds: unknown) => {
972
1068
  const b = bounds as { x?: unknown; y?: unknown; width?: unknown; height?: unknown } | null;
973
1069
  const n = (v: unknown) => (typeof v === "number" && Number.isFinite(v) ? Math.round(v) : null);
@@ -1037,7 +1133,7 @@ async function fetchForPage(
1037
1133
  request: unknown,
1038
1134
  deliver: (response: NetworkResponse) => void,
1039
1135
  ): Promise<NetworkResponse> {
1040
- const asked = request as { url?: unknown; purpose?: unknown } | null;
1136
+ const asked = request as { url?: unknown; purpose?: unknown; method?: unknown } | null;
1041
1137
  const url = typeof asked?.url === "string" ? asked.url : "";
1042
1138
  const shown = url.slice(0, 2048);
1043
1139
  const movie = asked?.purpose === "movie" && sandbox.startRemote(url);
@@ -1048,13 +1144,27 @@ async function fetchForPage(
1048
1144
 
1049
1145
  const controller = new AbortController();
1050
1146
  requests.set(id, controller);
1147
+ const started = performance.now();
1148
+ const method = typeof asked?.method === "string" ? asked.method.slice(0, 16) : "GET";
1051
1149
  try {
1052
- return await network.fetch(request, controller.signal, deliver);
1150
+ const response = await network.fetch(request, controller.signal, deliver);
1151
+ netLog?.(
1152
+ requestLine(
1153
+ method,
1154
+ url,
1155
+ response.status,
1156
+ response.bytes?.length ?? null,
1157
+ performance.now() - started,
1158
+ ),
1159
+ );
1160
+ return response;
1053
1161
  } catch (error) {
1054
1162
  // A refusal says why; anything else, which should not happen, is said too.
1055
1163
  if (!controller.signal.aborted) {
1056
1164
  const why = error instanceof Refused ? error.message : `failed: ${error}`;
1057
1165
  process.stderr.write(`swf2es: not loading ${shown}: ${why}\n`);
1166
+ } else {
1167
+ netLog?.(failureLine(method, url, "aborted", performance.now() - started));
1058
1168
  }
1059
1169
 
1060
1170
  throw error;
@@ -1093,15 +1203,18 @@ app.on("second-instance", (_event, argv, workingDirectory) => {
1093
1203
  });
1094
1204
 
1095
1205
  let pending = movieFromArguments(process.argv);
1206
+ // Whether `pending` came from the system's "open with", not the command line: asked as a drop is.
1207
+ let pendingChosen = false;
1096
1208
  // An AIR application named adl's way, which the command line's SWF gives way to.
1097
1209
  const pendingAir = airCommand(process.argv.slice(process.defaultApp ? 2 : 1), process.cwd());
1098
1210
  // macOS hands a file opened from the Finder as an event, perhaps before the app is ready.
1099
1211
  app.on("open-file", (event, path) => {
1100
1212
  event.preventDefault();
1101
1213
  if (settings) {
1102
- void open(path);
1214
+ void openChosen(path);
1103
1215
  } else {
1104
1216
  pending = path;
1217
+ pendingChosen = true;
1105
1218
  }
1106
1219
  });
1107
1220
 
@@ -1141,6 +1254,6 @@ void app.whenReady().then(() => {
1141
1254
  if (pendingAir) {
1142
1255
  void openAir(pendingAir, process.cwd());
1143
1256
  } else if (pending) {
1144
- void openEntry(pending);
1257
+ void (pendingChosen ? openChosen(pending) : openEntry(pending));
1145
1258
  }
1146
1259
  });
@@ -0,0 +1,108 @@
1
+ // --log-network's lines: what a SWF asked of the network, as the terminal
2
+ // shows it. The page has no network of its own, so its DevTools' Network
3
+ // tab shows none of this; the main process, which sends the requests, says
4
+ // them here instead. Apart from Electron, so that node tests it.
5
+
6
+ /** A size as a person reads it: bytes, then kB and MB to one decimal. */
7
+ function size(bytes: number): string {
8
+ if (bytes < 1024) {
9
+ return `${bytes} B`;
10
+ }
11
+
12
+ return bytes < 1024 * 1024
13
+ ? `${(bytes / 1024).toFixed(1)} kB`
14
+ : `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
15
+ }
16
+
17
+ /** A URL short enough for a line, and with nothing a terminal would act on. */
18
+ function shown(url: string): string {
19
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: control characters are what is taken out
20
+ return url.slice(0, 2048).replace(/[\u0000-\u001f\u007f]/g, "?");
21
+ }
22
+
23
+ /** A request answered: its method, URL, status, body's size and how long it took. */
24
+ export function requestLine(
25
+ method: string,
26
+ url: string,
27
+ status: number,
28
+ bytes: number | null,
29
+ ms: number,
30
+ ): string {
31
+ const body = bytes === null ? "no body" : size(bytes);
32
+ return `swf2es: net ${method} ${shown(url)} ${status} ${body} ${Math.round(ms)} ms`;
33
+ }
34
+
35
+ /** A request that got no answer: refused, failed or aborted, and why. */
36
+ export function failureLine(method: string, url: string, why: string, ms: number): string {
37
+ return `swf2es: net ${method} ${shown(url)} ${why} ${Math.round(ms)} ms`;
38
+ }
39
+
40
+ /** A socket's connection asked for, opened, refused or closed. */
41
+ export function socketLine(id: number, what: string): string {
42
+ return `swf2es: socket #${id} ${shown(what)}`;
43
+ }
44
+
45
+ /** How much of a message --log-sockets shows. */
46
+ const PREVIEW = 512;
47
+
48
+ /** The start of `bytes`: as text where it is text, bytes that are no printable ASCII as dots, else in hex. */
49
+ function preview(bytes: Uint8Array): string {
50
+ const shownBytes = bytes.subarray(0, PREVIEW);
51
+ let printable = 0;
52
+ for (let i = 0; i < shownBytes.length; i++) {
53
+ const b = shownBytes[i];
54
+ if ((b >= 32 && b < 127) || b === 9 || b === 10 || b === 13 || b === 0) {
55
+ printable++;
56
+ }
57
+ }
58
+
59
+ let text = "";
60
+ // Mostly text, as a line protocol's messages are, a NUL ending each: shown as text.
61
+ if (printable * 10 >= shownBytes.length * 9) {
62
+ for (let i = 0; i < shownBytes.length; i++) {
63
+ const b = shownBytes[i];
64
+ text += b >= 32 && b < 127 ? String.fromCharCode(b) : ".";
65
+ }
66
+ } else {
67
+ for (let i = 0; i < shownBytes.length; i++) {
68
+ text += (i > 0 ? " " : "") + shownBytes[i].toString(16).padStart(2, "0");
69
+ }
70
+ }
71
+
72
+ return text + (bytes.length > PREVIEW ? "…" : "");
73
+ }
74
+
75
+ /**
76
+ * What a socket sent (">") or received ("<"): its size and its start, as
77
+ * text where it is text, bytes that are no printable ASCII as dots, else
78
+ * in hex. A message past PREVIEW bytes ends in an ellipsis.
79
+ */
80
+ export function socketDataLine(id: number, sent: boolean, bytes: Uint8Array): string {
81
+ return `swf2es: socket #${id} ${sent ? ">" : "<"} ${bytes.length} B ${preview(bytes)}`;
82
+ }
83
+
84
+ /** A WebSocket's connection asked for, opened, refused, failed or closed. */
85
+ export function webSocketLine(id: number, what: string): string {
86
+ return `swf2es: websocket #${id} ${shown(what)}`;
87
+ }
88
+
89
+ /**
90
+ * What a WebSocket sent (">") or received ("<"): a text message's size
91
+ * in UTF-8 and its first PREVIEW characters, control characters as dots;
92
+ * a binary one's as a socket's.
93
+ */
94
+ export function webSocketDataLine(id: number, sent: boolean, data: string | Uint8Array): string {
95
+ const head = `swf2es: websocket #${id} ${sent ? ">" : "<"}`;
96
+ if (typeof data !== "string") {
97
+ return `${head} binary ${data.length} B ${preview(data)}`;
98
+ }
99
+
100
+ // By code point, so that no pair is cut in two.
101
+ const characters = Array.from(data.slice(0, PREVIEW * 2)).slice(0, PREVIEW);
102
+ const text = characters
103
+ .join("")
104
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: control characters are what is taken out
105
+ .replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, ".");
106
+ const more = text.length < data.length ? "…" : "";
107
+ return `${head} text ${Buffer.byteLength(data)} B ${text}${more}`;
108
+ }
@@ -3,15 +3,20 @@
3
3
  // connections (connections.ts) close as it navigates, reloads or goes.
4
4
  import { type IpcMainEvent, ipcMain, type WebContents } from "electron";
5
5
  import { Connections, validEndpoint } from "./connections.ts";
6
+ import { socketDataLine, socketLine } from "./netlog.ts";
6
7
 
7
8
  /**
8
9
  * Carry socket messages from pages that `trusted` accepts, each connection
9
10
  * made only as `permit` allows it, to the address it gives where it gives
10
- * one; null refuses it.
11
+ * one; null refuses it. `log`, where given, is told each connection asked
12
+ * for, opened, refused, failed and closed (--log-network), and with
13
+ * `data` what each sends and receives too (--log-sockets).
11
14
  */
12
15
  export function bridgeSockets(
13
16
  trusted: (event: IpcMainEvent) => boolean,
14
17
  permit: (host: string, port: number) => Promise<{ address?: string } | null>,
18
+ log: ((line: string) => void) | null = null,
19
+ data = false,
15
20
  ): void {
16
21
  const pages = new Map<WebContents, Connections>();
17
22
 
@@ -22,6 +27,12 @@ export function bridgeSockets(
22
27
  }
23
28
 
24
29
  const connections = new Connections((id, event) => {
30
+ if (log && event.type !== "data") {
31
+ log(socketLine(id, event.type === "error" ? `error: ${event.message}` : event.type));
32
+ } else if (log && data && event.type === "data") {
33
+ log(socketDataLine(id, false, event.bytes));
34
+ }
35
+
25
36
  if (!contents.isDestroyed()) {
26
37
  contents.send("socket:event", id, event);
27
38
  }
@@ -59,6 +70,7 @@ export function bridgeSockets(
59
70
  return;
60
71
  }
61
72
 
73
+ log?.(socketLine(id, `${allowed ? "connecting to" : "refused"} ${host}:${port}`));
62
74
  if (allowed) {
63
75
  connections.connect(id, host, port, allowed.address);
64
76
  } else {
@@ -69,6 +81,10 @@ export function bridgeSockets(
69
81
 
70
82
  ipcMain.on("socket:send", (event, id: unknown, bytes: unknown) => {
71
83
  if (trusted(event) && typeof id === "number" && bytes instanceof Uint8Array) {
84
+ if (log && data) {
85
+ log(socketDataLine(id, true, bytes));
86
+ }
87
+
72
88
  pages.get(event.sender)?.send(id, bytes);
73
89
  }
74
90
  });