@push.rocks/smartpuppeteer 2.5.0 → 2.6.0

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.
@@ -82,10 +82,12 @@ const maxProxySecuritySessions = 256;
82
82
  const maxTrackedProxyRequests = 1024;
83
83
  const maxTotalTrackedProxyRequests = 4096;
84
84
  const maxProxySecurityOperations = 2048;
85
+ const maxTrackedServiceWorkerVersions = 512;
86
+ const proxySecurityCommandTimeoutMs = 5000;
87
+ const proxySecurityCommandOptions = { timeout: proxySecurityCommandTimeoutMs } as const;
85
88
  const proxySeedTargetTypes = new Set([
86
89
  'background_page',
87
90
  'page',
88
- 'service_worker',
89
91
  'shared_worker',
90
92
  'webview',
91
93
  ]);
@@ -99,6 +101,9 @@ type TProxyAuthRequiredEvent = plugins.puppeteer.Protocol.Fetch.AuthRequiredEven
99
101
  type TProxyRequestPausedEvent = plugins.puppeteer.Protocol.Fetch.RequestPausedEvent;
100
102
  type TNetworkLoadingFinishedEvent = plugins.puppeteer.Protocol.Network.LoadingFinishedEvent;
101
103
  type TNetworkLoadingFailedEvent = plugins.puppeteer.Protocol.Network.LoadingFailedEvent;
104
+ type TServiceWorkerVersionUpdatedEvent = (
105
+ plugins.puppeteer.Protocol.ServiceWorker.WorkerVersionUpdatedEvent
106
+ );
102
107
 
103
108
  interface IProxySecuritySession {
104
109
  session: plugins.puppeteer.CDPSession;
@@ -108,6 +113,7 @@ interface IProxySecuritySession {
108
113
  fetchEnabled: boolean;
109
114
  allowLiveTargetDetach: boolean;
110
115
  ownedByProxySecurity: boolean;
116
+ retireWhenIdle: boolean;
111
117
  attemptedAuthentications: Set<string>;
112
118
  requestIdsByNetworkId: Map<string, string>;
113
119
  authRequiredListener: (event: TProxyAuthRequiredEvent) => void;
@@ -146,7 +152,8 @@ interface IPrivateLiveBrowserTab {
146
152
  closing: boolean;
147
153
  stateUpdateQueued: boolean;
148
154
  stateUpdatePending: boolean;
149
- navigationResetPending: boolean;
155
+ navigationResetRevision: number;
156
+ restoredNavigationRevision: number;
150
157
  evaluationExecutionContextId?: number;
151
158
  securityCdpSession?: plugins.puppeteer.CDPSession;
152
159
  cdpSession?: plugins.puppeteer.CDPSession;
@@ -396,8 +403,18 @@ export class LiveBrowserSession {
396
403
  private browserSecurityCdpSession?: plugins.puppeteer.CDPSession;
397
404
  private browserSecurityConnection?: plugins.puppeteer.Connection;
398
405
  private browserSecuritySessionAttachedListener?: (session: plugins.puppeteer.CDPSession) => void;
406
+ private browserSecurityOwnedSessionAttachedListener?: (
407
+ session: plugins.puppeteer.CDPSession,
408
+ ) => void;
399
409
  private browserSecuritySessionDetachedListener?: TCdpSessionDetachedListener;
410
+ private browserSecurityWorkerLifecycleCdpSession?: plugins.puppeteer.CDPSession;
411
+ private browserSecurityWorkerVersionUpdatedListener?: (
412
+ event: TServiceWorkerVersionUpdatedEvent,
413
+ ) => void;
400
414
  private readonly proxySecuritySessions = new Map<string, IProxySecuritySession>();
415
+ private readonly ownedProxySecuritySessions = new Set<plugins.puppeteer.CDPSession>();
416
+ private readonly browserSecurityParentOwnedSessions = new Set<plugins.puppeteer.CDPSession>();
417
+ private readonly serviceWorkerTargetIdsByVersionId = new Map<string, string>();
401
418
  private readonly proxySecurityOperations = new Set<Promise<void>>();
402
419
  private proxySecurityGeneration = 0;
403
420
  private proxySecurityStopping = false;
@@ -2276,7 +2293,10 @@ export class LiveBrowserSession {
2276
2293
  tab.securityCdpSession = undefined;
2277
2294
  if (securityCdpSession && !securityCdpSession.detached) {
2278
2295
  try {
2279
- await securityCdpSession.detach();
2296
+ await this.detachCdpSessionWithTimeout(
2297
+ securityCdpSession,
2298
+ 'page security session',
2299
+ );
2280
2300
  } catch {
2281
2301
  // Browser lifetime cancellation may close the target before explicit detach settles.
2282
2302
  }
@@ -2346,11 +2366,18 @@ export class LiveBrowserSession {
2346
2366
  if (!proxyCredentials) {
2347
2367
  try {
2348
2368
  if (denyPermissions) {
2349
- await cdpSession.send('Browser.grantPermissions', { permissions: [] });
2369
+ await cdpSession.send(
2370
+ 'Browser.grantPermissions',
2371
+ { permissions: [] },
2372
+ proxySecurityCommandOptions,
2373
+ );
2350
2374
  }
2351
2375
  } finally {
2352
2376
  if (!cdpSession.detached) {
2353
- await cdpSession.detach();
2377
+ await this.detachCdpSessionWithTimeout(
2378
+ cdpSession,
2379
+ 'browser permission session',
2380
+ );
2354
2381
  }
2355
2382
  }
2356
2383
  return;
@@ -2365,7 +2392,11 @@ export class LiveBrowserSession {
2365
2392
  this.browserSecurityCdpSession = cdpSession;
2366
2393
  try {
2367
2394
  if (denyPermissions) {
2368
- await cdpSession.send('Browser.grantPermissions', { permissions: [] });
2395
+ await cdpSession.send(
2396
+ 'Browser.grantPermissions',
2397
+ { permissions: [] },
2398
+ proxySecurityCommandOptions,
2399
+ );
2369
2400
  }
2370
2401
  const browserSecurityConnection = cdpSession.connection();
2371
2402
  if (!browserSecurityConnection) {
@@ -2373,13 +2404,69 @@ export class LiveBrowserSession {
2373
2404
  }
2374
2405
  this.browserSecurityConnection = browserSecurityConnection;
2375
2406
  this.browserSecuritySessionAttachedListener = (attachedSession) => {
2407
+ const setupPromise = this.configureProxySecuritySession(attachedSession, generation);
2408
+ queueMicrotask(() => {
2409
+ const securityRecord = this.proxySecuritySessions.get(attachedSession.id());
2410
+ if (
2411
+ securityRecord?.generation === generation
2412
+ && securityRecord.ownedByProxySecurity
2413
+ ) {
2414
+ return;
2415
+ }
2416
+ this.trackProxySecurityOperation(
2417
+ setupPromise,
2418
+ generation,
2419
+ 'proxy_security_session_setup_failed',
2420
+ );
2421
+ });
2422
+ };
2423
+ this.browserSecurityOwnedSessionAttachedListener = (attachedSession) => {
2424
+ const securityRecord = this.proxySecuritySessions.get(attachedSession.id());
2425
+ if (!securityRecord || securityRecord.generation !== generation) {
2426
+ return;
2427
+ }
2428
+ securityRecord.ownedByProxySecurity = true;
2429
+ this.ownedProxySecuritySessions.add(attachedSession);
2430
+ this.browserSecurityParentOwnedSessions.add(attachedSession);
2431
+ const setupPromise = securityRecord.setupPromise;
2432
+ securityRecord.setupPromise = setupPromise.then(async () => {
2433
+ try {
2434
+ await attachedSession.send(
2435
+ 'Runtime.runIfWaitingForDebugger',
2436
+ undefined,
2437
+ proxySecurityCommandOptions,
2438
+ );
2439
+ } catch (error) {
2440
+ this.removeProxySecuritySession(securityRecord);
2441
+ if (attachedSession.detached) {
2442
+ this.forgetOwnedProxySecuritySession(attachedSession);
2443
+ return;
2444
+ }
2445
+ if (this.proxySecurityStopping || this.normalStopRequested) {
2446
+ await this.detachOwnedProxySecuritySession(securityRecord);
2447
+ return;
2448
+ }
2449
+ if (
2450
+ securityRecord.targetId
2451
+ && await this.waitForProxySecurityTargetCoverage(
2452
+ securityRecord.targetId,
2453
+ generation,
2454
+ )
2455
+ ) {
2456
+ await this.detachOwnedProxySecuritySession(securityRecord);
2457
+ return;
2458
+ }
2459
+ throw error;
2460
+ }
2461
+ });
2376
2462
  this.trackProxySecurityOperation(
2377
- this.configureProxySecuritySession(attachedSession, generation),
2463
+ securityRecord.setupPromise,
2378
2464
  generation,
2379
2465
  'proxy_security_session_setup_failed',
2380
2466
  );
2381
2467
  };
2382
2468
  this.browserSecuritySessionDetachedListener = (detachedSession) => {
2469
+ this.forgetOwnedProxySecuritySession(detachedSession);
2383
2470
  if (detachedSession === cdpSession) {
2384
2471
  this.handleProxySecurityFailure(
2385
2472
  'proxy_security_browser_session_detached',
@@ -2388,6 +2475,14 @@ export class LiveBrowserSession {
2388
2475
  );
2389
2476
  return;
2390
2477
  }
2478
+ if (detachedSession === this.browserSecurityWorkerLifecycleCdpSession) {
2479
+ this.handleProxySecurityFailure(
2480
+ 'proxy_security_worker_lifecycle_detached',
2481
+ new Error('Proxy security service-worker lifecycle session detached unexpectedly'),
2482
+ generation,
2483
+ );
2484
+ return;
2485
+ }
2391
2486
  this.trackProxySecurityOperation(
2392
2487
  this.verifyProxySecuritySessionDetached(detachedSession, generation),
2393
2488
  generation,
@@ -2398,11 +2493,30 @@ export class LiveBrowserSession {
2398
2493
  'sessionattached',
2399
2494
  this.browserSecuritySessionAttachedListener,
2400
2495
  );
2496
+ cdpSession.on(
2497
+ plugins.puppeteer.CDPSessionEvent.SessionAttached,
2498
+ this.browserSecurityOwnedSessionAttachedListener,
2499
+ );
2401
2500
  browserSecurityConnection.on(
2402
2501
  'sessiondetached',
2403
2502
  this.browserSecuritySessionDetachedListener,
2404
2503
  );
2405
2504
 
2505
+ await cdpSession.send('Target.setAutoAttach', {
2506
+ autoAttach: true,
2507
+ waitForDebuggerOnStart: true,
2508
+ flatten: true,
2509
+ filter: [
2510
+ { type: 'service_worker' },
2511
+ { exclude: true },
2512
+ ],
2513
+ }, proxySecurityCommandOptions);
2514
+ await Promise.all(
2515
+ [...this.proxySecuritySessions.values()]
2516
+ .filter((record) => record.generation === generation && record.ownedByProxySecurity)
2517
+ .map((record) => record.setupPromise),
2518
+ );
2519
+
2406
2520
  for (const target of browser.targets()) {
2407
2521
  if (!proxySeedTargetTypes.has(target.type())) {
2408
2522
  continue;
@@ -2413,14 +2527,121 @@ export class LiveBrowserSession {
2413
2527
  throw new Error(`Proxy security did not observe session ${securitySession.id()}`);
2414
2528
  }
2415
2529
  securityRecord.ownedByProxySecurity = true;
2530
+ this.ownedProxySecuritySessions.add(securitySession);
2416
2531
  await securityRecord.setupPromise;
2417
2532
  }
2533
+ await this.configureProxySecurityWorkerLifecycle(cdpSession, generation);
2418
2534
  } catch (error) {
2419
2535
  await this.teardownProxySecurity();
2420
2536
  throw error;
2421
2537
  }
2422
2538
  }
2423
2539
 
2540
+ private async configureProxySecurityWorkerLifecycle(
2541
+ browserSecurityCdpSession: plugins.puppeteer.CDPSession,
2542
+ generation: number,
2543
+ ): Promise<void> {
2544
+ const lifecycleUrl = `data:text/html,<title>smartpuppeteer-proxy-security-${generation}</title>`;
2545
+ const { targetId } = await browserSecurityCdpSession.send('Target.createTarget', {
2546
+ url: lifecycleUrl,
2547
+ background: true,
2548
+ hidden: true,
2549
+ }, proxySecurityCommandOptions);
2550
+ const attachedSessions = new Map<string, plugins.puppeteer.CDPSession>();
2551
+ const captureAttachedSession = (session: plugins.puppeteer.CDPSession): void => {
2552
+ attachedSessions.set(session.id(), session);
2553
+ };
2554
+ browserSecurityCdpSession.on(
2555
+ plugins.puppeteer.CDPSessionEvent.SessionAttached,
2556
+ captureAttachedSession,
2557
+ );
2558
+ let lifecycleSession: plugins.puppeteer.CDPSession;
2559
+ try {
2560
+ const { sessionId } = await browserSecurityCdpSession.send('Target.attachToTarget', {
2561
+ targetId,
2562
+ flatten: true,
2563
+ }, proxySecurityCommandOptions);
2564
+ const attachedSession = attachedSessions.get(sessionId);
2565
+ if (!attachedSession) {
2566
+ throw new Error('Proxy security did not observe its hidden lifecycle session');
2567
+ }
2568
+ lifecycleSession = attachedSession;
2569
+ } finally {
2570
+ browserSecurityCdpSession.off(
2571
+ plugins.puppeteer.CDPSessionEvent.SessionAttached,
2572
+ captureAttachedSession,
2573
+ );
2574
+ }
2575
+
2576
+ const securityRecord = this.proxySecuritySessions.get(lifecycleSession.id());
2577
+ if (!securityRecord || !securityRecord.ownedByProxySecurity) {
2578
+ throw new Error('Proxy security did not configure its hidden lifecycle session');
2579
+ }
2580
+ await securityRecord.setupPromise;
2581
+ if (lifecycleSession.detached) {
2582
+ throw new Error('Proxy security hidden lifecycle session detached during setup');
2583
+ }
2584
+
2585
+ this.browserSecurityWorkerLifecycleCdpSession = lifecycleSession;
2586
+ this.browserSecurityWorkerVersionUpdatedListener = (event) => {
2587
+ const stoppedTargetIds = new Set<string>();
2588
+ const redundantTargetIds = new Set<string>();
2589
+ for (const version of event.versions) {
2590
+ if (version.targetId) {
2591
+ if (
2592
+ !this.serviceWorkerTargetIdsByVersionId.has(version.versionId)
2593
+ && this.serviceWorkerTargetIdsByVersionId.size >= maxTrackedServiceWorkerVersions
2594
+ ) {
2595
+ this.handleProxySecurityFailure(
2596
+ 'proxy_security_worker_tracking_capacity_exceeded',
2597
+ new Error(
2598
+ `Proxy security exceeded ${maxTrackedServiceWorkerVersions} service-worker versions`,
2599
+ ),
2600
+ generation,
2601
+ );
2602
+ return;
2603
+ }
2604
+ this.serviceWorkerTargetIdsByVersionId.set(version.versionId, version.targetId);
2605
+ }
2606
+ const knownTargetId = version.targetId
2607
+ ?? this.serviceWorkerTargetIdsByVersionId.get(version.versionId);
2608
+ if (version.status === 'redundant' && knownTargetId) {
2609
+ redundantTargetIds.add(knownTargetId);
2610
+ }
2611
+ if (version.runningStatus === 'stopped') {
2612
+ if (knownTargetId) {
2613
+ stoppedTargetIds.add(knownTargetId);
2614
+ }
2615
+ this.serviceWorkerTargetIdsByVersionId.delete(version.versionId);
2616
+ }
2617
+ }
2618
+ for (const workerSecurityRecord of [...this.proxySecuritySessions.values()]) {
2619
+ if (
2620
+ !workerSecurityRecord.ownedByProxySecurity
2621
+ || workerSecurityRecord.targetType !== 'service_worker'
2622
+ || !workerSecurityRecord.targetId
2623
+ || !this.browserSecurityParentOwnedSessions.has(workerSecurityRecord.session)
2624
+ ) {
2625
+ continue;
2626
+ }
2627
+ if (stoppedTargetIds.has(workerSecurityRecord.targetId)) {
2628
+ this.retireOwnedProxySecuritySession(workerSecurityRecord, true);
2629
+ } else if (redundantTargetIds.has(workerSecurityRecord.targetId)) {
2630
+ this.retireOwnedProxySecuritySession(workerSecurityRecord, false);
2631
+ }
2632
+ }
2633
+ };
2634
+ lifecycleSession.on(
2635
+ 'ServiceWorker.workerVersionUpdated',
2636
+ this.browserSecurityWorkerVersionUpdatedListener,
2637
+ );
2638
+ await lifecycleSession.send(
2639
+ 'ServiceWorker.enable',
2640
+ undefined,
2641
+ proxySecurityCommandOptions,
2642
+ );
2643
+ }
2644
+
2424
2645
  private configureProxySecuritySession(
2425
2646
  session: plugins.puppeteer.CDPSession,
2426
2647
  generation: number,
@@ -2483,7 +2704,7 @@ export class LiveBrowserSession {
2483
2704
  session.send('Fetch.continueWithAuth', {
2484
2705
  requestId: event.requestId,
2485
2706
  authChallengeResponse,
2486
- }).then(() => undefined),
2707
+ }, proxySecurityCommandOptions).then(() => undefined),
2487
2708
  generation,
2488
2709
  'proxy_security_protocol_failed',
2489
2710
  );
@@ -2516,7 +2737,11 @@ export class LiveBrowserSession {
2516
2737
  }
2517
2738
  }
2518
2739
  this.trackProxySecurityOperation(
2519
- session.send('Fetch.continueRequest', { requestId: event.requestId }).then(() => undefined),
2740
+ session.send(
2741
+ 'Fetch.continueRequest',
2742
+ { requestId: event.requestId },
2743
+ proxySecurityCommandOptions,
2744
+ ).then(() => undefined),
2520
2745
  generation,
2521
2746
  'proxy_security_protocol_failed',
2522
2747
  );
@@ -2528,6 +2753,13 @@ export class LiveBrowserSession {
2528
2753
  }
2529
2754
  requestIdsByNetworkId.delete(networkId);
2530
2755
  attemptedAuthentications.delete(requestId);
2756
+ if (
2757
+ proxySecuritySession.retireWhenIdle
2758
+ && attemptedAuthentications.size === 0
2759
+ && requestIdsByNetworkId.size === 0
2760
+ ) {
2761
+ this.retireOwnedProxySecuritySession(proxySecuritySession, false);
2762
+ }
2531
2763
  };
2532
2764
  const loadingFinishedListener = (event: TNetworkLoadingFinishedEvent): void => {
2533
2765
  forgetAuthentication(event.requestId);
@@ -2546,6 +2778,7 @@ export class LiveBrowserSession {
2546
2778
  fetchEnabled: false,
2547
2779
  allowLiveTargetDetach: false,
2548
2780
  ownedByProxySecurity: false,
2781
+ retireWhenIdle: false,
2549
2782
  attemptedAuthentications,
2550
2783
  requestIdsByNetworkId,
2551
2784
  authRequiredListener,
@@ -2554,12 +2787,20 @@ export class LiveBrowserSession {
2554
2787
  loadingFailedListener,
2555
2788
  setupPromise: Promise.resolve(),
2556
2789
  };
2557
- const targetInfoPromise = session.send('Target.getTargetInfo');
2558
- const networkEnablePromise = session.send('Network.enable');
2790
+ const targetInfoPromise = session.send(
2791
+ 'Target.getTargetInfo',
2792
+ undefined,
2793
+ proxySecurityCommandOptions,
2794
+ );
2795
+ const networkEnablePromise = session.send(
2796
+ 'Network.enable',
2797
+ undefined,
2798
+ proxySecurityCommandOptions,
2799
+ );
2559
2800
  const fetchEnablePromise = session.send('Fetch.enable', {
2560
2801
  handleAuthRequests: true,
2561
2802
  patterns: [{ urlPattern: '*' }],
2562
- });
2803
+ }, proxySecurityCommandOptions);
2563
2804
  const commandResultsPromise = Promise.allSettled([
2564
2805
  targetInfoPromise,
2565
2806
  networkEnablePromise,
@@ -2590,10 +2831,28 @@ export class LiveBrowserSession {
2590
2831
  proxySecuritySession.fetchEnabled = true;
2591
2832
  } catch (error) {
2592
2833
  this.removeProxySecuritySession(proxySecuritySession);
2593
- if (session.detached || this.proxySecurityStopping || this.normalStopRequested) {
2834
+ if (session.detached) {
2835
+ this.forgetOwnedProxySecuritySession(session);
2594
2836
  return;
2595
2837
  }
2596
- throw error;
2838
+ if (this.proxySecurityStopping || this.normalStopRequested) {
2839
+ await this.detachOwnedProxySecuritySession(proxySecuritySession);
2840
+ return;
2841
+ }
2842
+ if (
2843
+ proxySecuritySession.targetId
2844
+ && await this.waitForProxySecurityTargetCoverage(
2845
+ proxySecuritySession.targetId,
2846
+ generation,
2847
+ )
2848
+ ) {
2849
+ await this.detachOwnedProxySecuritySession(proxySecuritySession);
2850
+ return;
2851
+ }
2852
+ throw new Error(
2853
+ `Proxy security ${proxySecuritySession.ownedByProxySecurity ? 'owned' : 'observed'} session setup failed for ${proxySecuritySession.targetType ?? 'unknown'} target ${proxySecuritySession.targetId ?? 'unknown'}: ${normalizeErrorMessage(error)}`,
2854
+ { cause: error },
2855
+ );
2597
2856
  }
2598
2857
  })();
2599
2858
  proxySecuritySession.setupPromise = setupPromise;
@@ -2619,6 +2878,18 @@ export class LiveBrowserSession {
2619
2878
  if (!proxySecuritySession.targetId) {
2620
2879
  throw new Error(`Proxy security session detached before target identification: ${session.id()}`);
2621
2880
  }
2881
+ if (await this.waitForProxySecurityTargetCoverage(proxySecuritySession.targetId, generation)) {
2882
+ return;
2883
+ }
2884
+ throw new Error(
2885
+ `Proxy security detached from live target ${proxySecuritySession.targetId}`,
2886
+ );
2887
+ }
2888
+
2889
+ private async waitForProxySecurityTargetCoverage(
2890
+ targetId: string,
2891
+ generation: number,
2892
+ ): Promise<boolean> {
2622
2893
  for (let attempt = 0; attempt < 3; attempt += 1) {
2623
2894
  const generationSessions = [...this.proxySecuritySessions.values()].filter((candidate) => (
2624
2895
  candidate.generation === generation
@@ -2626,12 +2897,12 @@ export class LiveBrowserSession {
2626
2897
  await Promise.allSettled(generationSessions.map((candidate) => candidate.setupPromise));
2627
2898
  const hasReplacement = [...this.proxySecuritySessions.values()].some((candidate) => (
2628
2899
  candidate.generation === generation
2629
- && candidate.targetId === proxySecuritySession.targetId
2900
+ && candidate.targetId === targetId
2630
2901
  && candidate.fetchEnabled
2631
2902
  && !candidate.session.detached
2632
2903
  ));
2633
2904
  if (hasReplacement) {
2634
- return;
2905
+ return true;
2635
2906
  }
2636
2907
  if (attempt < 2) {
2637
2908
  await delay(25);
@@ -2641,12 +2912,12 @@ export class LiveBrowserSession {
2641
2912
  if (!browserSecurityCdpSession || browserSecurityCdpSession.detached) {
2642
2913
  throw new Error('Browser security CDP session detached unexpectedly');
2643
2914
  }
2644
- const { targetInfos } = await browserSecurityCdpSession.send('Target.getTargets');
2645
- if (targetInfos.some((targetInfo) => targetInfo.targetId === proxySecuritySession.targetId)) {
2646
- throw new Error(
2647
- `Proxy security detached from live target ${proxySecuritySession.targetId}`,
2648
- );
2649
- }
2915
+ const { targetInfos } = await browserSecurityCdpSession.send(
2916
+ 'Target.getTargets',
2917
+ undefined,
2918
+ proxySecurityCommandOptions,
2919
+ );
2920
+ return !targetInfos.some((targetInfo) => targetInfo.targetId === targetId);
2650
2921
  }
2651
2922
 
2652
2923
  private removeProxySecuritySession(proxySecuritySession: IProxySecuritySession): void {
@@ -2668,6 +2939,113 @@ export class LiveBrowserSession {
2668
2939
  this.proxySecuritySessions.delete(proxySecuritySession.session.id());
2669
2940
  }
2670
2941
 
2942
+ private async detachOwnedProxySecuritySession(
2943
+ proxySecuritySession: IProxySecuritySession,
2944
+ ): Promise<void> {
2945
+ if (!proxySecuritySession.ownedByProxySecurity) {
2946
+ return;
2947
+ }
2948
+ await this.detachTrackedOwnedProxySecuritySession(proxySecuritySession.session);
2949
+ }
2950
+
2951
+ private forgetOwnedProxySecuritySession(session: plugins.puppeteer.CDPSession): void {
2952
+ this.ownedProxySecuritySessions.delete(session);
2953
+ this.browserSecurityParentOwnedSessions.delete(session);
2954
+ }
2955
+
2956
+ private async detachCdpSessionWithTimeout(
2957
+ session: plugins.puppeteer.CDPSession,
2958
+ description: string,
2959
+ ): Promise<void> {
2960
+ let timeout: ReturnType<typeof setTimeout> | undefined;
2961
+ const timeoutPromise = new Promise<never>((_resolve, reject) => {
2962
+ timeout = setTimeout(() => {
2963
+ reject(new Error(
2964
+ `${description} did not detach within ${proxySecurityCommandTimeoutMs}ms`,
2965
+ ));
2966
+ }, proxySecurityCommandTimeoutMs);
2967
+ });
2968
+ try {
2969
+ await Promise.race([session.detach(), timeoutPromise]);
2970
+ } finally {
2971
+ if (timeout) {
2972
+ clearTimeout(timeout);
2973
+ }
2974
+ }
2975
+ }
2976
+
2977
+ private retireOwnedProxySecuritySession(
2978
+ proxySecuritySession: IProxySecuritySession,
2979
+ force: boolean,
2980
+ ): void {
2981
+ if (
2982
+ !proxySecuritySession.ownedByProxySecurity
2983
+ || this.proxySecuritySessions.get(proxySecuritySession.session.id())
2984
+ !== proxySecuritySession
2985
+ ) {
2986
+ return;
2987
+ }
2988
+ proxySecuritySession.retireWhenIdle = true;
2989
+ if (
2990
+ !force
2991
+ && (
2992
+ proxySecuritySession.attemptedAuthentications.size > 0
2993
+ || proxySecuritySession.requestIdsByNetworkId.size > 0
2994
+ )
2995
+ ) {
2996
+ return;
2997
+ }
2998
+ if (proxySecuritySession.targetId) {
2999
+ for (const [versionId, targetId] of this.serviceWorkerTargetIdsByVersionId) {
3000
+ if (targetId === proxySecuritySession.targetId) {
3001
+ this.serviceWorkerTargetIdsByVersionId.delete(versionId);
3002
+ }
3003
+ }
3004
+ }
3005
+ proxySecuritySession.allowLiveTargetDetach = true;
3006
+ this.removeProxySecuritySession(proxySecuritySession);
3007
+ this.trackProxySecurityOperation(
3008
+ this.detachOwnedProxySecuritySession(proxySecuritySession),
3009
+ proxySecuritySession.generation,
3010
+ 'proxy_security_worker_retirement_failed',
3011
+ );
3012
+ }
3013
+
3014
+ private async detachTrackedOwnedProxySecuritySession(
3015
+ session: plugins.puppeteer.CDPSession,
3016
+ ): Promise<void> {
3017
+ if (session.detached) {
3018
+ this.forgetOwnedProxySecuritySession(session);
3019
+ return;
3020
+ }
3021
+ try {
3022
+ if (this.browserSecurityParentOwnedSessions.has(session)) {
3023
+ const browserSecurityCdpSession = this.browserSecurityCdpSession;
3024
+ if (!browserSecurityCdpSession || browserSecurityCdpSession.detached) {
3025
+ throw new Error('Browser security parent session is unavailable');
3026
+ }
3027
+ await browserSecurityCdpSession.send('Target.detachFromTarget', {
3028
+ sessionId: session.id(),
3029
+ }, proxySecurityCommandOptions);
3030
+ } else {
3031
+ await this.detachCdpSessionWithTimeout(session, 'proxy security session');
3032
+ }
3033
+ this.forgetOwnedProxySecuritySession(session);
3034
+ } catch (error) {
3035
+ if (session.detached) {
3036
+ this.forgetOwnedProxySecuritySession(session);
3037
+ return;
3038
+ }
3039
+ if (this.proxySecurityStopping || this.normalStopRequested) {
3040
+ return;
3041
+ }
3042
+ throw new Error(
3043
+ `Failed to detach an owned proxy security session: ${normalizeErrorMessage(error)}`,
3044
+ { cause: error },
3045
+ );
3046
+ }
3047
+ }
3048
+
2671
3049
  private allowOperationalCdpSessionDetach(session: plugins.puppeteer.CDPSession): void {
2672
3050
  const proxySecuritySession = this.proxySecuritySessions.get(session.id());
2673
3051
  if (proxySecuritySession) {
@@ -2730,12 +3108,51 @@ export class LiveBrowserSession {
2730
3108
 
2731
3109
  private async teardownProxySecurity(): Promise<void> {
2732
3110
  this.proxySecurityStopping = true;
3111
+ this.serviceWorkerTargetIdsByVersionId.clear();
3112
+ const browserSecurityCdpSession = this.browserSecurityCdpSession;
3113
+ if (browserSecurityCdpSession && !browserSecurityCdpSession.detached) {
3114
+ try {
3115
+ await browserSecurityCdpSession.send('Target.setAutoAttach', {
3116
+ autoAttach: false,
3117
+ waitForDebuggerOnStart: false,
3118
+ flatten: true,
3119
+ }, proxySecurityCommandOptions);
3120
+ } catch {
3121
+ // Browser shutdown may detach the browser target before auto-attach is disabled.
3122
+ }
3123
+ }
3124
+ const workerLifecycleCdpSession = this.browserSecurityWorkerLifecycleCdpSession;
3125
+ if (workerLifecycleCdpSession && !workerLifecycleCdpSession.detached) {
3126
+ try {
3127
+ await workerLifecycleCdpSession.send(
3128
+ 'ServiceWorker.disable',
3129
+ undefined,
3130
+ proxySecurityCommandOptions,
3131
+ );
3132
+ } catch {
3133
+ // Browser shutdown can close the hidden lifecycle target first.
3134
+ }
3135
+ }
3136
+ if (workerLifecycleCdpSession && this.browserSecurityWorkerVersionUpdatedListener) {
3137
+ workerLifecycleCdpSession.off(
3138
+ 'ServiceWorker.workerVersionUpdated',
3139
+ this.browserSecurityWorkerVersionUpdatedListener,
3140
+ );
3141
+ }
3142
+ this.browserSecurityWorkerVersionUpdatedListener = undefined;
3143
+ this.browserSecurityWorkerLifecycleCdpSession = undefined;
2733
3144
  if (this.browserSecurityConnection && this.browserSecuritySessionAttachedListener) {
2734
3145
  this.browserSecurityConnection.off(
2735
3146
  'sessionattached',
2736
3147
  this.browserSecuritySessionAttachedListener,
2737
3148
  );
2738
3149
  }
3150
+ if (browserSecurityCdpSession && this.browserSecurityOwnedSessionAttachedListener) {
3151
+ browserSecurityCdpSession.off(
3152
+ plugins.puppeteer.CDPSessionEvent.SessionAttached,
3153
+ this.browserSecurityOwnedSessionAttachedListener,
3154
+ );
3155
+ }
2739
3156
  if (this.browserSecurityConnection && this.browserSecuritySessionDetachedListener) {
2740
3157
  this.browserSecurityConnection.off(
2741
3158
  'sessiondetached',
@@ -2743,28 +3160,31 @@ export class LiveBrowserSession {
2743
3160
  );
2744
3161
  }
2745
3162
  this.browserSecuritySessionAttachedListener = undefined;
3163
+ this.browserSecurityOwnedSessionAttachedListener = undefined;
2746
3164
  this.browserSecuritySessionDetachedListener = undefined;
2747
3165
  this.browserSecurityConnection = undefined;
2748
- const ownedSessions = [...this.proxySecuritySessions.values()]
2749
- .filter((record) => record.ownedByProxySecurity)
2750
- .map((record) => record.session);
3166
+ const ownedSessions = [...this.ownedProxySecuritySessions];
2751
3167
  for (const proxySecuritySession of [...this.proxySecuritySessions.values()]) {
2752
3168
  this.removeProxySecuritySession(proxySecuritySession);
2753
3169
  }
2754
3170
  for (const ownedSession of ownedSessions) {
2755
3171
  if (!ownedSession.detached) {
2756
3172
  try {
2757
- await ownedSession.detach();
3173
+ await this.detachTrackedOwnedProxySecuritySession(ownedSession);
2758
3174
  } catch {
2759
3175
  // Browser shutdown can close a target before explicit detach settles.
2760
3176
  }
2761
3177
  }
2762
3178
  }
2763
- const browserSecurityCdpSession = this.browserSecurityCdpSession;
3179
+ this.ownedProxySecuritySessions.clear();
3180
+ this.browserSecurityParentOwnedSessions.clear();
2764
3181
  this.browserSecurityCdpSession = undefined;
2765
3182
  if (browserSecurityCdpSession && !browserSecurityCdpSession.detached) {
2766
3183
  try {
2767
- await browserSecurityCdpSession.detach();
3184
+ await this.detachCdpSessionWithTimeout(
3185
+ browserSecurityCdpSession,
3186
+ 'browser security session',
3187
+ );
2768
3188
  } catch {
2769
3189
  // Browser shutdown can detach the browser target first.
2770
3190
  }
@@ -2927,7 +3347,8 @@ export class LiveBrowserSession {
2927
3347
  closing: false,
2928
3348
  stateUpdateQueued: false,
2929
3349
  stateUpdatePending: false,
2930
- navigationResetPending: false,
3350
+ navigationResetRevision: 0,
3351
+ restoredNavigationRevision: 0,
2931
3352
  removeListeners: [],
2932
3353
  };
2933
3354
  this.tabs.set(tab.id, tab);
@@ -2939,11 +3360,11 @@ export class LiveBrowserSession {
2939
3360
  tab.securityCdpSession = securityCdpSession;
2940
3361
  await securityCdpSession.send('Page.enable', {
2941
3362
  enableFileChooserOpenedEvent: true,
2942
- });
3363
+ }, proxySecurityCommandOptions);
2943
3364
  await securityCdpSession.send('Page.setInterceptFileChooserDialog', {
2944
3365
  enabled: true,
2945
3366
  cancel: true,
2946
- });
3367
+ }, proxySecurityCommandOptions);
2947
3368
  }
2948
3369
  await this.ensureTabViewport(tab);
2949
3370
  await this.refreshTab(tab);
@@ -3078,7 +3499,10 @@ export class LiveBrowserSession {
3078
3499
  } catch (error) {
3079
3500
  if (tab.securityCdpSession && !tab.securityCdpSession.detached) {
3080
3501
  try {
3081
- await tab.securityCdpSession.detach();
3502
+ await this.detachCdpSessionWithTimeout(
3503
+ tab.securityCdpSession,
3504
+ 'page security session',
3505
+ );
3082
3506
  } catch {
3083
3507
  // The page may have closed while security setup was failing.
3084
3508
  }
@@ -3117,31 +3541,58 @@ export class LiveBrowserSession {
3117
3541
  return;
3118
3542
  }
3119
3543
  tab.stateUpdatePending = true;
3120
- tab.navigationResetPending ||= navigationReset;
3544
+ if (navigationReset) {
3545
+ tab.navigationResetRevision += 1;
3546
+ }
3121
3547
  if (tab.stateUpdateQueued) {
3122
3548
  return;
3123
3549
  }
3124
3550
  tab.stateUpdateQueued = true;
3125
3551
  const wasScheduled = this.scheduleOperation(async () => {
3126
3552
  try {
3127
- const shouldResetNavigation = tab.navigationResetPending;
3553
+ const navigationResetRevision = tab.navigationResetRevision;
3554
+ const shouldResetNavigation = (
3555
+ tab.restoredNavigationRevision < navigationResetRevision
3556
+ );
3128
3557
  tab.stateUpdatePending = false;
3129
- tab.navigationResetPending = false;
3130
3558
  if (!this.tabs.has(tab.id) || tab.closing) {
3131
3559
  return;
3132
3560
  }
3133
3561
  if (shouldResetNavigation && this.activeTabId === tab.id) {
3134
3562
  await this.stopScreencast(tab);
3135
3563
  }
3136
- await this.refreshTab(tab);
3137
- this.emitState();
3564
+ let stateUpdateError: unknown;
3565
+ try {
3566
+ await this.refreshTab(tab);
3567
+ this.emitState();
3568
+ } catch (error) {
3569
+ stateUpdateError = error;
3570
+ }
3138
3571
  if (
3139
3572
  shouldResetNavigation
3140
3573
  && this.activeTabId === tab.id
3141
3574
  && this.status === 'running'
3142
3575
  && tab.status === 'open'
3143
3576
  ) {
3144
- await this.startScreencast(tab);
3577
+ try {
3578
+ await this.startScreencast(tab);
3579
+ } catch (error) {
3580
+ throw stateUpdateError
3581
+ ? new AggregateError(
3582
+ [stateUpdateError, error],
3583
+ 'Page state refresh and screencast restart failed',
3584
+ )
3585
+ : error;
3586
+ }
3587
+ }
3588
+ if (shouldResetNavigation) {
3589
+ tab.restoredNavigationRevision = Math.max(
3590
+ tab.restoredNavigationRevision,
3591
+ navigationResetRevision,
3592
+ );
3593
+ }
3594
+ if (stateUpdateError) {
3595
+ throw stateUpdateError;
3145
3596
  }
3146
3597
  } finally {
3147
3598
  tab.stateUpdateQueued = false;
@@ -3346,15 +3797,32 @@ export class LiveBrowserSession {
3346
3797
  } finally {
3347
3798
  tab.navigationInProgress = false;
3348
3799
  if (this.tabs.has(tab.id) && !tab.page.isClosed()) {
3349
- await this.refreshTab(tab);
3350
- this.emitState();
3800
+ let stateUpdateError: unknown;
3801
+ try {
3802
+ await this.refreshTab(tab);
3803
+ this.emitState();
3804
+ } catch (error) {
3805
+ stateUpdateError = error;
3806
+ }
3351
3807
  if (
3352
3808
  isActive
3353
3809
  && this.activeTabId === tab.id
3354
3810
  && this.status === 'running'
3355
3811
  && tab.status === 'open'
3356
3812
  ) {
3357
- await this.startScreencast(tab);
3813
+ try {
3814
+ await this.startScreencast(tab);
3815
+ } catch (error) {
3816
+ throw stateUpdateError
3817
+ ? new AggregateError(
3818
+ [stateUpdateError, error],
3819
+ 'Navigation state refresh and screencast restart failed',
3820
+ )
3821
+ : error;
3822
+ }
3823
+ }
3824
+ if (stateUpdateError) {
3825
+ throw stateUpdateError;
3358
3826
  }
3359
3827
  }
3360
3828
  }