@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 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 22+). */
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
  }
@@ -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 22+). */
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 22+):
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. Not clamped: the gateway still answers 400 under 60 s.
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
- return Math.ceil(ms / 1000);
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: ${JSON.stringify(data)}`);
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: ${JSON.stringify(data)}`);
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 22+):
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solarisdk/browser",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Official client SDK for Solari Browser.",
5
5
  "license": "MIT",
6
6
  "type": "module",