@nextop-os/browser-node 0.0.27 → 0.0.29

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.
@@ -1,4 +1,4 @@
1
- import { BrowserWindow, WebPreferences, WebContents } from 'electron';
1
+ import { BrowserWindow, WebContents, WebPreferences } from 'electron';
2
2
 
3
3
  interface BrowserNodeLoopbackPreviewTarget {
4
4
  readonly targetUrl: string;
@@ -19,15 +19,23 @@ interface BrowserNodeLoopbackPreviewRoutingOptions {
19
19
  type BrowserPreferredColorScheme = "dark" | "light";
20
20
  interface BrowserNodeElectronLogger {
21
21
  debug?(message: string, metadata?: Record<string, unknown>): void;
22
+ info?(message: string, metadata?: Record<string, unknown>): void;
22
23
  warn?(message: string, metadata?: Record<string, unknown>): void;
23
24
  }
24
25
  interface BrowserGuestWebContents {
25
26
  readonly id?: number;
27
+ readonly navigationHistory?: {
28
+ canGoBack(): boolean;
29
+ canGoForward(): boolean;
30
+ goBack(): void;
31
+ goForward(): void;
32
+ };
26
33
  canGoBack(): boolean;
27
34
  canGoForward(): boolean;
28
35
  capturePage?(): Promise<BrowserGuestNativeImage>;
29
36
  getTitle(): string;
30
37
  getURL(): string;
38
+ getUserAgent?(): string;
31
39
  goBack(): void;
32
40
  goForward(): void;
33
41
  isDestroyed(): boolean;
@@ -36,6 +44,7 @@ interface BrowserGuestWebContents {
36
44
  off(event: string, listener: (...args: unknown[]) => void): this;
37
45
  on(event: string, listener: (...args: unknown[]) => void): this;
38
46
  reload(): void;
47
+ setUserAgent?(userAgent: string): void;
39
48
  setWindowOpenHandler?(handler: (details: {
40
49
  url: string;
41
50
  }) => {
@@ -64,7 +73,9 @@ interface BrowserNodeElectronMainChannels {
64
73
  readonly event: string;
65
74
  readonly goBack: string;
66
75
  readonly goForward: string;
76
+ readonly guestOpenUrl?: string;
67
77
  readonly navigate: string;
78
+ readonly openExternal?: string;
68
79
  readonly prepareSession: string;
69
80
  readonly registerGuest: string;
70
81
  readonly reload: string;
@@ -78,6 +89,7 @@ interface RegisterBrowserNodeElectronMainInput {
78
89
  readonly loopbackPreviewRouting?: BrowserNodeLoopbackPreviewRoutingOptions;
79
90
  readonly openExternal: (url: string) => Promise<void> | void;
80
91
  readonly registerHandler: <TPayload, TResult>(channel: string, handler: (event: unknown, payload: TPayload) => Promise<TResult> | TResult) => void;
92
+ readonly registerListener?: <TPayload>(channel: string, handler: (event: unknown, payload: TPayload) => void) => void;
81
93
  readonly resolveWebContents: (input: {
82
94
  event: unknown;
83
95
  ownerWindow: BrowserWindow;
@@ -88,7 +100,15 @@ interface RegisterBrowserNodeElectronMainInput {
88
100
  }
89
101
  declare function registerBrowserNodeElectronMain(input: RegisterBrowserNodeElectronMainInput): void;
90
102
 
103
+ declare function sanitizeBrowserGuestUserAgent(userAgent: string): string;
104
+ declare function applyBrowserGuestUserAgent(contents: WebContents, logger?: BrowserNodeElectronLogger): void;
105
+
106
+ interface BrowserSessionPartitionAllowedOptions {
107
+ additionalAllowedPrefixes?: readonly string[];
108
+ }
109
+
91
110
  interface BrowserWebviewSecurityInput {
111
+ allowedSessionPartitions?: BrowserSessionPartitionAllowedOptions;
92
112
  params: Record<string, string>;
93
113
  resolvePreload?: BrowserWebviewPreloadResolver;
94
114
  webPreferences: WebPreferences;
@@ -102,9 +122,10 @@ interface BrowserWebviewPreloadResolverInput {
102
122
  params: Readonly<Record<string, string>>;
103
123
  }
104
124
  type BrowserWebviewPreloadResolver = (input: BrowserWebviewPreloadResolverInput) => string | null | undefined;
105
- declare function isBrowserNodeWebviewAttach(params: Record<string, string>): boolean;
106
- declare function enforceBrowserWebviewSecurity({ params, resolvePreload, webPreferences }: BrowserWebviewSecurityInput): BrowserWebviewSecurityResult;
125
+ declare function isBrowserNodeWebviewAttach(params: Record<string, string>, allowedSessionPartitions?: BrowserSessionPartitionAllowedOptions): boolean;
126
+ declare function enforceBrowserWebviewSecurity({ allowedSessionPartitions, params, resolvePreload, webPreferences }: BrowserWebviewSecurityInput): BrowserWebviewSecurityResult;
107
127
  interface InstallBrowserWebviewSecurityInput {
128
+ allowedSessionPartitions?: BrowserSessionPartitionAllowedOptions;
108
129
  contents: WebContents;
109
130
  logger?: BrowserNodeElectronLogger;
110
131
  onGuestAttached?: (guestContents: WebContents) => void;
@@ -112,6 +133,6 @@ interface InstallBrowserWebviewSecurityInput {
112
133
  resolvePreload?: BrowserWebviewPreloadResolver;
113
134
  shouldHandleWebview?: BrowserNodeWebviewMatcher;
114
135
  }
115
- declare function installBrowserWebviewSecurity({ contents, logger, onGuestAttached, openExternal, resolvePreload, shouldHandleWebview }: InstallBrowserWebviewSecurityInput): () => void;
136
+ declare function installBrowserWebviewSecurity({ allowedSessionPartitions, contents, logger, onGuestAttached, openExternal, resolvePreload, shouldHandleWebview }: InstallBrowserWebviewSecurityInput): () => void;
116
137
 
117
- export { type BrowserNodeElectronLogger, type BrowserNodeElectronMainChannels, type BrowserNodeLoopbackPreviewResolver, type BrowserNodeLoopbackPreviewRoutingOptions, type BrowserNodeLoopbackPreviewTarget, type BrowserNodeWebviewMatcher, type BrowserWebviewPreloadResolver, type BrowserWebviewPreloadResolverInput, type BrowserWebviewSecurityInput, type BrowserWebviewSecurityResult, type InstallBrowserWebviewSecurityInput, type RegisterBrowserNodeElectronMainInput, enforceBrowserWebviewSecurity, installBrowserWebviewSecurity, isBrowserNodeWebviewAttach, registerBrowserNodeElectronMain };
138
+ export { type BrowserNodeElectronLogger, type BrowserNodeElectronMainChannels, type BrowserNodeLoopbackPreviewResolver, type BrowserNodeLoopbackPreviewRoutingOptions, type BrowserNodeLoopbackPreviewTarget, type BrowserNodeWebviewMatcher, type BrowserWebviewPreloadResolver, type BrowserWebviewPreloadResolverInput, type BrowserWebviewSecurityInput, type BrowserWebviewSecurityResult, type InstallBrowserWebviewSecurityInput, type RegisterBrowserNodeElectronMainInput, applyBrowserGuestUserAgent, enforceBrowserWebviewSecurity, installBrowserWebviewSecurity, isBrowserNodeWebviewAttach, registerBrowserNodeElectronMain, sanitizeBrowserGuestUserAgent };
@@ -1,11 +1,11 @@
1
+ import {
2
+ isBrowserSessionPartitionAllowed,
3
+ resolveBrowserSessionPartition
4
+ } from "../chunk-UTXZLRPE.js";
1
5
  import {
2
6
  normalizeBrowserComparableUrl,
3
7
  resolveBrowserNavigationUrl
4
8
  } from "../chunk-2ZAMKGGP.js";
5
- import {
6
- isBrowserSessionPartitionAllowed,
7
- resolveBrowserSessionPartition
8
- } from "../chunk-OTK5YBCK.js";
9
9
 
10
10
  // src/electron-main/loopbackPreviewProxy.ts
11
11
  import {
@@ -13,12 +13,8 @@ import {
13
13
  request as httpRequest
14
14
  } from "http";
15
15
  import { request as httpsRequest } from "https";
16
- import { createRequire } from "module";
17
16
  import { pipeline } from "stream";
18
- var require2 = createRequire(import.meta.url);
19
- var wsModule = require2("ws");
20
- var WebSocket = wsModule;
21
- var { WebSocketServer } = wsModule;
17
+ import WebSocket, { WebSocketServer } from "ws";
22
18
  var defaultLoopbackPreviewCacheTtlMs = 3e4;
23
19
  var loopbackHostPattern = /^(localhost|127(?:\.\d{1,3}){0,3})$/i;
24
20
  var loopbackPreviewProxyBypassRules = "*;<-loopback>;https://*;wss://*";
@@ -463,6 +459,26 @@ function normalizeWebSocketCloseCode(code) {
463
459
  var browserPreviewMaxWidth = 260;
464
460
  var browserPreviewMaxHeight = 170;
465
461
  var abortedNavigationErrorCode = -3;
462
+ function canGuestGoBack(contents) {
463
+ return contents.navigationHistory?.canGoBack() ?? contents.canGoBack();
464
+ }
465
+ function canGuestGoForward(contents) {
466
+ return contents.navigationHistory?.canGoForward() ?? contents.canGoForward();
467
+ }
468
+ function goGuestBack(contents) {
469
+ if (contents.navigationHistory) {
470
+ contents.navigationHistory.goBack();
471
+ return;
472
+ }
473
+ contents.goBack();
474
+ }
475
+ function goGuestForward(contents) {
476
+ if (contents.navigationHistory) {
477
+ contents.navigationHistory.goForward();
478
+ return;
479
+ }
480
+ contents.goForward();
481
+ }
466
482
  function resolveBrowserNodeUrlError(resolved) {
467
483
  if (resolved.errorCode === "invalid-url") {
468
484
  return { code: "invalid-url" };
@@ -500,6 +516,28 @@ function resizeBrowserPreviewImage(image) {
500
516
  function isAbortedNavigationError(input) {
501
517
  return input.errorCode === abortedNavigationErrorCode || input.errorDescription === "ERR_ABORTED";
502
518
  }
519
+ function resolveBrowserNavigationOrigin(url) {
520
+ const resolved = resolveBrowserNavigationUrl(url);
521
+ if (!resolved.url) {
522
+ return null;
523
+ }
524
+ try {
525
+ return new URL(resolved.url).origin;
526
+ } catch {
527
+ return null;
528
+ }
529
+ }
530
+ function isBrowserNavigationAllowedByPolicy(input) {
531
+ if (!input.policy) {
532
+ return true;
533
+ }
534
+ if (input.policy.mode === "same-origin") {
535
+ const policyOrigin = resolveBrowserNavigationOrigin(input.policy.originUrl);
536
+ const nextOrigin = resolveBrowserNavigationOrigin(input.url);
537
+ return policyOrigin !== null && nextOrigin !== null && policyOrigin === nextOrigin;
538
+ }
539
+ return true;
540
+ }
503
541
  async function applyPreferredColorSchemeToGuest(session, logger, syncPreferredColorScheme, scheme) {
504
542
  const contents = session.contents;
505
543
  if (!contents || contents.isDestroyed() || !syncPreferredColorScheme || scheme === null || session.appliedColorScheme === scheme) {
@@ -540,6 +578,12 @@ function createBrowserGuestManager({
540
578
  if (input?.sessionMode !== void 0) {
541
579
  existing.sessionMode = input.sessionMode;
542
580
  }
581
+ if (input?.sessionPartition !== void 0) {
582
+ existing.sessionPartition = input.sessionPartition;
583
+ }
584
+ if (input?.navigationPolicy !== void 0) {
585
+ existing.navigationPolicy = input.navigationPolicy;
586
+ }
543
587
  if (input?.url !== void 0) {
544
588
  existing.desiredUrl = input.url;
545
589
  }
@@ -551,9 +595,11 @@ function createBrowserGuestManager({
551
595
  desiredUrl: input?.url ?? "about:blank",
552
596
  lifecycle: "cold",
553
597
  listeners: [],
598
+ navigationPolicy: input?.navigationPolicy ?? null,
554
599
  nodeId,
555
600
  profileId: input?.profileId ?? null,
556
601
  sessionMode: input?.sessionMode ?? "shared",
602
+ sessionPartition: input?.sessionPartition ?? null,
557
603
  webContentsId: null
558
604
  };
559
605
  sessions.set(nodeId, session);
@@ -562,8 +608,8 @@ function createBrowserGuestManager({
562
608
  const publishState = (session) => {
563
609
  const contents = session.contents && !session.contents.isDestroyed() ? session.contents : null;
564
610
  emit({
565
- canGoBack: contents ? contents.canGoBack() : false,
566
- canGoForward: contents ? contents.canGoForward() : false,
611
+ canGoBack: contents ? canGuestGoBack(contents) : false,
612
+ canGoForward: contents ? canGuestGoForward(contents) : false,
567
613
  isAttachedToWindow: Boolean(contents),
568
614
  isLoading: contents ? contents.isLoading() : false,
569
615
  isOccluded: session.lifecycle === "cold",
@@ -613,10 +659,22 @@ function createBrowserGuestManager({
613
659
  const onFailLoad = (...args) => {
614
660
  const errorCode = typeof args[1] === "number" ? args[1] : void 0;
615
661
  const errorDescription = typeof args[2] === "string" ? args[2] : void 0;
662
+ const validatedUrl = typeof args[3] === "string" ? args[3] : void 0;
663
+ const isMainFrame = typeof args[4] === "boolean" ? args[4] : void 0;
616
664
  if (isAbortedNavigationError({ errorCode, errorDescription })) {
617
665
  publishState(session);
618
666
  return;
619
667
  }
668
+ logger?.warn?.("Browser Node guest navigation failed", {
669
+ currentUrl: contents.getURL(),
670
+ desiredUrl: session.desiredUrl,
671
+ errorCode,
672
+ errorDescription,
673
+ isMainFrame,
674
+ nodeId: session.nodeId,
675
+ validatedUrl,
676
+ webContentsId: session.webContentsId
677
+ });
620
678
  emit({
621
679
  code: "navigation-failed",
622
680
  diagnosticMessage: errorDescription,
@@ -627,12 +685,26 @@ function createBrowserGuestManager({
627
685
  publishState(session);
628
686
  };
629
687
  const onDestroyed = () => detachGuest(session);
688
+ const onWillNavigate = (...args) => {
689
+ const event = args[0] && typeof args[0] === "object" && "preventDefault" in args[0] ? args[0] : null;
690
+ const url = typeof args[1] === "string" ? args[1] : "";
691
+ if (url.length === 0 || isBrowserNavigationAllowedByPolicy({
692
+ policy: session.navigationPolicy,
693
+ url
694
+ })) {
695
+ return;
696
+ }
697
+ event?.preventDefault?.();
698
+ emitOpenUrlFromGuest(session, url);
699
+ publishState(session);
700
+ };
630
701
  const records = [
631
702
  { event: "did-start-loading", listener: onStateChange },
632
703
  { event: "did-stop-loading", listener: onStateChange },
633
704
  { event: "did-navigate", listener: onStateChange },
634
705
  { event: "did-navigate-in-page", listener: onStateChange },
635
706
  { event: "page-title-updated", listener: onStateChange },
707
+ { event: "will-navigate", listener: onWillNavigate },
636
708
  { event: "did-fail-load", listener: onFailLoad },
637
709
  { event: "destroyed", listener: onDestroyed }
638
710
  ];
@@ -657,26 +729,57 @@ function createBrowserGuestManager({
657
729
  publishState(session);
658
730
  return;
659
731
  }
732
+ if (!isBrowserNavigationAllowedByPolicy({
733
+ policy: session.navigationPolicy,
734
+ url: resolved.url
735
+ })) {
736
+ emitOpenUrlFromGuest(session, resolved.url);
737
+ publishState(session);
738
+ return;
739
+ }
660
740
  const currentComparable = normalizeBrowserComparableUrl(contents.getURL());
661
741
  const nextComparable = normalizeBrowserComparableUrl(resolved.url);
662
742
  if (currentComparable && currentComparable === nextComparable) {
663
743
  publishState(session);
664
744
  return;
665
745
  }
666
- await contents.loadURL(resolved.url);
667
- publishState(session);
746
+ try {
747
+ await contents.loadURL(resolved.url);
748
+ } catch (error) {
749
+ logger?.warn?.("Browser Node guest loadURL failed", {
750
+ currentUrl: contents.getURL(),
751
+ desiredUrl: session.desiredUrl,
752
+ error: error instanceof Error ? error.message : String(error),
753
+ nodeId: session.nodeId,
754
+ resolvedUrl: resolved.url,
755
+ webContentsId: session.webContentsId
756
+ });
757
+ throw error;
758
+ } finally {
759
+ publishState(session);
760
+ }
668
761
  };
669
- const openExternalFromGuest = (url) => {
762
+ const emitOpenUrlFromGuest = (session, url) => {
670
763
  const resolved = resolveBrowserNavigationUrl(url);
671
764
  if (resolved.url) {
672
- void Promise.resolve(openExternal(resolved.url)).catch(
673
- (error) => {
674
- logger?.warn?.("Browser Node openExternal failed", {
675
- error: error instanceof Error ? error.message : String(error)
676
- });
677
- }
678
- );
765
+ logger?.info?.("Browser Node guest emitted open-url", {
766
+ nodeId: session.nodeId,
767
+ url: resolved.url,
768
+ webContentsId: session.webContentsId
769
+ });
770
+ emit({
771
+ reuseIfOpen: false,
772
+ sourceNodeId: session.nodeId,
773
+ type: "open-url",
774
+ url: resolved.url
775
+ });
776
+ return { action: "deny" };
679
777
  }
778
+ void Promise.resolve(openExternal(url)).catch((error) => {
779
+ logger?.warn?.("Browser Node openExternal failed", {
780
+ error: error instanceof Error ? error.message : String(error)
781
+ });
782
+ });
680
783
  return { action: "deny" };
681
784
  };
682
785
  return {
@@ -686,8 +789,10 @@ function createBrowserGuestManager({
686
789
  throw new Error("Browser Node rejected navigation URL");
687
790
  }
688
791
  const session = getSession(input.nodeId, {
792
+ navigationPolicy: input.navigationPolicy,
689
793
  profileId: input.profileId,
690
794
  sessionMode: input.sessionMode,
795
+ sessionPartition: input.sessionPartition,
691
796
  url: resolved.url
692
797
  });
693
798
  session.lifecycle = "active";
@@ -721,8 +826,8 @@ function createBrowserGuestManager({
721
826
  }
722
827
  const contents = session.contents && !session.contents.isDestroyed() ? session.contents : null;
723
828
  return {
724
- canGoBack: contents ? contents.canGoBack() : false,
725
- canGoForward: contents ? contents.canGoForward() : false,
829
+ canGoBack: contents ? canGuestGoBack(contents) : false,
830
+ canGoForward: contents ? canGuestGoForward(contents) : false,
726
831
  currentUrl: contents ? contents.getURL() : null,
727
832
  desiredUrl: session.desiredUrl,
728
833
  isAttachedToWindow: Boolean(contents),
@@ -731,46 +836,79 @@ function createBrowserGuestManager({
731
836
  nodeId: session.nodeId,
732
837
  profileId: session.profileId,
733
838
  sessionMode: session.sessionMode,
839
+ sessionPartition: session.sessionPartition,
734
840
  title: contents ? contents.getTitle() : null,
841
+ userAgent: contents?.getUserAgent?.() ?? null,
735
842
  webContentsDestroyed: session.contents ? session.contents.isDestroyed() : null,
736
843
  webContentsId: session.webContentsId
737
844
  };
738
845
  },
739
846
  goBack(input) {
740
847
  const contents = sessions.get(input.nodeId)?.contents;
741
- if (contents && !contents.isDestroyed() && contents.canGoBack()) {
742
- contents.goBack();
848
+ if (contents && !contents.isDestroyed() && canGuestGoBack(contents)) {
849
+ goGuestBack(contents);
743
850
  }
744
851
  return Promise.resolve();
745
852
  },
746
853
  goForward(input) {
747
854
  const contents = sessions.get(input.nodeId)?.contents;
748
- if (contents && !contents.isDestroyed() && contents.canGoForward()) {
749
- contents.goForward();
855
+ if (contents && !contents.isDestroyed() && canGuestGoForward(contents)) {
856
+ goGuestForward(contents);
750
857
  }
751
858
  return Promise.resolve();
752
859
  },
860
+ handleGuestOpenUrl(webContentsId, input) {
861
+ const nodeId = nodeIdByWebContentsId.get(webContentsId);
862
+ const session = nodeId ? sessions.get(nodeId) : null;
863
+ if (!session) {
864
+ logger?.warn?.("Browser Node ignored guest open-url request", {
865
+ url: input.url,
866
+ webContentsId
867
+ });
868
+ return;
869
+ }
870
+ logger?.info?.("Browser Node handling guest open-url request", {
871
+ nodeId: session.nodeId,
872
+ url: input.url,
873
+ webContentsId
874
+ });
875
+ emitOpenUrlFromGuest(session, input.url);
876
+ },
753
877
  async navigate(input) {
754
878
  const resolved = resolveBrowserNavigationUrl(input.url);
755
879
  if (!resolved.url) {
756
880
  throw new Error("Browser Node rejected navigation URL");
757
881
  }
758
- const session = getSession(input.nodeId, { url: resolved.url });
882
+ const session = getSession(input.nodeId, {
883
+ navigationPolicy: input.navigationPolicy,
884
+ url: resolved.url
885
+ });
759
886
  session.lifecycle = "active";
760
887
  await loadDesiredUrl(session);
761
888
  },
889
+ async openExternal(input) {
890
+ const resolved = resolveBrowserNavigationUrl(input.url);
891
+ if (!resolved.url) {
892
+ throw new Error("Browser Node rejected external URL");
893
+ }
894
+ await Promise.resolve(openExternal(resolved.url));
895
+ },
762
896
  async prepareSession(input) {
763
897
  await prepareSession?.(input);
764
898
  getSession(input.nodeId, {
899
+ navigationPolicy: input.navigationPolicy,
765
900
  profileId: input.profileId,
766
- sessionMode: input.sessionMode
901
+ sessionMode: input.sessionMode,
902
+ sessionPartition: input.sessionPartition
767
903
  });
768
904
  },
769
905
  async registerGuest(input) {
770
906
  await prepareSession?.({
771
907
  nodeId: input.nodeId,
772
908
  profileId: input.profileId,
773
- sessionMode: input.sessionMode
909
+ sessionMode: input.sessionMode,
910
+ sessionPartition: input.sessionPartition,
911
+ navigationPolicy: input.navigationPolicy
774
912
  });
775
913
  const contents = resolveWebContents(input.webContentsId);
776
914
  if (!contents || contents.isDestroyed()) {
@@ -785,8 +923,10 @@ function createBrowserGuestManager({
785
923
  );
786
924
  }
787
925
  const session = getSession(input.nodeId, {
926
+ navigationPolicy: input.navigationPolicy,
788
927
  profileId: input.profileId,
789
- sessionMode: input.sessionMode
928
+ sessionMode: input.sessionMode,
929
+ sessionPartition: input.sessionPartition
790
930
  });
791
931
  if (session.webContentsId === input.webContentsId && session.contents === contents) {
792
932
  publishState(session);
@@ -799,7 +939,14 @@ function createBrowserGuestManager({
799
939
  session.webContentsId = input.webContentsId;
800
940
  nodeIdByWebContentsId.set(input.webContentsId, input.nodeId);
801
941
  session.lifecycle = "active";
802
- contents.setWindowOpenHandler?.(({ url }) => openExternalFromGuest(url));
942
+ logger?.info?.("Browser Node registered guest owner", {
943
+ nodeId: input.nodeId,
944
+ sessionPartition: session.sessionPartition,
945
+ webContentsId: input.webContentsId
946
+ });
947
+ contents.setWindowOpenHandler?.(
948
+ ({ url }) => emitOpenUrlFromGuest(session, url)
949
+ );
803
950
  attachGuestListeners(session);
804
951
  await applyPreferredColorSchemeToGuest(
805
952
  session,
@@ -836,12 +983,17 @@ function registerBrowserNodeElectronMain(input) {
836
983
  const managersByWindow = /* @__PURE__ */ new WeakMap();
837
984
  const loopbackPreviewProxy = input.loopbackPreviewRouting !== void 0 ? createBrowserNodeLoopbackPreviewProxy({
838
985
  logger: input.logger,
839
- resolveSession: async ({ profileId, sessionMode }) => {
986
+ resolveSession: async ({
987
+ profileId,
988
+ sessionMode,
989
+ sessionPartition
990
+ }) => {
840
991
  const { session } = await import("electron");
841
992
  return session.fromPartition(
842
993
  resolveBrowserSessionPartition({
843
994
  profileId,
844
- sessionMode
995
+ sessionMode,
996
+ sessionPartition
845
997
  })
846
998
  );
847
999
  },
@@ -913,6 +1065,14 @@ function registerBrowserNodeElectronMain(input) {
913
1065
  input.channels.navigate,
914
1066
  (event, payload) => resolveManager(event).navigate(payload)
915
1067
  );
1068
+ if (input.channels.openExternal) {
1069
+ input.registerHandler(
1070
+ input.channels.openExternal,
1071
+ (event, payload) => resolveManager(event).openExternal(
1072
+ payload
1073
+ )
1074
+ );
1075
+ }
916
1076
  input.registerHandler(
917
1077
  input.channels.goBack,
918
1078
  (event, payload) => resolveManager(event).goBack(payload)
@@ -935,13 +1095,52 @@ function registerBrowserNodeElectronMain(input) {
935
1095
  (event, payload) => resolveManager(event).debugDump(payload)
936
1096
  );
937
1097
  }
1098
+ if (input.channels.guestOpenUrl && input.registerListener) {
1099
+ input.registerListener(input.channels.guestOpenUrl, (event, payload) => {
1100
+ const senderId = readBrowserNodeIpcSenderId(event);
1101
+ const openUrlPayload = payload;
1102
+ if (typeof senderId !== "number" || !Number.isFinite(senderId) || !openUrlPayload || typeof openUrlPayload.url !== "string") {
1103
+ return;
1104
+ }
1105
+ resolveManager(event).handleGuestOpenUrl(senderId, openUrlPayload);
1106
+ });
1107
+ }
1108
+ }
1109
+ function readBrowserNodeIpcSenderId(event) {
1110
+ const sender = event?.sender;
1111
+ return typeof sender?.id === "number" ? sender.id : null;
1112
+ }
1113
+
1114
+ // src/electron-main/userAgent.ts
1115
+ var electronUserAgentTokenPattern = /\sElectron\/[^\s]+/g;
1116
+ function sanitizeBrowserGuestUserAgent(userAgent) {
1117
+ return userAgent.trim().replace(electronUserAgentTokenPattern, "").replace(/\s{2,}/g, " ");
1118
+ }
1119
+ function applyBrowserGuestUserAgent(contents, logger) {
1120
+ const guestContents = contents;
1121
+ if (typeof guestContents.getUserAgent !== "function" || typeof guestContents.setUserAgent !== "function") {
1122
+ return;
1123
+ }
1124
+ const currentUserAgent = guestContents.getUserAgent().trim();
1125
+ const nextUserAgent = sanitizeBrowserGuestUserAgent(currentUserAgent);
1126
+ if (!nextUserAgent || nextUserAgent === currentUserAgent) {
1127
+ return;
1128
+ }
1129
+ guestContents.setUserAgent(nextUserAgent);
1130
+ logger?.debug?.("Browser Node sanitized guest user agent", {
1131
+ webContentsId: contents.id ?? null
1132
+ });
938
1133
  }
939
1134
 
940
1135
  // src/electron-main/webviewSecurity.ts
941
- function isBrowserNodeWebviewAttach(params) {
942
- return params["data-browser-node-webview"] === "true" || isBrowserSessionPartitionAllowed(params.partition);
1136
+ function isBrowserNodeInitialWebviewUrl(url) {
1137
+ return (url ?? "").trim() === "about:blank";
1138
+ }
1139
+ function isBrowserNodeWebviewAttach(params, allowedSessionPartitions) {
1140
+ return params["data-browser-node-webview"] === "true" || isBrowserSessionPartitionAllowed(params.partition, allowedSessionPartitions);
943
1141
  }
944
1142
  function enforceBrowserWebviewSecurity({
1143
+ allowedSessionPartitions,
945
1144
  params,
946
1145
  resolvePreload,
947
1146
  webPreferences
@@ -955,20 +1154,24 @@ function enforceBrowserWebviewSecurity({
955
1154
  webPreferences.webSecurity = true;
956
1155
  delete webPreferences.preload;
957
1156
  const partition = params.partition;
958
- if (!partition || !isBrowserSessionPartitionAllowed(partition)) {
1157
+ if (!partition || !isBrowserSessionPartitionAllowed(partition, allowedSessionPartitions)) {
959
1158
  return {
960
1159
  allowed: false,
961
1160
  reason: "Unsupported Browser Node session partition"
962
1161
  };
963
1162
  }
964
- const resolved = resolveBrowserNavigationUrl(params.src ?? "about:blank");
965
- if (!resolved.url) {
966
- return {
967
- allowed: false,
968
- reason: "Unsupported browser URL"
969
- };
1163
+ if (isBrowserNodeInitialWebviewUrl(params.src)) {
1164
+ params.src = "about:blank";
1165
+ } else {
1166
+ const resolved = resolveBrowserNavigationUrl(params.src ?? "about:blank");
1167
+ if (!resolved.url) {
1168
+ return {
1169
+ allowed: false,
1170
+ reason: "Unsupported browser URL"
1171
+ };
1172
+ }
1173
+ params.src = resolved.url;
970
1174
  }
971
- params.src = resolved.url;
972
1175
  const preload = resolvePreload?.({ params: { ...params } });
973
1176
  const resolvedPreload = typeof preload === "string" ? preload.trim() : "";
974
1177
  if (resolvedPreload.length > 0) {
@@ -977,20 +1180,28 @@ function enforceBrowserWebviewSecurity({
977
1180
  return { allowed: true, reason: null };
978
1181
  }
979
1182
  function installBrowserWebviewSecurity({
1183
+ allowedSessionPartitions,
980
1184
  contents,
981
1185
  logger,
982
1186
  onGuestAttached,
983
1187
  openExternal,
984
1188
  resolvePreload,
985
- shouldHandleWebview = isBrowserNodeWebviewAttach
1189
+ shouldHandleWebview
986
1190
  }) {
987
1191
  let pendingBrowserAttachCount = 0;
988
1192
  const handleWillAttachWebview = (event, webPreferences, params) => {
989
- if (!shouldHandleWebview(params)) {
1193
+ const shouldHandle = shouldHandleWebview?.(params) ?? isBrowserNodeWebviewAttach(params, allowedSessionPartitions);
1194
+ logger?.debug?.("Browser Node webview will attach", {
1195
+ partition: params.partition ?? null,
1196
+ shouldHandle,
1197
+ src: params.src ?? null
1198
+ });
1199
+ if (!shouldHandle) {
990
1200
  return;
991
1201
  }
992
1202
  pendingBrowserAttachCount += 1;
993
1203
  const result = enforceBrowserWebviewSecurity({
1204
+ allowedSessionPartitions,
994
1205
  params,
995
1206
  resolvePreload,
996
1207
  webPreferences
@@ -999,14 +1210,28 @@ function installBrowserWebviewSecurity({
999
1210
  pendingBrowserAttachCount = Math.max(0, pendingBrowserAttachCount - 1);
1000
1211
  logger?.warn?.("Browser Node webview blocked", { reason: result.reason });
1001
1212
  event.preventDefault();
1213
+ return;
1002
1214
  }
1215
+ logger?.debug?.("Browser Node webview attach allowed", {
1216
+ partition: params.partition ?? null,
1217
+ src: params.src ?? null
1218
+ });
1003
1219
  };
1004
1220
  const handleDidAttachWebview = (_event, guestContents) => {
1005
1221
  if (pendingBrowserAttachCount <= 0) {
1222
+ logger?.debug?.("Browser Node webview did attach ignored", {
1223
+ guestWebContentsId: guestContents.id ?? null,
1224
+ pendingBrowserAttachCount
1225
+ });
1006
1226
  return;
1007
1227
  }
1008
1228
  pendingBrowserAttachCount -= 1;
1229
+ applyBrowserGuestUserAgent(guestContents, logger);
1009
1230
  onGuestAttached?.(guestContents);
1231
+ logger?.debug?.("Browser Node webview guest attached", {
1232
+ guestWebContentsId: guestContents.id ?? null,
1233
+ pendingBrowserAttachCount
1234
+ });
1010
1235
  guestContents.setWindowOpenHandler(({ url }) => {
1011
1236
  const resolved = resolveBrowserNavigationUrl(url);
1012
1237
  if (resolved.url) {
@@ -1023,9 +1248,11 @@ function installBrowserWebviewSecurity({
1023
1248
  };
1024
1249
  }
1025
1250
  export {
1251
+ applyBrowserGuestUserAgent,
1026
1252
  enforceBrowserWebviewSecurity,
1027
1253
  installBrowserWebviewSecurity,
1028
1254
  isBrowserNodeWebviewAttach,
1029
- registerBrowserNodeElectronMain
1255
+ registerBrowserNodeElectronMain,
1256
+ sanitizeBrowserGuestUserAgent
1030
1257
  };
1031
1258
  //# sourceMappingURL=index.js.map