surfsky 0.0.3 → 0.0.5

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
@@ -2,419 +2,134 @@
2
2
 
3
3
  TypeScript SDK for [Surfsky](https://surfsky.io), a cloud-based antidetect browser.
4
4
 
5
- ![Node 22+](https://img.shields.io/badge/node-22%2B-blue)
6
- ![Bun](https://img.shields.io/badge/bun-1.x-blue)
7
- ![License: MIT](https://img.shields.io/badge/license-MIT-green)
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
5
+ ## Install
23
6
 
24
7
  ```sh
25
8
  npm install surfsky
26
9
  ```
27
10
 
28
- or `bun add surfsky`, `pnpm add surfsky`. Requires Node 22 or newer, or Bun.
29
- ESM only.
11
+ Or `bun add surfsky` / `pnpm add surfsky`. ESM only; requires Node 22+ or Bun.
30
12
 
31
13
  ## Quick start
32
14
 
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:
15
+ Get your API token and base URL from the [dashboard](https://app.surfsky.io):
36
16
 
37
17
  ```sh
38
- export SURFSKY_API_TOKEN=...
39
- export SURFSKY_API_BASE_URL=...
18
+ export SURFSKY_API_TOKEN='your-token'
19
+ export SURFSKY_API_BASE_URL='your-base-url'
40
20
  ```
41
21
 
42
- Then start a browser:
43
-
44
22
  ```ts
45
23
  import { Surfsky } from "surfsky";
46
24
 
47
- // or new Surfsky({ apiToken: "...", baseUrl: "..." }) instead of env vars
48
25
  await using client = new Surfsky();
49
- // starts a session, stops it when the block ends so it doesn't keep billing
50
26
  await using browser = await client.browser({
51
27
  proxy: { tier: "premium", country: "us" },
52
28
  });
53
- await browser.goto("https://duckduckgo.com", { waitUntil: "domcontentloaded" });
54
- await browser.type('[name="q"]', "surfsky cloud browser");
55
- await browser.click("#searchbox_homepage button[type=submit]");
56
- console.log(await browser.waitForUrl("?q="));
29
+ await browser.goto("https://example.com");
30
+ console.log(await browser.title());
57
31
  ```
58
32
 
59
- `await using` needs Node 24+ or Bun (TypeScript compiles it down for Node 22).
60
- Without it, `try { ... } finally { await browser.close() }` does the same.
61
-
62
- More examples in [`examples/`](https://github.com/surfskyio/surfsky-js/tree/main/examples).
33
+ `await using` closes the browser and stops its session when the scope ends.
34
+ It requires Node 24+ or Bun; compile TypeScript for Node 22, or use
35
+ `try/finally` with `await browser.close()`. Sessions are billed until stopped,
36
+ including idle time.
63
37
 
64
- ## Browser automation
38
+ You can also pass `apiToken` and `baseUrl` to `new Surfsky()`.
39
+ Browser methods are async. SDK timeouts are in milliseconds; cloud input
40
+ timing options use the server's units. Input uses Surfsky's
41
+ [human emulation](https://docs.surfsky.io/human_emulation).
65
42
 
66
- The API sticks to Playwright's names and semantics where it can: `click`,
67
- `fill`, `hover`, `waitForSelector`, `innerText`, `selectOption`,
68
- `keyboard.press`, `mouse.move` and so on. Input goes through Surfsky's
69
- [human emulation](https://docs.surfsky.io/human_emulation). Reads use plain CDP
70
- and run no JavaScript in the page.
43
+ ## Profiles and proxies
71
44
 
72
- When you need JavaScript:
45
+ A profile preserves its fingerprint, proxy and cookies across sessions:
73
46
 
74
47
  ```ts
75
- await browser.evaluate("document.title");
76
- await browser.evaluate("(a, b) => a + b", { args: [1, 2] });
77
- await browser.evaluate(() => document.title); // a function is stringified
48
+ const profile = await client.profiles.create({
49
+ title: "account-1",
50
+ fingerprint: { os: "win", os_arch: "x86", os_version: "11" },
51
+ proxy: { tier: "premium", country: "us" },
52
+ });
53
+ await using saved = await client.browser({ profileUuid: profile.uuid });
54
+ await saved.goto("https://example.com/login");
78
55
  ```
79
56
 
80
- Scripts run in an isolated world the page can't see. Pass `isolated: false` to
81
- use the page's own context.
82
-
83
- Also useful:
57
+ Reuse the profile ID for subsequent sessions. `proxy` accepts a premium or
58
+ shared configuration, or your own proxy URL. Premium supports residential and
59
+ mobile IPs; shared is for testing. `client.proxies` lists locations and quota.
84
60
 
85
- - `client.browser({ blockResources: ["image", "font", "media"] })` skips those
86
- downloads and saves proxy traffic.
87
- - Grab the JSON a page fetches instead of parsing its HTML:
61
+ SDK methods and options use camelCase. API fields keep snake_case, such as
62
+ `internal_uuid`, `os_arch` and `storage_options`.
88
63
 
89
- ```ts
90
- await browser.captureResponses("/api/search");
91
- await browser.goto(url);
92
- const data = (await browser.waitForResponse("/api/search")).json();
93
- ```
64
+ ## Parallel browsers
94
65
 
95
- - Work with several pages at once. `browser.pages` has every open tab, popups
96
- included, and `newPage()` adds one:
97
-
98
- ```ts
99
- await browser.newPage();
100
- await browser.pages[1].goto("https://example.com");
101
- console.log(await browser.pages[0].title(), await browser.pages[1].title());
102
- ```
103
-
104
- Every method is listed in the [API reference](#api-reference).
105
-
106
- ## Running multiple browsers
107
-
108
- `client.map` runs a function over a list of items in parallel, one browser per
109
- item, and collects the results:
66
+ `client.map` distributes items across a browser pool:
110
67
 
111
68
  ```ts
69
+ import type { Browser } from "surfsky";
70
+
112
71
  async function title(browser: Browser, url: string) {
113
72
  await browser.goto(url);
114
73
  return browser.title();
115
74
  }
116
75
 
117
- for (const o of await client.map(title, urls)) {
118
- console.log(o.item, o.ok ? o.value : o.error);
76
+ const urls = ["https://example.com", "https://example.org"];
77
+ for (const result of await client.map(title, urls, { concurrency: 2 })) {
78
+ console.log(result.item, result.ok ? result.value : result.error);
119
79
  }
120
80
  ```
121
81
 
122
- Errors land in `o.error` instead of throwing, so one bad page doesn't stop the
123
- run. By default the pool uses every browser your plan allows
124
- (`concurrency: "auto"`). Pass `concurrency: 5` to cap it.
82
+ Without a concurrency limit, the pool uses your plan's maximum. Results include
83
+ per-item errors.
125
84
 
126
- If you need more control, write your own loop on top of the pool:
85
+ To use a browser from the pool:
127
86
 
128
87
  ```ts
129
88
  await using pool = await client.browsers();
130
89
  await pool.lease(async (browser) => {
131
- // waits for a free browser
132
90
  await browser.goto("https://example.com");
133
91
  console.log(await browser.title());
134
92
  });
135
93
  ```
136
94
 
137
- A browser keeps its fingerprint, proxy and cookies between leases.
138
- `browser.data` carries your own state across them, `browser.retire()` swaps in
139
- a fresh browser.
140
-
141
- Neither `map` nor `lease` retries: an error is reported and the run goes on.
142
- Retries belong to your handler, since only it knows a timeout from a 404. Loop
143
- around `lease` and `retire()` the browser first, so the next attempt gets a
144
- fresh browser: [`examples/retry.ts`](https://github.com/surfskyio/surfsky-js/blob/main/examples/retry.ts).
95
+ `lease()` waits for a free browser and returns it to the pool after the callback.
96
+ Cookies and browser state persist between leases. Handlers are not retried.
145
97
 
146
- ## Using Playwright or Puppeteer
98
+ ## Reference and examples
147
99
 
148
- You don't have to use the SDK's browser API. `client.session` starts a browser
149
- and gives you its WebSocket URL, so Playwright, Puppeteer or any other CDP
150
- client can connect to it. The SDK still handles profiles, proxies and the
151
- session lifecycle:
100
+ - [SDK API reference](https://github.com/surfskyio/surfsky-js/blob/main/docs/api.md)
101
+ - [REST API](https://docs.surfsky.io/api-reference)
102
+ - [Examples](https://github.com/surfskyio/surfsky-js/tree/main/examples): forms, tabs, retries, profiles and CDP connections.
152
103
 
153
- ```ts
154
- import { chromium } from "playwright-core";
155
- import { Surfsky } from "surfsky";
156
-
157
- await using client = new Surfsky();
158
- await using session = await client.session();
159
- const browser = await chromium.connectOverCDP(session.ws_url);
160
- const page = browser.contexts()[0].pages()[0];
161
- await page.goto("https://example.com");
162
- ```
104
+ `client.session()` exposes a WebSocket URL for external browser clients.
105
+ See the [Playwright](https://github.com/surfskyio/surfsky-js/blob/main/examples/playwright_connect.ts),
106
+ [Puppeteer](https://github.com/surfskyio/surfsky-js/blob/main/examples/puppeteer_connect.ts)
107
+ and [Selenium](https://github.com/surfskyio/surfsky-js/blob/main/examples/selenium_connect.ts) examples.
163
108
 
164
- Don't use stock Playwright or Puppeteer: both put back the traces this SDK
165
- avoids, `Runtime.enable` above all. The patched forks strip them and are
166
- drop-in replacements.
167
-
168
- Selenium works too (`enable_chromedriver: true`, see
169
- [`examples/selenium_connect.ts`](https://github.com/surfskyio/surfsky-js/blob/main/examples/selenium_connect.ts)) but isn't
170
- recommended: chromedriver is easy to detect.
171
-
172
- ## Proxies
173
-
174
- Every session start accepts `proxy`. Use your own, or one of Surfsky's
175
- built-in pools:
176
-
177
- - Premium: clean residential and mobile IPs, targeted by country, region,
178
- city, ASN or coordinates, with sticky sessions. Use it for production.
179
- - Shared: a pool for testing. Don't rely on it against sites that matter.
180
-
181
- ```ts
182
- import { ProxyCycle, ProxyTemplate } from "surfsky";
183
-
184
- proxy = { tier: "premium", country: "us", region: "ny", type: "mobile" }; // Surfsky premium
185
- proxy = { tier: "shared", country: "us" }; // Surfsky shared, for tests
186
- proxy = { country: "de" }; // premium if set up, else shared
187
- proxy = "socks5://user:pass@host:1080"; // your own
188
- proxy = new ProxyCycle(myProxies); // round-robin over your own list
189
- proxy = new ProxyTemplate(
190
- "http://user-sessid-{session}:pw@gate.example.com:7000",
191
- );
192
- proxy = async () => pickOne(); // any function, sync or async
193
- ```
194
-
195
- `client.proxies` lists available countries, regions and cities, plus your quota.
196
-
197
- ## Profiles and the REST API
198
-
199
- Profiles, proxies, fingerprints, extensions and account limits are all typed
200
- calls on the client. You'll mostly use profiles: a profile is a saved browser,
201
- so every session started on it gets the same fingerprint, proxy and cookies.
202
-
203
- ```ts
204
- await using client = new Surfsky();
205
- const profile = await client.profiles.create({
206
- title: "account-1",
207
- fingerprint: { os: "win", os_arch: "x86", os_version: "11" },
208
- proxy: { tier: "premium", country: "us" },
209
- });
210
- await using browser = await client.browser({ profileUuid: profile.uuid });
211
- await browser.goto("https://example.com/login");
212
- // log in once, the cookies stay with the profile
213
- ```
214
-
215
- Next time, `client.browser({ profileUuid })` brings back the same browser,
216
- still logged in. `client.profiles.iterAll()` lists your profiles and
217
- `delete(uuid)` removes one.
218
-
219
- Fields on request and response objects use the API's own names
220
- (`internal_uuid`, `os_arch`, `storage_options`), so the
221
- [API reference](https://docs.surfsky.io/api-reference) maps 1:1. For endpoints
222
- the SDK doesn't cover, `client.request(method, path, options)` sends the
223
- request and returns the raw fetch `Response`.
224
-
225
- ## API reference
226
-
227
- Every browser method is async. `Browser` is a `Page` plus the connection: page
228
- methods on it act on the session's first tab. Waits take `timeout` in
229
- milliseconds, default 30 000, and throw `BrowserTimeoutError`.
230
-
231
- ### Client
232
-
233
- `new Surfsky({ apiToken, baseUrl, timeout: 30_000, maxRetries: 3, backoff: 500, headers, logger, fetch })`.
234
-
235
- | Method | Description |
236
- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
237
- | `session({ profileUuid, ...options })` | Start a session. Returns the `Session` (`internal_uuid`, `ws_url`) with `stop()` and `await using` support. |
238
- | `browser({ profileUuid, blockResources, blockUrls, connectTimeout, commandTimeout, ...options })` | Start a session and connect a `Browser`. `close()` stops both. |
239
- | `browsers({ concurrency: "auto", blockResources, blockUrls, ...options })` | An open `BrowserPool`. `close()` stops every browser. |
240
- | `map(handler, items, poolOptions)` | `browsers()` and `pool.map()` in one call. |
241
- | `withOptions({ timeout, maxRetries, headers })` | Copy with overrides. Same fetch and logger. |
242
- | `request(method, path, { json, params, body, headers, timeout })` | Raw call. Returns the fetch `Response`, never throws on status. |
243
- | `close()` | Nothing to release; here for `await using`. |
244
-
245
- Session options: `fingerprint`, `proxy`, `browser_settings`
246
- (`inactive_kill_timeout`, `cache_enabled`, `cache_key`), `enable_chromedriver`,
247
- `extensions` (up to 5 uuids), `proxy_blacklist`, `domain_routes`, `cookies`.
248
- `fingerprint` and `cookies` apply to one-time sessions only. Requests are
249
- validated with [zod](https://zod.dev) before they leave: an unknown key, a
250
- bad range or an impossible proxy targeting throws `ValidationError`, so a typo
251
- never starts a billed session.
252
-
253
- `logger` takes `{ debug, info, warn, error }` (any subset). By default warnings
254
- and errors go to the console; `logger: null` silences the SDK.
255
-
256
- ### Pool
257
-
258
- | Member | Description |
259
- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
260
- | `pool.lease(fn)` | Runs `fn(browser)` on a live browser and hands it back after. Waits while all are busy. |
261
- | `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. |
262
- | `pool.capacity` | Max live browsers. `"auto"` is the plan's limit, `SURFSKY_MAX_BROWSERS` overrides it. |
263
- | `browser.data` | Per-browser object. Survives leases. |
264
- | `browser.useCount` | Leases so far, current included. |
265
- | `browser.retire()` | Replace this browser with a fresh one after the lease. |
266
- | `browser.internalUuid` | Session id. |
267
- | `browser.connected` | Socket is up. |
268
-
269
- The plan limit counts browsers started elsewhere with the same token. `lease()`
270
- waits for one of its own and throws `RateLimitError` only if it has none.
271
-
272
- ### Navigation
273
-
274
- | Method | Description |
275
- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------- |
276
- | `goto(url, { waitUntil: "load", timeout })` | Navigate. `waitUntil`: `commit`, `domcontentloaded`, `load`, `networkidle`. Follows redirects. |
277
- | `reload({ waitUntil, timeout })` | Reload. |
278
- | `goBack({ timeout })`, `goForward({ timeout })` | Returns the new URL, `null` at the end of history. |
279
- | `waitForLoadState(state = "load", { timeout })` | Wait for the current document to reach `state`. |
280
- | `waitForUrl(fragment, { timeout })` | Wait until the URL contains `fragment`. Returns the URL. |
281
- | `status` | HTTP status of the current document. Set even when `goto` throws. |
282
-
283
- ### Reading
284
-
285
- `selector` is CSS, or XPath when it starts with `//`, `..` or `xpath=`. XPath
286
- covers the DOM reads below and `screenshot({ selector })`; `innerText`,
287
- `allInnerTexts`, `selectOption` and the input methods take CSS only.
288
-
289
- | Method | Description |
290
- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
291
- | `url()`, `title()` | Current URL and title. |
292
- | `content()` | Full HTML. |
293
- | `outerHtml(selector)` | HTML of the first match, `null` if none. |
294
- | `innerText(selector)`, `allInnerTexts(selector)` | Rendered text of the first match, or of every match. Runs script in the isolated world. |
295
- | `getAttribute(selector, name)` | `null` if missing. |
296
- | `count(selector)` | Number of matches. |
297
- | `isVisible(selector)` | First match has a bounding box. |
298
- | `waitForSelector(selector, { visible: true, timeout })` | Wait for the element, visible by default. |
299
- | `screenshot({ selector, fullPage, format: "png", quality })` | `Uint8Array`. Viewport, one element or the full page. `format`: `png`, `jpeg`, `webp`. |
300
-
301
- ### Input
302
-
303
- Server-side human emulation. The first CSS match is used. `click`, `dblclick`
304
- and `hover` also take `waitForVisible`, `scrollIntoView`, `preDelay`,
305
- `postDelay`, `timeout`.
306
-
307
- | Method | Description |
308
- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
309
- | `click(selector, { button, clickCount, modifiers })` | `button`: `left`, `right`, `middle`. `modifiers`: `Alt`, `Control`, `Meta`, `Shift`. Waits up to 30s for the element. |
310
- | `dblclick(selector, ...)` | Double-click. |
311
- | `hover(selector)` | Move the mouse over it. |
312
- | `type(selector, text)` | Click, then type after the existing text. |
313
- | `fill(selector, text)` | Select the existing text, then type over it. |
314
- | `selectOption(selector, value)` / `selectOption(selector, { label })` | Pick an `<option>` by value or label. Returns the value. |
315
- | `scroll({ deltaX, deltaY, duration })` | Animated scroll. |
316
- | `scrollIntoView(selector, { behavior })`, `scrollTo({ x, y, behavior })` | `behavior`: `smooth`, `instant`. |
317
- | `keyboard.type(text)`, `keyboard.press(key, { modifiers, delay })` | Keys to the focused element. `press("Enter")` submits a form; older pods need the button. |
318
- | `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. |
319
-
320
- ### Script
321
-
322
- | Method | Description |
323
- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
324
- | `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. |
325
- | `waitForFunction(expression, { args, isolated, timeout })` | Poll until truthy. Returns the value. |
326
- | `send(method, params)` | Raw page-level CDP command. |
327
- | `browser.cdp` | Raw browser-level client: `send`, `post`, `on`. |
328
-
329
- ### Cookies and storage
330
-
331
- | Method | Description |
332
- | ----------------------------------------------- | --------------------------------- |
333
- | `cookies()` | All cookies, `httpOnly` included. |
334
- | `setCookies(cookies)` | Cookie objects. |
335
- | `clearCookies()` | Remove every cookie. |
336
- | `localStorage()`, `setLocalStorage(values)` | Current origin, as an object. |
337
- | `sessionStorage()`, `setSessionStorage(values)` | Same for sessionStorage. |
338
-
339
- ### Network
340
-
341
- | Method | Description |
342
- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
343
- | `captureResponses(...fragments)` | Record responses whose URL contains a fragment. Call before navigating. |
344
- | `waitForResponse(fragment, { timeout })` | First captured match. `CapturedResponse`: `url`, `status`, `headers`, `body` (`Uint8Array`), `text`, `json()`. |
345
- | `responses` | Everything captured, oldest first. |
346
- | `stopCapturing()` | Drop captures, stop recording. |
347
-
348
- ### Dialogs
349
-
350
- `page.onDialog = (kind, message) => ...`. `kind`: `alert`, `confirm`, `prompt`,
351
- `beforeunload`. Return `true` to accept, `false` to dismiss, a string to answer
352
- a prompt, `undefined` for the default. Default: dismiss, except `beforeunload`
353
- is accepted.
354
-
355
- ### Pages
356
-
357
- | Member | Description |
358
- | ------------------------------------------ | ------------------------------------------------------------------------------ |
359
- | `browser.pages` | Every open page. The browser's own first, newest last. |
360
- | `browser.newPage()` | Blank page in a new window. |
361
- | `browser.waitForPage(action, { timeout })` | Await `action` (a click promise, or a function) and return the page it opened. |
362
- | `page.close()` | Close the tab. On the browser itself: close the connection. |
363
- | `page.closed` | `true` once gone. Commands then throw `PageClosedError`. |
364
- | `page.bringToFront()` | Make it the visible tab. Screenshots of hidden tabs hang. |
365
- | `page.targetId` | CDP target id. |
366
-
367
- ### REST
368
-
369
- | Namespace | Methods |
370
- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
371
- | `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 })` |
372
- | `client.proxies` | `countries()`, `regions(country)`, `cities(country, region)`, `quota()`, `premiumStats()`, `sharedCountries()`, `sharedQuota()`, `sharedStats()`. The first four need a premium provider on the account. |
373
- | `client.fingerprints` | `renderers(os, osArch)`, `screens(os, osArch)`, `deviceModels({ os, os_arch, os_version, device_type })` |
374
- | `client.extensions` | `upload(file, name)` (path, bytes or Blob, zip up to 100 MB), `listAll()`, `get(uuid)`, `update(uuid, { name })`, `delete(uuid)` |
375
- | `client.account` | `sessionLimits()`, `browserLimits()`, `maxBrowsers()` |
376
-
377
- ### Errors
378
-
379
- All subclasses of `SurfskyError`. `ValidationError` for a request that failed
380
- validation before it was sent (`issues` lists the fields). HTTP: `APIError`
381
- subclasses named after the status (`NotFoundError`, `RateLimitError`, ...) with
382
- `statusCode`, `code`, `body`, `requestId`, `retryAfter` (ms). Browser:
383
- `CDPError`, `BrowserTimeoutError`, `PageClosedError`. Idempotent requests retry
384
- on 429, 5xx and connection errors. POST and PATCH retry on a 429 or a failure
385
- that proves the request never left (a refused connection, a DNS error), so a
386
- lost reply can't start a second billed session.
387
-
388
- ## Examples
389
-
390
- More examples in [`examples/`](https://github.com/surfskyio/surfsky-js/tree/main/examples). They import `surfsky` by name:
109
+ From the repository, run examples with Bun or Node 24+:
391
110
 
392
111
  ```sh
393
- bun run examples/one_time.ts # Bun resolves it to src/ directly
394
- bun run build && node examples/one_time.ts # Node 24 runs .ts (and `await using`) natively, resolves dist/
112
+ bun install
113
+ bun run examples/one_time.ts
114
+ # Or, on Node 24+:
115
+ bun run build && node examples/one_time.ts
395
116
  ```
396
117
 
397
- On Node 22 compile the examples first (`await using` is syntax Node 22 lacks):
398
- `npx tsc -p tsconfig.json --noEmit false --outDir out` and run the output.
399
-
400
- The Playwright, Puppeteer and Selenium examples need their dev dependencies
401
- (`bun install` brings them in). Run `playwright_connect.ts` on Node: under Bun,
402
- Playwright's own WebSocket transport opens the socket and then never sends a
403
- command, so `connectOverCDP` times out. Bun runs the SDK and the Puppeteer
404
- example fine.
118
+ On Node 22, compile the examples first. Run the Playwright example on Node;
119
+ its WebSocket connection can hang under Bun.
405
120
 
406
121
  ## Development
407
122
 
408
123
  ```sh
409
124
  bun install
410
- bun run check # lint, typecheck, tests
125
+ bun run check
411
126
  bun run build
412
127
  ```
413
128
 
414
- Live tests start real sessions and bill your account:
129
+ Live tests require credentials and bill your account:
415
130
 
416
131
  ```sh
417
- SURFSKY_LIVE_TESTS=1 SURFSKY_API_TOKEN=... SURFSKY_API_BASE_URL=... bun run test:live
132
+ SURFSKY_LIVE_TESTS=1 bun run test:live
418
133
  ```
419
134
 
420
135
  ## License
package/dist/index.d.ts CHANGED
@@ -1097,8 +1097,6 @@ declare abstract class Actions {
1097
1097
  click(selector: string, options?: ClickOptions): Promise<any>;
1098
1098
  dblclick(selector: string, options?: DblclickOptions): Promise<any>;
1099
1099
  hover(selector: string, options?: HoverOptions): Promise<any>;
1100
- type(selector: string, text: string): Promise<any>;
1101
- fill(selector: string, text: string): Promise<any>;
1102
1100
  scroll(options?: ScrollOptions): Promise<any>;
1103
1101
  scrollIntoView(selector: string, options?: {
1104
1102
  behavior?: ScrollBehavior;
@@ -1368,6 +1366,8 @@ declare class Page extends Actions {
1368
1366
  waitForFunction(expression: string | ((...args: any[]) => unknown), options?: EvaluateOptions & WaitOptions): Promise<any>;
1369
1367
  innerText(selector: string): Promise<string | null>;
1370
1368
  allInnerTexts(selector: string): Promise<string[]>;
1369
+ type(selector: string, text: string): Promise<any>;
1370
+ fill(selector: string, text: string): Promise<any>;
1371
1371
  getAttribute(selector: string, name: string): Promise<string | null>;
1372
1372
  count(selector: string): Promise<number>;
1373
1373
  selectOption(selector: string, option: string | {
package/dist/index.js CHANGED
@@ -85,14 +85,6 @@ var Actions = class {
85
85
  ...options
86
86
  }));
87
87
  }
88
- async type(selector, text) {
89
- await this.click(selector);
90
- return this.keyboard.type(text);
91
- }
92
- async fill(selector, text) {
93
- await this.click(selector, { clickCount: 3 });
94
- return this.keyboard.type(text);
95
- }
96
88
  scroll(options = {}) {
97
89
  return this.send("Human.scroll", cdpParams({ ...options }));
98
90
  }
@@ -1323,6 +1315,20 @@ var Page = class extends Actions {
1323
1315
  allInnerTexts(selector) {
1324
1316
  return this.evaluate("s => Array.from(document.querySelectorAll(s), e => e.innerText)", { args: [selector] });
1325
1317
  }
1318
+ async type(selector, text) {
1319
+ await this.click(selector);
1320
+ await this.#validateFocus(selector);
1321
+ return this.keyboard.type(text);
1322
+ }
1323
+ async fill(selector, text) {
1324
+ await this.click(selector, { clickCount: 3 });
1325
+ await this.#validateFocus(selector);
1326
+ if (text === "") return this.keyboard.press("Backspace");
1327
+ return this.keyboard.type(text);
1328
+ }
1329
+ async #validateFocus(selector) {
1330
+ if (!await this.evaluate("s => document.activeElement === document.querySelector(s)", { args: [selector] })) throw new Error(`'${selector}' did not take focus`);
1331
+ }
1326
1332
  async getAttribute(selector, name) {
1327
1333
  const nodeId = await this.#nodeId(selector);
1328
1334
  if (nodeId === void 0) return null;
@@ -2618,7 +2624,7 @@ var Proxies = class {
2618
2624
  };
2619
2625
  //#endregion
2620
2626
  //#region src/version.ts
2621
- const VERSION = "0.0.3";
2627
+ const VERSION = "0.0.5";
2622
2628
  //#endregion
2623
2629
  //#region src/client.ts
2624
2630
  function userAgent() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "surfsky",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "TypeScript SDK for the Surfsky antidetect browser cloud.",
5
5
  "keywords": [
6
6
  "surfsky",