@solarisdk/browser 0.5.0 → 0.5.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/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
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",
@@ -437,7 +482,7 @@ class SessionsResource {
437
482
  }
438
483
  const data = (await res.json());
439
484
  if (!data.sessionId || !data.wsEndpoint) {
440
- throw new SolariError(`Solari: unexpected session response: ${JSON.stringify(data)}`);
485
+ throw new SolariError(`Solari: unexpected session response (fields: ${responseFields(data)})`);
441
486
  }
442
487
  const cdpUpstream = data.cdpEndpoint ?? deriveCdpFromWs(data.wsEndpoint);
443
488
  const localWsEndpoint = await this.client._wrapSessionEndpoint(data.sessionId, data.wsEndpoint, "ws");
@@ -472,12 +517,12 @@ class SessionsResource {
472
517
  void this.client.request("DELETE", `/sessions/${encodeURIComponent(id)}`).then(async (res) => {
473
518
  const rejection = await releaseRejection(res);
474
519
  if (rejection) {
475
- console.warn(`[Solari] DELETE /sessions/${id} returned ${res.status}: ` +
520
+ console.warn(`[Solari] ${redactSessionPath(`DELETE /sessions/${id}`)} returned ${res.status}: ` +
476
521
  `${rejection.body} — session was NOT released; its pool ` +
477
522
  `slot stays held until the server-side grace timer reaps it`);
478
523
  }
479
524
  }, (err) => {
480
- console.warn(`[Solari] DELETE /sessions/${id} failed (fire-and-forget):`, err);
525
+ console.warn(`[Solari] ${redactSessionPath(`DELETE /sessions/${id}`)} failed (fire-and-forget):`, err);
481
526
  });
482
527
  }
483
528
  async releaseAndWait(id) {
@@ -485,7 +530,7 @@ class SessionsResource {
485
530
  const res = await this.client.request("DELETE", `/sessions/${encodeURIComponent(id)}`);
486
531
  const rejection = await releaseRejection(res);
487
532
  if (rejection) {
488
- throw new SolariError(`Solari DELETE /sessions/${id} failed: ${res.status} ${rejection.body}`, res.status, undefined, rejection.code);
533
+ throw new SolariError(`Solari ${redactSessionPath(`DELETE /sessions/${id}`)} failed: ${res.status} ${rejection.body}`, res.status, undefined, rejection.code);
489
534
  }
490
535
  }
491
536
  /** The gateway's current view of one session (`GET /sessions/:id`). */
@@ -524,7 +569,7 @@ class SessionsResource {
524
569
  throw await apiError(`GET /sessions/${id}/replay-url`, res);
525
570
  const data = (await res.json());
526
571
  if (!data.url) {
527
- throw new SolariError(`Solari: unexpected replay-url response: ${JSON.stringify(data)}`);
572
+ throw new SolariError(`Solari: unexpected replay-url response (fields: ${responseFields(data)})`);
528
573
  }
529
574
  return {
530
575
  url: data.url,
@@ -662,7 +707,7 @@ export class Solari {
662
707
  await sleep(100 * attempt);
663
708
  continue;
664
709
  }
665
- throw err;
710
+ throw scrubbedLaunchError(err, [session.wsEndpoint, session.cdpEndpoint]);
666
711
  }
667
712
  }
668
713
  throw lastErr ?? new SolariError("Solari.launch: exhausted attempts");
@@ -704,7 +749,7 @@ export class Solari {
704
749
  upstream = new URL(upstreamEndpoint);
705
750
  }
706
751
  catch (e) {
707
- throw new SolariError(`Solari: invalid upstream ${kind}Endpoint: ${upstreamEndpoint}`, undefined, e);
752
+ throw new SolariError(`Solari: invalid upstream ${kind}Endpoint: ${redactUrl(upstreamEndpoint)}`, undefined, e);
708
753
  }
709
754
  await this.localProxy.start();
710
755
  this.localProxy.register(sessionId, upstream, kind);
@@ -752,7 +797,7 @@ export class Solari {
752
797
  if (!retryThis || attempt === maxAttempts)
753
798
  return res;
754
799
  await res.text().catch(() => { });
755
- lastErr = new SolariError(`Solari ${method} ${path}: ${res.status}`, res.status);
800
+ lastErr = new SolariError(`Solari ${method} ${redactSessionPath(path)}: ${res.status}`, res.status);
756
801
  }
757
802
  catch (e) {
758
803
  clearTimeout(timer);
@@ -764,6 +809,6 @@ export class Solari {
764
809
  await sleep(this.backoffMs);
765
810
  }
766
811
  }
767
- throw new SolariError(`Solari ${method} ${path}: exhausted ${maxAttempts} attempts`, undefined, lastErr);
812
+ throw new SolariError(`Solari ${method} ${redactSessionPath(path)}: exhausted ${maxAttempts} attempts`, undefined, lastErr);
768
813
  }
769
814
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solarisdk/browser",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Official client SDK for Solari Browser.",
5
5
  "license": "MIT",
6
6
  "type": "module",