scrapely 1.2.6 → 1.2.8

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.
package/README.md CHANGED
@@ -1,118 +1,65 @@
1
- # Scrapely
2
-
3
- Scrapely is an Actor-based web scraping platform. Developers push an Actor (a scraping program), the platform builds it into a Docker image, runs it with structured input, and keeps its output in datasets, key-value stores and request queues. The API and SDKs follow Apify's shape, so most Apify Actors run with few or no changes.
4
-
5
- This repo holds the platform server, the Actor SDKs, the API clients, the CLI and the MCP servers.
6
-
7
- ## Repo layout
8
-
9
- | Path | What it is |
10
- |------|------------|
11
- | `src/` → `dist/` | Node.js Actor SDK, published as `scrapely`. `dist/` is committed because it is the package output. |
12
- | `scrapely-python/` | Python Actor SDK (`scrapely` on PyPI) |
13
- | `scrapely-server/` | API server, build/run workers, autoscaler, egress proxy |
14
- | `scrapely-cli/` | `scrapely` CLI: login, create/init, push, pull, run, builds, runs, storages, secrets |
15
- | `scrapely-client/` | JavaScript API client (`scrapely-client`, drop-in for `apify-client`) |
16
- | `scrapely-client-python/` | Python API client (`scrapely-client-python`) |
17
- | `scrapely-mcp/` | MCP server (TypeScript) |
18
- | `scrapely-mcp-python/` | MCP server (Python) |
19
- | `docker/` | Runner and Actor base images, Dragonfly compose file, Kaniko patch |
20
- | `docs/` | Platform documentation. Start at [docs/README.md](docs/README.md). |
21
- | `scripts/` | Server setup, deploy, migrations, benchmarks |
22
- | `tests/` | SDK/runtime tests |
23
- | `examples/basic-actor/` | Smallest working Actor |
24
- | `npm-claim-scrapely/` | Old placeholder for the `scrapely` npm name, now superseded by the SDK itself (1.2.4+). |
25
-
26
- Each package has its own README with install and usage details.
27
-
28
- ## How it fits together
29
-
30
- - The **API server** (`scrapely-server/`) serves the Apify-style `/v2` API: auth, Actors and versions, builds, runs, tasks, schedules, webhooks, storages, billing and admin routes.
31
- - **Builds and runs** are queued with BullMQ and picked up by workers. Builds produce Docker images that go to a private registry. Runs execute in Docker on worker servers.
32
- - **The autoscaler** adds and removes worker servers based on load. See [docs/autoscaling.md](docs/autoscaling.md).
33
- - **Storage**: run state and storages live in DragonflyDB (Redis-compatible). Accounts, Actors, runs and billing live in SingleStore. Both are backed up to S3.
34
- - **Browsers**: remote browsers come from Evomi over CDP. See [docs/evomi-integration.md](docs/evomi-integration.md).
35
- - **Actor code** imports the SDK, which patches Crawlee storage to go through the platform:
1
+ # Scrapely Node.js SDK
36
2
 
37
- ```ts
38
- import { Actor, PlaywrightCrawler, Dataset } from 'scrapely';
39
- ```
40
-
41
- ```python
42
- from scrapely import Actor
43
- ```
44
-
45
- ## Local development
3
+ Build JavaScript and TypeScript Actors for the [Scrapely](https://scrape.ly) platform. The SDK gives you the Actor lifecycle, input and output, datasets, key-value stores and request queues, Crawlee crawlers, remote browsers over CDP, platform events, run control, webhooks and pay-per-event charging.
46
4
 
47
- ### Prerequisites
48
-
49
- - Node.js 22
50
- - Docker
51
- - Doppler CLI with access to the `scrapely` project (all secrets come from Doppler; there are no `.env.example` files)
52
- - Reachable DragonflyDB and SingleStore instances (Doppler `stg` points at staging)
53
-
54
- ### Install
5
+ ## Install
55
6
 
56
7
  ```bash
57
- npm install
58
- cd scrapely-server && npm install
59
- cd ../scrapely-cli && npm install
8
+ npm install scrapely
60
9
  ```
61
10
 
62
- ### Run the API server
63
-
64
- ```bash
65
- cd scrapely-server
66
- doppler run -p scrapely -c stg -- npm run dev
67
- curl http://localhost:3000/health
68
- ```
11
+ ## Quick start
69
12
 
70
- To run a local Dragonfly instead of staging: `docker compose -f docker/docker-compose.dragonfly.yml up -d`.
13
+ ```ts
14
+ import { Actor } from 'scrapely';
71
15
 
72
- ### Build the runner image
16
+ await Actor.main(async () => {
17
+ const input = await Actor.getInputOrThrow<{ url: string }>();
18
+ await Actor.pushData({ url: input.url, status: 'processed' });
19
+ });
20
+ ```
73
21
 
74
- Needed before the server can run Actors locally:
22
+ `Actor.main()` initializes the Actor, awaits your function, closes events and storage, and exits the process. Always `await` writes so they reach the platform before the process ends.
75
23
 
76
- ```bash
77
- make build-runner
78
- ```
24
+ ## Crawling
79
25
 
80
- ### Push and run an Actor
26
+ ```ts
27
+ import { Actor, PlaywrightCrawler } from 'scrapely';
81
28
 
82
- ```bash
83
- scrapely login
84
- cd examples/basic-actor
85
- scrapely push
86
- scrapely run --wait
87
- ```
29
+ await Actor.main(async () => {
30
+ const { startUrls } = await Actor.getInputOrThrow<{ startUrls: string[] }>();
88
31
 
89
- ## Tests
32
+ const crawler = new PlaywrightCrawler({
33
+ maxConcurrency: 5,
34
+ async requestHandler({ request, page }) {
35
+ await Actor.pushData({ url: request.url, title: await page.title() });
36
+ },
37
+ });
90
38
 
91
- ```bash
92
- npm test # root SDK
93
- cd scrapely-server && npm test
39
+ await crawler.run(startUrls);
40
+ });
94
41
  ```
95
42
 
96
- Both packages have many more targeted `test:*` scripts in their `package.json`. Most tests are integration-style, and some need a valid `CDP_URL` or live services.
43
+ On the platform, browser crawlers connect to a remote Chromium over CDP instead of launching a local browser. Locally, they launch a normal browser unless you set `CDP_URL` or call `setCdpUrl()`.
97
44
 
98
- ## Deploy
45
+ ## What's included
99
46
 
100
- | Command | What it does |
101
- |---------|--------------|
102
- | `npm run prod` | Full production deploy (`scripts/deploy-prod.sh`) |
103
- | `npm run prod:update` | Update-only deploy |
104
- | `npm run setup:server` | Bootstrap a fresh Linux server |
105
- | `npm run setup:gvisor` | Install gVisor on a Linux host |
106
- | `npm run migrate` | Run a SingleStore migration |
47
+ - **Actor lifecycle:** `Actor.main()`, `init()`, `exit()`, `fail()`, status messages, reboot, metamorph and abort
48
+ - **Input and output:** `getInput()`, `getInputOrThrow()`, `pushData()`, `getValue()`, `setValue()`
49
+ - **Storage:** `Dataset`, `KeyValueStore` and `RequestQueue`, backed by the Scrapely API on the platform and by local files during development
50
+ - **Crawlers:** `PlaywrightCrawler`, `PuppeteerCrawler`, `CheerioCrawler`, `HttpCrawler`, `BasicCrawler`, `BrowserCrawler`, `JSDOMCrawler` and `LinkeDOMCrawler`
51
+ - **Remote browsers:** automatic CDP wiring through `CDP_URL`, or `setCdpUrl()`
52
+ - **Events:** state persistence, migration and graceful shutdown
53
+ - **Run control:** `Actor.call()`, `Actor.callTask()`, `Actor.start()` and ad-hoc webhooks
54
+ - **Charging:** `Actor.charge()`, `ChargingManager` and `pushDataAndCharge()` for pay-per-event Actors
55
+ - **Logging and utilities:** `log`, `utils` and `enqueueLinks`
107
56
 
108
- The main server uses `scrapely-server/docker-compose.yml` and workers use `docker-compose.worker.yml`. Secrets are injected at container start with `doppler run`.
57
+ ## Related packages
109
58
 
110
- `make start` / `stop` / `restart` / `logs` wrap the server compose stack.
59
+ - [`scrapely-client`](https://www.npmjs.com/package/scrapely-client): JavaScript API client
60
+ - [`scrapely-cli`](https://www.npmjs.com/package/scrapely-cli): create, run and push Actors from your terminal
61
+ - [`scrapely-python`](https://pypi.org/project/scrapely-python/): the Python SDK
111
62
 
112
- ## Where to look next
63
+ ## License
113
64
 
114
- - [docs/README.md](docs/README.md): documentation index (getting started, SDKs, CLI, webhooks, troubleshooting)
115
- - [scrapely-server/docs/API_ENDPOINTS.md](scrapely-server/docs/API_ENDPOINTS.md): API reference
116
- - [docs/network-architecture.md](docs/network-architecture.md): fleet topology and data path
117
- - `scrapely-server/src/index.ts`: server startup and route wiring
118
- - `src/actor.ts`: Node SDK Actor runtime
65
+ ISC
package/dist/actor.d.ts CHANGED
@@ -546,8 +546,8 @@ export declare class Actor {
546
546
  * });
547
547
  * ```
548
548
  *
549
- * For compatibility with existing Actor Input UI (Input Schema), this function returns
550
- * `undefined` when `{ useApifyProxy: false }` is passed.
549
+ * Pass the proxy input field (`editor: "proxy"`) as is: this function returns
550
+ * `undefined` when `{ useScrapelyProxy: false }` is passed without custom proxies.
551
551
  *
552
552
  * @param options - Proxy configuration options
553
553
  * @returns Promise resolving to ProxyConfiguration instance, or undefined
package/dist/actor.js CHANGED
@@ -1015,8 +1015,8 @@ class Actor {
1015
1015
  * });
1016
1016
  * ```
1017
1017
  *
1018
- * For compatibility with existing Actor Input UI (Input Schema), this function returns
1019
- * `undefined` when `{ useApifyProxy: false }` is passed.
1018
+ * Pass the proxy input field (`editor: "proxy"`) as is: this function returns
1019
+ * `undefined` when `{ useScrapelyProxy: false }` is passed without custom proxies.
1020
1020
  *
1021
1021
  * @param options - Proxy configuration options
1022
1022
  * @returns Promise resolving to ProxyConfiguration instance, or undefined
@@ -53,14 +53,17 @@ export interface ProxyConfigurationOptions extends CoreProxyConfigurationOptions
53
53
  /**
54
54
  * Input-schema alias for groups. Normalized to groups.
55
55
  */
56
- apifyProxyGroups?: string[];
56
+ scrapelyProxyGroups?: string[];
57
57
  /**
58
58
  * Input-schema alias for subdivisionCode. Normalized to subdivisionCode.
59
59
  */
60
- apifyProxySubdivision?: string;
60
+ scrapelyProxySubdivision?: string;
61
61
  /**
62
62
  * Input-schema alias for countryCode. Normalized to countryCode.
63
63
  */
64
+ scrapelyProxyCountry?: string;
65
+ apifyProxyGroups?: string[];
66
+ apifyProxySubdivision?: string;
64
67
  apifyProxyCountry?: string;
65
68
  /**
66
69
  * Platform proxy password. Defaults to the password injected by the
@@ -73,9 +76,10 @@ export interface ProxyConfigurationOptions extends CoreProxyConfigurationOptions
73
76
  */
74
77
  export interface CreateProxyConfigurationOptions extends ProxyConfigurationOptions {
75
78
  /**
76
- * For compatibility with existing Actor Input UI (Input Schema).
77
- * When set to `false`, this method returns `undefined`.
79
+ * The proxy input field's switch (input schema `editor: "proxy"`). When
80
+ * `false` and no custom proxies are given, this method returns `undefined`.
78
81
  */
82
+ useScrapelyProxy?: boolean;
79
83
  useApifyProxy?: boolean;
80
84
  /**
81
85
  * Whether to verify the proxy configuration by testing a connection.
@@ -39,14 +39,17 @@ function hasCustomProxyOptions(options) {
39
39
  }
40
40
  function normalizeProxyInput(options) {
41
41
  const normalized = { ...options };
42
- if (normalized.apifyProxyGroups && !normalized.groups) {
43
- normalized.groups = normalized.apifyProxyGroups;
42
+ const groups = normalized.scrapelyProxyGroups ?? normalized.apifyProxyGroups;
43
+ if (groups && !normalized.groups) {
44
+ normalized.groups = groups;
44
45
  }
45
- if (normalized.apifyProxyCountry && !normalized.countryCode) {
46
- normalized.countryCode = normalized.apifyProxyCountry;
46
+ const countryCode = normalized.scrapelyProxyCountry ?? normalized.apifyProxyCountry;
47
+ if (countryCode && !normalized.countryCode) {
48
+ normalized.countryCode = countryCode;
47
49
  }
48
- if (normalized.apifyProxySubdivision && !normalized.subdivisionCode) {
49
- normalized.subdivisionCode = normalized.apifyProxySubdivision;
50
+ const subdivisionCode = normalized.scrapelyProxySubdivision ?? normalized.apifyProxySubdivision;
51
+ if (subdivisionCode && !normalized.subdivisionCode) {
52
+ normalized.subdivisionCode = subdivisionCode;
50
53
  }
51
54
  return normalized;
52
55
  }
@@ -60,17 +63,19 @@ function normalizeProxyInput(options) {
60
63
  */
61
64
  async function createProxyConfiguration(options = {}) {
62
65
  const normalized = normalizeProxyInput(options);
63
- const { useApifyProxy, checkAccess = true, ...proxyOptions } = normalized;
64
- // Apify parity: `useApifyProxy: false` explicitly disables the PLATFORM proxy
66
+ const { useScrapelyProxy, useApifyProxy, checkAccess = true, ...proxyOptions } = normalized;
67
+ // `useScrapelyProxy: false` (or the legacy `useApifyProxy: false`; a boolean
68
+ // useScrapelyProxy wins) explicitly disables the PLATFORM proxy
65
69
  // only — custom proxies from the same input must still be used. The standard
66
- // "Custom proxy URLs" input shape is `{ useApifyProxy: false, proxyUrls: [...] }`;
70
+ // "Custom proxy URLs" input shape is `{ useScrapelyProxy: false, proxyUrls: [...] }`;
67
71
  // returning undefined for it would silently drop the user's own proxies and
68
72
  // crawl on their real IPs. We only bail when NO custom proxy mode is given —
69
73
  // proxyUrls, tieredProxyUrls, tieredProxyConfig and newUrlFunction all count
70
74
  // (Python SDK Actor.create_proxy_configuration parity).
71
- const dontUseApifyProxy = useApifyProxy === false;
75
+ const usePlatformProxy = typeof useScrapelyProxy === 'boolean' ? useScrapelyProxy : useApifyProxy;
76
+ const dontUsePlatformProxy = usePlatformProxy === false;
72
77
  const dontUseCustomProxies = !hasCustomProxyOptions(proxyOptions);
73
- if (dontUseApifyProxy && dontUseCustomProxies) {
78
+ if (dontUsePlatformProxy && dontUseCustomProxies) {
74
79
  return undefined;
75
80
  }
76
81
  if (!hasCustomProxyOptions(proxyOptions)) {
@@ -89,7 +89,10 @@ export declare class ProxyConfiguration extends CrawleeProxyConfiguration {
89
89
  /**
90
90
  * Verifies the proxy configuration. For platform proxy mode with an
91
91
  * injected status URL, performs the Apify-format access check through the
92
- * gateway (`GET {statusUrl}/?format=json`, best-effort, never fatal);
92
+ * gateway (`GET {statusUrl}/?format=json`). When the gateway answers
93
+ * `connected: false` (e.g. an unknown proxy group), it throws with the
94
+ * gateway's `connectionError` on the platform and warns locally, like
95
+ * Apify's `_checkAccess`; timeouts and HTTP errors only warn.
93
96
  * BYOP mode only verifies that a test URL can be generated.
94
97
  */
95
98
  initialize(options?: {
@@ -73,6 +73,12 @@ function validateProxySessionId(sessionId) {
73
73
  }
74
74
  return sessionId;
75
75
  }
76
+ /** `groups`, else the input-schema alias `scrapelyProxyGroups`, else the legacy `apifyProxyGroups`. */
77
+ function platformGroups(options) {
78
+ if (options.groups?.length)
79
+ return options.groups;
80
+ return options.scrapelyProxyGroups ?? options.apifyProxyGroups;
81
+ }
76
82
  /**
77
83
  * Resolves and validates the country/subdivision options (Apify parity).
78
84
  * Country must be a two-letter ISO code; subdivision an ISO 3166-2 code
@@ -80,8 +86,8 @@ function validateProxySessionId(sessionId) {
80
86
  * the normalized values used by the username grammar.
81
87
  */
82
88
  function resolveCountryOptions(options) {
83
- const countryCode = options.countryCode ?? options.apifyProxyCountry;
84
- const subdivisionCode = options.subdivisionCode ?? options.apifyProxySubdivision;
89
+ const countryCode = options.countryCode ?? options.scrapelyProxyCountry ?? options.apifyProxyCountry;
90
+ const subdivisionCode = options.subdivisionCode ?? options.scrapelyProxySubdivision ?? options.apifyProxySubdivision;
85
91
  if (countryCode != null && !COUNTRY_CODE_REGEX.test(countryCode)) {
86
92
  throw new Error(`ProxyConfiguration: "countryCode" must be a two-letter ISO country code (got "${countryCode}").`);
87
93
  }
@@ -225,7 +231,7 @@ async function requestThroughProxy(targetUrl, proxyUrl, timeoutMs) {
225
231
  * server.
226
232
  */
227
233
  function buildPlatformProxyUrl(options, env, sessionId) {
228
- const groups = options.groups?.length ? options.groups : options.apifyProxyGroups;
234
+ const groups = platformGroups(options);
229
235
  const { countryCode, subdivisionCode } = resolveCountryOptions(options);
230
236
  const password = options.password ?? env.password;
231
237
  // Apify username grammar: groups, then session, then country — with the
@@ -334,7 +340,7 @@ function toCrawleeOptions(options, env, placeholderOptions) {
334
340
  return crawleeOptions;
335
341
  }
336
342
  function hasApifyOnlyOptions(options) {
337
- const groups = options.groups?.length ? options.groups : options.apifyProxyGroups;
343
+ const groups = platformGroups(options);
338
344
  const { countryCode, subdivisionCode } = resolveCountryOptions(options);
339
345
  return !!(groups?.length || countryCode || subdivisionCode || options.password);
340
346
  }
@@ -382,10 +388,10 @@ class ProxyConfiguration extends core_1.ProxyConfiguration {
382
388
  // values throw at construction, not on the first newUrl() call).
383
389
  resolveCountryOptions(options);
384
390
  if (hasRealCustomProxyOptions(options) && hasApifyOnlyOptions(options)) {
385
- throw new Error('Cannot combine custom proxies with Apify Proxy! ' +
391
+ throw new Error('Cannot combine custom proxies with Scrapely Proxy! ' +
386
392
  'It is not allowed to set "options.proxyUrls" or "options.newUrlFunction" combined with ' +
387
- '"options.groups" or "options.apifyProxyGroups" and "options.countryCode" or ' +
388
- '"options.apifyProxyCountry", "options.subdivisionCode" or "options.apifyProxySubdivision".');
393
+ '"options.groups" or "options.scrapelyProxyGroups" and "options.countryCode" or ' +
394
+ '"options.scrapelyProxyCountry", "options.subdivisionCode" or "options.scrapelyProxySubdivision".');
389
395
  }
390
396
  if (needsPlatformProxyUrls(options) && !platformEnv) {
391
397
  throw new Error('[Scrapely] The platform proxy was requested (via the "auto" sentinel, tieredProxyConfig tiers without ' +
@@ -445,9 +451,7 @@ class ProxyConfiguration extends core_1.ProxyConfiguration {
445
451
  const sid = sessionId != null ? validateProxySessionId(String(sessionId)) : undefined;
446
452
  const info = await super.newProxyInfo(sid, options);
447
453
  if (info && this.platformEnv && info.hostname === this.platformEnv.hostname) {
448
- const groups = this.platformOptions.groups?.length
449
- ? this.platformOptions.groups
450
- : this.platformOptions.apifyProxyGroups;
454
+ const groups = platformGroups(this.platformOptions);
451
455
  const { countryCode, subdivisionCode } = resolveCountryOptions(this.platformOptions);
452
456
  return {
453
457
  ...info,
@@ -483,6 +487,7 @@ class ProxyConfiguration extends core_1.ProxyConfiguration {
483
487
  connectionError: 'Platform proxy authentication failed (HTTP 407). Check the injected ' +
484
488
  'SCRAPELY_PROXY_*/APIFY_PROXY_* credentials.',
485
489
  isManInTheMiddle: false,
490
+ fromPayload: false,
486
491
  };
487
492
  }
488
493
  if (statusCode >= 590 && statusCode <= 599) {
@@ -491,6 +496,7 @@ class ProxyConfiguration extends core_1.ProxyConfiguration {
491
496
  connected: false,
492
497
  connectionError: `Platform proxy upstream error (HTTP ${statusCode}: ${description}).`,
493
498
  isManInTheMiddle: false,
499
+ fromPayload: false,
494
500
  };
495
501
  }
496
502
  if (statusCode >= 200 && statusCode < 300) {
@@ -500,6 +506,7 @@ class ProxyConfiguration extends core_1.ProxyConfiguration {
500
506
  connected: parsed.connected === true,
501
507
  connectionError: parsed.connectionError ?? '',
502
508
  isManInTheMiddle: parsed.isManInTheMiddle === true,
509
+ fromPayload: true,
503
510
  };
504
511
  }
505
512
  catch {
@@ -516,7 +523,10 @@ class ProxyConfiguration extends core_1.ProxyConfiguration {
516
523
  /**
517
524
  * Verifies the proxy configuration. For platform proxy mode with an
518
525
  * injected status URL, performs the Apify-format access check through the
519
- * gateway (`GET {statusUrl}/?format=json`, best-effort, never fatal);
526
+ * gateway (`GET {statusUrl}/?format=json`). When the gateway answers
527
+ * `connected: false` (e.g. an unknown proxy group), it throws with the
528
+ * gateway's `connectionError` on the platform and warns locally, like
529
+ * Apify's `_checkAccess`; timeouts and HTTP errors only warn.
520
530
  * BYOP mode only verifies that a test URL can be generated.
521
531
  */
522
532
  async initialize(options = {}) {
@@ -526,7 +536,7 @@ class ProxyConfiguration extends core_1.ProxyConfiguration {
526
536
  }
527
537
  // Apify-parity access check: GET {statusUrl}/?format=json *through the
528
538
  // platform gateway* so the proxy handshake itself authenticates the
529
- // run's credentials. Best-effort — never hard fails the run.
539
+ // run's credentials.
530
540
  if (checkAccess && this.platformEnv?.statusUrl) {
531
541
  const status = await this.fetchProxyStatus();
532
542
  if (!status) {
@@ -535,6 +545,9 @@ class ProxyConfiguration extends core_1.ProxyConfiguration {
535
545
  }
536
546
  else {
537
547
  this.isManInTheMiddle = status.isManInTheMiddle;
548
+ if (!status.connected && status.fromPayload && this.config.get('isAtHome')) {
549
+ throw new Error(status.connectionError);
550
+ }
538
551
  if (!status.connected) {
539
552
  console.warn(`[Scrapely] Platform proxy access check failed: ${status.connectionError}`);
540
553
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "scrapely",
3
- "version": "1.2.6",
4
- "description": "Apify-like scraping platform using Evomi browser infrastructure",
3
+ "version": "1.2.8",
4
+ "description": "Scrapely SDK for building JavaScript and TypeScript Actors",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [