@solarisdk/browser 0.4.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/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
package/dist/index.d.ts CHANGED
@@ -96,6 +96,18 @@ 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
+ }
99
111
  /** What `blockHeavy` skips. Every field is optional. */
100
112
  export interface BlockHeavyOptions {
101
113
  /** Block video and audio. Default true. */
@@ -218,6 +230,50 @@ export declare class SolariError extends Error {
218
230
  readonly code?: (SolariErrorCode | string) | undefined;
219
231
  constructor(message: string, status?: number | undefined, cause?: unknown | undefined, code?: (SolariErrorCode | string) | undefined);
220
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
+ }
221
277
  declare class ProfilesResource {
222
278
  private readonly client;
223
279
  constructor(client: Solari);
@@ -225,6 +281,11 @@ declare class ProfilesResource {
225
281
  name: string;
226
282
  }): Promise<Profile>;
227
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>;
228
289
  delete(id: string): Promise<void>;
229
290
  save(id: string, storageState: StorageState): Promise<{
230
291
  version: number;
@@ -290,5 +351,7 @@ export declare class Solari {
290
351
  _unregisterSession(sessionId: string): void;
291
352
  request(method: string, path: string, body?: unknown, opts?: {
292
353
  idempotencyKey?: string;
354
+ headers?: Record<string, string>;
355
+ singleAttempt?: boolean;
293
356
  }): Promise<Response>;
294
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)
@@ -640,10 +723,14 @@ export class Solari {
640
723
  ...(opts?.idempotencyKey
641
724
  ? { "Idempotency-Key": opts.idempotencyKey }
642
725
  : {}),
726
+ ...(opts?.headers ?? {}),
643
727
  };
644
728
  const payload = body === undefined ? undefined : JSON.stringify(body);
645
729
  let lastErr;
646
- for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
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++) {
647
734
  const ac = new AbortController();
648
735
  const timer = setTimeout(() => ac.abort(), this.timeoutMs);
649
736
  try {
@@ -662,7 +749,7 @@ export class Solari {
662
749
  // Out of attempts: hand the last response back so the caller maps its
663
750
  // status and `code` like any other error, instead of a status-less
664
751
  // "exhausted" error that hides why the gateway refused.
665
- if (!retryThis || attempt === this.maxAttempts)
752
+ if (!retryThis || attempt === maxAttempts)
666
753
  return res;
667
754
  await res.text().catch(() => { });
668
755
  lastErr = new SolariError(`Solari ${method} ${path}: ${res.status}`, res.status);
@@ -673,10 +760,10 @@ export class Solari {
673
760
  throw e;
674
761
  lastErr = e;
675
762
  }
676
- if (attempt < this.maxAttempts) {
763
+ if (attempt < maxAttempts) {
677
764
  await sleep(this.backoffMs);
678
765
  }
679
766
  }
680
- throw new SolariError(`Solari ${method} ${path}: exhausted ${this.maxAttempts} attempts`, undefined, lastErr);
767
+ throw new SolariError(`Solari ${method} ${path}: exhausted ${maxAttempts} attempts`, undefined, lastErr);
681
768
  }
682
769
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solarisdk/browser",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Official client SDK for Solari Browser.",
5
5
  "license": "MIT",
6
6
  "type": "module",