@solarisdk/browser 0.5.0 → 0.5.2
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 +1 -1
- package/dist/browser-session.d.ts +1 -1
- package/dist/browser-session.js +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +63 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ console.log(await page.title());
|
|
|
29
29
|
await browser.close();
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Or with explicit resource management (Node 22
|
|
32
|
+
Or with explicit resource management (compiled TypeScript 5.2+, or uncompiled on Node 24+; Node 22 rejects the syntax):
|
|
33
33
|
|
|
34
34
|
```ts
|
|
35
35
|
await using browser = await solari.launch({ stealth: true });
|
|
@@ -54,6 +54,6 @@ export declare class BrowserSession {
|
|
|
54
54
|
newPage(): Promise<Page>;
|
|
55
55
|
/** Close the browser and release the session. Idempotent. */
|
|
56
56
|
close(): Promise<void>;
|
|
57
|
-
/** Supports `await using browser = await solari.launch()` (Node
|
|
57
|
+
/** Supports `await using browser = await solari.launch()` (compiled TypeScript 5.2+, or uncompiled on Node 24+). */
|
|
58
58
|
[Symbol.asyncDispose](): Promise<void>;
|
|
59
59
|
}
|
package/dist/browser-session.js
CHANGED
|
@@ -101,7 +101,7 @@ export class BrowserSession {
|
|
|
101
101
|
if (browserErr)
|
|
102
102
|
throw browserErr;
|
|
103
103
|
}
|
|
104
|
-
/** Supports `await using browser = await solari.launch()` (Node
|
|
104
|
+
/** Supports `await using browser = await solari.launch()` (compiled TypeScript 5.2+, or uncompiled on Node 24+). */
|
|
105
105
|
async [Symbol.asyncDispose]() {
|
|
106
106
|
await this.close();
|
|
107
107
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -230,6 +230,18 @@ export declare class SolariError extends Error {
|
|
|
230
230
|
readonly code?: (SolariErrorCode | string) | undefined;
|
|
231
231
|
constructor(message: string, status?: number | undefined, cause?: unknown | undefined, code?: (SolariErrorCode | string) | undefined);
|
|
232
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* `text` with each of `secrets` (the session's ws / cdp endpoints) replaced by its redacted form, then any other
|
|
235
|
+
* URL-looking token cut to `scheme://host/…`: the Playwright driver prints the URL it dialled ("Call log: <ws
|
|
236
|
+
* connecting> ws://…/ws/<id>"), and that URL carries the session's credential.
|
|
237
|
+
*/
|
|
238
|
+
declare function scrubUrls(text: string, secrets?: string[]): string;
|
|
239
|
+
/** @internal Exposed for tests only. */
|
|
240
|
+
export declare const __testing: {
|
|
241
|
+
scrubUrls: typeof scrubUrls;
|
|
242
|
+
redactSessionPath: typeof redactSessionPath;
|
|
243
|
+
};
|
|
244
|
+
declare function redactSessionPath(text: string): string;
|
|
233
245
|
/**
|
|
234
246
|
* A profile editor launched by `solari.profiles.edit(id)`. Give `url` to the person who should
|
|
235
247
|
* sign in (or open it yourself). The editor starts when the person clicks "Open editor" on that page; it is only usable
|
|
@@ -341,7 +353,7 @@ export declare class Solari {
|
|
|
341
353
|
* await page.goto("https://example.com");
|
|
342
354
|
* await browser.close();
|
|
343
355
|
*
|
|
344
|
-
* Or with explicit resource management (Node
|
|
356
|
+
* Or with explicit resource management (compiled TypeScript 5.2+, or uncompiled on Node 24+):
|
|
345
357
|
*
|
|
346
358
|
* await using browser = await solari.launch({ stealth: true });
|
|
347
359
|
*/
|
package/dist/index.js
CHANGED
|
@@ -69,6 +69,51 @@ export class SolariError extends Error {
|
|
|
69
69
|
* `code`. Every REST call uses this so a caller can branch on `e.code` no matter
|
|
70
70
|
* which endpoint failed.
|
|
71
71
|
*/
|
|
72
|
+
/**
|
|
73
|
+
* `text` with the session id in every `/sessions/<id>` cut to its first 8 characters. The session id is
|
|
74
|
+
* the credential inside the ws/cdp URLs, so it must not appear in error messages or logs.
|
|
75
|
+
*/
|
|
76
|
+
/** Field names of a gateway response, never its values (they hold the session id and the ws / cdp URLs). */
|
|
77
|
+
function responseFields(data) {
|
|
78
|
+
return data && typeof data === "object" ? Object.keys(data).join(", ") || "none" : typeof data;
|
|
79
|
+
}
|
|
80
|
+
function redactUrl(url) {
|
|
81
|
+
try {
|
|
82
|
+
const u = new URL(url);
|
|
83
|
+
return `${u.protocol}//${u.host}/\u2026`;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return "<url>";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* `text` with each of `secrets` (the session's ws / cdp endpoints) replaced by its redacted form, then any other
|
|
91
|
+
* URL-looking token cut to `scheme://host/…`: the Playwright driver prints the URL it dialled ("Call log: <ws
|
|
92
|
+
* connecting> ws://…/ws/<id>"), and that URL carries the session's credential.
|
|
93
|
+
*/
|
|
94
|
+
function scrubUrls(text, secrets = []) {
|
|
95
|
+
let out = text;
|
|
96
|
+
for (const s of secrets)
|
|
97
|
+
if (s)
|
|
98
|
+
out = out.split(s).join(redactUrl(s));
|
|
99
|
+
return out.replace(/[a-z][a-z0-9+.-]*:\/\/[^\s"'<>)\],]+/gi, (m) => redactUrl(m));
|
|
100
|
+
}
|
|
101
|
+
/** @internal Exposed for tests only. */
|
|
102
|
+
export const __testing = { scrubUrls, redactSessionPath };
|
|
103
|
+
/** The error to surface from launch(): a foreign error (patchright's connect failure) gets its message scrubbed and its
|
|
104
|
+
* cause dropped, since both print the endpoint; SolariErrors only get a scrub of their message. */
|
|
105
|
+
function scrubbedLaunchError(err, secrets) {
|
|
106
|
+
if (err instanceof SolariError) {
|
|
107
|
+
err.message = scrubUrls(err.message, secrets);
|
|
108
|
+
return err;
|
|
109
|
+
}
|
|
110
|
+
if (err instanceof Error)
|
|
111
|
+
return new SolariError(scrubUrls(err.message, secrets));
|
|
112
|
+
return err;
|
|
113
|
+
}
|
|
114
|
+
function redactSessionPath(text) {
|
|
115
|
+
return text.replace(/(\/sessions\/)([^/?#\s]+)/g, (_m, prefix, id) => `${prefix}${id.length > 8 ? `${id.slice(0, 8)}\u2026` : id}`);
|
|
116
|
+
}
|
|
72
117
|
async function apiError(what, res) {
|
|
73
118
|
const body = await res.text().catch(() => "");
|
|
74
119
|
let code;
|
|
@@ -78,7 +123,7 @@ async function apiError(what, res) {
|
|
|
78
123
|
code = parsed.code;
|
|
79
124
|
}
|
|
80
125
|
catch { }
|
|
81
|
-
return new SolariError(`Solari ${what} failed: ${res.status} ${body}`, res.status, undefined, code);
|
|
126
|
+
return new SolariError(`Solari ${redactSessionPath(what)} failed: ${res.status} ${body}`, res.status, undefined, code);
|
|
82
127
|
}
|
|
83
128
|
const REGION_URLS = {
|
|
84
129
|
"us-west": "https://api.getsolari.com",
|
|
@@ -96,7 +141,8 @@ const DEFAULT_TIMEOUT_MS = 90_000;
|
|
|
96
141
|
/**
|
|
97
142
|
* The gateway's `timeoutSeconds` for a `sessionTimeoutMs`: rounded UP to whole
|
|
98
143
|
* seconds (1001 -> 2). Zero/undefined means "omit". Throws for a negative, NaN,
|
|
99
|
-
* Infinity or non-integer value
|
|
144
|
+
* Infinity or non-integer value, and for one whose seconds do not fit the wire
|
|
145
|
+
* field (int32), as the Go and Rust SDKs do. Not clamped: the gateway still answers 400 under 60 s.
|
|
100
146
|
*/
|
|
101
147
|
function sessionTimeoutSeconds(ms) {
|
|
102
148
|
if (ms === undefined || ms === 0)
|
|
@@ -104,7 +150,11 @@ function sessionTimeoutSeconds(ms) {
|
|
|
104
150
|
if (typeof ms !== "number" || !Number.isInteger(ms) || ms < 0) {
|
|
105
151
|
throw new SolariError(`Solari: sessionTimeoutMs must be a non-negative integer number of milliseconds, got ${String(ms)}`);
|
|
106
152
|
}
|
|
107
|
-
|
|
153
|
+
const seconds = Math.ceil(ms / 1000);
|
|
154
|
+
if (seconds > 2_147_483_647) {
|
|
155
|
+
throw new SolariError(`Solari: sessionTimeoutMs is too large, got ${String(ms)}`);
|
|
156
|
+
}
|
|
157
|
+
return seconds;
|
|
108
158
|
}
|
|
109
159
|
function isRetryableStatus(status) {
|
|
110
160
|
return status === 502 || status === 503 || status === 504 || status === 507;
|
|
@@ -437,7 +487,7 @@ class SessionsResource {
|
|
|
437
487
|
}
|
|
438
488
|
const data = (await res.json());
|
|
439
489
|
if (!data.sessionId || !data.wsEndpoint) {
|
|
440
|
-
throw new SolariError(`Solari: unexpected session response: ${
|
|
490
|
+
throw new SolariError(`Solari: unexpected session response (fields: ${responseFields(data)})`);
|
|
441
491
|
}
|
|
442
492
|
const cdpUpstream = data.cdpEndpoint ?? deriveCdpFromWs(data.wsEndpoint);
|
|
443
493
|
const localWsEndpoint = await this.client._wrapSessionEndpoint(data.sessionId, data.wsEndpoint, "ws");
|
|
@@ -472,12 +522,12 @@ class SessionsResource {
|
|
|
472
522
|
void this.client.request("DELETE", `/sessions/${encodeURIComponent(id)}`).then(async (res) => {
|
|
473
523
|
const rejection = await releaseRejection(res);
|
|
474
524
|
if (rejection) {
|
|
475
|
-
console.warn(`[Solari] DELETE /sessions/${id} returned ${res.status}: ` +
|
|
525
|
+
console.warn(`[Solari] ${redactSessionPath(`DELETE /sessions/${id}`)} returned ${res.status}: ` +
|
|
476
526
|
`${rejection.body} — session was NOT released; its pool ` +
|
|
477
527
|
`slot stays held until the server-side grace timer reaps it`);
|
|
478
528
|
}
|
|
479
529
|
}, (err) => {
|
|
480
|
-
console.warn(`[Solari] DELETE /sessions/${id} failed (fire-and-forget):`, err);
|
|
530
|
+
console.warn(`[Solari] ${redactSessionPath(`DELETE /sessions/${id}`)} failed (fire-and-forget):`, err);
|
|
481
531
|
});
|
|
482
532
|
}
|
|
483
533
|
async releaseAndWait(id) {
|
|
@@ -485,7 +535,7 @@ class SessionsResource {
|
|
|
485
535
|
const res = await this.client.request("DELETE", `/sessions/${encodeURIComponent(id)}`);
|
|
486
536
|
const rejection = await releaseRejection(res);
|
|
487
537
|
if (rejection) {
|
|
488
|
-
throw new SolariError(`Solari DELETE /sessions/${id} failed: ${res.status} ${rejection.body}`, res.status, undefined, rejection.code);
|
|
538
|
+
throw new SolariError(`Solari ${redactSessionPath(`DELETE /sessions/${id}`)} failed: ${res.status} ${rejection.body}`, res.status, undefined, rejection.code);
|
|
489
539
|
}
|
|
490
540
|
}
|
|
491
541
|
/** The gateway's current view of one session (`GET /sessions/:id`). */
|
|
@@ -524,7 +574,7 @@ class SessionsResource {
|
|
|
524
574
|
throw await apiError(`GET /sessions/${id}/replay-url`, res);
|
|
525
575
|
const data = (await res.json());
|
|
526
576
|
if (!data.url) {
|
|
527
|
-
throw new SolariError(`Solari: unexpected replay-url response: ${
|
|
577
|
+
throw new SolariError(`Solari: unexpected replay-url response (fields: ${responseFields(data)})`);
|
|
528
578
|
}
|
|
529
579
|
return {
|
|
530
580
|
url: data.url,
|
|
@@ -582,7 +632,7 @@ export class Solari {
|
|
|
582
632
|
* await page.goto("https://example.com");
|
|
583
633
|
* await browser.close();
|
|
584
634
|
*
|
|
585
|
-
* Or with explicit resource management (Node
|
|
635
|
+
* Or with explicit resource management (compiled TypeScript 5.2+, or uncompiled on Node 24+):
|
|
586
636
|
*
|
|
587
637
|
* await using browser = await solari.launch({ stealth: true });
|
|
588
638
|
*/
|
|
@@ -662,7 +712,7 @@ export class Solari {
|
|
|
662
712
|
await sleep(100 * attempt);
|
|
663
713
|
continue;
|
|
664
714
|
}
|
|
665
|
-
throw err;
|
|
715
|
+
throw scrubbedLaunchError(err, [session.wsEndpoint, session.cdpEndpoint]);
|
|
666
716
|
}
|
|
667
717
|
}
|
|
668
718
|
throw lastErr ?? new SolariError("Solari.launch: exhausted attempts");
|
|
@@ -704,7 +754,7 @@ export class Solari {
|
|
|
704
754
|
upstream = new URL(upstreamEndpoint);
|
|
705
755
|
}
|
|
706
756
|
catch (e) {
|
|
707
|
-
throw new SolariError(`Solari: invalid upstream ${kind}Endpoint: ${upstreamEndpoint}`, undefined, e);
|
|
757
|
+
throw new SolariError(`Solari: invalid upstream ${kind}Endpoint: ${redactUrl(upstreamEndpoint)}`, undefined, e);
|
|
708
758
|
}
|
|
709
759
|
await this.localProxy.start();
|
|
710
760
|
this.localProxy.register(sessionId, upstream, kind);
|
|
@@ -752,7 +802,7 @@ export class Solari {
|
|
|
752
802
|
if (!retryThis || attempt === maxAttempts)
|
|
753
803
|
return res;
|
|
754
804
|
await res.text().catch(() => { });
|
|
755
|
-
lastErr = new SolariError(`Solari ${method} ${path}: ${res.status}`, res.status);
|
|
805
|
+
lastErr = new SolariError(`Solari ${method} ${redactSessionPath(path)}: ${res.status}`, res.status);
|
|
756
806
|
}
|
|
757
807
|
catch (e) {
|
|
758
808
|
clearTimeout(timer);
|
|
@@ -764,6 +814,6 @@ export class Solari {
|
|
|
764
814
|
await sleep(this.backoffMs);
|
|
765
815
|
}
|
|
766
816
|
}
|
|
767
|
-
throw new SolariError(`Solari ${method} ${path}: exhausted ${maxAttempts} attempts`, undefined, lastErr);
|
|
817
|
+
throw new SolariError(`Solari ${method} ${redactSessionPath(path)}: exhausted ${maxAttempts} attempts`, undefined, lastErr);
|
|
768
818
|
}
|
|
769
819
|
}
|