surfsky 0.0.1
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/LICENSE +21 -0
- package/README.md +471 -0
- package/dist/index.d.ts +1722 -0
- package/dist/index.js +2754 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Surfsky SDK contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
# surfsky
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for [Surfsky](https://surfsky.io), a cloud-based antidetect browser.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
- Raw CDP, without the automation traces stock Playwright, Puppeteer or
|
|
10
|
+
Selenium leave in the page.
|
|
11
|
+
- Input goes through our
|
|
12
|
+
[human-emulation](https://docs.surfsky.io/human_emulation) framework: real
|
|
13
|
+
timing, typing, cursor movement.
|
|
14
|
+
- Residential and mobile proxies, 100M+ IPs, with geo, ASN and sticky-session
|
|
15
|
+
targeting. Or use your own.
|
|
16
|
+
- Real browser fingerprints from a pool of 2.5M+ devices, not synthetic ones.
|
|
17
|
+
- Browsers run in the cloud. Playwright-style API, fully typed, Node 22+ and
|
|
18
|
+
Bun.
|
|
19
|
+
|
|
20
|
+
REST API docs for the service: https://docs.surfsky.io/api-reference
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install surfsky
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
or `bun add surfsky`, `pnpm add surfsky`. Requires Node 22 or newer, or Bun.
|
|
29
|
+
ESM only.
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
Sign up at [surfsky.io](https://surfsky.io). Once you're logged in, the
|
|
34
|
+
[dashboard](https://app.surfsky.io) shows your API token and base URL. Put them
|
|
35
|
+
in the environment:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
export SURFSKY_API_TOKEN=...
|
|
39
|
+
export SURFSKY_API_BASE_URL=...
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Then start a browser:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { Surfsky } from "surfsky";
|
|
46
|
+
|
|
47
|
+
// or new Surfsky({ apiToken: "...", baseUrl: "..." }) instead of env vars
|
|
48
|
+
await using client = new Surfsky();
|
|
49
|
+
// starts a session, stops it when the block ends so it doesn't keep billing
|
|
50
|
+
await using browser = await client.browser({ proxy: { tier: "premium", country: "us" } });
|
|
51
|
+
await browser.goto("https://duckduckgo.com", { waitUntil: "domcontentloaded" });
|
|
52
|
+
await browser.type('[name="q"]', "surfsky cloud browser");
|
|
53
|
+
await browser.click("#searchbox_homepage button[type=submit]");
|
|
54
|
+
console.log(await browser.waitForUrl("?q="));
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`await using` needs Node 24+ or Bun (TypeScript compiles it down for Node 22).
|
|
58
|
+
Without it, `try { ... } finally { await browser.close() }` does the same.
|
|
59
|
+
|
|
60
|
+
More examples in [`examples/`](https://github.com/surfskyio/surfsky-js/tree/main/examples).
|
|
61
|
+
|
|
62
|
+
## Browser automation
|
|
63
|
+
|
|
64
|
+
The API sticks to Playwright's names and semantics where it can: `click`,
|
|
65
|
+
`fill`, `hover`, `waitForSelector`, `innerText`, `selectOption`,
|
|
66
|
+
`keyboard.press`, `mouse.move` and so on. Input goes through Surfsky's
|
|
67
|
+
[human emulation](https://docs.surfsky.io/human_emulation). Reads use plain CDP
|
|
68
|
+
and run no JavaScript in the page.
|
|
69
|
+
|
|
70
|
+
When you need JavaScript:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
await browser.evaluate("document.title");
|
|
74
|
+
await browser.evaluate("(a, b) => a + b", { args: [1, 2] });
|
|
75
|
+
await browser.evaluate(() => document.title); // a function is stringified
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Scripts run in an isolated world the page can't see. Pass `isolated: false` to
|
|
79
|
+
use the page's own context.
|
|
80
|
+
|
|
81
|
+
Also useful:
|
|
82
|
+
|
|
83
|
+
- `client.browser({ blockResources: ["image", "font", "media"] })` skips those
|
|
84
|
+
downloads and saves proxy traffic.
|
|
85
|
+
- Grab the JSON a page fetches instead of parsing its HTML:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
await browser.captureResponses("/api/search");
|
|
89
|
+
await browser.goto(url);
|
|
90
|
+
const data = (await browser.waitForResponse("/api/search")).json();
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- Work with several pages at once. `browser.pages` has every open tab, popups
|
|
94
|
+
included, and `newPage()` adds one:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
await browser.newPage();
|
|
98
|
+
await browser.pages[1].goto("https://example.com");
|
|
99
|
+
console.log(await browser.pages[0].title(), await browser.pages[1].title());
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Every method is listed in the [API reference](#api-reference).
|
|
103
|
+
|
|
104
|
+
## Running multiple browsers
|
|
105
|
+
|
|
106
|
+
`client.map` runs a function over a list of items in parallel, one browser per
|
|
107
|
+
item, and collects the results:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
async function title(browser: Browser, url: string) {
|
|
111
|
+
await browser.goto(url);
|
|
112
|
+
return browser.title();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const o of await client.map(title, urls)) {
|
|
116
|
+
console.log(o.item, o.ok ? o.value : o.error);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Errors land in `o.error` instead of throwing, so one bad page doesn't stop the
|
|
121
|
+
run. By default the pool uses every browser your plan allows
|
|
122
|
+
(`concurrency: "auto"`). Pass `concurrency: 5` to cap it.
|
|
123
|
+
|
|
124
|
+
If you need more control, write your own loop on top of the pool:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
await using pool = await client.browsers();
|
|
128
|
+
await pool.lease(async (browser) => { // waits for a free browser
|
|
129
|
+
await browser.goto("https://example.com");
|
|
130
|
+
console.log(await browser.title());
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
A browser keeps its fingerprint, proxy and cookies between leases.
|
|
135
|
+
`browser.data` carries your own state across them, `browser.retire()` swaps in
|
|
136
|
+
a fresh browser.
|
|
137
|
+
|
|
138
|
+
Neither `map` nor `lease` retries: an error is reported and the run goes on.
|
|
139
|
+
Retries belong to your handler, since only it knows a timeout from a 404. Loop
|
|
140
|
+
around `lease` and `retire()` the browser first, so the next attempt gets a
|
|
141
|
+
fresh browser: [`examples/retry.ts`](https://github.com/surfskyio/surfsky-js/blob/main/examples/retry.ts).
|
|
142
|
+
|
|
143
|
+
## Using Playwright or Puppeteer
|
|
144
|
+
|
|
145
|
+
You don't have to use the SDK's browser API. `client.session` starts a browser
|
|
146
|
+
and gives you its WebSocket URL, so Playwright, Puppeteer or any other CDP
|
|
147
|
+
client can connect to it. The SDK still handles profiles, proxies and the
|
|
148
|
+
session lifecycle:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { chromium } from "playwright-core";
|
|
152
|
+
import { Surfsky } from "surfsky";
|
|
153
|
+
|
|
154
|
+
await using client = new Surfsky();
|
|
155
|
+
await using session = await client.session();
|
|
156
|
+
const browser = await chromium.connectOverCDP(session.ws_url);
|
|
157
|
+
const page = browser.contexts()[0].pages()[0];
|
|
158
|
+
await page.goto("https://example.com");
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Don't use stock Playwright or Puppeteer: both put back the traces this SDK
|
|
162
|
+
avoids, `Runtime.enable` above all. The patched forks strip them and are
|
|
163
|
+
drop-in replacements.
|
|
164
|
+
|
|
165
|
+
Selenium works too (`enable_chromedriver: true`, see
|
|
166
|
+
[`examples/selenium_connect.ts`](https://github.com/surfskyio/surfsky-js/blob/main/examples/selenium_connect.ts)) but isn't
|
|
167
|
+
recommended: chromedriver is easy to detect.
|
|
168
|
+
|
|
169
|
+
## Proxies
|
|
170
|
+
|
|
171
|
+
Every session start accepts `proxy`. Use your own, or one of Surfsky's
|
|
172
|
+
built-in pools:
|
|
173
|
+
|
|
174
|
+
- Premium: clean residential and mobile IPs, targeted by country, region,
|
|
175
|
+
city, ASN or coordinates, with sticky sessions. Use it for production.
|
|
176
|
+
- Shared: a pool for testing. Don't rely on it against sites that matter.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { ProxyCycle, ProxyTemplate } from "surfsky";
|
|
180
|
+
|
|
181
|
+
proxy = { tier: "premium", country: "us", region: "ny", type: "mobile" }; // Surfsky premium
|
|
182
|
+
proxy = { tier: "shared", country: "us" }; // Surfsky shared, for tests
|
|
183
|
+
proxy = { country: "de" }; // premium if set up, else shared
|
|
184
|
+
proxy = "socks5://user:pass@host:1080"; // your own
|
|
185
|
+
proxy = new ProxyCycle(myProxies); // round-robin over your own list
|
|
186
|
+
proxy = new ProxyTemplate("http://user-sessid-{session}:pw@gate.example.com:7000");
|
|
187
|
+
proxy = async () => pickOne(); // any function, sync or async
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`client.proxies` lists available countries, regions and cities, plus your quota.
|
|
191
|
+
|
|
192
|
+
## Profiles and the REST API
|
|
193
|
+
|
|
194
|
+
Profiles, proxies, fingerprints, extensions and account limits are all typed
|
|
195
|
+
calls on the client. You'll mostly use profiles: a profile is a saved browser,
|
|
196
|
+
so every session started on it gets the same fingerprint, proxy and cookies.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
await using client = new Surfsky();
|
|
200
|
+
const profile = await client.profiles.create({
|
|
201
|
+
title: "account-1",
|
|
202
|
+
fingerprint: { os: "win", os_arch: "x86", os_version: "11" },
|
|
203
|
+
proxy: { tier: "premium", country: "us" },
|
|
204
|
+
});
|
|
205
|
+
await using browser = await client.browser({ profileUuid: profile.uuid });
|
|
206
|
+
await browser.goto("https://example.com/login");
|
|
207
|
+
// log in once, the cookies stay with the profile
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Next time, `client.browser({ profileUuid })` brings back the same browser,
|
|
211
|
+
still logged in. `client.profiles.iterAll()` lists your profiles and
|
|
212
|
+
`delete(uuid)` removes one.
|
|
213
|
+
|
|
214
|
+
Fields on request and response objects use the API's own names
|
|
215
|
+
(`internal_uuid`, `os_arch`, `storage_options`), so the
|
|
216
|
+
[API reference](https://docs.surfsky.io/api-reference) maps 1:1. For endpoints
|
|
217
|
+
the SDK doesn't cover, `client.request(method, path, options)` sends the
|
|
218
|
+
request and returns the raw fetch `Response`.
|
|
219
|
+
|
|
220
|
+
## API reference
|
|
221
|
+
|
|
222
|
+
Every browser method is async. `Browser` is a `Page` plus the connection: page
|
|
223
|
+
methods on it act on the session's first tab. Waits take `timeout` in
|
|
224
|
+
milliseconds, default 30 000, and throw `BrowserTimeoutError`.
|
|
225
|
+
|
|
226
|
+
### Client
|
|
227
|
+
|
|
228
|
+
`new Surfsky({ apiToken, baseUrl, timeout: 30_000, maxRetries: 3, backoff: 500, headers, logger, fetch })`.
|
|
229
|
+
|
|
230
|
+
| Method | Description |
|
|
231
|
+
| ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
232
|
+
| `session({ profileUuid, ...options })` | Start a session. Returns the `Session` (`internal_uuid`, `ws_url`) with `stop()` and `await using` support. |
|
|
233
|
+
| `browser({ profileUuid, blockResources, blockUrls, connectTimeout, commandTimeout, ...options })` | Start a session and connect a `Browser`. `close()` stops both. |
|
|
234
|
+
| `browsers({ concurrency: "auto", blockResources, blockUrls, ...options })` | An open `BrowserPool`. `close()` stops every browser. |
|
|
235
|
+
| `map(handler, items, poolOptions)` | `browsers()` and `pool.map()` in one call. |
|
|
236
|
+
| `withOptions({ timeout, maxRetries, headers })` | Copy with overrides. Same fetch and logger. |
|
|
237
|
+
| `request(method, path, { json, params, body, headers, timeout })` | Raw call. Returns the fetch `Response`, never throws on status. |
|
|
238
|
+
| `close()` | Nothing to release; here for `await using`. |
|
|
239
|
+
|
|
240
|
+
Session options: `fingerprint`, `proxy`, `browser_settings`
|
|
241
|
+
(`inactive_kill_timeout`, `cache_enabled`, `cache_key`), `enable_chromedriver`,
|
|
242
|
+
`extensions` (up to 5 uuids), `proxy_blacklist`, `domain_routes`, `cookies`.
|
|
243
|
+
`fingerprint` and `cookies` apply to one-time sessions only. Requests are
|
|
244
|
+
validated with [zod](https://zod.dev) before they leave: an unknown key, a
|
|
245
|
+
bad range or an impossible proxy targeting throws `ValidationError`, so a typo
|
|
246
|
+
never starts a billed session.
|
|
247
|
+
|
|
248
|
+
`logger` takes `{ debug, info, warn, error }` (any subset). By default warnings
|
|
249
|
+
and errors go to the console; `logger: null` silences the SDK.
|
|
250
|
+
|
|
251
|
+
### Pool
|
|
252
|
+
|
|
253
|
+
| Member | Description |
|
|
254
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
255
|
+
| `pool.lease(fn)` | Runs `fn(browser)` on a live browser and hands it back after. Waits while all are busy. |
|
|
256
|
+
| `pool.map(handler, items)` | `handler(browser, item)` per item, `capacity` at a time. Returns `PoolOutcome` list in input order: `{ ok: true, item, index, value }` or `{ ok: false, item, index, error }`. Throw `StopRun` to end early. |
|
|
257
|
+
| `pool.capacity` | Max live browsers. `"auto"` is the plan's limit, `SURFSKY_MAX_BROWSERS` overrides it. |
|
|
258
|
+
| `browser.data` | Per-browser object. Survives leases. |
|
|
259
|
+
| `browser.useCount` | Leases so far, current included. |
|
|
260
|
+
| `browser.retire()` | Replace this browser with a fresh one after the lease. |
|
|
261
|
+
| `browser.internalUuid` | Session id. |
|
|
262
|
+
| `browser.connected` | Socket is up. |
|
|
263
|
+
|
|
264
|
+
The plan limit counts browsers started elsewhere with the same token. `lease()`
|
|
265
|
+
waits for one of its own and throws `RateLimitError` only if it has none.
|
|
266
|
+
|
|
267
|
+
### Navigation
|
|
268
|
+
|
|
269
|
+
| Method | Description |
|
|
270
|
+
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
271
|
+
| `goto(url, { waitUntil: "load", timeout })` | Navigate. `waitUntil`: `commit`, `domcontentloaded`, `load`, `networkidle`. Follows redirects. |
|
|
272
|
+
| `reload({ waitUntil, timeout })` | Reload. |
|
|
273
|
+
| `goBack({ timeout })`, `goForward({ timeout })` | Returns the new URL, `null` at the end of history. |
|
|
274
|
+
| `waitForLoadState(state = "load", { timeout })` | Wait for the current document to reach `state`. |
|
|
275
|
+
| `waitForUrl(fragment, { timeout })` | Wait until the URL contains `fragment`. Returns the URL. |
|
|
276
|
+
| `status` | HTTP status of the current document. Set even when `goto` throws. |
|
|
277
|
+
|
|
278
|
+
### Reading
|
|
279
|
+
|
|
280
|
+
| Method | Description |
|
|
281
|
+
| ------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
|
|
282
|
+
| `url()`, `title()` | Current URL and title. |
|
|
283
|
+
| `content()` | Full HTML. |
|
|
284
|
+
| `outerHtml(selector)` | HTML of the first match, `null` if none. |
|
|
285
|
+
| `innerText(selector)`, `allInnerTexts(selector)` | Rendered text of the first match, or of every match. Runs script in the isolated world. |
|
|
286
|
+
| `getAttribute(selector, name)` | `null` if missing. |
|
|
287
|
+
| `count(selector)` | Number of matches. |
|
|
288
|
+
| `isVisible(selector)` | First match has a bounding box. |
|
|
289
|
+
| `waitForSelector(selector, { visible: true, timeout })` | Wait for the element, visible by default. |
|
|
290
|
+
| `screenshot({ selector, fullPage, format: "png", quality })` | `Uint8Array`. Viewport, one element or the full page. `format`: `png`, `jpeg`, `webp`. |
|
|
291
|
+
|
|
292
|
+
### Input
|
|
293
|
+
|
|
294
|
+
Server-side human emulation. The first CSS match is used. `click`, `dblclick`
|
|
295
|
+
and `hover` also take `waitForVisible`, `scrollIntoView`, `preDelay`,
|
|
296
|
+
`postDelay`, `timeout`.
|
|
297
|
+
|
|
298
|
+
| Method | Description |
|
|
299
|
+
| -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
300
|
+
| `click(selector, { button, clickCount, modifiers })` | `button`: `left`, `right`, `middle`. `modifiers`: `Alt`, `Control`, `Meta`, `Shift`. Waits up to 30s for the element. |
|
|
301
|
+
| `dblclick(selector, ...)` | Double-click. |
|
|
302
|
+
| `hover(selector)` | Move the mouse over it. |
|
|
303
|
+
| `type(selector, text)` | Click, then type after the existing text. |
|
|
304
|
+
| `fill(selector, text)` | Select the existing text, then type over it. |
|
|
305
|
+
| `selectOption(selector, value)` / `selectOption(selector, { label })` | Pick an `<option>` by value or label. Returns the value. |
|
|
306
|
+
| `scroll({ deltaX, deltaY, duration })` | Animated scroll. |
|
|
307
|
+
| `scrollIntoView(selector, { behavior })`, `scrollTo({ x, y, behavior })` | `behavior`: `smooth`, `instant`. |
|
|
308
|
+
| `keyboard.type(text)`, `keyboard.press(key, { modifiers, delay })` | Keys to the focused element. `press("Enter")` submits a form; older pods need the button. |
|
|
309
|
+
| `mouse.move(x, y)`, `mouse.click(x, y)`, `mouse.down(x, y)`, `mouse.up(x, y)`, `mouse.wheel({ deltaX, deltaY })`, `mouse.drag({ startX, startY, endX, endY })` | Viewport coordinates. |
|
|
310
|
+
|
|
311
|
+
### Script
|
|
312
|
+
|
|
313
|
+
| Method | Description |
|
|
314
|
+
| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
315
|
+
| `evaluate(expression, { args, isolated: true, awaitPromise: true })` | Run JS. A function, or a string that looks like one, is called with `args` as JSON; anything else is an expression. Isolated world by default. |
|
|
316
|
+
| `waitForFunction(expression, { args, isolated, timeout })` | Poll until truthy. Returns the value. |
|
|
317
|
+
| `send(method, params)` | Raw page-level CDP command. |
|
|
318
|
+
| `browser.cdp` | Raw browser-level client: `send`, `post`, `on`. |
|
|
319
|
+
|
|
320
|
+
### Cookies and storage
|
|
321
|
+
|
|
322
|
+
| Method | Description |
|
|
323
|
+
| ----------------------------------------------- | --------------------------------- |
|
|
324
|
+
| `cookies()` | All cookies, `httpOnly` included. |
|
|
325
|
+
| `setCookies(cookies)` | Cookie objects. |
|
|
326
|
+
| `clearCookies()` | Remove every cookie. |
|
|
327
|
+
| `localStorage()`, `setLocalStorage(values)` | Current origin, as an object. |
|
|
328
|
+
| `sessionStorage()`, `setSessionStorage(values)` | Same for sessionStorage. |
|
|
329
|
+
|
|
330
|
+
### Network
|
|
331
|
+
|
|
332
|
+
| Method | Description |
|
|
333
|
+
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
334
|
+
| `captureResponses(...fragments)` | Record responses whose URL contains a fragment. Call before navigating. |
|
|
335
|
+
| `waitForResponse(fragment, { timeout })` | First captured match. `CapturedResponse`: `url`, `status`, `headers`, `body` (`Uint8Array`), `text`, `json()`. |
|
|
336
|
+
| `responses` | Everything captured, oldest first. |
|
|
337
|
+
| `stopCapturing()` | Drop captures, stop recording. |
|
|
338
|
+
|
|
339
|
+
### Dialogs
|
|
340
|
+
|
|
341
|
+
`page.onDialog = (kind, message) => ...`. `kind`: `alert`, `confirm`, `prompt`,
|
|
342
|
+
`beforeunload`. Return `true` to accept, `false` to dismiss, a string to answer
|
|
343
|
+
a prompt, `undefined` for the default. Default: dismiss, except `beforeunload`
|
|
344
|
+
is accepted.
|
|
345
|
+
|
|
346
|
+
### Pages
|
|
347
|
+
|
|
348
|
+
| Member | Description |
|
|
349
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------ |
|
|
350
|
+
| `browser.pages` | Every open page. The browser's own first, newest last. |
|
|
351
|
+
| `browser.newPage()` | Blank page in a new window. |
|
|
352
|
+
| `browser.waitForPage(action, { timeout })` | Await `action` (a click promise, or a function) and return the page it opened. |
|
|
353
|
+
| `page.close()` | Close the tab. On the browser itself: close the connection. |
|
|
354
|
+
| `page.closed` | `true` once gone. Commands then throw `PageClosedError`. |
|
|
355
|
+
| `page.bringToFront()` | Make it the visible tab. Screenshots of hidden tabs hang. |
|
|
356
|
+
| `page.targetId` | CDP target id. |
|
|
357
|
+
|
|
358
|
+
### REST
|
|
359
|
+
|
|
360
|
+
| Namespace | Methods |
|
|
361
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
362
|
+
| `client.profiles` | `startOneTime(options)`, `start(uuid, options)`, `stop(session)`, `stopAll()`, `listActive()`, `create({ title, fingerprint, description, proxy, cookies, storage_options })`, `get(uuid)`, `update(uuid, fields)`, `delete(uuid)`, `deleteMany(uuids)`, `listPage({ page, page_len, ordering })`, `iterAll({ page_len, ordering })`, `exportCookies(uuid, { export_format })`, `importCookies(uuid, cookies)`, `scrape(session, url, { screenshot, wait, wait_until, wait_for, human_actions })` |
|
|
363
|
+
| `client.proxies` | `countries()`, `regions(country)`, `cities(country, region)`, `quota()`, `premiumStats()`, `sharedCountries()`, `sharedQuota()`, `sharedStats()`. The first four need a premium provider on the account. |
|
|
364
|
+
| `client.fingerprints` | `renderers(os, osArch)`, `screens(os, osArch)`, `deviceModels({ os, os_arch, os_version, device_type })` |
|
|
365
|
+
| `client.extensions` | `upload(file, name)` (path, bytes or Blob, zip up to 100 MB), `listAll()`, `get(uuid)`, `update(uuid, { name })`, `delete(uuid)` |
|
|
366
|
+
| `client.account` | `sessionLimits()`, `browserLimits()`, `maxBrowsers()` |
|
|
367
|
+
|
|
368
|
+
### Errors
|
|
369
|
+
|
|
370
|
+
All subclasses of `SurfskyError`. `ValidationError` for a request that failed
|
|
371
|
+
validation before it was sent (`issues` lists the fields). HTTP: `APIError`
|
|
372
|
+
subclasses named after the status (`NotFoundError`, `RateLimitError`, ...) with
|
|
373
|
+
`statusCode`, `code`, `body`, `requestId`, `retryAfter` (ms). Browser:
|
|
374
|
+
`CDPError`, `BrowserTimeoutError`, `PageClosedError`. Idempotent requests retry
|
|
375
|
+
on 429, 5xx and connection errors. POST and PATCH retry on a 429 or a failure
|
|
376
|
+
that proves the request never left (a refused connection, a DNS error), so a
|
|
377
|
+
lost reply can't start a second billed session.
|
|
378
|
+
|
|
379
|
+
## Examples
|
|
380
|
+
|
|
381
|
+
More examples in [`examples/`](https://github.com/surfskyio/surfsky-js/tree/main/examples). They import `surfsky` by name:
|
|
382
|
+
|
|
383
|
+
```sh
|
|
384
|
+
bun run examples/one_time.ts # Bun resolves it to src/ directly
|
|
385
|
+
bun run build && node examples/one_time.ts # Node 24 runs .ts (and `await using`) natively, resolves dist/
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
On Node 22 compile the examples first (`await using` is syntax Node 22 lacks):
|
|
389
|
+
`npx tsc -p tsconfig.json --noEmit false --outDir out` and run the output.
|
|
390
|
+
|
|
391
|
+
The Playwright, Puppeteer and Selenium examples need their dev dependencies
|
|
392
|
+
(`bun install` brings them in). Run `playwright_connect.ts` on Node: under Bun,
|
|
393
|
+
Playwright's own WebSocket transport opens the socket and then never sends a
|
|
394
|
+
command, so `connectOverCDP` times out. Bun runs the SDK and the Puppeteer
|
|
395
|
+
example fine.
|
|
396
|
+
|
|
397
|
+
## Development
|
|
398
|
+
|
|
399
|
+
```sh
|
|
400
|
+
bun install
|
|
401
|
+
bun run check # lint, typecheck, tests
|
|
402
|
+
bun run build
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Live tests start real sessions and bill your account:
|
|
406
|
+
|
|
407
|
+
```sh
|
|
408
|
+
SURFSKY_LIVE_TESTS=1 SURFSKY_API_TOKEN=... SURFSKY_API_BASE_URL=... bun run test:live
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## Releasing
|
|
412
|
+
|
|
413
|
+
One-time, GitHub:
|
|
414
|
+
|
|
415
|
+
```sh
|
|
416
|
+
gh repo create surfskyio/surfsky-js --public --source=. --remote=origin --push
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
CI (`.github/workflows/ci.yml`) then runs on every push and PR: lint, typecheck,
|
|
420
|
+
tests and a build on Node 22, 24 and 26, plus the same on Bun.
|
|
421
|
+
|
|
422
|
+
One-time, npm: publishing runs from `.github/workflows/release.yml` with no npm
|
|
423
|
+
token, so it needs [trusted
|
|
424
|
+
publishing](https://docs.npmjs.com/trusted-publishers) configured for the
|
|
425
|
+
package — provider GitHub Actions, repository `surfskyio/surfsky-js`, workflow
|
|
426
|
+
`release.yml`, environment `npm`, publish allowed. A trusted publisher is a
|
|
427
|
+
setting *on a package*, so the first version goes out by hand:
|
|
428
|
+
|
|
429
|
+
```sh
|
|
430
|
+
npm login
|
|
431
|
+
bun run check && bun run build
|
|
432
|
+
npm publish # no --provenance: that needs the OIDC token only CI has
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
Then attach the publisher, on npmjs.com under the package's settings or, with
|
|
436
|
+
npm new enough to have the command, from the shell:
|
|
437
|
+
|
|
438
|
+
```sh
|
|
439
|
+
npm trust github surfsky --repo surfskyio/surfsky-js --file release.yml \
|
|
440
|
+
--env npm --allow-publish
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
The alternative to all of it is a granular access token in the repo as
|
|
444
|
+
`NPM_TOKEN`, with `NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}` added to the
|
|
445
|
+
publish step.
|
|
446
|
+
|
|
447
|
+
Two things bite here: npm never validates a trusted-publisher config when you
|
|
448
|
+
save it, so a wrong workflow name surfaces only as a failed publish, and
|
|
449
|
+
`repository.url` in `package.json` has to match the GitHub repository exactly.
|
|
450
|
+
|
|
451
|
+
The `npm` environment is also where a required reviewer goes, if a release
|
|
452
|
+
should wait for an approval.
|
|
453
|
+
|
|
454
|
+
Every release after that is a tag:
|
|
455
|
+
|
|
456
|
+
```sh
|
|
457
|
+
# bump "version" in package.json first
|
|
458
|
+
bun run check && bun run build
|
|
459
|
+
git commit -am "release: v0.1.0"
|
|
460
|
+
git tag v0.1.0
|
|
461
|
+
git push origin main --tags
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
The workflow refuses to publish when the tag and `package.json` disagree, then
|
|
465
|
+
runs `bun run check`, builds, imports `dist/index.js` once to prove the bundle
|
|
466
|
+
loads, and publishes. `files` limits the tarball to `dist/`, `README.md`,
|
|
467
|
+
`LICENSE` and `package.json` - check it with `npm pack --dry-run`.
|
|
468
|
+
|
|
469
|
+
## License
|
|
470
|
+
|
|
471
|
+
MIT
|