surfsky 0.0.1 → 0.0.3
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 +17 -66
- package/dist/index.js +21 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,7 +47,9 @@ import { Surfsky } from "surfsky";
|
|
|
47
47
|
// or new Surfsky({ apiToken: "...", baseUrl: "..." }) instead of env vars
|
|
48
48
|
await using client = new Surfsky();
|
|
49
49
|
// starts a session, stops it when the block ends so it doesn't keep billing
|
|
50
|
-
await using browser = await client.browser({
|
|
50
|
+
await using browser = await client.browser({
|
|
51
|
+
proxy: { tier: "premium", country: "us" },
|
|
52
|
+
});
|
|
51
53
|
await browser.goto("https://duckduckgo.com", { waitUntil: "domcontentloaded" });
|
|
52
54
|
await browser.type('[name="q"]', "surfsky cloud browser");
|
|
53
55
|
await browser.click("#searchbox_homepage button[type=submit]");
|
|
@@ -125,7 +127,8 @@ If you need more control, write your own loop on top of the pool:
|
|
|
125
127
|
|
|
126
128
|
```ts
|
|
127
129
|
await using pool = await client.browsers();
|
|
128
|
-
await pool.lease(async (browser) => {
|
|
130
|
+
await pool.lease(async (browser) => {
|
|
131
|
+
// waits for a free browser
|
|
129
132
|
await browser.goto("https://example.com");
|
|
130
133
|
console.log(await browser.title());
|
|
131
134
|
});
|
|
@@ -179,12 +182,14 @@ built-in pools:
|
|
|
179
182
|
import { ProxyCycle, ProxyTemplate } from "surfsky";
|
|
180
183
|
|
|
181
184
|
proxy = { tier: "premium", country: "us", region: "ny", type: "mobile" }; // Surfsky premium
|
|
182
|
-
proxy = { tier: "shared", country: "us" };
|
|
183
|
-
proxy = { country: "de" };
|
|
184
|
-
proxy = "socks5://user:pass@host:1080";
|
|
185
|
-
proxy = new ProxyCycle(myProxies);
|
|
186
|
-
proxy = new ProxyTemplate(
|
|
187
|
-
|
|
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
|
|
188
193
|
```
|
|
189
194
|
|
|
190
195
|
`client.proxies` lists available countries, regions and cities, plus your quota.
|
|
@@ -277,6 +282,10 @@ waits for one of its own and throws `RateLimitError` only if it has none.
|
|
|
277
282
|
|
|
278
283
|
### Reading
|
|
279
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
|
+
|
|
280
289
|
| Method | Description |
|
|
281
290
|
| ------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
|
|
282
291
|
| `url()`, `title()` | Current URL and title. |
|
|
@@ -408,64 +417,6 @@ Live tests start real sessions and bill your account:
|
|
|
408
417
|
SURFSKY_LIVE_TESTS=1 SURFSKY_API_TOKEN=... SURFSKY_API_BASE_URL=... bun run test:live
|
|
409
418
|
```
|
|
410
419
|
|
|
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
420
|
## License
|
|
470
421
|
|
|
471
422
|
MIT
|
package/dist/index.js
CHANGED
|
@@ -865,6 +865,9 @@ const WORLD_NAME = "utility";
|
|
|
865
865
|
/** A dialog is answered after this long, as a person would. */
|
|
866
866
|
const DIALOG_DELAY = [600, 1400];
|
|
867
867
|
const FUNCTION = /^\s*(async\s+)?(function\b|\([^()]*\)\s*=>|[\w$]+\s*=>)/;
|
|
868
|
+
function asXPath(s) {
|
|
869
|
+
return /^\s*(xpath=|\/\/|\.\.)/.test(s) ? s.trim().replace(/^xpath=/, "") : void 0;
|
|
870
|
+
}
|
|
868
871
|
const SELECT = `(selector, value, label) => {
|
|
869
872
|
const el = document.querySelector(selector);
|
|
870
873
|
if (!el) return null;
|
|
@@ -1329,6 +1332,8 @@ var Page = class extends Actions {
|
|
|
1329
1332
|
}
|
|
1330
1333
|
async count(selector) {
|
|
1331
1334
|
const document = await this.send("DOM.getDocument", { depth: 0 });
|
|
1335
|
+
const xpath = asXPath(selector);
|
|
1336
|
+
if (xpath !== void 0) return (await this.#search(xpath)).length;
|
|
1332
1337
|
return ((await this.send("DOM.querySelectorAll", {
|
|
1333
1338
|
nodeId: document.root.nodeId,
|
|
1334
1339
|
selector
|
|
@@ -1371,11 +1376,26 @@ var Page = class extends Actions {
|
|
|
1371
1376
|
}
|
|
1372
1377
|
async #nodeId(selector) {
|
|
1373
1378
|
const document = await this.send("DOM.getDocument", { depth: 0 });
|
|
1379
|
+
const xpath = asXPath(selector);
|
|
1380
|
+
if (xpath !== void 0) return (await this.#search(xpath))[0];
|
|
1374
1381
|
return (await this.send("DOM.querySelector", {
|
|
1375
1382
|
nodeId: document.root.nodeId,
|
|
1376
1383
|
selector
|
|
1377
1384
|
})).nodeId || void 0;
|
|
1378
1385
|
}
|
|
1386
|
+
async #search(query) {
|
|
1387
|
+
const { searchId, resultCount = 0 } = await this.send("DOM.performSearch", { query });
|
|
1388
|
+
try {
|
|
1389
|
+
if (!resultCount) return [];
|
|
1390
|
+
return (await this.send("DOM.getSearchResults", {
|
|
1391
|
+
searchId,
|
|
1392
|
+
fromIndex: 0,
|
|
1393
|
+
toIndex: resultCount
|
|
1394
|
+
})).nodeIds ?? [];
|
|
1395
|
+
} finally {
|
|
1396
|
+
await this.send("DOM.discardSearchResults", { searchId }).catch(() => void 0);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1379
1399
|
/** Wait until the URL contains the fragment. Returns the URL. */
|
|
1380
1400
|
async waitForUrl(fragment, options = {}) {
|
|
1381
1401
|
const deadline = new Deadline(options.timeout ?? 3e4, `the page did not reach '${fragment}'`);
|
|
@@ -2598,7 +2618,7 @@ var Proxies = class {
|
|
|
2598
2618
|
};
|
|
2599
2619
|
//#endregion
|
|
2600
2620
|
//#region src/version.ts
|
|
2601
|
-
const VERSION = "0.0.
|
|
2621
|
+
const VERSION = "0.0.3";
|
|
2602
2622
|
//#endregion
|
|
2603
2623
|
//#region src/client.ts
|
|
2604
2624
|
function userAgent() {
|