scrapely 1.2.3 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +119 -1
  2. package/dist/actor.d.ts +703 -0
  3. package/dist/actor.js +1441 -0
  4. package/dist/browser-configuration.d.ts +58 -0
  5. package/dist/browser-configuration.js +128 -0
  6. package/dist/browser-locations.d.ts +5 -0
  7. package/dist/browser-locations.js +5469 -0
  8. package/dist/charging.d.ts +153 -0
  9. package/dist/charging.js +782 -0
  10. package/dist/charging_client.d.ts +18 -0
  11. package/dist/charging_client.js +49 -0
  12. package/dist/configuration.d.ts +121 -0
  13. package/dist/configuration.js +128 -0
  14. package/dist/crypto/secrets.d.ts +44 -0
  15. package/dist/crypto/secrets.js +164 -0
  16. package/dist/events/events-client.d.ts +59 -0
  17. package/dist/events/events-client.js +120 -0
  18. package/dist/events/scrapely-event-manager.d.ts +13 -0
  19. package/dist/events/scrapely-event-manager.js +61 -0
  20. package/dist/index.d.ts +29 -0
  21. package/dist/index.js +95 -0
  22. package/dist/log-env.d.ts +1 -0
  23. package/dist/log-env.js +12 -0
  24. package/dist/manifest.d.ts +30 -0
  25. package/dist/manifest.js +47 -0
  26. package/dist/patch-browser-pool.d.ts +66 -0
  27. package/dist/patch-browser-pool.js +827 -0
  28. package/dist/proxy-configuration.d.ts +118 -0
  29. package/dist/proxy-configuration.js +88 -0
  30. package/dist/request-list.d.ts +57 -0
  31. package/dist/request-list.js +79 -0
  32. package/dist/runner/docker-manager.d.ts +88 -0
  33. package/dist/runner/docker-manager.js +283 -0
  34. package/dist/runner/index.d.ts +2 -0
  35. package/dist/runner/index.js +7 -0
  36. package/dist/runner/timeout-manager.d.ts +43 -0
  37. package/dist/runner/timeout-manager.js +90 -0
  38. package/dist/scrapely-proxy-configuration.d.ts +99 -0
  39. package/dist/scrapely-proxy-configuration.js +559 -0
  40. package/dist/storage/api-storage-client.d.ts +199 -0
  41. package/dist/storage/api-storage-client.js +910 -0
  42. package/dist/storage/billing-tracker.d.ts +163 -0
  43. package/dist/storage/billing-tracker.js +260 -0
  44. package/dist/storage/dataset-client.d.ts +43 -0
  45. package/dist/storage/dataset-client.js +232 -0
  46. package/dist/storage/dataset-collection-client.d.ts +8 -0
  47. package/dist/storage/dataset-collection-client.js +75 -0
  48. package/dist/storage/dataset-push-script.d.ts +18 -0
  49. package/dist/storage/dataset-push-script.js +38 -0
  50. package/dist/storage/dragonfly-client.d.ts +94 -0
  51. package/dist/storage/dragonfly-client.js +167 -0
  52. package/dist/storage/index.d.ts +11 -0
  53. package/dist/storage/index.js +25 -0
  54. package/dist/storage/key-value-store-client.d.ts +61 -0
  55. package/dist/storage/key-value-store-client.js +306 -0
  56. package/dist/storage/kv-store-collection-client.d.ts +8 -0
  57. package/dist/storage/kv-store-collection-client.js +76 -0
  58. package/dist/storage/kvs-iteration.d.ts +17 -0
  59. package/dist/storage/kvs-iteration.js +126 -0
  60. package/dist/storage/log-manager.d.ts +114 -0
  61. package/dist/storage/log-manager.js +244 -0
  62. package/dist/storage/patch-storage.d.ts +11 -0
  63. package/dist/storage/patch-storage.js +177 -0
  64. package/dist/storage/request-queue-client.d.ts +93 -0
  65. package/dist/storage/request-queue-client.js +522 -0
  66. package/dist/storage/rq-collection-client.d.ts +8 -0
  67. package/dist/storage/rq-collection-client.js +89 -0
  68. package/dist/storage/rq-list-requests.d.ts +59 -0
  69. package/dist/storage/rq-list-requests.js +114 -0
  70. package/dist/storage/rq-lock-script.d.ts +56 -0
  71. package/dist/storage/rq-lock-script.js +127 -0
  72. package/dist/storage/run-metadata.d.ts +98 -0
  73. package/dist/storage/run-metadata.js +256 -0
  74. package/dist/storage/s3-client.d.ts +103 -0
  75. package/dist/storage/s3-client.js +302 -0
  76. package/dist/storage/ttl-manager.d.ts +70 -0
  77. package/dist/storage/ttl-manager.js +115 -0
  78. package/dist/storage/utils.d.ts +3 -0
  79. package/dist/storage/utils.js +19 -0
  80. package/dist/storage-identifiers.d.ts +20 -0
  81. package/dist/storage-identifiers.js +6 -0
  82. package/dist/utils.d.ts +54 -0
  83. package/dist/utils.js +63 -0
  84. package/dist/validator.d.ts +24 -0
  85. package/dist/validator.js +158 -0
  86. package/package.json +90 -9
@@ -0,0 +1,827 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setCdpUrl = setCdpUrl;
4
+ exports.getCdpUrlForTests = getCdpUrlForTests;
5
+ exports.resetBrowserProxyWarningsForTests = resetBrowserProxyWarningsForTests;
6
+ exports.applyProxyInfoToCdpUrl = applyProxyInfoToCdpUrl;
7
+ exports.patchBrowserPoolModule = patchBrowserPoolModule;
8
+ exports.patchCrawleeCrawlerModule = patchCrawleeCrawlerModule;
9
+ exports.patchLoadedCrawleeCopies = patchLoadedCrawleeCopies;
10
+ exports.patchRawBrowserType = patchRawBrowserType;
11
+ /**
12
+ * Redirects every browser launch (Crawlee browser pool, raw playwright /
13
+ * puppeteer) to the Scrapely remote browser over CDP. The remote browser
14
+ * never takes a proxy: `proxy_url` is stripped from the CDP URL, custom
15
+ * (BYOP) proxies are warned about and ignored, and location comes from
16
+ * `browserConfiguration`, a platform ProxyConfiguration, or the run default
17
+ * (see applyProxyInfoToCdpUrl).
18
+ */
19
+ const browser_configuration_js_1 = require("./browser-configuration.js");
20
+ const browser_locations_js_1 = require("./browser-locations.js");
21
+ let _cdpUrl;
22
+ /**
23
+ * Lazily get CDP URL from environment or cached value.
24
+ * This allows non-browser actors to work without CDP being set.
25
+ */
26
+ function getCdpUrl() {
27
+ return _cdpUrl || process.env.CDP_URL;
28
+ }
29
+ /**
30
+ * Set CDP URL explicitly (for testing or advanced use cases).
31
+ * Pass `undefined` to clear the override and fall back to `CDP_URL` env.
32
+ * Normally the CDP URL is read from CDP_URL env var lazily.
33
+ */
34
+ function setCdpUrl(url) {
35
+ _cdpUrl = url;
36
+ }
37
+ /** @internal Exported for tests. */
38
+ function getCdpUrlForTests() {
39
+ return getCdpUrl();
40
+ }
41
+ /**
42
+ * Whether the process runs on the Scrapely platform (worker-injected env).
43
+ */
44
+ function isAtHome() {
45
+ return !!(process.env.SCRAPELY_IS_AT_HOME || process.env.APIFY_IS_AT_HOME);
46
+ }
47
+ /**
48
+ * Unpatched builds (browser patch off at build time) keep their browser
49
+ * binaries; the worker sets SCRAPELY_BROWSER_PATCH=0 so every launch goes to
50
+ * the original local launcher instead of the remote CDP browser. Read lazily
51
+ * at launch time.
52
+ */
53
+ function browserPatchDisabled() {
54
+ return process.env.SCRAPELY_BROWSER_PATCH === '0';
55
+ }
56
+ /** gVisor lacks crashpad support; ReadonlyRootfs + tmpfs /tmp loses crash dirs. */
57
+ const GVISOR_CHROME_ARGS = [
58
+ '--disable-breakpad',
59
+ '--disable-crash-reporter',
60
+ '--disable-dev-shm-usage',
61
+ '--crash-dumps-dir=/tmp',
62
+ ];
63
+ /** Add gVisor-safe Chrome args to local launch options when running under runsc. */
64
+ function withGVisorArgs(options) {
65
+ const opts = { ...(options ?? {}) };
66
+ if (process.env.SCRAPELY_CONTAINER_RUNTIME !== 'runsc')
67
+ return opts;
68
+ const args = [...(opts.args ?? [])];
69
+ for (const arg of GVISOR_CHROME_ARGS) {
70
+ if (!args.includes(arg))
71
+ args.push(arg);
72
+ }
73
+ opts.args = args;
74
+ return opts;
75
+ }
76
+ /**
77
+ * Require CDP_URL for platform browser runs. Local launch is disabled on the
78
+ * platform unless the build is unpatched (SCRAPELY_BROWSER_PATCH=0) — otherwise
79
+ * a worker container without CDP_URL is a misconfiguration and must fail
80
+ * loudly instead of launching Chromium inside the (gVisor) container.
81
+ */
82
+ function requireCdpUrl() {
83
+ const cdpUrl = getCdpUrl();
84
+ if (!cdpUrl) {
85
+ throw new Error('cdpUrl is required. Local browser launch is disabled — all runs must use the remote browser.');
86
+ }
87
+ return cdpUrl;
88
+ }
89
+ const BYOP_IGNORED_WARNING = '[scrapely] Custom proxies are ignored when using the Scrapely remote browser — it routes traffic through its own ' +
90
+ 'integrated proxy. Set the location with browserConfiguration (Actor.createBrowserConfiguration({ countryCode, state, city })) ' +
91
+ 'or ProxyConfiguration countryCode.';
92
+ const GROUPS_SESSION_INFO = '[scrapely] Proxy groups and session IDs do not apply to the Scrapely remote browser; routing is location-based ' +
93
+ '(proxy_country/proxy_state). Each browser connection is a fresh session.';
94
+ let byopWarned = false;
95
+ let groupsInfoLogged = false;
96
+ let unknownSubdivisionWarned = false;
97
+ /** @internal Reset the once-per-process browser proxy messages (tests only). */
98
+ function resetBrowserProxyWarningsForTests() {
99
+ byopWarned = false;
100
+ groupsInfoLogged = false;
101
+ unknownSubdivisionWarned = false;
102
+ }
103
+ /** Parse a proxy URL, accepting puppeteer-style scheme-less `host:port`. */
104
+ function parseProxyUrl(proxyUrl) {
105
+ for (const candidate of [proxyUrl, `http://${proxyUrl}`]) {
106
+ try {
107
+ const parsed = new URL(candidate);
108
+ if (parsed.hostname)
109
+ return parsed;
110
+ }
111
+ catch {
112
+ // try the next form
113
+ }
114
+ }
115
+ return undefined;
116
+ }
117
+ /**
118
+ * Platform gateway hostname injected by the worker (same vars as
119
+ * getPlatformProxyEnv). Read directly: this module also loads in SDK-less
120
+ * processes (base image register hook) and must not pull in @crawlee/core.
121
+ */
122
+ function platformProxyHostname() {
123
+ const hostname = (process.env.SCRAPELY_PROXY_HOSTNAME || process.env.APIFY_PROXY_HOSTNAME || '').trim().toLowerCase();
124
+ return hostname || undefined;
125
+ }
126
+ function isPlatformProxyUrl(proxyUrl) {
127
+ const platformHost = platformProxyHostname();
128
+ if (!platformHost)
129
+ return false;
130
+ return parseProxyUrl(proxyUrl)?.hostname.toLowerCase() === platformHost;
131
+ }
132
+ /** `scheme://***@host:port` — never log proxy credentials. */
133
+ function redactProxyUrl(proxyUrl) {
134
+ const parsed = parseProxyUrl(proxyUrl);
135
+ if (!parsed)
136
+ return '<unparseable proxy URL>';
137
+ const userinfo = parsed.username || parsed.password ? '***@' : '';
138
+ return `${parsed.protocol}//${userinfo}${parsed.host}`;
139
+ }
140
+ /**
141
+ * Location from the gateway username grammar (`sub,groups-X,session-Y,country-US_CA`).
142
+ */
143
+ function parseProxyUrlLocation(proxyUrl) {
144
+ if (!proxyUrl)
145
+ return {};
146
+ const parsed = parseProxyUrl(proxyUrl);
147
+ if (!parsed?.username)
148
+ return {};
149
+ let username;
150
+ try {
151
+ username = decodeURIComponent(parsed.username);
152
+ }
153
+ catch {
154
+ username = parsed.username;
155
+ }
156
+ for (const fragment of username.split(',')) {
157
+ const match = /^country-([A-Za-z]{2})(?:_([A-Za-z0-9]{1,3}))?$/.exec(fragment.trim());
158
+ if (match)
159
+ return { country: match[1].toUpperCase(), subdivision: match[2]?.toUpperCase() };
160
+ }
161
+ return {};
162
+ }
163
+ /** Location requested by a platform-gateway proxyInfo; `{}` for none or BYOP. */
164
+ function resolveLaunchLocation(proxyInfo) {
165
+ const proxyUrl = proxyInfo?.url;
166
+ if (typeof proxyUrl !== 'string' || !proxyUrl)
167
+ return {};
168
+ if (!isPlatformProxyUrl(proxyUrl)) {
169
+ if (!byopWarned) {
170
+ byopWarned = true;
171
+ console.warn(`${BYOP_IGNORED_WARNING} (ignored: ${redactProxyUrl(proxyUrl)})`);
172
+ }
173
+ return {};
174
+ }
175
+ if ((proxyInfo?.groups?.length || proxyInfo?.sessionId != null) && !groupsInfoLogged) {
176
+ groupsInfoLogged = true;
177
+ console.info(GROUPS_SESSION_INFO);
178
+ }
179
+ const grammar = parseProxyUrlLocation(proxyUrl);
180
+ return {
181
+ country: proxyInfo?.countryCode ?? grammar.country,
182
+ subdivision: proxyInfo?.subdivisionCode ?? grammar.subdivision,
183
+ };
184
+ }
185
+ /**
186
+ * Build the remote browser CDP URL for one browser connection.
187
+ *
188
+ * The Scrapely remote browser never takes a proxy: it routes through its own
189
+ * integrated proxy, selected by location params. Semantics:
190
+ * - The worker-injected `proxy_country` is the run default; `proxy_url` is
191
+ * never sent and is removed if present.
192
+ * - A platform-gateway `proxyInfo` (crawler ProxyConfiguration) overrides
193
+ * `proxy_country` / `proxy_state` (ISO subdivision codes mapped to region
194
+ * names). A custom (BYOP) proxy is warned about once and ignored.
195
+ * - `browserConfiguration` params always win.
196
+ * - When nothing changes, the CDP URL is returned byte-identical.
197
+ *
198
+ * @internal Exported for tests.
199
+ */
200
+ function applyProxyInfoToCdpUrl(cdpUrl, proxyInfo, browserConfiguration) {
201
+ if (browserConfiguration != null && !(0, browser_configuration_js_1.isBrowserConfiguration)(browserConfiguration)) {
202
+ throw new Error('browserConfiguration must be created with Actor.createBrowserConfiguration() or new BrowserConfiguration().');
203
+ }
204
+ const url = new URL(cdpUrl);
205
+ const params = url.searchParams;
206
+ let changed = false;
207
+ const setParam = (key, value) => {
208
+ if (params.get(key) === value)
209
+ return;
210
+ params.set(key, value);
211
+ changed = true;
212
+ };
213
+ if (params.has('proxy_url')) {
214
+ params.delete('proxy_url');
215
+ changed = true;
216
+ }
217
+ const location = resolveLaunchLocation(proxyInfo);
218
+ const country = location.country?.toUpperCase();
219
+ let state;
220
+ if (location.subdivision) {
221
+ state = (0, browser_locations_js_1.subdivisionToStateName)(country, location.subdivision);
222
+ if (!state && !unknownSubdivisionWarned) {
223
+ unknownSubdivisionWarned = true;
224
+ console.warn(`[scrapely] Subdivision "${country ?? '?'}-${location.subdivision}" has no Scrapely browser mapping; ` +
225
+ "routing by country only. Use browserConfiguration { state: '<name>' } to target a region.");
226
+ }
227
+ }
228
+ if (country)
229
+ setParam('proxy_country', country);
230
+ if (state)
231
+ setParam('proxy_state', state);
232
+ if (browserConfiguration != null) {
233
+ for (const [key, value] of Object.entries(browserConfiguration.toCdpParams())) {
234
+ setParam(key, value);
235
+ }
236
+ }
237
+ return changed ? url.toString() : cdpUrl;
238
+ }
239
+ /**
240
+ * @internal Normalize a browser-pool LaunchContext's proxy data into ProxyInfoLike.
241
+ *
242
+ * Crawlee's browser crawler sets both `launchContext.proxyInfo` (the ProxyInfo
243
+ * object, `.url` included) and `launchContext.proxyUrl` (the full proxy URL)
244
+ * per browser launch. The URL fallback covers callers that only provide the URL.
245
+ */
246
+ function proxyInfoFromLaunchContext(launchContext) {
247
+ const info = launchContext?.proxyInfo;
248
+ if (typeof info?.url === 'string' && info.url) {
249
+ return {
250
+ url: info.url,
251
+ countryCode: info.countryCode,
252
+ subdivisionCode: info.subdivisionCode,
253
+ groups: info.groups,
254
+ sessionId: info.sessionId,
255
+ };
256
+ }
257
+ const proxyUrl = launchContext?.proxyUrl;
258
+ if (typeof proxyUrl === 'string' && proxyUrl)
259
+ return { url: proxyUrl };
260
+ return undefined;
261
+ }
262
+ /**
263
+ * Resolve Playwright's chromium BrowserType for CDP connect.
264
+ *
265
+ * Crawlee's PlaywrightPlugin.library is a BrowserType (chromium|firefox|webkit).
266
+ * Remote browser is always Chromium CDP, so firefox/webkit/camoufox launchers
267
+ * are redirected to chromium.connectOverCDP — never denied, never local launch.
268
+ */
269
+ function getPlaywrightChromium(library) {
270
+ // Full playwright module passed as library (has .chromium)
271
+ if (library?.chromium && typeof library.chromium.connectOverCDP === 'function') {
272
+ return library.chromium;
273
+ }
274
+ // BrowserType that is already chromium
275
+ if (typeof library?.connectOverCDP === 'function' &&
276
+ typeof library?.name === 'function' &&
277
+ library.name() === 'chromium') {
278
+ return library;
279
+ }
280
+ // firefox / webkit / unknown BrowserType → force chromium from playwright
281
+ // Prefer playwright, fall back to playwright-core (images may ship either).
282
+ try {
283
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
284
+ const pw = require('playwright');
285
+ if (pw?.chromium?.connectOverCDP) {
286
+ return pw.chromium;
287
+ }
288
+ }
289
+ catch {
290
+ // ignore
291
+ }
292
+ try {
293
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
294
+ const pwCore = require('playwright-core');
295
+ if (pwCore?.chromium?.connectOverCDP) {
296
+ return pwCore.chromium;
297
+ }
298
+ }
299
+ catch {
300
+ // ignore
301
+ }
302
+ // Last resort: if library has connectOverCDP, use it (may be chromium-shaped)
303
+ if (typeof library?.connectOverCDP === 'function') {
304
+ return library;
305
+ }
306
+ throw new Error('Unable to resolve Playwright chromium for remote CDP connect. Install playwright or playwright-core.');
307
+ }
308
+ /**
309
+ * Redirect a PlaywrightPlugin class's `_launch` to remote Chromium CDP
310
+ * (chromium / firefox / webkit / camoufox) on the platform; a normal local
311
+ * launch when running locally without CDP_URL. Idempotent per class.
312
+ */
313
+ function patchPlaywrightPluginClass(PlaywrightPlugin) {
314
+ const proto = PlaywrightPlugin?.prototype;
315
+ if (!proto || typeof proto._launch !== 'function' || proto._scrapelyCdpPatched)
316
+ return;
317
+ const origPlaywrightLaunch = proto._launch;
318
+ proto._launch = async function (launchContext) {
319
+ if (browserPatchDisabled() || (!getCdpUrl() && !isAtHome())) {
320
+ return origPlaywrightLaunch.call(this, launchContext);
321
+ }
322
+ const cdpUrl = applyProxyInfoToCdpUrl(requireCdpUrl(), proxyInfoFromLaunchContext(launchContext), launchContext?.browserConfiguration);
323
+ const chromium = getPlaywrightChromium(this.library);
324
+ return chromium.connectOverCDP(cdpUrl);
325
+ };
326
+ proto._scrapelyCdpPatched = true;
327
+ }
328
+ /**
329
+ * Redirect a PuppeteerPlugin class's `_launch` to remote Chromium CDP via
330
+ * browserWSEndpoint on the platform; a normal local launch when running
331
+ * locally without CDP_URL. Idempotent per class.
332
+ */
333
+ function patchPuppeteerPluginClass(PuppeteerPlugin) {
334
+ const proto = PuppeteerPlugin?.prototype;
335
+ if (!proto || typeof proto._launch !== 'function' || proto._scrapelyCdpPatched)
336
+ return;
337
+ const origPuppeteerLaunch = proto._launch;
338
+ proto._launch = async function (launchContext) {
339
+ if (browserPatchDisabled() || (!getCdpUrl() && !isAtHome())) {
340
+ return origPuppeteerLaunch.call(this, launchContext);
341
+ }
342
+ const cdpUrl = applyProxyInfoToCdpUrl(requireCdpUrl(), proxyInfoFromLaunchContext(launchContext), launchContext?.browserConfiguration);
343
+ const launcher = this.library;
344
+ if (!launcher || typeof launcher.connect !== 'function') {
345
+ throw new Error('Puppeteer library is missing connect(). Install puppeteer or puppeteer-core.');
346
+ }
347
+ return launcher.connect({ browserWSEndpoint: cdpUrl });
348
+ };
349
+ proto._scrapelyCdpPatched = true;
350
+ }
351
+ /**
352
+ * Patch the browser plugins exported by one loaded copy of
353
+ * `@crawlee/browser-pool` (or one of its plugin files). Used for the SDK's own
354
+ * copy (eager pass below) and for every actor-local copy (Module._load hook),
355
+ * since an actor that installs its own Crawlee resolves a different instance.
356
+ *
357
+ * @internal Exported for tests.
358
+ */
359
+ function patchBrowserPoolModule(mod) {
360
+ if (!mod || (typeof mod !== 'object' && typeof mod !== 'function'))
361
+ return;
362
+ patchPlaywrightPluginClass(mod.PlaywrightPlugin);
363
+ patchPuppeteerPluginClass(mod.PuppeteerPlugin);
364
+ patchControllerProxyOptions(mod.PlaywrightController);
365
+ patchControllerProxyOptions(mod.PuppeteerController);
366
+ }
367
+ /**
368
+ * With incognito pages Crawlee turns the launch proxy into per-context proxy
369
+ * options. On the remote browser that would route pages through a custom
370
+ * proxy (or double-proxy the gateway), so no per-page proxy is set while
371
+ * connected over CDP.
372
+ */
373
+ function patchControllerProxyOptions(Controller) {
374
+ const proto = Controller?.prototype;
375
+ if (!proto || typeof proto.normalizeProxyOptions !== 'function' || proto._scrapelyProxyOptionsPatched)
376
+ return;
377
+ const origNormalize = proto.normalizeProxyOptions;
378
+ proto.normalizeProxyOptions = function (...args) {
379
+ if (!browserPatchDisabled() && getCdpUrl())
380
+ return {};
381
+ return origNormalize.apply(this, args);
382
+ };
383
+ proto._scrapelyProxyOptionsPatched = true;
384
+ }
385
+ // ─── Crawler `browserConfiguration` option ──────────────────────────────────
386
+ //
387
+ // Crawlee validates crawler options with ow.exactShape, and every crawler
388
+ // class copies its parent's optionsShape at module load, so the option is
389
+ // added to each class's own shape: BasicCrawler (browser crawlers forward
390
+ // unknown keys to it) and every browser crawler. HTTP crawlers never accept
391
+ // it — the key is removed from their (possibly copied) shapes so passing it
392
+ // there fails with Crawlee's normal unknown-option error.
393
+ //
394
+ // The predicate reuses the class's own `sessionPoolOptions` predicate
395
+ // (ow.optional.object): an ow predicate from a different ow copy is not
396
+ // recognized as a predicate by exactShape.
397
+ //
398
+ // Acceptance is never gated on CDP_URL / SCRAPELY_BROWSER_PATCH — only the
399
+ // `_launch` redirect is — so the same actor code runs locally unchanged.
400
+ function isCrawlerClass(value) {
401
+ return typeof value === 'function' && !!value.optionsShape && typeof value.optionsShape === 'object';
402
+ }
403
+ function patchCrawlerClass(CrawlerClass) {
404
+ const shape = CrawlerClass.optionsShape;
405
+ if (!Object.prototype.hasOwnProperty.call(CrawlerClass, 'optionsShape'))
406
+ return;
407
+ const isBrowserCrawler = 'launchContext' in shape;
408
+ const isBasicCrawler = CrawlerClass.name === 'BasicCrawler';
409
+ if (isBrowserCrawler || isBasicCrawler) {
410
+ if (!('browserConfiguration' in shape) && shape.sessionPoolOptions) {
411
+ shape.browserConfiguration = shape.sessionPoolOptions;
412
+ }
413
+ }
414
+ else if ('browserConfiguration' in shape) {
415
+ delete shape.browserConfiguration;
416
+ }
417
+ const proto = CrawlerClass.prototype;
418
+ if (proto &&
419
+ Object.prototype.hasOwnProperty.call(proto, '_extendLaunchContext') &&
420
+ typeof proto._extendLaunchContext === 'function' &&
421
+ !proto._scrapelyBrowserConfigPatched) {
422
+ const origExtend = proto._extendLaunchContext;
423
+ proto._extendLaunchContext = async function (pageId, launchContext) {
424
+ await origExtend.call(this, pageId, launchContext);
425
+ const browserConfiguration = this.options?.browserConfiguration;
426
+ if (browserConfiguration != null && launchContext && launchContext.browserConfiguration === undefined) {
427
+ if (typeof launchContext.extend === 'function') {
428
+ launchContext.extend({ browserConfiguration });
429
+ }
430
+ else {
431
+ launchContext.browserConfiguration = browserConfiguration;
432
+ }
433
+ }
434
+ };
435
+ proto._scrapelyBrowserConfigPatched = true;
436
+ }
437
+ }
438
+ /**
439
+ * Add the `browserConfiguration` option to the crawler classes exported by
440
+ * one loaded Crawlee module (package index or crawler file). Idempotent.
441
+ *
442
+ * @internal Exported for tests.
443
+ */
444
+ function patchCrawleeCrawlerModule(mod) {
445
+ if (!mod || (typeof mod !== 'object' && typeof mod !== 'function'))
446
+ return;
447
+ const candidates = isCrawlerClass(mod) ? [mod] : [];
448
+ for (const key of Object.keys(mod)) {
449
+ let value;
450
+ try {
451
+ value = mod[key];
452
+ }
453
+ catch {
454
+ continue;
455
+ }
456
+ if (isCrawlerClass(value))
457
+ candidates.push(value);
458
+ }
459
+ for (const CrawlerClass of candidates)
460
+ patchCrawlerClass(CrawlerClass);
461
+ }
462
+ const CRAWLER_PACKAGES = 'basic|browser|playwright|puppeteer|http|cheerio|jsdom|linkedom';
463
+ /** Requests that load (a part of) some copy of a Crawlee crawler package. */
464
+ const CRAWLER_REQUEST_RE = new RegExp(`^(@crawlee/(${CRAWLER_PACKAGES})|crawlee)$` +
465
+ `|[/\\\\]@crawlee[/\\\\](${CRAWLER_PACKAGES})[/\\\\]index\\.js$` +
466
+ `|[/\\\\]node_modules[/\\\\]crawlee[/\\\\]index\\.js$` +
467
+ '|(^|[/\\\\])internals[/\\\\][a-z-]+-crawler(\\.js)?$');
468
+ /** Cached module filenames of Crawlee crawler packages (any copy). */
469
+ const CRAWLER_FILENAME_RE = new RegExp(`[/\\\\](@crawlee[/\\\\](${CRAWLER_PACKAGES})|crawlee)[/\\\\](internals[/\\\\][a-z-]+-crawler\\.js|index\\.js)$`);
470
+ /**
471
+ * Patch Crawlee copies that loaded before this module (e.g. an actor that
472
+ * imports `crawlee` before `scrapely` in a local run). Copies loaded later are
473
+ * patched by the Module._load hook.
474
+ *
475
+ * @internal Exported for tests.
476
+ */
477
+ function patchLoadedCrawleeCopies() {
478
+ const cache = require.cache ?? {};
479
+ for (const [filename, cached] of Object.entries(cache)) {
480
+ if (cached?.exports && CRAWLER_FILENAME_RE.test(filename)) {
481
+ patchCrawleeCrawlerModule(cached.exports);
482
+ }
483
+ }
484
+ }
485
+ try {
486
+ // Use Crawlee's ow (0.28.x CJS). Do not import top-level ow@1.x (ESM-only).
487
+ const owModule = require(require.resolve('ow', { paths: [require.resolve('@crawlee/browser')] }));
488
+ const ow = owModule.default ?? owModule;
489
+ // Playwright + Puppeteer plugins of the SDK's own @crawlee/browser-pool copy.
490
+ // Actor-local copies are patched by the Module._load hook further down.
491
+ try {
492
+ patchBrowserPoolModule(require('@crawlee/browser-pool'));
493
+ }
494
+ catch {
495
+ // @crawlee/browser-pool not installed — skip plugin patches
496
+ }
497
+ try {
498
+ const launcherPath = require.resolve('@crawlee/playwright').replace(/[/\\][^/\\]+$/, '') +
499
+ '/internals/playwright-launcher.js';
500
+ const { PlaywrightLauncher } = require(launcherPath);
501
+ PlaywrightLauncher.optionsShape.cdpUrl = ow.optional.string;
502
+ }
503
+ catch {
504
+ // @crawlee/playwright not installed — skip Playwright patches
505
+ }
506
+ try {
507
+ const puppeteerLauncherPath = require.resolve('@crawlee/puppeteer').replace(/[/\\][^/\\]+$/, '') +
508
+ '/internals/puppeteer-launcher.js';
509
+ const { PuppeteerLauncher } = require(puppeteerLauncherPath);
510
+ PuppeteerLauncher.optionsShape.cdpUrl = ow.optional.string;
511
+ }
512
+ catch {
513
+ // @crawlee/puppeteer not installed — plugin patch above is enough
514
+ }
515
+ // Patch @crawlee/browser BrowserLauncher when available
516
+ const browserPkg = require.resolve('@crawlee/browser');
517
+ const browserLauncherPath = browserPkg.replace(/[/\\][^/\\]+$/, '') + '/internals/browser-launcher.js';
518
+ const { BrowserLauncher } = require(browserLauncherPath);
519
+ BrowserLauncher.optionsShape.cdpUrl = ow.optional.string;
520
+ // Patch createLaunchOptions to inject gVisor-safe Chrome args for any residual
521
+ // local launch path. Playwright/Puppeteer pool launches use CDP and skip this.
522
+ // Only applies when SCRAPELY_CONTAINER_RUNTIME=runsc (gVisor sandbox).
523
+ const origCreateLaunchOptions = BrowserLauncher.prototype.createLaunchOptions;
524
+ BrowserLauncher.prototype.createLaunchOptions = function () {
525
+ const launchOptions = origCreateLaunchOptions.call(this);
526
+ // Inject crashpad-disabling args when running under gVisor
527
+ // gVisor doesn't support Chrome's crashpad_handler process
528
+ // Also needed with ReadonlyRootfs + tmpfs /tmp where /tmp/.X11-unix
529
+ // and crashpad database dir don't persist from the base image.
530
+ if (process.env.SCRAPELY_CONTAINER_RUNTIME === 'runsc') {
531
+ launchOptions.args = launchOptions.args || [];
532
+ for (const arg of GVISOR_CHROME_ARGS) {
533
+ if (!launchOptions.args.includes(arg)) {
534
+ launchOptions.args.push(arg);
535
+ }
536
+ }
537
+ }
538
+ return launchOptions;
539
+ };
540
+ }
541
+ catch {
542
+ // @crawlee/browser not installed — skip browser pool patches (e.g. cheerio-only actors)
543
+ }
544
+ /**
545
+ * Split our `browserConfiguration` option off raw launch options. The options
546
+ * object is returned as-is when the key is absent.
547
+ */
548
+ function takeBrowserConfiguration(options) {
549
+ if (!options || typeof options !== 'object' || !('browserConfiguration' in options)) {
550
+ return { options: options ?? {}, browserConfiguration: undefined };
551
+ }
552
+ const { browserConfiguration, ...rest } = options;
553
+ return { options: rest, browserConfiguration };
554
+ }
555
+ /** Options that only make sense for a local launch (no remote-CDP equivalent). */
556
+ const RAW_LAUNCH_ONLY_KEYS = new Set([
557
+ 'headless',
558
+ 'args',
559
+ 'ignoreDefaultArgs',
560
+ 'ignoreArgs',
561
+ 'executablePath',
562
+ 'channel',
563
+ 'env',
564
+ 'devtools',
565
+ 'chromiumSandbox',
566
+ 'firefoxUserPrefs',
567
+ 'proxy',
568
+ 'tracesDir',
569
+ 'waitForExtensions',
570
+ 'userDataDir',
571
+ 'user_data_dir',
572
+ 'downloadsPath',
573
+ ]);
574
+ function composeRawProxyUrl(proxy) {
575
+ if (!proxy?.server)
576
+ return undefined;
577
+ if (proxy.username) {
578
+ try {
579
+ const url = new URL(proxy.server);
580
+ url.username = proxy.username;
581
+ url.password = proxy.password ?? '';
582
+ return url.toString();
583
+ }
584
+ catch {
585
+ return proxy.server;
586
+ }
587
+ }
588
+ return proxy.server;
589
+ }
590
+ function rawLaunchCdpUrl(options, browserConfiguration) {
591
+ const cdpUrl = getCdpUrl();
592
+ if (!cdpUrl)
593
+ return undefined;
594
+ const proxyUrl = composeRawProxyUrl(options?.proxy);
595
+ return applyProxyInfoToCdpUrl(cdpUrl, proxyUrl ? { url: proxyUrl } : undefined, browserConfiguration);
596
+ }
597
+ function rawConnectOptions(options) {
598
+ const out = {};
599
+ if (options.timeout !== undefined)
600
+ out.timeout = options.timeout;
601
+ if (options.slowMo !== undefined)
602
+ out.slowMo = options.slowMo;
603
+ return out;
604
+ }
605
+ function warnDroppedRawOptions(kind, name, options) {
606
+ const dropped = Object.keys(options).filter((k) => k !== 'timeout' && k !== 'slowMo');
607
+ if (dropped.length > 0) {
608
+ console.warn(`[scrapely] playwright ${name}.${kind} redirected to the remote CDP browser; ` +
609
+ `local launch options ignored: ${dropped.join(', ')} ` +
610
+ '(the remote browser manages headless mode, binary and flags).');
611
+ }
612
+ }
613
+ /** @internal Exported for tests. */
614
+ function patchRawBrowserType(chromium, browserType, name) {
615
+ if (!browserType || typeof browserType.launch !== 'function')
616
+ return;
617
+ if (browserType._scrapelyRawCdpPatched)
618
+ return;
619
+ const origLaunch = browserType.launch;
620
+ const origLpc = browserType.launchPersistentContext;
621
+ browserType.launch = async function rawLaunchPatched(launchOptions = {}) {
622
+ const { options, browserConfiguration } = takeBrowserConfiguration(launchOptions);
623
+ if (browserPatchDisabled())
624
+ return origLaunch.call(this, withGVisorArgs(options));
625
+ const cdpUrl = rawLaunchCdpUrl(options, browserConfiguration);
626
+ if (!cdpUrl) {
627
+ if (!isAtHome())
628
+ return origLaunch.call(this, options);
629
+ throw new Error('cdpUrl is required. Local browser launch is disabled — all runs must use the remote browser.');
630
+ }
631
+ warnDroppedRawOptions('launch', name, options);
632
+ return chromium.connectOverCDP(cdpUrl, rawConnectOptions(options));
633
+ };
634
+ if (typeof origLpc === 'function') {
635
+ browserType.launchPersistentContext = async function rawLpcPatched(...lpcArgs) {
636
+ // Playwright signature: launchPersistentContext(userDataDir, options);
637
+ // an options-only call is accepted too.
638
+ const optionsIndex = typeof lpcArgs[0] === 'string' ? 1 : 0;
639
+ const userDataDir = optionsIndex === 1 ? lpcArgs[0] : undefined;
640
+ const { options, browserConfiguration } = takeBrowserConfiguration(lpcArgs[optionsIndex]);
641
+ const withOptions = (opts) => optionsIndex === 1 ? [userDataDir, opts] : [opts];
642
+ if (browserPatchDisabled())
643
+ return origLpc.call(this, ...withOptions(withGVisorArgs(options)));
644
+ const cdpUrl = rawLaunchCdpUrl(options, browserConfiguration);
645
+ if (!cdpUrl) {
646
+ if (!isAtHome()) {
647
+ return origLpc.call(this, ...withOptions(lpcArgs[optionsIndex] === undefined ? undefined : options));
648
+ }
649
+ throw new Error('cdpUrl is required. Local browser launch is disabled — all runs must use the remote browser.');
650
+ }
651
+ const profileDir = userDataDir ?? options.userDataDir;
652
+ if (profileDir) {
653
+ console.warn(`[scrapely] playwright ${name}.launchPersistentContext(userDataDir=${profileDir}) redirected to the ` +
654
+ 'remote CDP browser — persistent profiles have no remote equivalent; a fresh context is returned.');
655
+ }
656
+ warnDroppedRawOptions('launchPersistentContext', name, options);
657
+ const browser = await chromium.connectOverCDP(cdpUrl, rawConnectOptions(options));
658
+ const contextOptions = {};
659
+ for (const [k, v] of Object.entries(options)) {
660
+ if (!RAW_LAUNCH_ONLY_KEYS.has(k) && k !== 'timeout' && k !== 'slowMo')
661
+ contextOptions[k] = v;
662
+ }
663
+ return browser.newContext(contextOptions);
664
+ };
665
+ }
666
+ browserType._scrapelyRawCdpPatched = true;
667
+ }
668
+ /** Patch the chromium/firefox/webkit BrowserTypes of a playwright module. */
669
+ function patchRawPlaywrightModule(pw) {
670
+ if (!pw?.chromium || typeof pw.chromium.connectOverCDP !== 'function')
671
+ return;
672
+ for (const name of ['chromium', 'firefox', 'webkit']) {
673
+ patchRawBrowserType(pw.chromium, pw[name], name);
674
+ }
675
+ }
676
+ /**
677
+ * Patch a raw puppeteer launcher: the default export (also exposed as module
678
+ * props). Both the full puppeteer package and puppeteer-core ship the same
679
+ * launcher shape (launch/connect) — patch whichever was loaded.
680
+ */
681
+ function patchRawPuppeteerModule(mod, moduleName) {
682
+ const launcher = mod?.default ?? mod;
683
+ if (typeof launcher?.launch !== 'function' || launcher._scrapelyRawCdpPatched) {
684
+ return;
685
+ }
686
+ const origLaunch = launcher.launch.bind(launcher);
687
+ launcher.launch = async function rawPuppeteerLaunchPatched(launchOptions = {}) {
688
+ const { options, browserConfiguration } = takeBrowserConfiguration(launchOptions);
689
+ if (browserPatchDisabled())
690
+ return origLaunch(withGVisorArgs(options));
691
+ const cdpUrl = rawLaunchCdpUrl(options, browserConfiguration);
692
+ if (!cdpUrl) {
693
+ if (!isAtHome())
694
+ return origLaunch(options);
695
+ throw new Error('cdpUrl is required. Local browser launch is disabled — all runs must use the remote browser.');
696
+ }
697
+ const dropped = Object.keys(options).filter((k) => k !== 'timeout' && k !== 'slowMo' && k !== 'protocol');
698
+ if (dropped.length > 0) {
699
+ console.warn(`[scrapely] ${moduleName}.launch redirected to the remote CDP browser; ` +
700
+ `local launch options ignored: ${dropped.join(', ')}.`);
701
+ }
702
+ return launcher.connect({ browserWSEndpoint: cdpUrl, ...rawConnectOptions(options) });
703
+ };
704
+ launcher._scrapelyRawCdpPatched = true;
705
+ }
706
+ /**
707
+ * Eager pass: patch whatever playwright/puppeteer is resolvable from this
708
+ * module's own dependency tree right now (the SDK tree under
709
+ * /opt/scrapely/node_modules in the base image). Actor-local installs are
710
+ * covered by the Module._load hook below — a bare require() from this file
711
+ * can never resolve them.
712
+ */
713
+ function patchSdkTreeRawModules() {
714
+ for (const pwModuleName of ['playwright', 'playwright-core']) {
715
+ try {
716
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
717
+ patchRawPlaywrightModule(require(pwModuleName));
718
+ }
719
+ catch {
720
+ // not installed in the SDK tree — actor-local copies still get the hook
721
+ }
722
+ }
723
+ // Raw puppeteer API: default export launcher (also exposed as module props).
724
+ // Both the full puppeteer package and puppeteer-core ship the same launcher
725
+ // shape (launch/connect) — patch whichever is installed.
726
+ for (const puppeteerModuleName of ['puppeteer', 'puppeteer-core']) {
727
+ try {
728
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
729
+ patchRawPuppeteerModule(require(puppeteerModuleName), puppeteerModuleName);
730
+ }
731
+ catch {
732
+ // not installed in the SDK tree — actor-local copies still get the hook
733
+ }
734
+ }
735
+ }
736
+ /**
737
+ * Apply the raw patches to a module coming back from the hooked loader.
738
+ * `request` is the specifier as written by the caller, `loaded` is whatever
739
+ * instance it actually resolved to, and `loadRoot` resolves a package root
740
+ * through the original loader (no hook recursion) for subpath requests.
741
+ */
742
+ /**
743
+ * Requests that load (a part of) some copy of @crawlee/browser-pool:
744
+ * - the bare package name (require from @crawlee/playwright etc.),
745
+ * - an absolute path to its index.js (ESM → CJS interop loads by filename),
746
+ * - the relative plugin files the package's own index.js requires.
747
+ */
748
+ const BROWSER_POOL_REQUEST_RE = /^@crawlee\/browser-pool$|[/\\]@crawlee[/\\]browser-pool[/\\]index\.js$|(^|[/\\])(playwright|puppeteer)-plugin(\.js)?$/;
749
+ function patchLoadedRawModule(request, loaded, loadRoot) {
750
+ if (typeof request !== 'string')
751
+ return;
752
+ // Crawlee browser pool — the actor's own copy (e.g. /usr/src/app/node_modules)
753
+ // is a different instance from the SDK's, so its plugins need the CDP patch too.
754
+ if (BROWSER_POOL_REQUEST_RE.test(request)) {
755
+ patchBrowserPoolModule(loaded);
756
+ return;
757
+ }
758
+ if (CRAWLER_REQUEST_RE.test(request)) {
759
+ patchCrawleeCrawlerModule(loaded);
760
+ return;
761
+ }
762
+ if (request === 'playwright' || request === 'playwright-core') {
763
+ patchRawPlaywrightModule(loaded);
764
+ return;
765
+ }
766
+ if (request === 'puppeteer' || request === 'puppeteer-core') {
767
+ patchRawPuppeteerModule(loaded, request);
768
+ return;
769
+ }
770
+ // BrowserType subpath loads, e.g. require('playwright/chromium') — the
771
+ // export is the BrowserType itself, not the package root. Resolve the root
772
+ // for its chromium (the connectOverCDP anchor), patch the root, then the
773
+ // subpath export. The launch() shape check ignores deep imports into the
774
+ // package that are not BrowserTypes.
775
+ if (!request.startsWith('playwright/') && !request.startsWith('playwright-core/'))
776
+ return;
777
+ const subpath = /^(playwright|playwright-core)\/(.+)$/.exec(request);
778
+ if (subpath && typeof loaded?.launch === 'function') {
779
+ try {
780
+ const root = loadRoot(subpath[1]);
781
+ patchRawPlaywrightModule(root);
782
+ if (root?.chromium)
783
+ patchRawBrowserType(root.chromium, loaded, subpath[2]);
784
+ }
785
+ catch {
786
+ // package root unresolvable — no chromium anchor to connect through
787
+ }
788
+ }
789
+ }
790
+ /**
791
+ * Hook the CommonJS loader so the raw patches reach actor-local installs.
792
+ *
793
+ * The eager pass above can only see this module's own dependency tree; an
794
+ * actor bundling playwright/puppeteer in its workdir resolves a different,
795
+ * unpatched instance. Module._load is the chokepoint every require() passes
796
+ * through AFTER resolution — including CJS packages imported from ESM, which
797
+ * Node funnels through the CJS loader — so patching here covers whichever
798
+ * copy (SDK tree or actor workdir) the request actually resolved to. Chained
799
+ * onto the current _load (composes with tsx's hook in dev) and idempotent.
800
+ */
801
+ function installRawModuleLoadHook() {
802
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
803
+ const Module = require('module');
804
+ const origLoad = Module._load;
805
+ if (typeof origLoad !== 'function' || origLoad._scrapelyRawCdpHook)
806
+ return;
807
+ const hookedLoad = function (request, parent, isMain) {
808
+ const loaded = origLoad.apply(this, [request, parent, isMain]);
809
+ try {
810
+ patchLoadedRawModule(request, loaded, (name) => origLoad(name, parent, isMain));
811
+ }
812
+ catch (err) {
813
+ // Never break the actor's module loading — warn and hand back the
814
+ // unpatched module rather than crashing the process.
815
+ console.warn(`[scrapely] CDP patch failed for '${String(request)}':`, err?.message);
816
+ }
817
+ return loaded;
818
+ };
819
+ hookedLoad._scrapelyRawCdpHook = true;
820
+ Module._load = hookedLoad;
821
+ }
822
+ function applyRawBrowserPatches() {
823
+ patchSdkTreeRawModules();
824
+ installRawModuleLoadHook();
825
+ patchLoadedCrawleeCopies();
826
+ }
827
+ applyRawBrowserPatches();