@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 +17 -13
- package/README.md +32 -1
- package/dist/index.d.ts +64 -0
- package/dist/index.js +74 -20
- package/package.json +2 -2
package/LICENSE
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
Version 2.0, January 2004
|
|
3
|
-
http://www.apache.org/licenses/
|
|
1
|
+
MIT License
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
Copyright (c) 2026 Solari
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Official client SDK for Solari Browser.",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|