@solarisdk/browser 0.1.7 → 0.3.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/README.md +33 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +95 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
Official client SDK for [Solari Browser](https://getsolari.com) — managed,
|
|
4
4
|
stealth-hardened browsers as an API.
|
|
5
5
|
|
|
6
|
+
Feature coverage across all five languages is on the
|
|
7
|
+
[SDK feature matrix](https://docs.getsolari.com/sdk-capabilities).
|
|
8
|
+
|
|
6
9
|
## Install
|
|
7
10
|
|
|
8
11
|
```bash
|
|
@@ -55,6 +58,36 @@ const session = await solari.sessions.create();
|
|
|
55
58
|
const browser = await chromium.connectOverCDP(session.cdpEndpoint);
|
|
56
59
|
```
|
|
57
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
|
+
|
|
76
|
+
### Timeout, keep-alive and metadata
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
const session = await solari.sessions.create({
|
|
80
|
+
sessionTimeoutMs: 600_000, // end earlier than the plan maximum, in ms (at least 60 000)
|
|
81
|
+
keepAlive: true, // hold the session if the client drops, until its deadline or release
|
|
82
|
+
userMetadata: { job: "nightly-42" }, // stored with the session
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Every duration option is in milliseconds. `sessionTimeoutMs` is rounded up to whole
|
|
87
|
+
seconds for the gateway (1001 ms is 2 s); the gateway refuses anything under 60 s;
|
|
88
|
+
zero or unset leaves the plan maximum, and a negative, `NaN`, `Infinity` or
|
|
89
|
+
fractional value throws before any request. `launch()` accepts the same options.
|
|
90
|
+
|
|
58
91
|
## Regions
|
|
59
92
|
|
|
60
93
|
Solari runs in `us-west` (N. California) today, with additional regions
|
package/dist/index.d.ts
CHANGED
|
@@ -88,6 +88,8 @@ export interface Session {
|
|
|
88
88
|
/** Present only when a managed proxy was requested. Confirmation only
|
|
89
89
|
* (country/tier/timezone) — egress is applied server-side. */
|
|
90
90
|
proxy?: ResolvedProxyConfig;
|
|
91
|
+
/** Echo of the `userMetadata` sent at create; absent when none was sent. */
|
|
92
|
+
userMetadata?: Record<string, unknown>;
|
|
91
93
|
}
|
|
92
94
|
export interface Profile {
|
|
93
95
|
id: string;
|
|
@@ -114,12 +116,59 @@ export interface CreateSessionOptions {
|
|
|
114
116
|
* proxy: "off"
|
|
115
117
|
*/
|
|
116
118
|
proxy?: string | ProxyRequest | "off" | "smart";
|
|
119
|
+
/** End the session earlier than the plan maximum, in milliseconds. The gateway
|
|
120
|
+
* counts whole seconds, so the value is rounded up (1001 ms is 2 s), and it
|
|
121
|
+
* refuses anything under 60 s (400). Zero/undefined leaves the plan maximum;
|
|
122
|
+
* a negative, NaN, Infinity or non-integer value throws. */
|
|
123
|
+
sessionTimeoutMs?: number;
|
|
124
|
+
/** Hold the session after the client disconnects, until its deadline
|
|
125
|
+
* (`sessionTimeoutMs`, else an hour, never past the plan maximum) or until
|
|
126
|
+
* it is released, so a dropped client can reconnect. */
|
|
127
|
+
keepAlive?: boolean;
|
|
128
|
+
/** Free-form JSON object stored with the session (labels, job ids, ...). */
|
|
129
|
+
userMetadata?: Record<string, unknown>;
|
|
117
130
|
}
|
|
118
131
|
export interface ReplayUrl {
|
|
119
132
|
url: string;
|
|
120
133
|
expiresInSeconds: number;
|
|
121
134
|
contentEncoding: string;
|
|
122
135
|
}
|
|
136
|
+
/** `GET /sessions/:id`. Fields the gateway adds later are kept (index signature). */
|
|
137
|
+
export interface SessionView {
|
|
138
|
+
id: string;
|
|
139
|
+
status: string;
|
|
140
|
+
kind?: string | null;
|
|
141
|
+
org?: string;
|
|
142
|
+
createdAt?: string | null;
|
|
143
|
+
expiresAt?: string | null;
|
|
144
|
+
userMetadata?: Record<string, unknown>;
|
|
145
|
+
/** Present while the session is active. */
|
|
146
|
+
wsEndpoint?: string;
|
|
147
|
+
cdpEndpoint?: string;
|
|
148
|
+
[k: string]: unknown;
|
|
149
|
+
}
|
|
150
|
+
/** One row of `sessions.list()`. Never carries the websocket endpoints. */
|
|
151
|
+
export interface SessionSummary {
|
|
152
|
+
id: string;
|
|
153
|
+
status: string;
|
|
154
|
+
kind?: string | null;
|
|
155
|
+
createdAt?: string | null;
|
|
156
|
+
expiresAt?: string | null;
|
|
157
|
+
userMetadata?: Record<string, unknown>;
|
|
158
|
+
}
|
|
159
|
+
export interface SessionList {
|
|
160
|
+
sessions: SessionSummary[];
|
|
161
|
+
/** Absent on the last page; pass it as `ListSessionsOptions.cursor`. */
|
|
162
|
+
nextCursor?: string;
|
|
163
|
+
}
|
|
164
|
+
export interface ListSessionsOptions {
|
|
165
|
+
/** Page size; unset uses the gateway default. */
|
|
166
|
+
limit?: number;
|
|
167
|
+
/** Continue from a previous page's `nextCursor`. */
|
|
168
|
+
cursor?: string;
|
|
169
|
+
/** Keep sessions whose `userMetadata` has all these string values. */
|
|
170
|
+
metadata?: Record<string, string>;
|
|
171
|
+
}
|
|
123
172
|
/** Options for `Solari.launch()`. Strict superset of `CreateSessionOptions`. */
|
|
124
173
|
export interface LaunchOptions extends CreateSessionOptions {
|
|
125
174
|
/** Additional re-launch attempts after the first failure. Default 0. */
|
|
@@ -178,6 +227,15 @@ declare class SessionsResource {
|
|
|
178
227
|
/** Fire-and-forget release. Use `releaseAndWait` for confirmation. */
|
|
179
228
|
release(id: string): void;
|
|
180
229
|
releaseAndWait(id: string): Promise<void>;
|
|
230
|
+
/** The gateway's current view of one session (`GET /sessions/:id`). */
|
|
231
|
+
get(id: string): Promise<SessionView>;
|
|
232
|
+
/**
|
|
233
|
+
* One page of the organisation's sessions (`GET /sessions`). Rows never carry the
|
|
234
|
+
* websocket endpoints (they are bearer capabilities): call `get(id)` for those.
|
|
235
|
+
* `metadata` keeps sessions whose `userMetadata` has all these string values
|
|
236
|
+
* (at most 10 pairs; keys and values cannot contain a single quote).
|
|
237
|
+
*/
|
|
238
|
+
list(options?: ListSessionsOptions): Promise<SessionList>;
|
|
181
239
|
/** Presigned URL for the session's replay. Available ~1-3s after `releaseAndWait`. */
|
|
182
240
|
getReplayUrl(id: string): Promise<ReplayUrl>;
|
|
183
241
|
/** 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
|
};
|
|
@@ -60,6 +93,19 @@ const DEFAULT_TIMEOUT_MS = 90_000;
|
|
|
60
93
|
// transient, and a client's correctness must not depend on which gateway
|
|
61
94
|
// build it reaches. Inert today, and deliberately so — do not delete it as
|
|
62
95
|
// dead code.
|
|
96
|
+
/**
|
|
97
|
+
* The gateway's `timeoutSeconds` for a `sessionTimeoutMs`: rounded UP to whole
|
|
98
|
+
* 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.
|
|
100
|
+
*/
|
|
101
|
+
function sessionTimeoutSeconds(ms) {
|
|
102
|
+
if (ms === undefined || ms === 0)
|
|
103
|
+
return undefined;
|
|
104
|
+
if (typeof ms !== "number" || !Number.isInteger(ms) || ms < 0) {
|
|
105
|
+
throw new SolariError(`Solari: sessionTimeoutMs must be a non-negative integer number of milliseconds, got ${String(ms)}`);
|
|
106
|
+
}
|
|
107
|
+
return Math.ceil(ms / 1000);
|
|
108
|
+
}
|
|
63
109
|
function isRetryableStatus(status) {
|
|
64
110
|
return status === 502 || status === 503 || status === 504 || status === 507;
|
|
65
111
|
}
|
|
@@ -197,25 +243,19 @@ class ProfilesResource {
|
|
|
197
243
|
}
|
|
198
244
|
async list() {
|
|
199
245
|
const res = await this.client.request("GET", "/profiles");
|
|
200
|
-
if (!res.ok)
|
|
201
|
-
|
|
202
|
-
throw new SolariError(`Solari GET /profiles failed: ${res.status} ${body}`, res.status);
|
|
203
|
-
}
|
|
246
|
+
if (!res.ok)
|
|
247
|
+
throw await apiError("GET /profiles", res);
|
|
204
248
|
return (await res.json());
|
|
205
249
|
}
|
|
206
250
|
async delete(id) {
|
|
207
251
|
const res = await this.client.request("DELETE", `/profiles/${encodeURIComponent(id)}`);
|
|
208
|
-
if (!res.ok && res.status !== 404)
|
|
209
|
-
|
|
210
|
-
throw new SolariError(`Solari DELETE /profiles/${id} failed: ${res.status} ${body}`, res.status);
|
|
211
|
-
}
|
|
252
|
+
if (!res.ok && res.status !== 404)
|
|
253
|
+
throw await apiError(`DELETE /profiles/${id}`, res);
|
|
212
254
|
}
|
|
213
255
|
async save(id, storageState) {
|
|
214
256
|
const res = await this.client.request("POST", `/profiles/${encodeURIComponent(id)}/save`, { storageState });
|
|
215
|
-
if (!res.ok)
|
|
216
|
-
|
|
217
|
-
throw new SolariError(`Solari POST /profiles/${id}/save failed: ${res.status} ${body}`, res.status);
|
|
218
|
-
}
|
|
257
|
+
if (!res.ok)
|
|
258
|
+
throw await apiError(`POST /profiles/${id}/save`, res);
|
|
219
259
|
const data = (await res.json());
|
|
220
260
|
return {
|
|
221
261
|
version: data.version ?? 0,
|
|
@@ -266,10 +306,8 @@ class ProxyResource {
|
|
|
266
306
|
* `proxy.countries()` and C++ `proxyCountries()`. */
|
|
267
307
|
async countries() {
|
|
268
308
|
const res = await this.client.request("GET", "/proxy/countries");
|
|
269
|
-
if (!res.ok)
|
|
270
|
-
|
|
271
|
-
throw new SolariError(`Solari GET /proxy/countries failed: ${res.status} ${body}`, res.status);
|
|
272
|
-
}
|
|
309
|
+
if (!res.ok)
|
|
310
|
+
throw await apiError("GET /proxy/countries", res);
|
|
273
311
|
return (await res.json());
|
|
274
312
|
}
|
|
275
313
|
}
|
|
@@ -290,6 +328,13 @@ class SessionsResource {
|
|
|
290
328
|
body.captcha = true;
|
|
291
329
|
if (options?.proxy !== undefined)
|
|
292
330
|
body.proxy = options.proxy;
|
|
331
|
+
const timeoutSeconds = sessionTimeoutSeconds(options?.sessionTimeoutMs);
|
|
332
|
+
if (timeoutSeconds !== undefined)
|
|
333
|
+
body.timeoutSeconds = timeoutSeconds;
|
|
334
|
+
if (options?.keepAlive === true)
|
|
335
|
+
body.keepAlive = true;
|
|
336
|
+
if (options?.userMetadata !== undefined)
|
|
337
|
+
body.userMetadata = options.userMetadata;
|
|
293
338
|
// A create allocates a slot and launches a browser before it can answer,
|
|
294
339
|
// so a response lost after that point makes a retry a SECOND session.
|
|
295
340
|
// The key lets the gateway answer the retry from the first result.
|
|
@@ -331,6 +376,9 @@ class SessionsResource {
|
|
|
331
376
|
if (data.proxy) {
|
|
332
377
|
session.proxy = data.proxy;
|
|
333
378
|
}
|
|
379
|
+
if (data.userMetadata) {
|
|
380
|
+
session.userMetadata = data.userMetadata;
|
|
381
|
+
}
|
|
334
382
|
return session;
|
|
335
383
|
}
|
|
336
384
|
/** Fire-and-forget release. Use `releaseAndWait` for confirmation. */
|
|
@@ -355,13 +403,40 @@ class SessionsResource {
|
|
|
355
403
|
throw new SolariError(`Solari DELETE /sessions/${id} failed: ${res.status} ${rejection.body}`, res.status, undefined, rejection.code);
|
|
356
404
|
}
|
|
357
405
|
}
|
|
406
|
+
/** The gateway's current view of one session (`GET /sessions/:id`). */
|
|
407
|
+
async get(id) {
|
|
408
|
+
const res = await this.client.request("GET", `/sessions/${encodeURIComponent(id)}`);
|
|
409
|
+
if (!res.ok)
|
|
410
|
+
throw await apiError(`GET /sessions/${id}`, res);
|
|
411
|
+
return (await res.json());
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* One page of the organisation's sessions (`GET /sessions`). Rows never carry the
|
|
415
|
+
* websocket endpoints (they are bearer capabilities): call `get(id)` for those.
|
|
416
|
+
* `metadata` keeps sessions whose `userMetadata` has all these string values
|
|
417
|
+
* (at most 10 pairs; keys and values cannot contain a single quote).
|
|
418
|
+
*/
|
|
419
|
+
async list(options) {
|
|
420
|
+
const params = new URLSearchParams();
|
|
421
|
+
if (options?.limit !== undefined && options.limit > 0)
|
|
422
|
+
params.set("limit", String(options.limit));
|
|
423
|
+
if (options?.cursor)
|
|
424
|
+
params.set("cursor", options.cursor);
|
|
425
|
+
if (options?.metadata && Object.keys(options.metadata).length > 0) {
|
|
426
|
+
params.set("q", metadataFilter(options.metadata));
|
|
427
|
+
}
|
|
428
|
+
const qs = params.toString();
|
|
429
|
+
const res = await this.client.request("GET", `/sessions${qs ? `?${qs}` : ""}`);
|
|
430
|
+
if (!res.ok)
|
|
431
|
+
throw await apiError("GET /sessions", res);
|
|
432
|
+
const data = (await res.json());
|
|
433
|
+
return { sessions: data.sessions ?? [], ...(data.nextCursor ? { nextCursor: data.nextCursor } : {}) };
|
|
434
|
+
}
|
|
358
435
|
/** Presigned URL for the session's replay. Available ~1-3s after `releaseAndWait`. */
|
|
359
436
|
async getReplayUrl(id) {
|
|
360
437
|
const res = await this.client.request("GET", `/sessions/${encodeURIComponent(id)}/replay-url`);
|
|
361
|
-
if (!res.ok)
|
|
362
|
-
|
|
363
|
-
throw new SolariError(`Solari GET /sessions/${id}/replay-url failed: ${res.status} ${body}`, res.status);
|
|
364
|
-
}
|
|
438
|
+
if (!res.ok)
|
|
439
|
+
throw await apiError(`GET /sessions/${id}/replay-url`, res);
|
|
365
440
|
const data = (await res.json());
|
|
366
441
|
if (!data.url) {
|
|
367
442
|
throw new SolariError(`Solari: unexpected replay-url response: ${JSON.stringify(data)}`);
|