@solarisdk/browser 0.2.0 → 0.4.0

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/LICENSE CHANGED
@@ -1,17 +1,21 @@
1
- Apache License
2
- Version 2.0, January 2004
3
- http://www.apache.org/licenses/
1
+ MIT License
4
2
 
5
- Copyright 2026 Solari Browser
3
+ Copyright (c) 2026 Solari
6
4
 
7
- Licensed under the Apache License, Version 2.0 (the "License");
8
- you may not use this file except in compliance with the License.
9
- You may obtain a copy of the License at
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
10
11
 
11
- http://www.apache.org/licenses/LICENSE-2.0
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
12
14
 
13
- Unless required by applicable law or agreed to in writing, software
14
- distributed under the License is distributed on an "AS IS" BASIS,
15
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- See the License for the specific language governing permissions and
17
- limitations under the License.
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -58,6 +58,21 @@ const session = await solari.sessions.create();
58
58
  const browser = await chromium.connectOverCDP(session.cdpEndpoint);
59
59
  ```
60
60
 
61
+ ### Get and list sessions
62
+
63
+ ```ts
64
+ const view = await solari.sessions.get(session.id); // status, kind, userMetadata, endpoints while active
65
+ let page = await solari.sessions.list({ limit: 50, metadata: { job: "nightly-42" } });
66
+ // page.sessions, page.nextCursor (absent on the last page)
67
+ ```
68
+
69
+ `list` never returns the websocket endpoints (they are bearer capabilities): call `get`
70
+ for a single session's. Pass `page.nextCursor` as `cursor` for the next page.
71
+
72
+ Every REST error carries the gateway's `code` when it sends one, so you can branch on
73
+ it whichever call failed: `e.code === "ReplayPending"` (a replay still uploading; wait and
74
+ call again), `"SessionNotFound"`, `"InvalidSessionId"`, `"PlanLimitExceeded"` and so on.
75
+
61
76
  ### Timeout, keep-alive and metadata
62
77
 
63
78
  ```ts
@@ -87,6 +102,22 @@ For staging or self-hosted gateways, pass `baseUrl` instead of `region`.
87
102
 
88
103
  ## Managed proxies
89
104
 
105
+ Proxy bandwidth is billed per GB, and pages heavy in video and images can cost tens of megabytes
106
+ per session. `blockHeavy` (off by default) skips them:
107
+
108
+ ```ts
109
+ await solari.launch({
110
+ stealth: true,
111
+ proxy: "us",
112
+ blockHeavy: true, // video/audio + images over 100 KB; or { fonts: true, imageMaxBytes: 50_000 }
113
+ });
114
+ ```
115
+
116
+ Blocked requests fail inside the browser, so nothing is fetched and nothing leaves through a different
117
+ route; the page sees an ordinary failed load and the blocked image or video does not appear. Images that
118
+ declare no size, and requests made by service workers or cross-origin iframes, are not covered.
119
+ Replays still play: they load media from the original site.
120
+
90
121
  ```ts
91
122
  await solari.launch({
92
123
  stealth: true,
@@ -122,4 +153,4 @@ try {
122
153
 
123
154
  ## License
124
155
 
125
- Apache-2.0
156
+ MIT
package/dist/index.d.ts CHANGED
@@ -96,6 +96,16 @@ export interface Profile {
96
96
  name: string;
97
97
  [k: string]: unknown;
98
98
  }
99
+ /** What `blockHeavy` skips. Every field is optional. */
100
+ export interface BlockHeavyOptions {
101
+ /** Block video and audio. Default true. */
102
+ media?: boolean;
103
+ /** Block web fonts. Default false (blocking fonts changes how the page renders and fingerprints). */
104
+ fonts?: boolean;
105
+ /** Block images whose declared size (Content-Length, as sent over the wire) exceeds this many bytes;
106
+ * `null` keeps every image. Default 102400 (100 KB). */
107
+ imageMaxBytes?: number | null;
108
+ }
99
109
  export interface CreateSessionOptions {
100
110
  /** Attach a stored browser profile (cookies + localStorage). */
101
111
  profileId?: string;
@@ -125,6 +135,15 @@ export interface CreateSessionOptions {
125
135
  * (`sessionTimeoutMs`, else an hour, never past the plan maximum) or until
126
136
  * it is released, so a dropped client can reconnect. */
127
137
  keepAlive?: boolean;
138
+ /** Skip heavy files so a proxied session does not pay proxy bandwidth for them. Off by default.
139
+ *
140
+ * blockHeavy: true // video/audio + images over 100 KB
141
+ * blockHeavy: { fonts: true, imageMaxBytes: 50_000 }
142
+ *
143
+ * Blocked requests fail inside the browser (nothing is fetched and nothing goes out directly), so the page sees an
144
+ * ordinary failed load: a blocked image or video simply does not appear. Images without a declared size, and requests
145
+ * made by service workers or cross-origin iframes, are not covered. The gateway rejects a malformed value with 400. */
146
+ blockHeavy?: boolean | BlockHeavyOptions;
128
147
  /** Free-form JSON object stored with the session (labels, job ids, ...). */
129
148
  userMetadata?: Record<string, unknown>;
130
149
  }
@@ -133,6 +152,42 @@ export interface ReplayUrl {
133
152
  expiresInSeconds: number;
134
153
  contentEncoding: string;
135
154
  }
155
+ /** `GET /sessions/:id`. Fields the gateway adds later are kept (index signature). */
156
+ export interface SessionView {
157
+ id: string;
158
+ status: string;
159
+ kind?: string | null;
160
+ org?: string;
161
+ createdAt?: string | null;
162
+ expiresAt?: string | null;
163
+ userMetadata?: Record<string, unknown>;
164
+ /** Present while the session is active. */
165
+ wsEndpoint?: string;
166
+ cdpEndpoint?: string;
167
+ [k: string]: unknown;
168
+ }
169
+ /** One row of `sessions.list()`. Never carries the websocket endpoints. */
170
+ export interface SessionSummary {
171
+ id: string;
172
+ status: string;
173
+ kind?: string | null;
174
+ createdAt?: string | null;
175
+ expiresAt?: string | null;
176
+ userMetadata?: Record<string, unknown>;
177
+ }
178
+ export interface SessionList {
179
+ sessions: SessionSummary[];
180
+ /** Absent on the last page; pass it as `ListSessionsOptions.cursor`. */
181
+ nextCursor?: string;
182
+ }
183
+ export interface ListSessionsOptions {
184
+ /** Page size; unset uses the gateway default. */
185
+ limit?: number;
186
+ /** Continue from a previous page's `nextCursor`. */
187
+ cursor?: string;
188
+ /** Keep sessions whose `userMetadata` has all these string values. */
189
+ metadata?: Record<string, string>;
190
+ }
136
191
  /** Options for `Solari.launch()`. Strict superset of `CreateSessionOptions`. */
137
192
  export interface LaunchOptions extends CreateSessionOptions {
138
193
  /** Additional re-launch attempts after the first failure. Default 0. */
@@ -191,6 +246,15 @@ declare class SessionsResource {
191
246
  /** Fire-and-forget release. Use `releaseAndWait` for confirmation. */
192
247
  release(id: string): void;
193
248
  releaseAndWait(id: string): Promise<void>;
249
+ /** The gateway's current view of one session (`GET /sessions/:id`). */
250
+ get(id: string): Promise<SessionView>;
251
+ /**
252
+ * One page of the organisation's sessions (`GET /sessions`). Rows never carry the
253
+ * websocket endpoints (they are bearer capabilities): call `get(id)` for those.
254
+ * `metadata` keeps sessions whose `userMetadata` has all these string values
255
+ * (at most 10 pairs; keys and values cannot contain a single quote).
256
+ */
257
+ list(options?: ListSessionsOptions): Promise<SessionList>;
194
258
  /** Presigned URL for the session's replay. Available ~1-3s after `releaseAndWait`. */
195
259
  getReplayUrl(id: string): Promise<ReplayUrl>;
196
260
  /** Download the replay as NDJSON bytes. */
package/dist/index.js CHANGED
@@ -35,6 +35,21 @@ function toPlaywrightStorageState(state) {
35
35
  })),
36
36
  };
37
37
  }
38
+ /** The gateway's list filter grammar, in key order; throws for a pair it cannot express. */
39
+ function metadataFilter(m) {
40
+ const keys = Object.keys(m).sort();
41
+ if (keys.length > 10)
42
+ throw new SolariError("Solari: at most 10 metadata filter pairs");
43
+ return keys
44
+ .map((k) => {
45
+ const v = m[k];
46
+ if (k.length < 1 || k.length > 128 || k.includes("'") || v.includes("'")) {
47
+ throw new SolariError(`Solari: metadata filter key ${JSON.stringify(k)} or its value cannot be expressed (1-128 characters, no single quote)`);
48
+ }
49
+ return `user_metadata['${k}']:'${v}'`;
50
+ })
51
+ .join(" AND ");
52
+ }
38
53
  export class SolariError extends Error {
39
54
  status;
40
55
  cause;
@@ -47,6 +62,24 @@ export class SolariError extends Error {
47
62
  this.name = "SolariError";
48
63
  }
49
64
  }
65
+ /**
66
+ * Build the error for a non-ok API response: the status, the body text in the
67
+ * message, and the gateway's `code` (for example `ReplayPending`,
68
+ * `SessionNotFound`, `InvalidSessionId`) when the body is JSON with a string
69
+ * `code`. Every REST call uses this so a caller can branch on `e.code` no matter
70
+ * which endpoint failed.
71
+ */
72
+ async function apiError(what, res) {
73
+ const body = await res.text().catch(() => "");
74
+ let code;
75
+ try {
76
+ const parsed = JSON.parse(body);
77
+ if (typeof parsed?.code === "string")
78
+ code = parsed.code;
79
+ }
80
+ catch { }
81
+ return new SolariError(`Solari ${what} failed: ${res.status} ${body}`, res.status, undefined, code);
82
+ }
50
83
  const REGION_URLS = {
51
84
  "us-west": "https://api.getsolari.com",
52
85
  };
@@ -210,25 +243,19 @@ class ProfilesResource {
210
243
  }
211
244
  async list() {
212
245
  const res = await this.client.request("GET", "/profiles");
213
- if (!res.ok) {
214
- const body = await res.text().catch(() => "");
215
- throw new SolariError(`Solari GET /profiles failed: ${res.status} ${body}`, res.status);
216
- }
246
+ if (!res.ok)
247
+ throw await apiError("GET /profiles", res);
217
248
  return (await res.json());
218
249
  }
219
250
  async delete(id) {
220
251
  const res = await this.client.request("DELETE", `/profiles/${encodeURIComponent(id)}`);
221
- if (!res.ok && res.status !== 404) {
222
- const body = await res.text().catch(() => "");
223
- throw new SolariError(`Solari DELETE /profiles/${id} failed: ${res.status} ${body}`, res.status);
224
- }
252
+ if (!res.ok && res.status !== 404)
253
+ throw await apiError(`DELETE /profiles/${id}`, res);
225
254
  }
226
255
  async save(id, storageState) {
227
256
  const res = await this.client.request("POST", `/profiles/${encodeURIComponent(id)}/save`, { storageState });
228
- if (!res.ok) {
229
- const body = await res.text().catch(() => "");
230
- throw new SolariError(`Solari POST /profiles/${id}/save failed: ${res.status} ${body}`, res.status);
231
- }
257
+ if (!res.ok)
258
+ throw await apiError(`POST /profiles/${id}/save`, res);
232
259
  const data = (await res.json());
233
260
  return {
234
261
  version: data.version ?? 0,
@@ -279,10 +306,8 @@ class ProxyResource {
279
306
  * `proxy.countries()` and C++ `proxyCountries()`. */
280
307
  async countries() {
281
308
  const res = await this.client.request("GET", "/proxy/countries");
282
- if (!res.ok) {
283
- const body = await res.text().catch(() => "");
284
- throw new SolariError(`Solari GET /proxy/countries failed: ${res.status} ${body}`, res.status);
285
- }
309
+ if (!res.ok)
310
+ throw await apiError("GET /proxy/countries", res);
286
311
  return (await res.json());
287
312
  }
288
313
  }
@@ -308,6 +333,8 @@ class SessionsResource {
308
333
  body.timeoutSeconds = timeoutSeconds;
309
334
  if (options?.keepAlive === true)
310
335
  body.keepAlive = true;
336
+ if (options?.blockHeavy !== undefined && options.blockHeavy !== false)
337
+ body.blockHeavy = options.blockHeavy;
311
338
  if (options?.userMetadata !== undefined)
312
339
  body.userMetadata = options.userMetadata;
313
340
  // A create allocates a slot and launches a browser before it can answer,
@@ -378,13 +405,40 @@ class SessionsResource {
378
405
  throw new SolariError(`Solari DELETE /sessions/${id} failed: ${res.status} ${rejection.body}`, res.status, undefined, rejection.code);
379
406
  }
380
407
  }
408
+ /** The gateway's current view of one session (`GET /sessions/:id`). */
409
+ async get(id) {
410
+ const res = await this.client.request("GET", `/sessions/${encodeURIComponent(id)}`);
411
+ if (!res.ok)
412
+ throw await apiError(`GET /sessions/${id}`, res);
413
+ return (await res.json());
414
+ }
415
+ /**
416
+ * One page of the organisation's sessions (`GET /sessions`). Rows never carry the
417
+ * websocket endpoints (they are bearer capabilities): call `get(id)` for those.
418
+ * `metadata` keeps sessions whose `userMetadata` has all these string values
419
+ * (at most 10 pairs; keys and values cannot contain a single quote).
420
+ */
421
+ async list(options) {
422
+ const params = new URLSearchParams();
423
+ if (options?.limit !== undefined && options.limit > 0)
424
+ params.set("limit", String(options.limit));
425
+ if (options?.cursor)
426
+ params.set("cursor", options.cursor);
427
+ if (options?.metadata && Object.keys(options.metadata).length > 0) {
428
+ params.set("q", metadataFilter(options.metadata));
429
+ }
430
+ const qs = params.toString();
431
+ const res = await this.client.request("GET", `/sessions${qs ? `?${qs}` : ""}`);
432
+ if (!res.ok)
433
+ throw await apiError("GET /sessions", res);
434
+ const data = (await res.json());
435
+ return { sessions: data.sessions ?? [], ...(data.nextCursor ? { nextCursor: data.nextCursor } : {}) };
436
+ }
381
437
  /** Presigned URL for the session's replay. Available ~1-3s after `releaseAndWait`. */
382
438
  async getReplayUrl(id) {
383
439
  const res = await this.client.request("GET", `/sessions/${encodeURIComponent(id)}/replay-url`);
384
- if (!res.ok) {
385
- const body = await res.text().catch(() => "");
386
- throw new SolariError(`Solari GET /sessions/${id}/replay-url failed: ${res.status} ${body}`, res.status);
387
- }
440
+ if (!res.ok)
441
+ throw await apiError(`GET /sessions/${id}/replay-url`, res);
388
442
  const data = (await res.json());
389
443
  if (!data.url) {
390
444
  throw new SolariError(`Solari: unexpected replay-url response: ${JSON.stringify(data)}`);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@solarisdk/browser",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Official client SDK for Solari Browser.",
5
- "license": "Apache-2.0",
5
+ "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",