@solarisdk/browser 0.3.0 → 0.5.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 +34 -1
- package/dist/index.d.ts +82 -0
- package/dist/index.js +93 -4
- 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
|
@@ -88,6 +88,23 @@ seconds for the gateway (1001 ms is 2 s); the gateway refuses anything under 60
|
|
|
88
88
|
zero or unset leaves the plan maximum, and a negative, `NaN`, `Infinity` or
|
|
89
89
|
fractional value throws before any request. `launch()` accepts the same options.
|
|
90
90
|
|
|
91
|
+
## Edit a profile
|
|
92
|
+
|
|
93
|
+
`solari.profiles.edit(id)` opens the profile editor, where a person signs in once and the result is saved into the profile. It returns a handle; give its `url` to whoever should sign in, or open it yourself.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const editor = await solari.profiles.edit("prof_123", { reason: "Sign in to Acme" });
|
|
97
|
+
console.log(editor.url); // open this in a browser
|
|
98
|
+
|
|
99
|
+
await editor.waitForSave({ timeoutMs: 10 * 60_000 }); // the person clicks Save in the editor page
|
|
100
|
+
// or drive it from code once they are done:
|
|
101
|
+
await editor.saveAndClose(); // save and close the editor
|
|
102
|
+
await editor.exit(); // close it WITHOUT saving
|
|
103
|
+
console.log(await editor.status());
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The link works for 30 minutes and the editor starts when the person clicks "Open editor" on the page. After that the editor stops by itself and these calls fail with a clear 404 (link expired) or 409 (editor not running). There is no save-and-keep-editing: `saveAndClose()` saves and closes.
|
|
107
|
+
|
|
91
108
|
## Regions
|
|
92
109
|
|
|
93
110
|
Solari runs in `us-west` (N. California) today, with additional regions
|
|
@@ -102,6 +119,22 @@ For staging or self-hosted gateways, pass `baseUrl` instead of `region`.
|
|
|
102
119
|
|
|
103
120
|
## Managed proxies
|
|
104
121
|
|
|
122
|
+
Proxy bandwidth is billed per GB, and pages heavy in video and images can cost tens of megabytes
|
|
123
|
+
per session. `blockHeavy` (off by default) skips them:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
await solari.launch({
|
|
127
|
+
stealth: true,
|
|
128
|
+
proxy: "us",
|
|
129
|
+
blockHeavy: true, // video/audio + images over 100 KB; or { fonts: true, imageMaxBytes: 50_000 }
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Blocked requests fail inside the browser, so nothing is fetched and nothing leaves through a different
|
|
134
|
+
route; the page sees an ordinary failed load and the blocked image or video does not appear. Images that
|
|
135
|
+
declare no size, and requests made by service workers or cross-origin iframes, are not covered.
|
|
136
|
+
Replays still play: they load media from the original site.
|
|
137
|
+
|
|
105
138
|
```ts
|
|
106
139
|
await solari.launch({
|
|
107
140
|
stealth: true,
|
|
@@ -137,4 +170,4 @@ try {
|
|
|
137
170
|
|
|
138
171
|
## License
|
|
139
172
|
|
|
140
|
-
|
|
173
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -96,6 +96,28 @@ export interface Profile {
|
|
|
96
96
|
name: string;
|
|
97
97
|
[k: string]: unknown;
|
|
98
98
|
}
|
|
99
|
+
/** Options for `profiles.edit()`. */
|
|
100
|
+
export interface EditProfileOptions {
|
|
101
|
+
/** Shown to whoever opens the link ("Sign in once so your agent can reuse this login" by default). Max 300 chars. */
|
|
102
|
+
reason?: string;
|
|
103
|
+
}
|
|
104
|
+
/** Options for `ProfileEditor.waitForSave()`. */
|
|
105
|
+
export interface WaitForSaveOptions {
|
|
106
|
+
/** Give up after this many ms. Default: until the link expires. */
|
|
107
|
+
timeoutMs?: number;
|
|
108
|
+
/** How often to check. Default 2000. */
|
|
109
|
+
pollMs?: number;
|
|
110
|
+
}
|
|
111
|
+
/** What `blockHeavy` skips. Every field is optional. */
|
|
112
|
+
export interface BlockHeavyOptions {
|
|
113
|
+
/** Block video and audio. Default true. */
|
|
114
|
+
media?: boolean;
|
|
115
|
+
/** Block web fonts. Default false (blocking fonts changes how the page renders and fingerprints). */
|
|
116
|
+
fonts?: boolean;
|
|
117
|
+
/** Block images whose declared size (Content-Length, as sent over the wire) exceeds this many bytes;
|
|
118
|
+
* `null` keeps every image. Default 102400 (100 KB). */
|
|
119
|
+
imageMaxBytes?: number | null;
|
|
120
|
+
}
|
|
99
121
|
export interface CreateSessionOptions {
|
|
100
122
|
/** Attach a stored browser profile (cookies + localStorage). */
|
|
101
123
|
profileId?: string;
|
|
@@ -125,6 +147,15 @@ export interface CreateSessionOptions {
|
|
|
125
147
|
* (`sessionTimeoutMs`, else an hour, never past the plan maximum) or until
|
|
126
148
|
* it is released, so a dropped client can reconnect. */
|
|
127
149
|
keepAlive?: boolean;
|
|
150
|
+
/** Skip heavy files so a proxied session does not pay proxy bandwidth for them. Off by default.
|
|
151
|
+
*
|
|
152
|
+
* blockHeavy: true // video/audio + images over 100 KB
|
|
153
|
+
* blockHeavy: { fonts: true, imageMaxBytes: 50_000 }
|
|
154
|
+
*
|
|
155
|
+
* Blocked requests fail inside the browser (nothing is fetched and nothing goes out directly), so the page sees an
|
|
156
|
+
* ordinary failed load: a blocked image or video simply does not appear. Images without a declared size, and requests
|
|
157
|
+
* made by service workers or cross-origin iframes, are not covered. The gateway rejects a malformed value with 400. */
|
|
158
|
+
blockHeavy?: boolean | BlockHeavyOptions;
|
|
128
159
|
/** Free-form JSON object stored with the session (labels, job ids, ...). */
|
|
129
160
|
userMetadata?: Record<string, unknown>;
|
|
130
161
|
}
|
|
@@ -199,6 +230,50 @@ export declare class SolariError extends Error {
|
|
|
199
230
|
readonly code?: (SolariErrorCode | string) | undefined;
|
|
200
231
|
constructor(message: string, status?: number | undefined, cause?: unknown | undefined, code?: (SolariErrorCode | string) | undefined);
|
|
201
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* A profile editor launched by `solari.profiles.edit(id)`. Give `url` to the person who should
|
|
235
|
+
* sign in (or open it yourself). The editor starts when the person clicks "Open editor" on that page; it is only usable
|
|
236
|
+
* while the link is valid (30 minutes from `edit()`), after which the editor stops by itself.
|
|
237
|
+
*/
|
|
238
|
+
export declare class ProfileEditor {
|
|
239
|
+
private readonly client;
|
|
240
|
+
readonly profileId: string;
|
|
241
|
+
/** Open this in a browser. */
|
|
242
|
+
readonly url: string;
|
|
243
|
+
readonly handoffId: string;
|
|
244
|
+
/** ISO time after which the link stops working. */
|
|
245
|
+
readonly expiresAt: string;
|
|
246
|
+
/** The profile's version when the editor was launched; a save raises it. */
|
|
247
|
+
readonly startVersion: number;
|
|
248
|
+
constructor(client: Solari, profileId: string,
|
|
249
|
+
/** Open this in a browser. */
|
|
250
|
+
url: string, handoffId: string,
|
|
251
|
+
/** ISO time after which the link stops working. */
|
|
252
|
+
expiresAt: string,
|
|
253
|
+
/** The profile's version when the editor was launched; a save raises it. */
|
|
254
|
+
startVersion: number);
|
|
255
|
+
private path;
|
|
256
|
+
private headers;
|
|
257
|
+
/** The editor's state as the console reports it (`editorStatus`: launching | editing | saving | idle | error, or null before it was ever opened, meaning idle). */
|
|
258
|
+
status(): Promise<{
|
|
259
|
+
editorStatus: string;
|
|
260
|
+
version: number;
|
|
261
|
+
[k: string]: unknown;
|
|
262
|
+
}>;
|
|
263
|
+
/** Save what was signed in and close the editor. Resolves with the new profile version. */
|
|
264
|
+
saveAndClose(): Promise<{
|
|
265
|
+
version: number;
|
|
266
|
+
}>;
|
|
267
|
+
/** Close the editor WITHOUT saving; the profile keeps its previous version. */
|
|
268
|
+
exit(): Promise<void>;
|
|
269
|
+
/**
|
|
270
|
+
* Wait until the person saves in the editor page (the profile's version rises). Resolves with the
|
|
271
|
+
* new version; throws a `SolariError` with status 408 on timeout or when the link expires first.
|
|
272
|
+
*/
|
|
273
|
+
waitForSave(opts?: WaitForSaveOptions): Promise<{
|
|
274
|
+
version: number;
|
|
275
|
+
}>;
|
|
276
|
+
}
|
|
202
277
|
declare class ProfilesResource {
|
|
203
278
|
private readonly client;
|
|
204
279
|
constructor(client: Solari);
|
|
@@ -206,6 +281,11 @@ declare class ProfilesResource {
|
|
|
206
281
|
name: string;
|
|
207
282
|
}): Promise<Profile>;
|
|
208
283
|
list(): Promise<Profile[]>;
|
|
284
|
+
/**
|
|
285
|
+
* Open the profile editor for an existing (or newly created) profile, to sign in or fix its
|
|
286
|
+
* login. Returns a handle whose `url` is the link to the editor page; see `ProfileEditor`.
|
|
287
|
+
*/
|
|
288
|
+
edit(id: string, opts?: EditProfileOptions): Promise<ProfileEditor>;
|
|
209
289
|
delete(id: string): Promise<void>;
|
|
210
290
|
save(id: string, storageState: StorageState): Promise<{
|
|
211
291
|
version: number;
|
|
@@ -271,5 +351,7 @@ export declare class Solari {
|
|
|
271
351
|
_unregisterSession(sessionId: string): void;
|
|
272
352
|
request(method: string, path: string, body?: unknown, opts?: {
|
|
273
353
|
idempotencyKey?: string;
|
|
354
|
+
headers?: Record<string, string>;
|
|
355
|
+
singleAttempt?: boolean;
|
|
274
356
|
}): Promise<Response>;
|
|
275
357
|
}
|
package/dist/index.js
CHANGED
|
@@ -221,6 +221,78 @@ function deriveCdpFromWs(wsEndpoint) {
|
|
|
221
221
|
catch { }
|
|
222
222
|
return wsEndpoint;
|
|
223
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* A profile editor launched by `solari.profiles.edit(id)`. Give `url` to the person who should
|
|
226
|
+
* sign in (or open it yourself). The editor starts when the person clicks "Open editor" on that page; it is only usable
|
|
227
|
+
* while the link is valid (30 minutes from `edit()`), after which the editor stops by itself.
|
|
228
|
+
*/
|
|
229
|
+
export class ProfileEditor {
|
|
230
|
+
client;
|
|
231
|
+
profileId;
|
|
232
|
+
url;
|
|
233
|
+
handoffId;
|
|
234
|
+
expiresAt;
|
|
235
|
+
startVersion;
|
|
236
|
+
constructor(client, profileId,
|
|
237
|
+
/** Open this in a browser. */
|
|
238
|
+
url, handoffId,
|
|
239
|
+
/** ISO time after which the link stops working. */
|
|
240
|
+
expiresAt,
|
|
241
|
+
/** The profile's version when the editor was launched; a save raises it. */
|
|
242
|
+
startVersion) {
|
|
243
|
+
this.client = client;
|
|
244
|
+
this.profileId = profileId;
|
|
245
|
+
this.url = url;
|
|
246
|
+
this.handoffId = handoffId;
|
|
247
|
+
this.expiresAt = expiresAt;
|
|
248
|
+
this.startVersion = startVersion;
|
|
249
|
+
}
|
|
250
|
+
path(suffix = "") {
|
|
251
|
+
return `/profiles/${encodeURIComponent(this.profileId)}/editor${suffix}`;
|
|
252
|
+
}
|
|
253
|
+
headers(singleAttempt = false) {
|
|
254
|
+
return { headers: { "X-Handoff-Id": this.handoffId }, singleAttempt };
|
|
255
|
+
}
|
|
256
|
+
/** The editor's state as the console reports it (`editorStatus`: launching | editing | saving | idle | error, or null before it was ever opened, meaning idle). */
|
|
257
|
+
async status() {
|
|
258
|
+
const res = await this.client.request("GET", this.path(), undefined, this.headers());
|
|
259
|
+
if (!res.ok)
|
|
260
|
+
throw await apiError(`GET ${this.path()}`, res);
|
|
261
|
+
return (await res.json());
|
|
262
|
+
}
|
|
263
|
+
/** Save what was signed in and close the editor. Resolves with the new profile version. */
|
|
264
|
+
async saveAndClose() {
|
|
265
|
+
const res = await this.client.request("POST", this.path("/save"), undefined, this.headers(true));
|
|
266
|
+
if (!res.ok)
|
|
267
|
+
throw await apiError(`POST ${this.path("/save")}`, res);
|
|
268
|
+
const data = (await res.json());
|
|
269
|
+
return { version: data.version ?? this.startVersion + 1 };
|
|
270
|
+
}
|
|
271
|
+
/** Close the editor WITHOUT saving; the profile keeps its previous version. */
|
|
272
|
+
async exit() {
|
|
273
|
+
const res = await this.client.request("DELETE", this.path(), undefined, this.headers(true));
|
|
274
|
+
if (!res.ok)
|
|
275
|
+
throw await apiError(`DELETE ${this.path()}`, res);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Wait until the person saves in the editor page (the profile's version rises). Resolves with the
|
|
279
|
+
* new version; throws a `SolariError` with status 408 on timeout or when the link expires first.
|
|
280
|
+
*/
|
|
281
|
+
async waitForSave(opts = {}) {
|
|
282
|
+
const pollMs = opts.pollMs ?? 2000;
|
|
283
|
+
const deadline = Math.min(Date.parse(this.expiresAt) || Number.POSITIVE_INFINITY, opts.timeoutMs === undefined ? Number.POSITIVE_INFINITY : Date.now() + opts.timeoutMs);
|
|
284
|
+
for (;;) {
|
|
285
|
+
const p = (await this.client.profiles.list()).find((x) => x.id === this.profileId);
|
|
286
|
+
const v = typeof p?.version === "number" ? p.version : this.startVersion;
|
|
287
|
+
if (v > this.startVersion)
|
|
288
|
+
return { version: v };
|
|
289
|
+
if (Date.now() >= deadline) {
|
|
290
|
+
throw new SolariError(`Profile ${this.profileId} was not saved before the editor link expired or the wait timed out`, 408);
|
|
291
|
+
}
|
|
292
|
+
await new Promise((r) => setTimeout(r, Math.min(pollMs, Math.max(0, deadline - Date.now()))));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
224
296
|
class ProfilesResource {
|
|
225
297
|
client;
|
|
226
298
|
constructor(client) {
|
|
@@ -247,6 +319,17 @@ class ProfilesResource {
|
|
|
247
319
|
throw await apiError("GET /profiles", res);
|
|
248
320
|
return (await res.json());
|
|
249
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Open the profile editor for an existing (or newly created) profile, to sign in or fix its
|
|
324
|
+
* login. Returns a handle whose `url` is the link to the editor page; see `ProfileEditor`.
|
|
325
|
+
*/
|
|
326
|
+
async edit(id, opts = {}) {
|
|
327
|
+
const res = await this.client.request("POST", `/profiles/${encodeURIComponent(id)}/login-handoff`, opts.reason === undefined ? {} : { reason: opts.reason });
|
|
328
|
+
if (!res.ok)
|
|
329
|
+
throw await apiError(`POST /profiles/${id}/login-handoff`, res);
|
|
330
|
+
const d = (await res.json());
|
|
331
|
+
return new ProfileEditor(this.client, id, d.url, d.handoffId, d.expiresAt, d.version ?? 0);
|
|
332
|
+
}
|
|
250
333
|
async delete(id) {
|
|
251
334
|
const res = await this.client.request("DELETE", `/profiles/${encodeURIComponent(id)}`);
|
|
252
335
|
if (!res.ok && res.status !== 404)
|
|
@@ -333,6 +416,8 @@ class SessionsResource {
|
|
|
333
416
|
body.timeoutSeconds = timeoutSeconds;
|
|
334
417
|
if (options?.keepAlive === true)
|
|
335
418
|
body.keepAlive = true;
|
|
419
|
+
if (options?.blockHeavy !== undefined && options.blockHeavy !== false)
|
|
420
|
+
body.blockHeavy = options.blockHeavy;
|
|
336
421
|
if (options?.userMetadata !== undefined)
|
|
337
422
|
body.userMetadata = options.userMetadata;
|
|
338
423
|
// A create allocates a slot and launches a browser before it can answer,
|
|
@@ -638,10 +723,14 @@ export class Solari {
|
|
|
638
723
|
...(opts?.idempotencyKey
|
|
639
724
|
? { "Idempotency-Key": opts.idempotencyKey }
|
|
640
725
|
: {}),
|
|
726
|
+
...(opts?.headers ?? {}),
|
|
641
727
|
};
|
|
642
728
|
const payload = body === undefined ? undefined : JSON.stringify(body);
|
|
643
729
|
let lastErr;
|
|
644
|
-
|
|
730
|
+
// singleAttempt: a state-changing call that must not be replayed (the first try may have done
|
|
731
|
+
// its work; a replay would then answer 409 and hide the real result).
|
|
732
|
+
const maxAttempts = opts?.singleAttempt ? 1 : this.maxAttempts;
|
|
733
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
645
734
|
const ac = new AbortController();
|
|
646
735
|
const timer = setTimeout(() => ac.abort(), this.timeoutMs);
|
|
647
736
|
try {
|
|
@@ -660,7 +749,7 @@ export class Solari {
|
|
|
660
749
|
// Out of attempts: hand the last response back so the caller maps its
|
|
661
750
|
// status and `code` like any other error, instead of a status-less
|
|
662
751
|
// "exhausted" error that hides why the gateway refused.
|
|
663
|
-
if (!retryThis || attempt ===
|
|
752
|
+
if (!retryThis || attempt === maxAttempts)
|
|
664
753
|
return res;
|
|
665
754
|
await res.text().catch(() => { });
|
|
666
755
|
lastErr = new SolariError(`Solari ${method} ${path}: ${res.status}`, res.status);
|
|
@@ -671,10 +760,10 @@ export class Solari {
|
|
|
671
760
|
throw e;
|
|
672
761
|
lastErr = e;
|
|
673
762
|
}
|
|
674
|
-
if (attempt <
|
|
763
|
+
if (attempt < maxAttempts) {
|
|
675
764
|
await sleep(this.backoffMs);
|
|
676
765
|
}
|
|
677
766
|
}
|
|
678
|
-
throw new SolariError(`Solari ${method} ${path}: exhausted ${
|
|
767
|
+
throw new SolariError(`Solari ${method} ${path}: exhausted ${maxAttempts} attempts`, undefined, lastErr);
|
|
679
768
|
}
|
|
680
769
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solarisdk/browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|