@push.rocks/smartpuppeteer 2.1.0 → 2.2.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.
@@ -3,6 +3,7 @@ import type {
3
3
  ILiveBrowserClickOptions,
4
4
  ILiveBrowserCreateTabOptions,
5
5
  ILiveBrowserError,
6
+ ILiveBrowserEvaluateOptions,
6
7
  ILiveBrowserFillOptions,
7
8
  ILiveBrowserFrame,
8
9
  ILiveBrowserFrameAcknowledgement,
@@ -15,6 +16,7 @@ import type {
15
16
  ILiveBrowserNavigationOptions,
16
17
  ILiveBrowserObservation,
17
18
  ILiveBrowserObserveOptions,
19
+ ILiveBrowserOperationOptions,
18
20
  ILiveBrowserPressOptions,
19
21
  ILiveBrowserSessionOptions,
20
22
  ILiveBrowserSnapshot,
@@ -26,6 +28,7 @@ import type {
26
28
  TLiveBrowserEvent,
27
29
  TLiveBrowserEventListener,
28
30
  TLiveBrowserImageFormat,
31
+ TLiveBrowserJsonValue,
29
32
  TLiveBrowserWaitUntil,
30
33
  } from './smartpuppeteer.interfaces.livebrowser.js';
31
34
  import * as plugins from './smartpuppeteer.plugins.js';
@@ -47,6 +50,16 @@ const maxTimeoutMs = 60000;
47
50
  const maxOutstandingFrames = 3;
48
51
  const maxQueuedPublicOperations = 64;
49
52
  const maxQueuedInternalOperations = 128;
53
+ const maxEvaluationScriptBytes = 262144;
54
+ const maxEvaluationOutputBytes = 1048576;
55
+ const maxEvaluationDepth = 32;
56
+ const maxEvaluationNodes = 50000;
57
+ const maxEvaluationStringBytes = 262144;
58
+ const maxEvaluationArrayLength = 10000;
59
+ const maxEvaluationObjectKeys = 10000;
60
+ const evaluationBootstrapKey = '__smartpuppeteerEvaluate';
61
+ const evaluationCancelKey = '__smartpuppeteerCancel';
62
+ const evaluationCleanupKey = '__smartpuppeteerCleanup';
50
63
 
51
64
  type TScreencastFrameEvent = plugins.puppeteer.Protocol.Page.ScreencastFrameEvent;
52
65
  type TScreencastFrameListener = (event: TScreencastFrameEvent) => void;
@@ -67,6 +80,8 @@ interface IPrivateLiveBrowserTab {
67
80
  stateUpdateQueued: boolean;
68
81
  stateUpdatePending: boolean;
69
82
  navigationResetPending: boolean;
83
+ evaluationExecutionContextId?: number;
84
+ securityCdpSession?: plugins.puppeteer.CDPSession;
70
85
  cdpSession?: plugins.puppeteer.CDPSession;
71
86
  cdpConnection?: plugins.puppeteer.Connection;
72
87
  screencastFrameListener?: TScreencastFrameListener;
@@ -87,14 +102,34 @@ interface IImageDimensions {
87
102
  height: number;
88
103
  }
89
104
 
105
+ interface INormalizedEvaluationOptions {
106
+ timeoutMs: number;
107
+ maxOutputBytes: number;
108
+ maxDepth: number;
109
+ maxNodes: number;
110
+ maxStringBytes: number;
111
+ maxArrayLength: number;
112
+ maxObjectKeys: number;
113
+ }
114
+
115
+ interface IEvaluationEnvelope {
116
+ ok: boolean;
117
+ json?: string;
118
+ error?: string;
119
+ }
120
+
90
121
  type TQueuedOperationKind = 'public' | 'internal' | 'shutdown';
122
+ type TQueuedOperationState = 'queued' | 'active' | 'settled';
91
123
 
92
124
  interface IQueuedOperation {
93
125
  kind: TQueuedOperationKind;
126
+ state: TQueuedOperationState;
94
127
  controller: AbortController;
95
128
  run: (signal: AbortSignal) => Promise<unknown>;
96
129
  resolve: (value: unknown) => void;
97
130
  reject: (error: unknown) => void;
131
+ capacityReleased: boolean;
132
+ removeCallerAbortListener?: () => void;
98
133
  }
99
134
 
100
135
  const validateBoundedString = (
@@ -163,6 +198,10 @@ const normalizeErrorMessage = (error: unknown): string => {
163
198
  return truncate(String(error), 2048);
164
199
  };
165
200
 
201
+ const normalizeAbortReason = (signal: AbortSignal): unknown => {
202
+ return signal.reason ?? new Error('The browser operation was aborted');
203
+ };
204
+
166
205
  const readUint32 = (data: Uint8Array, offset: number): number => {
167
206
  return (
168
207
  data[offset]! * 0x1000000
@@ -291,6 +330,7 @@ export class LiveBrowserSession {
291
330
  private viewportRevision = 1;
292
331
  private tabSequence = 0;
293
332
  private frameSequence = 0;
333
+ private evaluationSequence = 0;
294
334
  private normalStopRequested = false;
295
335
  private lastError?: ILiveBrowserError;
296
336
 
@@ -310,6 +350,10 @@ export class LiveBrowserSession {
310
350
  if (optionsArg.launchOptions && 'signal' in optionsArg.launchOptions) {
311
351
  throw new Error('LiveBrowserSession owns launch cancellation; launchOptions.signal is unsupported');
312
352
  }
353
+ validateOptionalBoolean(optionsArg.allowEvaluation, 'allowEvaluation');
354
+ for (const [name, value] of Object.entries(optionsArg.security ?? {})) {
355
+ validateOptionalBoolean(value, `security.${name}`);
356
+ }
313
357
  const launchViewport = optionsArg.launchOptions?.defaultViewport;
314
358
  const viewport = normalizeViewport(
315
359
  optionsArg.viewport
@@ -327,9 +371,13 @@ export class LiveBrowserSession {
327
371
  ...optionsArg.launchOptions,
328
372
  args: [...(optionsArg.launchOptions?.args ?? [])],
329
373
  defaultViewport: createPuppeteerViewport(viewport),
374
+ ...(optionsArg.security?.denyDownloads
375
+ ? { downloadBehavior: { policy: 'deny' as const } }
376
+ : {}),
330
377
  },
331
378
  viewport,
332
379
  screencast: optionsArg.screencast ? { ...optionsArg.screencast } : undefined,
380
+ security: optionsArg.security ? { ...optionsArg.security } : undefined,
333
381
  };
334
382
  this.viewport = { ...viewport };
335
383
  this.validateScreencastOptions();
@@ -356,7 +404,7 @@ export class LiveBrowserSession {
356
404
  };
357
405
  }
358
406
 
359
- public async start(): Promise<void> {
407
+ public async start(operationOptions: ILiveBrowserOperationOptions = {}): Promise<void> {
360
408
  return this.enqueuePublicOperation(async (signal) => {
361
409
  if (this.status === 'running' || this.status === 'starting') {
362
410
  return;
@@ -377,19 +425,31 @@ export class LiveBrowserSession {
377
425
  if (browserLifetimeController.signal.aborted) {
378
426
  throw browserLifetimeController.signal.reason;
379
427
  }
380
- this.browser = await getEnvAwareBrowserInstance({
381
- forceNoSandbox: this.options.forceNoSandbox,
382
- usePipe: this.options.usePipe,
383
- launchOptions: {
384
- ...this.options.launchOptions,
385
- protocol: 'cdp',
386
- signal: browserLifetimeController.signal,
387
- },
388
- });
428
+ const abortBrowserLaunch = (): void => {
429
+ if (!browserLifetimeController.signal.aborted) {
430
+ browserLifetimeController.abort(normalizeAbortReason(signal));
431
+ }
432
+ };
433
+ signal.addEventListener('abort', abortBrowserLaunch, { once: true });
434
+ try {
435
+ this.browser = await getEnvAwareBrowserInstance({
436
+ forceNoSandbox: this.options.forceNoSandbox,
437
+ requireSandbox: this.options.requireSandbox,
438
+ usePipe: this.options.usePipe,
439
+ launchOptions: {
440
+ ...this.options.launchOptions,
441
+ protocol: 'cdp',
442
+ signal: browserLifetimeController.signal,
443
+ },
444
+ });
445
+ } finally {
446
+ signal.removeEventListener('abort', abortBrowserLaunch);
447
+ }
389
448
  if (signal.aborted) {
390
449
  throw signal.reason;
391
450
  }
392
451
  this.browserContext = this.browser.defaultBrowserContext();
452
+ await this.configureBrowserSecurity(this.browser);
393
453
  this.browserDisconnectedListener = () => {
394
454
  if (this.normalStopRequested || this.status === 'stopped' || this.status === 'stopping') {
395
455
  return;
@@ -432,6 +492,9 @@ export class LiveBrowserSession {
432
492
  this.status = 'running';
433
493
  this.emitState();
434
494
  await this.startScreencast(firstTab);
495
+ if (signal.aborted) {
496
+ throw normalizeAbortReason(signal);
497
+ }
435
498
  } catch (error) {
436
499
  if (signal.aborted || this.normalStopRequested) {
437
500
  await this.stopInternal();
@@ -446,7 +509,7 @@ export class LiveBrowserSession {
446
509
  await this.stopInternal(startError);
447
510
  throw error;
448
511
  }
449
- });
512
+ }, operationOptions);
450
513
  }
451
514
 
452
515
  public async stop(): Promise<void> {
@@ -508,6 +571,7 @@ export class LiveBrowserSession {
508
571
 
509
572
  public async createTab(
510
573
  optionsArg: ILiveBrowserCreateTabOptions = {},
574
+ operationOptions: ILiveBrowserOperationOptions = {},
511
575
  ): Promise<ILiveBrowserTabState> {
512
576
  const url = optionsArg.url === undefined ? undefined : this.validateUrl(optionsArg.url);
513
577
  const activate = optionsArg.activate ?? true;
@@ -563,16 +627,22 @@ export class LiveBrowserSession {
563
627
  }
564
628
  throw error;
565
629
  }
566
- });
630
+ }, operationOptions);
567
631
  }
568
632
 
569
- public async activateTab(tabId: string): Promise<void> {
633
+ public async activateTab(
634
+ tabId: string,
635
+ operationOptions: ILiveBrowserOperationOptions = {},
636
+ ): Promise<void> {
570
637
  return this.enqueuePublicOperation(async () => {
571
638
  await this.activateTabInternal(tabId);
572
- });
639
+ }, operationOptions);
573
640
  }
574
641
 
575
- public async closeTab(tabId: string): Promise<void> {
642
+ public async closeTab(
643
+ tabId: string,
644
+ operationOptions: ILiveBrowserOperationOptions = {},
645
+ ): Promise<void> {
576
646
  return this.enqueuePublicOperation(async () => {
577
647
  const tab = this.requireTab(tabId);
578
648
  const wasActive = this.activeTabId === tab.id;
@@ -609,10 +679,13 @@ export class LiveBrowserSession {
609
679
  } else {
610
680
  this.emitState();
611
681
  }
612
- });
682
+ }, operationOptions);
613
683
  }
614
684
 
615
- public async navigate(optionsArg: ILiveBrowserNavigateOptions): Promise<void> {
685
+ public async navigate(
686
+ optionsArg: ILiveBrowserNavigateOptions,
687
+ operationOptions: ILiveBrowserOperationOptions = {},
688
+ ): Promise<void> {
616
689
  const url = this.validateUrl(optionsArg.url);
617
690
  return this.enqueuePublicOperation(async (signal) => {
618
691
  const tab = this.resolveNavigationTab(optionsArg.tabId);
@@ -623,10 +696,13 @@ export class LiveBrowserSession {
623
696
  },
624
697
  signal,
625
698
  );
626
- });
699
+ }, operationOptions);
627
700
  }
628
701
 
629
- public async back(optionsArg: ILiveBrowserNavigationOptions = {}): Promise<void> {
702
+ public async back(
703
+ optionsArg: ILiveBrowserNavigationOptions = {},
704
+ operationOptions: ILiveBrowserOperationOptions = {},
705
+ ): Promise<void> {
630
706
  return this.enqueuePublicOperation(async (signal) => {
631
707
  const tab = this.resolveNavigationTab(optionsArg.tabId);
632
708
  await this.navigateTab(
@@ -636,10 +712,13 @@ export class LiveBrowserSession {
636
712
  },
637
713
  signal,
638
714
  );
639
- });
715
+ }, operationOptions);
640
716
  }
641
717
 
642
- public async forward(optionsArg: ILiveBrowserNavigationOptions = {}): Promise<void> {
718
+ public async forward(
719
+ optionsArg: ILiveBrowserNavigationOptions = {},
720
+ operationOptions: ILiveBrowserOperationOptions = {},
721
+ ): Promise<void> {
643
722
  return this.enqueuePublicOperation(async (signal) => {
644
723
  const tab = this.resolveNavigationTab(optionsArg.tabId);
645
724
  await this.navigateTab(
@@ -649,10 +728,13 @@ export class LiveBrowserSession {
649
728
  },
650
729
  signal,
651
730
  );
652
- });
731
+ }, operationOptions);
653
732
  }
654
733
 
655
- public async reload(optionsArg: ILiveBrowserNavigationOptions = {}): Promise<void> {
734
+ public async reload(
735
+ optionsArg: ILiveBrowserNavigationOptions = {},
736
+ operationOptions: ILiveBrowserOperationOptions = {},
737
+ ): Promise<void> {
656
738
  return this.enqueuePublicOperation(async (signal) => {
657
739
  const tab = this.resolveNavigationTab(optionsArg.tabId);
658
740
  await this.navigateTab(
@@ -662,10 +744,13 @@ export class LiveBrowserSession {
662
744
  },
663
745
  signal,
664
746
  );
665
- });
747
+ }, operationOptions);
666
748
  }
667
749
 
668
- public async setViewport(viewportArg: ILiveBrowserViewport): Promise<void> {
750
+ public async setViewport(
751
+ viewportArg: ILiveBrowserViewport,
752
+ operationOptions: ILiveBrowserOperationOptions = {},
753
+ ): Promise<void> {
669
754
  const viewport = normalizeViewport(viewportArg);
670
755
  return this.enqueuePublicOperation(async (signal) => {
671
756
  const tab = this.requireActiveTab();
@@ -684,7 +769,7 @@ export class LiveBrowserSession {
684
769
  await this.startScreencast(tab);
685
770
  }
686
771
  }
687
- });
772
+ }, operationOptions);
688
773
  }
689
774
 
690
775
  public async dispatchMouse(input: ILiveBrowserMouseInput): Promise<void> {
@@ -815,6 +900,7 @@ export class LiveBrowserSession {
815
900
 
816
901
  public async captureSnapshot(
817
902
  optionsArg: ILiveBrowserSnapshotOptions = {},
903
+ operationOptions: ILiveBrowserOperationOptions = {},
818
904
  ): Promise<ILiveBrowserSnapshot> {
819
905
  const format = optionsArg.format ?? 'jpeg';
820
906
  if (format !== 'jpeg' && format !== 'png') {
@@ -858,10 +944,13 @@ export class LiveBrowserSession {
858
944
  ...dimensions,
859
945
  data,
860
946
  };
861
- });
947
+ }, operationOptions);
862
948
  }
863
949
 
864
- public async observe(optionsArg: ILiveBrowserObserveOptions = {}): Promise<ILiveBrowserObservation> {
950
+ public async observe(
951
+ optionsArg: ILiveBrowserObserveOptions = {},
952
+ operationOptions: ILiveBrowserOperationOptions = {},
953
+ ): Promise<ILiveBrowserObservation> {
865
954
  const maxCharacters = optionsArg.maxCharacters === undefined
866
955
  ? 12000
867
956
  : validateInteger(optionsArg.maxCharacters, 'maxCharacters', 256, 50000);
@@ -948,10 +1037,202 @@ export class LiveBrowserSession {
948
1037
  text: truncatedText,
949
1038
  truncated: reachedTraversalLimit || unboundedText.length > maxCharacters,
950
1039
  };
951
- });
1040
+ }, operationOptions);
952
1041
  }
953
1042
 
954
- public async click(optionsArg: ILiveBrowserClickOptions): Promise<void> {
1043
+ public async evaluate(
1044
+ expressionArg: string,
1045
+ optionsArg: ILiveBrowserEvaluateOptions = {},
1046
+ operationOptions: ILiveBrowserOperationOptions = {},
1047
+ ): Promise<TLiveBrowserJsonValue> {
1048
+ if (!this.options.allowEvaluation) {
1049
+ throw new Error('LiveBrowserSession evaluation is disabled');
1050
+ }
1051
+ const expression = validateBoundedString(
1052
+ expressionArg,
1053
+ 'expression',
1054
+ 1,
1055
+ maxEvaluationScriptBytes,
1056
+ );
1057
+ const expressionBytes = new TextEncoder().encode(expression).byteLength;
1058
+ if (expressionBytes > maxEvaluationScriptBytes) {
1059
+ throw new Error(`expression must not exceed ${maxEvaluationScriptBytes} UTF-8 bytes`);
1060
+ }
1061
+ const evaluationOptions = this.normalizeEvaluationOptions(optionsArg);
1062
+ const evaluationId = ++this.evaluationSequence;
1063
+ const cancellationKey = `__smartpuppeteerCancel${evaluationId}`;
1064
+ const evaluationExpression = this.createEvaluationExpression(
1065
+ expression,
1066
+ evaluationOptions,
1067
+ cancellationKey,
1068
+ );
1069
+
1070
+ return this.enqueuePublicOperation(async (signal) => {
1071
+ const tab = this.resolveActionTab(optionsArg.tabId);
1072
+ const cdpSession = await tab.page.createCDPSession();
1073
+ let executionContextId: number | undefined;
1074
+ let cancellationPromise: Promise<void> | undefined;
1075
+ let terminationPromise: Promise<void> | undefined;
1076
+ let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
1077
+ let evaluationTimedOut = false;
1078
+ const terminateEvaluation = (): void => {
1079
+ terminationPromise ??= cdpSession.send('Runtime.terminateExecution').then(() => undefined);
1080
+ };
1081
+ const cancelEvaluation = (): void => {
1082
+ if (executionContextId === undefined) {
1083
+ return;
1084
+ }
1085
+ cancellationPromise ??= cdpSession.send('Runtime.evaluate', {
1086
+ expression: `globalThis[${JSON.stringify(evaluationCancelKey)}]?.(${JSON.stringify(cancellationKey)})`,
1087
+ contextId: executionContextId,
1088
+ returnByValue: true,
1089
+ awaitPromise: false,
1090
+ includeCommandLineAPI: false,
1091
+ userGesture: false,
1092
+ disableBreaks: true,
1093
+ }).then(() => undefined);
1094
+ };
1095
+ signal.addEventListener('abort', cancelEvaluation, { once: true });
1096
+ let evaluationError: unknown;
1097
+ let evaluationFailed = false;
1098
+ try {
1099
+ if (signal.aborted) {
1100
+ throw normalizeAbortReason(signal);
1101
+ }
1102
+ executionContextId = tab.evaluationExecutionContextId;
1103
+ if (executionContextId === undefined) {
1104
+ const frameTreeResponse = await cdpSession.send('Page.getFrameTree');
1105
+ if (signal.aborted) {
1106
+ throw normalizeAbortReason(signal);
1107
+ }
1108
+ const isolatedWorld = await cdpSession.send('Page.createIsolatedWorld', {
1109
+ frameId: frameTreeResponse.frameTree.frame.id,
1110
+ worldName: 'smartpuppeteer-evaluation',
1111
+ grantUniveralAccess: false,
1112
+ });
1113
+ executionContextId = isolatedWorld.executionContextId;
1114
+ tab.evaluationExecutionContextId = executionContextId;
1115
+ }
1116
+ if (signal.aborted) {
1117
+ throw normalizeAbortReason(signal);
1118
+ }
1119
+ const bootstrapResponse = await cdpSession.send('Runtime.evaluate', {
1120
+ expression: this.createEvaluationBootstrapExpression(),
1121
+ contextId: executionContextId,
1122
+ returnByValue: true,
1123
+ awaitPromise: true,
1124
+ includeCommandLineAPI: false,
1125
+ userGesture: false,
1126
+ disableBreaks: true,
1127
+ });
1128
+ if (bootstrapResponse.exceptionDetails) {
1129
+ throw new Error(this.readCdpExceptionMessage(bootstrapResponse.exceptionDetails));
1130
+ }
1131
+ if (signal.aborted) {
1132
+ throw normalizeAbortReason(signal);
1133
+ }
1134
+ timeoutHandle = setTimeout(() => {
1135
+ evaluationTimedOut = true;
1136
+ terminateEvaluation();
1137
+ }, evaluationOptions.timeoutMs + 250);
1138
+ const evaluationResponse = await cdpSession.send('Runtime.evaluate', {
1139
+ expression: evaluationExpression,
1140
+ contextId: executionContextId,
1141
+ returnByValue: true,
1142
+ awaitPromise: true,
1143
+ timeout: evaluationOptions.timeoutMs + 250,
1144
+ includeCommandLineAPI: false,
1145
+ userGesture: false,
1146
+ disableBreaks: true,
1147
+ allowUnsafeEvalBlockedByCSP: true,
1148
+ });
1149
+ if (evaluationTimedOut) {
1150
+ throw new Error(`Evaluation timed out after ${evaluationOptions.timeoutMs}ms`);
1151
+ }
1152
+ if (signal.aborted) {
1153
+ throw normalizeAbortReason(signal);
1154
+ }
1155
+ if (evaluationResponse.exceptionDetails) {
1156
+ throw new Error(this.readCdpExceptionMessage(evaluationResponse.exceptionDetails));
1157
+ }
1158
+ const envelope = evaluationResponse.result.value as IEvaluationEnvelope | undefined;
1159
+ if (!envelope || typeof envelope !== 'object' || typeof envelope.ok !== 'boolean') {
1160
+ throw new Error('Evaluation returned an invalid result envelope');
1161
+ }
1162
+ if (!envelope.ok) {
1163
+ throw new Error(
1164
+ typeof envelope.error === 'string'
1165
+ ? truncate(envelope.error, 2048)
1166
+ : 'Evaluation failed',
1167
+ );
1168
+ }
1169
+ if (typeof envelope.json !== 'string') {
1170
+ throw new Error('Evaluation returned an invalid JSON result');
1171
+ }
1172
+ if (new TextEncoder().encode(envelope.json).byteLength > evaluationOptions.maxOutputBytes) {
1173
+ throw new Error('Evaluation result exceeded maxOutputBytes during transfer');
1174
+ }
1175
+ return JSON.parse(envelope.json) as TLiveBrowserJsonValue;
1176
+ } catch (error) {
1177
+ evaluationError = error;
1178
+ evaluationFailed = true;
1179
+ throw error;
1180
+ } finally {
1181
+ signal.removeEventListener('abort', cancelEvaluation);
1182
+ if (timeoutHandle) {
1183
+ clearTimeout(timeoutHandle);
1184
+ }
1185
+ const cleanupErrors: unknown[] = [];
1186
+ if (cancellationPromise) {
1187
+ try {
1188
+ await cancellationPromise;
1189
+ } catch (error) {
1190
+ cleanupErrors.push(error);
1191
+ }
1192
+ }
1193
+ if (terminationPromise) {
1194
+ try {
1195
+ await terminationPromise;
1196
+ } catch (error) {
1197
+ cleanupErrors.push(error);
1198
+ }
1199
+ }
1200
+ if (executionContextId !== undefined && !cdpSession.detached) {
1201
+ try {
1202
+ await cdpSession.send('Runtime.evaluate', {
1203
+ expression: `globalThis[${JSON.stringify(evaluationCleanupKey)}]?.(${JSON.stringify(cancellationKey)})`,
1204
+ contextId: executionContextId,
1205
+ returnByValue: true,
1206
+ awaitPromise: false,
1207
+ includeCommandLineAPI: false,
1208
+ userGesture: false,
1209
+ disableBreaks: true,
1210
+ });
1211
+ } catch {
1212
+ // Navigation destroys the old execution context and its cancellation registry.
1213
+ }
1214
+ }
1215
+ if (!cdpSession.detached) {
1216
+ try {
1217
+ await cdpSession.detach();
1218
+ } catch (error) {
1219
+ cleanupErrors.push(error);
1220
+ }
1221
+ }
1222
+ if (cleanupErrors.length > 0) {
1223
+ throw new AggregateError(
1224
+ evaluationFailed ? [evaluationError, ...cleanupErrors] : cleanupErrors,
1225
+ 'Evaluation cleanup was incomplete',
1226
+ );
1227
+ }
1228
+ }
1229
+ }, operationOptions);
1230
+ }
1231
+
1232
+ public async click(
1233
+ optionsArg: ILiveBrowserClickOptions,
1234
+ operationOptions: ILiveBrowserOperationOptions = {},
1235
+ ): Promise<void> {
955
1236
  const selector = validateBoundedString(
956
1237
  optionsArg.selector,
957
1238
  'selector',
@@ -977,10 +1258,13 @@ export class LiveBrowserSession {
977
1258
  });
978
1259
  await this.refreshTab(actionTab);
979
1260
  this.emitState();
980
- });
1261
+ }, operationOptions);
981
1262
  }
982
1263
 
983
- public async fill(optionsArg: ILiveBrowserFillOptions): Promise<void> {
1264
+ public async fill(
1265
+ optionsArg: ILiveBrowserFillOptions,
1266
+ operationOptions: ILiveBrowserOperationOptions = {},
1267
+ ): Promise<void> {
984
1268
  const selector = validateBoundedString(
985
1269
  optionsArg.selector,
986
1270
  'selector',
@@ -996,10 +1280,13 @@ export class LiveBrowserSession {
996
1280
  await actionTab.page.locator(selector).setTimeout(timeout).fill(text, { signal });
997
1281
  await this.refreshTab(actionTab);
998
1282
  this.emitState();
999
- });
1283
+ }, operationOptions);
1000
1284
  }
1001
1285
 
1002
- public async press(optionsArg: ILiveBrowserPressOptions): Promise<void> {
1286
+ public async press(
1287
+ optionsArg: ILiveBrowserPressOptions,
1288
+ operationOptions: ILiveBrowserOperationOptions = {},
1289
+ ): Promise<void> {
1003
1290
  const selector = validateBoundedString(
1004
1291
  optionsArg.selector,
1005
1292
  'selector',
@@ -1027,12 +1314,27 @@ export class LiveBrowserSession {
1027
1314
  }
1028
1315
  await this.refreshTab(actionTab);
1029
1316
  this.emitState();
1030
- });
1317
+ }, operationOptions);
1031
1318
  }
1032
1319
 
1033
1320
  private enqueuePublicOperation<T>(
1034
1321
  operation: (signal: AbortSignal) => Promise<T>,
1322
+ operationOptions: ILiveBrowserOperationOptions = {},
1035
1323
  ): Promise<T> {
1324
+ const callerSignal = operationOptions.signal;
1325
+ if (
1326
+ callerSignal !== undefined
1327
+ && (
1328
+ typeof callerSignal !== 'object'
1329
+ || typeof callerSignal.addEventListener !== 'function'
1330
+ || typeof callerSignal.removeEventListener !== 'function'
1331
+ )
1332
+ ) {
1333
+ return Promise.reject(new Error('operationOptions.signal must be an AbortSignal'));
1334
+ }
1335
+ if (callerSignal?.aborted) {
1336
+ return Promise.reject(normalizeAbortReason(callerSignal));
1337
+ }
1036
1338
  if (
1037
1339
  this.status === 'stopping'
1038
1340
  || this.normalStopRequested
@@ -1044,7 +1346,7 @@ export class LiveBrowserSession {
1044
1346
  return Promise.reject(new Error('LiveBrowserSession operation queue is full'));
1045
1347
  }
1046
1348
  this.admittedPublicOperations += 1;
1047
- return this.enqueueQueuedOperation('public', operation);
1349
+ return this.enqueueQueuedOperation('public', operation, false, callerSignal);
1048
1350
  }
1049
1351
 
1050
1352
  private enqueueInternalOperation(
@@ -1062,15 +1364,45 @@ export class LiveBrowserSession {
1062
1364
  kind: TQueuedOperationKind,
1063
1365
  operation: (signal: AbortSignal) => Promise<T>,
1064
1366
  priority = false,
1367
+ callerSignal?: AbortSignal,
1065
1368
  ): Promise<T> {
1066
1369
  return new Promise<T>((resolve, reject) => {
1067
1370
  const queuedOperation: IQueuedOperation = {
1068
1371
  kind,
1372
+ state: 'queued',
1069
1373
  controller: new AbortController(),
1070
1374
  run: operation,
1071
1375
  resolve: (value) => resolve(value as T),
1072
1376
  reject,
1377
+ capacityReleased: false,
1073
1378
  };
1379
+ if (callerSignal) {
1380
+ const onCallerAbort = (): void => {
1381
+ const abortReason = normalizeAbortReason(callerSignal);
1382
+ if (queuedOperation.state === 'queued') {
1383
+ const operationIndex = this.operationQueue.indexOf(queuedOperation);
1384
+ if (operationIndex >= 0) {
1385
+ this.operationQueue.splice(operationIndex, 1);
1386
+ }
1387
+ queuedOperation.controller.abort(abortReason);
1388
+ queuedOperation.reject(abortReason);
1389
+ this.finalizeQueuedOperation(queuedOperation);
1390
+ this.drainOperationQueue();
1391
+ } else if (queuedOperation.state === 'active') {
1392
+ queuedOperation.controller.abort(abortReason);
1393
+ }
1394
+ };
1395
+ callerSignal.addEventListener('abort', onCallerAbort, { once: true });
1396
+ queuedOperation.removeCallerAbortListener = () => {
1397
+ callerSignal.removeEventListener('abort', onCallerAbort);
1398
+ };
1399
+ if (callerSignal.aborted) {
1400
+ onCallerAbort();
1401
+ }
1402
+ }
1403
+ if (queuedOperation.state !== 'queued') {
1404
+ return;
1405
+ }
1074
1406
  if (priority) {
1075
1407
  this.operationQueue.unshift(queuedOperation);
1076
1408
  } else {
@@ -1090,6 +1422,7 @@ export class LiveBrowserSession {
1090
1422
  }
1091
1423
  this.operationRunning = true;
1092
1424
  this.activeOperation = queuedOperation;
1425
+ queuedOperation.state = 'active';
1093
1426
  void this.executeQueuedOperation(queuedOperation).catch((error) => {
1094
1427
  this.operationRunning = false;
1095
1428
  this.activeOperation = undefined;
@@ -1107,15 +1440,15 @@ export class LiveBrowserSession {
1107
1440
  if (queuedOperation.controller.signal.aborted) {
1108
1441
  throw queuedOperation.controller.signal.reason;
1109
1442
  }
1110
- queuedOperation.resolve(await queuedOperation.run(queuedOperation.controller.signal));
1443
+ const value = await queuedOperation.run(queuedOperation.controller.signal);
1444
+ if (queuedOperation.controller.signal.aborted) {
1445
+ throw queuedOperation.controller.signal.reason;
1446
+ }
1447
+ queuedOperation.resolve(value);
1111
1448
  } catch (error) {
1112
1449
  queuedOperation.reject(error);
1113
1450
  } finally {
1114
- if (queuedOperation.kind === 'public') {
1115
- this.admittedPublicOperations -= 1;
1116
- } else if (queuedOperation.kind === 'internal') {
1117
- this.admittedInternalOperations -= 1;
1118
- }
1451
+ this.finalizeQueuedOperation(queuedOperation);
1119
1452
  if (this.activeOperation === queuedOperation) {
1120
1453
  this.activeOperation = undefined;
1121
1454
  }
@@ -1130,15 +1463,29 @@ export class LiveBrowserSession {
1130
1463
  }
1131
1464
  }
1132
1465
 
1466
+ private finalizeQueuedOperation(queuedOperation: IQueuedOperation): void {
1467
+ if (queuedOperation.state === 'settled') {
1468
+ return;
1469
+ }
1470
+ queuedOperation.state = 'settled';
1471
+ queuedOperation.removeCallerAbortListener?.();
1472
+ queuedOperation.removeCallerAbortListener = undefined;
1473
+ if (queuedOperation.capacityReleased) {
1474
+ return;
1475
+ }
1476
+ queuedOperation.capacityReleased = true;
1477
+ if (queuedOperation.kind === 'public') {
1478
+ this.admittedPublicOperations -= 1;
1479
+ } else if (queuedOperation.kind === 'internal') {
1480
+ this.admittedInternalOperations -= 1;
1481
+ }
1482
+ }
1483
+
1133
1484
  private cancelQueuedOperations(error: Error): void {
1134
1485
  for (const queuedOperation of this.operationQueue.splice(0)) {
1135
1486
  queuedOperation.controller.abort(error);
1136
- if (queuedOperation.kind === 'public') {
1137
- this.admittedPublicOperations -= 1;
1138
- } else if (queuedOperation.kind === 'internal') {
1139
- this.admittedInternalOperations -= 1;
1140
- }
1141
1487
  queuedOperation.reject(error);
1488
+ this.finalizeQueuedOperation(queuedOperation);
1142
1489
  }
1143
1490
  }
1144
1491
 
@@ -1297,6 +1644,15 @@ export class LiveBrowserSession {
1297
1644
  const shutdownErrors: unknown[] = [];
1298
1645
  for (const tab of [...this.tabs.values()]) {
1299
1646
  tab.closing = true;
1647
+ const securityCdpSession = tab.securityCdpSession;
1648
+ tab.securityCdpSession = undefined;
1649
+ if (securityCdpSession && !securityCdpSession.detached) {
1650
+ try {
1651
+ await securityCdpSession.detach();
1652
+ } catch {
1653
+ // Browser lifetime cancellation may close the target before explicit detach settles.
1654
+ }
1655
+ }
1300
1656
  try {
1301
1657
  await this.stopScreencast(tab);
1302
1658
  } catch (cleanupError) {
@@ -1338,6 +1694,22 @@ export class LiveBrowserSession {
1338
1694
  }
1339
1695
  }
1340
1696
 
1697
+ private async configureBrowserSecurity(
1698
+ browser: plugins.puppeteer.Browser,
1699
+ ): Promise<void> {
1700
+ if (!this.options.security?.denyPermissions) {
1701
+ return;
1702
+ }
1703
+ const cdpSession = await browser.target().createCDPSession();
1704
+ try {
1705
+ await cdpSession.send('Browser.grantPermissions', { permissions: [] });
1706
+ } finally {
1707
+ if (!cdpSession.detached) {
1708
+ await cdpSession.detach();
1709
+ }
1710
+ }
1711
+ }
1712
+
1341
1713
  private requireBrowserContext(): plugins.puppeteer.BrowserContext {
1342
1714
  if (this.status !== 'running' || !this.browserContext) {
1343
1715
  throw new Error('LiveBrowserSession is not running');
@@ -1433,6 +1805,17 @@ export class LiveBrowserSession {
1433
1805
  this.tabs.set(tab.id, tab);
1434
1806
  this.tabIdsByPage.set(page, tab.id);
1435
1807
  try {
1808
+ if (this.options.security?.denyFileChoosers) {
1809
+ const securityCdpSession = await page.createCDPSession();
1810
+ tab.securityCdpSession = securityCdpSession;
1811
+ await securityCdpSession.send('Page.enable', {
1812
+ enableFileChooserOpenedEvent: true,
1813
+ });
1814
+ await securityCdpSession.send('Page.setInterceptFileChooserDialog', {
1815
+ enabled: true,
1816
+ cancel: true,
1817
+ });
1818
+ }
1436
1819
  await this.ensureTabViewport(tab);
1437
1820
  await this.refreshTab(tab);
1438
1821
 
@@ -1488,6 +1871,7 @@ export class LiveBrowserSession {
1488
1871
  if (frame !== page.mainFrame() || tab.closing) {
1489
1872
  return;
1490
1873
  }
1874
+ tab.evaluationExecutionContextId = undefined;
1491
1875
  const navigationReset = !tab.navigationInProgress;
1492
1876
  if (navigationReset) {
1493
1877
  tab.streamInvalidated = true;
@@ -1563,6 +1947,13 @@ export class LiveBrowserSession {
1563
1947
  );
1564
1948
  return tab;
1565
1949
  } catch (error) {
1950
+ if (tab.securityCdpSession && !tab.securityCdpSession.detached) {
1951
+ try {
1952
+ await tab.securityCdpSession.detach();
1953
+ } catch {
1954
+ // The page may have closed while security setup was failing.
1955
+ }
1956
+ }
1566
1957
  this.removePageListeners(tab);
1567
1958
  this.tabs.delete(tab.id);
1568
1959
  this.tabIdsByPage.delete(page);
@@ -1813,6 +2204,7 @@ export class LiveBrowserSession {
1813
2204
  signal: AbortSignal,
1814
2205
  ): Promise<void> {
1815
2206
  const isActive = this.activeTabId === tab.id;
2207
+ tab.evaluationExecutionContextId = undefined;
1816
2208
  tab.navigationInProgress = true;
1817
2209
  if (isActive) {
1818
2210
  await this.stopScreencast(tab);
@@ -1832,7 +2224,6 @@ export class LiveBrowserSession {
1832
2224
  && this.activeTabId === tab.id
1833
2225
  && this.status === 'running'
1834
2226
  && tab.status === 'open'
1835
- && !signal.aborted
1836
2227
  ) {
1837
2228
  await this.startScreencast(tab);
1838
2229
  }
@@ -2242,14 +2633,300 @@ export class LiveBrowserSession {
2242
2633
 
2243
2634
  private validateUrl(url: unknown): string {
2244
2635
  const validatedUrl = validateBoundedString(url, 'url', 1, maxUrlLength);
2636
+ let parsedUrl: URL;
2245
2637
  try {
2246
- new URL(validatedUrl);
2638
+ parsedUrl = new URL(validatedUrl);
2247
2639
  } catch {
2248
2640
  throw new Error('url must be absolute');
2249
2641
  }
2642
+ if (
2643
+ this.options.security?.httpNavigationOnly
2644
+ && parsedUrl.protocol !== 'http:'
2645
+ && parsedUrl.protocol !== 'https:'
2646
+ ) {
2647
+ throw new Error('url protocol must be http or https');
2648
+ }
2250
2649
  return validatedUrl;
2251
2650
  }
2252
2651
 
2652
+ private normalizeEvaluationOptions(
2653
+ options: ILiveBrowserEvaluateOptions,
2654
+ ): INormalizedEvaluationOptions {
2655
+ return {
2656
+ timeoutMs: options.timeoutMs === undefined
2657
+ ? 5000
2658
+ : validateInteger(options.timeoutMs, 'timeoutMs', 1, 30000),
2659
+ maxOutputBytes: options.maxOutputBytes === undefined
2660
+ ? 262144
2661
+ : validateInteger(
2662
+ options.maxOutputBytes,
2663
+ 'maxOutputBytes',
2664
+ 1,
2665
+ maxEvaluationOutputBytes,
2666
+ ),
2667
+ maxDepth: options.maxDepth === undefined
2668
+ ? 16
2669
+ : validateInteger(options.maxDepth, 'maxDepth', 1, maxEvaluationDepth),
2670
+ maxNodes: options.maxNodes === undefined
2671
+ ? 10000
2672
+ : validateInteger(options.maxNodes, 'maxNodes', 1, maxEvaluationNodes),
2673
+ maxStringBytes: options.maxStringBytes === undefined
2674
+ ? 65536
2675
+ : validateInteger(
2676
+ options.maxStringBytes,
2677
+ 'maxStringBytes',
2678
+ 0,
2679
+ maxEvaluationStringBytes,
2680
+ ),
2681
+ maxArrayLength: options.maxArrayLength === undefined
2682
+ ? 1000
2683
+ : validateInteger(
2684
+ options.maxArrayLength,
2685
+ 'maxArrayLength',
2686
+ 0,
2687
+ maxEvaluationArrayLength,
2688
+ ),
2689
+ maxObjectKeys: options.maxObjectKeys === undefined
2690
+ ? 1000
2691
+ : validateInteger(
2692
+ options.maxObjectKeys,
2693
+ 'maxObjectKeys',
2694
+ 0,
2695
+ maxEvaluationObjectKeys,
2696
+ ),
2697
+ };
2698
+ }
2699
+
2700
+ private readCdpExceptionMessage(
2701
+ exceptionDetails: plugins.puppeteer.Protocol.Runtime.ExceptionDetails,
2702
+ ): string {
2703
+ const description = exceptionDetails.exception?.description;
2704
+ if (typeof description === 'string' && description.length > 0) {
2705
+ return truncate(description, 2048);
2706
+ }
2707
+ return truncate(exceptionDetails.text, 2048);
2708
+ }
2709
+
2710
+ private createEvaluationBootstrapExpression(): string {
2711
+ const bootstrapKeyLiteral = JSON.stringify(evaluationBootstrapKey);
2712
+ const cancelKeyLiteral = JSON.stringify(evaluationCancelKey);
2713
+ const cleanupKeyLiteral = JSON.stringify(evaluationCleanupKey);
2714
+ return `(() => {
2715
+ 'use strict';
2716
+ if (typeof globalThis[${bootstrapKeyLiteral}] === 'function') {
2717
+ return true;
2718
+ }
2719
+ const safeGlobalThis = globalThis;
2720
+ const SafeArray = Array;
2721
+ const SafeError = Error;
2722
+ const SafeFunction = Function;
2723
+ const SafeJSON = JSON;
2724
+ const SafeMap = Map;
2725
+ const SafeNumber = Number;
2726
+ const SafeObject = Object;
2727
+ const SafePromise = Promise;
2728
+ const SafeString = String;
2729
+ const SafeTextEncoder = TextEncoder;
2730
+ const SafeWeakSet = WeakSet;
2731
+ const SafeClearTimeout = clearTimeout;
2732
+ const SafeSetTimeout = setTimeout;
2733
+ const safeCreate = SafeObject.create;
2734
+ const safeDefineProperty = SafeObject.defineProperty;
2735
+ const safeGetOwnPropertyDescriptor = SafeObject.getOwnPropertyDescriptor;
2736
+ const safeGetOwnPropertySymbols = SafeObject.getOwnPropertySymbols;
2737
+ const safeGetPrototypeOf = SafeObject.getPrototypeOf;
2738
+ const safeIsArray = SafeArray.isArray;
2739
+ const safeIsFinite = SafeNumber.isFinite;
2740
+ const safeKeys = SafeObject.keys;
2741
+ const safeSetPrototypeOf = SafeObject.setPrototypeOf;
2742
+ const safeStringify = SafeJSON.stringify;
2743
+ const safeMapDelete = SafeFunction.prototype.call.bind(SafeMap.prototype.delete);
2744
+ const safeMapGet = SafeFunction.prototype.call.bind(SafeMap.prototype.get);
2745
+ const safeMapSet = SafeFunction.prototype.call.bind(SafeMap.prototype.set);
2746
+ const safePromiseThen = SafeFunction.prototype.call.bind(SafePromise.prototype.then);
2747
+ const safeWeakSetAdd = SafeFunction.prototype.call.bind(SafeWeakSet.prototype.add);
2748
+ const safeWeakSetHas = SafeFunction.prototype.call.bind(SafeWeakSet.prototype.has);
2749
+ const safeEncode = SafeFunction.prototype.call.bind(SafeTextEncoder.prototype.encode);
2750
+ const encoder = new SafeTextEncoder();
2751
+ const cancellationHandlers = new SafeMap();
2752
+
2753
+ const createEnvelope = (ok, value) => {
2754
+ const envelope = safeCreate(null);
2755
+ envelope.ok = ok;
2756
+ if (ok) {
2757
+ envelope.json = value;
2758
+ } else {
2759
+ envelope.error = value;
2760
+ }
2761
+ return envelope;
2762
+ };
2763
+ const byteLength = (value) => safeEncode(encoder, value).byteLength;
2764
+ const cancelEvaluation = (cancellationKey) => {
2765
+ const handler = safeMapGet(cancellationHandlers, cancellationKey);
2766
+ if (typeof handler === 'function') {
2767
+ handler();
2768
+ }
2769
+ return safeMapDelete(cancellationHandlers, cancellationKey);
2770
+ };
2771
+ const evaluate = async (source, options, cancellationKey) => {
2772
+ const seen = new SafeWeakSet();
2773
+ let visitedNodes = 0;
2774
+ const normalize = (value, depth) => {
2775
+ visitedNodes += 1;
2776
+ if (visitedNodes > options.maxNodes) {
2777
+ throw new SafeError('Evaluation result exceeded maxNodes');
2778
+ }
2779
+ if (depth > options.maxDepth) {
2780
+ throw new SafeError('Evaluation result exceeded maxDepth');
2781
+ }
2782
+ if (value === null || typeof value === 'boolean') {
2783
+ return value;
2784
+ }
2785
+ if (typeof value === 'number') {
2786
+ if (!safeIsFinite(value)) {
2787
+ throw new SafeError('Evaluation result contains a non-finite number');
2788
+ }
2789
+ return value;
2790
+ }
2791
+ if (typeof value === 'string') {
2792
+ if (byteLength(value) > options.maxStringBytes) {
2793
+ throw new SafeError('Evaluation result string exceeded maxStringBytes');
2794
+ }
2795
+ return value;
2796
+ }
2797
+ if (typeof value !== 'object') {
2798
+ throw new SafeError('Evaluation result contains a non-JSON value');
2799
+ }
2800
+ if (safeWeakSetHas(seen, value)) {
2801
+ throw new SafeError('Evaluation result contains a cycle or repeated object');
2802
+ }
2803
+ safeWeakSetAdd(seen, value);
2804
+
2805
+ if (safeIsArray(value)) {
2806
+ if (value.length > options.maxArrayLength) {
2807
+ throw new SafeError('Evaluation result array exceeded maxArrayLength');
2808
+ }
2809
+ const keys = safeKeys(value);
2810
+ if (keys.length !== value.length) {
2811
+ throw new SafeError('Evaluation result contains a sparse or extended array');
2812
+ }
2813
+ const output = SafeArray(value.length);
2814
+ safeSetPrototypeOf(output, null);
2815
+ for (let index = 0; index < value.length; index += 1) {
2816
+ const descriptor = safeGetOwnPropertyDescriptor(value, SafeString(index));
2817
+ if (!descriptor || !('value' in descriptor)) {
2818
+ throw new SafeError('Evaluation result contains an array accessor');
2819
+ }
2820
+ output[index] = normalize(descriptor.value, depth + 1);
2821
+ }
2822
+ return output;
2823
+ }
2824
+
2825
+ const prototype = safeGetPrototypeOf(value);
2826
+ if (prototype !== SafeObject.prototype && prototype !== null) {
2827
+ throw new SafeError('Evaluation result contains a non-plain object');
2828
+ }
2829
+ if (safeGetOwnPropertySymbols(value).length > 0) {
2830
+ throw new SafeError('Evaluation result contains symbol properties');
2831
+ }
2832
+ const keys = safeKeys(value);
2833
+ if (keys.length > options.maxObjectKeys) {
2834
+ throw new SafeError('Evaluation result object exceeded maxObjectKeys');
2835
+ }
2836
+ const output = safeCreate(null);
2837
+ for (let index = 0; index < keys.length; index += 1) {
2838
+ const key = keys[index];
2839
+ if (byteLength(key) > options.maxStringBytes) {
2840
+ throw new SafeError('Evaluation result key exceeded maxStringBytes');
2841
+ }
2842
+ const descriptor = safeGetOwnPropertyDescriptor(value, key);
2843
+ if (!descriptor || !('value' in descriptor)) {
2844
+ throw new SafeError('Evaluation result contains an object accessor');
2845
+ }
2846
+ output[key] = normalize(descriptor.value, depth + 1);
2847
+ }
2848
+ return output;
2849
+ };
2850
+
2851
+ try {
2852
+ const execute = SafeFunction(
2853
+ '\"use strict\"; return (async () => (' + source + '\\n))();',
2854
+ );
2855
+ const boundedExecution = new SafePromise((resolve, reject) => {
2856
+ let timeout;
2857
+ safeMapSet(cancellationHandlers, cancellationKey, () => {
2858
+ if (timeout !== undefined) {
2859
+ SafeClearTimeout(timeout);
2860
+ }
2861
+ reject(new SafeError('Evaluation cancelled'));
2862
+ });
2863
+ timeout = SafeSetTimeout(() => {
2864
+ reject(new SafeError('Evaluation timed out after ' + options.timeoutMs + 'ms'));
2865
+ }, options.timeoutMs);
2866
+ safePromiseThen(
2867
+ execute(),
2868
+ (value) => {
2869
+ SafeClearTimeout(timeout);
2870
+ resolve(value);
2871
+ },
2872
+ (error) => {
2873
+ SafeClearTimeout(timeout);
2874
+ reject(error);
2875
+ },
2876
+ );
2877
+ });
2878
+ let normalizedResult;
2879
+ try {
2880
+ normalizedResult = normalize(await boundedExecution, 0);
2881
+ } finally {
2882
+ safeMapDelete(cancellationHandlers, cancellationKey);
2883
+ }
2884
+ const json = safeStringify(normalizedResult);
2885
+ if (typeof json !== 'string' || byteLength(json) > options.maxOutputBytes) {
2886
+ throw new SafeError('Evaluation result exceeded maxOutputBytes');
2887
+ }
2888
+ return createEnvelope(true, json);
2889
+ } catch (error) {
2890
+ let message = 'Evaluation failed';
2891
+ if (typeof error === 'string') {
2892
+ message = error;
2893
+ } else if (error && typeof error === 'object') {
2894
+ const descriptor = safeGetOwnPropertyDescriptor(error, 'message');
2895
+ if (descriptor && 'value' in descriptor && typeof descriptor.value === 'string') {
2896
+ message = descriptor.value;
2897
+ }
2898
+ }
2899
+ if (byteLength(message) > 2048) {
2900
+ message = 'Evaluation failed with an oversized error';
2901
+ }
2902
+ return createEnvelope(false, message);
2903
+ }
2904
+ };
2905
+
2906
+ for (const [key, value] of [
2907
+ [${bootstrapKeyLiteral}, evaluate],
2908
+ [${cancelKeyLiteral}, cancelEvaluation],
2909
+ [${cleanupKeyLiteral}, cancelEvaluation],
2910
+ ]) {
2911
+ safeDefineProperty(safeGlobalThis, key, {
2912
+ value,
2913
+ configurable: false,
2914
+ enumerable: false,
2915
+ writable: false,
2916
+ });
2917
+ }
2918
+ return true;
2919
+ })()`;
2920
+ }
2921
+
2922
+ private createEvaluationExpression(
2923
+ expression: string,
2924
+ options: INormalizedEvaluationOptions,
2925
+ cancellationKey: string,
2926
+ ): string {
2927
+ return `globalThis[${JSON.stringify(evaluationBootstrapKey)}](${JSON.stringify(expression)}, ${JSON.stringify(options)}, ${JSON.stringify(cancellationKey)})`;
2928
+ }
2929
+
2253
2930
  private validateTimeout(timeoutMs?: number, defaultValue = 5000): number {
2254
2931
  if (timeoutMs === undefined) {
2255
2932
  return defaultValue;