@scalebrowser/sdk 1.2.0 → 1.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/dist/index.js CHANGED
@@ -31,7 +31,14 @@ var ErrorCode = {
31
31
  * the proxy): here the proxy is fine and the address is simply spoken for. Wait
32
32
  * for a term to run out, let the daemon draw again, or turn exclusivity off for
33
33
  * this profile. */
34
- ExitAddressTaken: 4013
34
+ ExitAddressTaken: 4013,
35
+ /** The plan does not allow this: a 4th profile or workflow on Free, a schedule
36
+ * or webhook trigger on Free, or another feature the plan leaves out.
37
+ * Distinct from ConcurrencyLimit (4008 = too many browsers at once, free one or
38
+ * upgrade): here nothing is running too much, the plan itself stops it. The
39
+ * body's `plan_limit` names which limit, the allowed number, the plan and
40
+ * whether the 7-day trial is still open. */
41
+ PlanLimit: 4014
35
42
  };
36
43
  var CODE_LABELS = {
37
44
  4001: "Not found",
@@ -44,7 +51,8 @@ var CODE_LABELS = {
44
51
  4010: "Unauthorized",
45
52
  4011: "No account connected to this machine",
46
53
  4012: "This profile is open on another machine",
47
- 4013: "This exit address belongs to another profile"
54
+ 4013: "This exit address belongs to another profile",
55
+ 4014: "The plan does not allow this"
48
56
  };
49
57
  var ScalebrowserError = class extends Error {
50
58
  constructor(message) {
@@ -55,12 +63,15 @@ var ScalebrowserError = class extends Error {
55
63
  var ApiError = class extends ScalebrowserError {
56
64
  status;
57
65
  code;
66
+ /** The text code of the case, where the daemon names one. */
67
+ reason;
58
68
  body;
59
69
  constructor(status, body, fallbackMessage) {
60
70
  const label = body?.code ? CODE_LABELS[body.code] : void 0;
61
71
  super(body?.message || label || fallbackMessage || `Request failed (HTTP ${status})`);
62
72
  this.status = status;
63
73
  this.code = body?.code;
74
+ this.reason = body?.reason;
64
75
  this.body = body;
65
76
  }
66
77
  /** True for auth failures that must trigger re-authentication. */
@@ -251,6 +262,30 @@ var ControlApi = class extends ClientCore {
251
262
  query: { origin: params.origin, kind: params.kind, profile_id: params.profile_id }
252
263
  });
253
264
  }
265
+ /**
266
+ * What waits in a tab of a running profile. Operator locks and standing rules
267
+ * answer what they decide first; those come back under `already_answered`.
268
+ * Asking keeps the browser's prompts waiting for an answer for two minutes,
269
+ * instead of the browser refusing them on its own.
270
+ */
271
+ listPendingInterruptions(profileId, opts = {}) {
272
+ return this.request("GET", `/v1/profiles/${enc(profileId)}/interruptions`, void 0, {
273
+ query: { target_id: opts.targetId },
274
+ signal: opts.signal
275
+ });
276
+ }
277
+ /**
278
+ * Answer one prompt the way its own button would. A kind an operator lock
279
+ * decides is refused with 409: change the lock to answer it from here. A prompt
280
+ * that is gone comes back as `applied: false`, not as an error.
281
+ */
282
+ respondInterruption(profileId, body) {
283
+ return this.request(
284
+ "POST",
285
+ `/v1/profiles/${enc(profileId)}/interruptions/respond`,
286
+ body
287
+ );
288
+ }
254
289
  // ── Artifacts ──────────────────────────────────────────────────────────────
255
290
  /**
256
291
  * Hand the daemon a file so `interact action=upload` has something to attach.
@@ -490,6 +525,14 @@ var IdentityApi = class extends ClientCore {
490
525
  unbindInbox(profileId, channel) {
491
526
  return this.request("DELETE", `/v1/profiles/${enc(profileId)}/inbox/${enc(channel)}`);
492
527
  }
528
+ /**
529
+ * Read the profile's bound mailbox: the messages, the code candidates in each,
530
+ * and every link with its full address. Needs the vault password, because a
531
+ * sign-in link IS the account. Waits up to `timeout_ms` for the first message.
532
+ */
533
+ revealInbox(profileId, body) {
534
+ return this.request("POST", `/v1/profiles/${enc(profileId)}/inbox/reveal`, body);
535
+ }
493
536
  // ── Passkeys ───────────────────────────────────────────────────────────────
494
537
  /** What this profile can sign in to without a password. Metadata only. */
495
538
  listPasskeys(profileId, signal) {
@@ -594,1044 +637,1708 @@ var IdentityApi = class extends ClientCore {
594
637
  }
595
638
  };
596
639
 
597
- // src/client-tasks.ts
598
- var TasksApi = class extends ClientCore {
599
- // -- Tasks ------------------------------------------------------------------
600
- //
601
- // Fleet-wide by default, and deliberately so: the question this surface exists
602
- // for is "what is outstanding across my twenty accounts", and a per-profile
603
- // route would make a client ask it twenty times and sort the answers itself.
604
- //
605
- // None of these starts a browser. A task is a row, so reading the whole fleet's
606
- // list costs nothing from the plan's concurrent-browser allowance. Reserve only
607
- // the profiles that turn out to have work.
608
- /**
609
- * What is still to be done. No filter means every task of every profile.
610
- *
611
- * Pass `state: 'open'` for what is outstanding; without it, tasks finished
612
- * within the last day are included too.
613
- */
614
- listTasks(params, signal) {
615
- return this.request("GET", "/v1/tasks", void 0, {
616
- query: { ...params },
617
- signal
618
- });
640
+ // src/client-presets.ts
641
+ var PresetsApi = class extends ClientCore {
642
+ // ── Groups & presets ───────────────────────────────────────────────────────
643
+ listGroups(signal) {
644
+ return this.request("GET", "/v1/groups", void 0, { signal });
619
645
  }
620
- /**
621
- * Put the same task on one or many profiles, one row each.
622
- *
623
- * A task is a LINE, not a document: name the work and leave the material where
624
- * it lives. `due_at` is a deadline, not a trigger; nothing runs by itself.
625
- *
626
- * Profiles that no longer exist come back in `missing` instead of failing the
627
- * whole call.
628
- */
629
- createTasks(body) {
630
- return this.request("POST", "/v1/tasks", body);
646
+ getGroup(id, signal) {
647
+ return this.request("GET", `/v1/groups/${enc(id)}`, void 0, { signal });
631
648
  }
632
- /**
633
- * Call a task off.
634
- *
635
- * Only a task nobody has claimed can be cancelled here. One an agent is working
636
- * on right now is stopped by telling that agent, which is a channel that already
637
- * reaches it; this surface answers `409` for it rather than pretending.
638
- */
639
- cancelTask(id) {
640
- return this.request("PATCH", `/v1/tasks/${enc(id)}`, { state: "cancelled" });
649
+ createGroup(body) {
650
+ return this.request("POST", "/v1/groups", body);
641
651
  }
642
- /** Remove a task outright. */
643
- deleteTask(id) {
644
- return this.request("DELETE", `/v1/tasks/${enc(id)}`);
652
+ updateGroup(id, body) {
653
+ return this.request("PATCH", `/v1/groups/${enc(id)}`, body);
645
654
  }
646
- // -- Profile memory ---------------------------------------------------------
647
- /**
648
- * A profile's durable memory, the `PROFILE.md` at the top of its directory.
649
- *
650
- * `content` is `null` while nobody has written one: a profile starts with no
651
- * file, not with an empty one.
652
- */
653
- getProfileMemory(profileId, signal) {
654
- return this.request("GET", `/v1/profiles/${enc(profileId)}/memory`, void 0, {
655
- signal
656
- });
655
+ deleteGroup(id) {
656
+ return this.request("DELETE", `/v1/groups/${enc(id)}`);
657
657
  }
658
- /**
659
- * Replace a profile's memory.
660
- *
661
- * Stored verbatim: no trimming, no appended newline, no line-ending
662
- * translation. The last write wins and nothing warns, exactly as with a file
663
- * two people can open.
664
- */
665
- putProfileMemory(profileId, content) {
666
- return this.request("PUT", `/v1/profiles/${enc(profileId)}/memory`, { content });
658
+ listPresets(signal) {
659
+ return this.request("GET", "/v1/presets", void 0, { signal });
667
660
  }
668
- /** Remove a profile's memory. Removing one that is not there is not an error. */
669
- deleteProfileMemory(profileId) {
670
- return this.request("DELETE", `/v1/profiles/${enc(profileId)}/memory`);
661
+ getPreset(id, signal) {
662
+ return this.request("GET", `/v1/presets/${enc(id)}`, void 0, { signal });
671
663
  }
672
- };
673
-
674
- // src/client-runs.ts
675
- var RunsApi = class extends ClientCore {
676
- // ── Agent runs ─────────────────────────────────────────────────────────────
677
- /** Runs, newest first. Read-only, all of it. */
678
- listRuns(params = {}, signal) {
679
- return this.request("GET", "/v1/runs", void 0, {
680
- query: { profile_id: params.profile_id, limit: params.limit, offset: params.offset },
681
- signal
682
- });
664
+ createPreset(body) {
665
+ return this.request("POST", "/v1/presets", body);
683
666
  }
684
- /** One run. */
685
- getRun(runId, signal) {
686
- return this.request("GET", `/v1/runs/${enc(runId)}`, void 0, { signal });
667
+ updatePreset(id, body) {
668
+ return this.request("PATCH", `/v1/presets/${enc(id)}`, body);
687
669
  }
688
- /** A run's steps, oldest first: the order they happened in. */
689
- listRunSteps(runId, params = {}, signal) {
690
- return this.request("GET", `/v1/runs/${enc(runId)}/steps`, void 0, {
691
- query: { limit: params.limit, offset: params.offset },
692
- signal
693
- });
670
+ deletePreset(id) {
671
+ return this.request("DELETE", `/v1/presets/${enc(id)}`);
672
+ }
673
+ // ── Video presets ──────────────────────────────────────────────────────────
674
+ listVideoPresets(signal) {
675
+ return this.request("GET", "/v1/video-presets", void 0, { signal });
676
+ }
677
+ getVideoPreset(id, signal) {
678
+ return this.request("GET", `/v1/video-presets/${enc(id)}`, void 0, { signal });
679
+ }
680
+ /** The daemon validates `config` strictly and names the failing field path. */
681
+ createVideoPreset(body) {
682
+ return this.request("POST", "/v1/video-presets", body);
683
+ }
684
+ /** A built-in preset refuses edits. Duplicate it and edit the copy. */
685
+ updateVideoPreset(id, body) {
686
+ return this.request("PATCH", `/v1/video-presets/${enc(id)}`, body);
687
+ }
688
+ /** The only way to make a built-in preset yours: copy, then edit the copy. */
689
+ duplicateVideoPreset(id, body = {}) {
690
+ return this.request("POST", `/v1/video-presets/${enc(id)}/duplicate`, body);
691
+ }
692
+ deleteVideoPreset(id) {
693
+ return this.request("DELETE", `/v1/video-presets/${enc(id)}`);
694
+ }
695
+ /** One still of the sample film under an UNSAVED config, drawn by the daemon's own renderer.
696
+ * `moment` wins over `frame`; both absent means the daemon's default moment. */
697
+ previewVideoPreset(body, signal) {
698
+ return this.request("POST", "/v1/video-presets/preview", body, { signal });
699
+ }
700
+ /** A clip of the sample film, blocking until it is encoded (the editor uses the SSE form of the
701
+ * same route; a script waits). Fetch the bytes with `downloadVideoPresetClip`. */
702
+ previewVideoPresetClip(body, signal) {
703
+ return this.request("POST", "/v1/video-presets/preview/clip", body, { signal });
704
+ }
705
+ /** The clip's MP4 bytes. */
706
+ downloadVideoPresetClip(id, signal) {
707
+ return this.requestBytes("GET", `/v1/video-presets/preview/clip/${enc(id)}`, { signal });
694
708
  }
695
709
  /**
696
- * The still taken at one step, as JPEG bytes.
697
- *
698
- * The picture shows a customer's account, so it is decrypted only on the way
699
- * out and never cached anywhere shared.
710
+ * The values a preset's `constraints` may take. Ask rather than hardcode: the
711
+ * region list comes from the daemon's embedded persona model, so a pinned copy
712
+ * drifts the moment that model changes and every create 400s.
700
713
  */
701
- getRunShot(runId, seq, signal) {
702
- return this.requestBytes("GET", `/v1/runs/${enc(runId)}/shots/${seq}`, { signal });
703
- }
704
- /** What every running profile currently shows. */
705
- getActivity(signal) {
706
- return this.request("GET", "/v1/activity", void 0, { signal });
714
+ getPersonaConstraints(signal) {
715
+ return this.request(
716
+ "GET",
717
+ "/v1/persona/constraints",
718
+ void 0,
719
+ { signal }
720
+ );
707
721
  }
708
722
  };
709
723
 
710
- // src/client-videos.ts
711
- var VideosApi = class extends ClientCore {
712
- // ── Videos ────────────────────────────────────────────────────────────────
713
- /** One gallery page, newest first. Running renders are videos too. */
714
- listVideos(params = {}, signal) {
715
- return this.request("GET", "/v1/videos", void 0, {
724
+ // src/client-profiles.ts
725
+ var ProfilesApi = class extends ClientCore {
726
+ // ── Profiles ───────────────────────────────────────────────────────────────
727
+ listProfiles(params = {}, signal) {
728
+ return this.request("GET", "/v1/profiles", void 0, {
716
729
  query: {
717
- q: params.q,
718
- profile_id: params.profile_id,
719
- run_id: params.run_id,
720
- kind: params.kind,
721
- preset: params.preset,
730
+ group: params.group,
722
731
  state: params.state,
723
- page: params.page
732
+ q: params.q,
733
+ limit: params.limit,
734
+ offset: params.offset,
735
+ sort: params.sort,
736
+ order: params.order
724
737
  },
725
738
  signal
726
739
  });
727
740
  }
728
- /** One video row, progress included while it renders. */
729
- getVideo(videoId, signal) {
730
- return this.request("GET", `/v1/videos/${enc(videoId)}`, void 0, { signal });
731
- }
732
- /** Rename. The title also lands in the MP4's metadata on the NEXT render. */
733
- renameVideo(videoId, title, signal) {
734
- return this.request("PATCH", `/v1/videos/${enc(videoId)}`, { title }, { signal });
735
- }
736
741
  /**
737
- * Delete a video (`raw: true` also purges its recording's raw material).
738
- * A live share link is revoked first; 409 `job_running` while it renders.
742
+ * Every id matching `params`, unpaged: what "act on all matches" needs.
743
+ *
744
+ * `listProfiles` is paged, so acting on "everything" from it only ever covers the
745
+ * page you fetched. Asking for ids costs one request and no persona payloads.
739
746
  */
740
- deleteVideo(videoId, opts = {}, signal) {
741
- return this.request("DELETE", `/v1/videos/${enc(videoId)}`, void 0, {
742
- query: { raw: opts.raw },
747
+ listProfileIds(params = {}, signal) {
748
+ return this.request("GET", "/v1/profiles/ids", void 0, {
749
+ query: {
750
+ group: params.group,
751
+ state: params.state,
752
+ q: params.q,
753
+ sort: params.sort,
754
+ order: params.order
755
+ },
743
756
  signal
744
757
  });
745
758
  }
746
- /** Cancel a queued or rendering video. `done` stays done (409). */
747
- cancelVideo(videoId, signal) {
748
- return this.request("POST", `/v1/videos/${enc(videoId)}/cancel`, void 0, {
759
+ /**
760
+ * How many profiles match `params`, split by runtime state. One request, and
761
+ * its size does not grow with the fleet.
762
+ *
763
+ * `listProfiles` is paged and answers a bare array, so counting what came back
764
+ * counts the page. `listProfileIds` carries the same total but ships an
765
+ * identifier per profile to do it, and cannot split by state at all.
766
+ *
767
+ * `params.state` is ignored here on purpose: a caller labelling every state is
768
+ * sitting in one of them.
769
+ */
770
+ countProfiles(params = {}, signal) {
771
+ return this.request("GET", "/v1/profiles/count", void 0, {
772
+ query: { group: params.group, q: params.q },
749
773
  signal
750
774
  });
751
775
  }
752
- /** Render a failed or cancelled video again: same id, new job. */
753
- rerenderVideo(videoId, body = {}, signal) {
754
- return this.request("POST", `/v1/videos/${enc(videoId)}/renders`, body, { signal });
776
+ getProfile(id, signal) {
777
+ return this.request("GET", `/v1/profiles/${enc(id)}`, void 0, { signal });
755
778
  }
756
- /** A short-lived fetchable address for one asset (default the video). */
757
- videoAccess(videoId, asset, signal) {
758
- return this.request(
759
- "POST",
760
- `/v1/videos/${enc(videoId)}/access`,
761
- asset ? { asset } : {},
762
- { signal }
763
- );
779
+ createProfile(body) {
780
+ return this.request("POST", "/v1/profiles", body);
764
781
  }
765
- /** One asset's bytes, through a freshly minted grant. */
766
- async downloadVideoFile(videoId, asset = "video", signal) {
767
- const grant = await this.videoAccess(videoId, asset, signal);
768
- const grantParam = new URL(grant.url, "http://x").searchParams.get("grant") ?? "";
769
- return this.requestBytes("GET", `/v1/videos/${enc(videoId)}/file`, {
770
- query: { asset, grant: grantParam },
771
- signal
772
- });
782
+ updateProfile(id, body) {
783
+ return this.request("PATCH", `/v1/profiles/${enc(id)}`, body);
773
784
  }
774
- /** What recordings and videos cost on disk, plus the caps. */
775
- videosStorage(signal) {
776
- return this.request("GET", "/v1/videos/storage", void 0, { signal });
785
+ deleteProfile(id) {
786
+ return this.request("DELETE", `/v1/profiles/${enc(id)}`);
777
787
  }
778
- // ── Recordings ────────────────────────────────────────────────────────────
779
- /** Recordings, newest first. The row outlives its raw bytes. */
780
- listRecordings(params = {}, signal) {
781
- return this.request("GET", "/v1/recordings", void 0, {
782
- query: { profile_id: params.profile_id },
783
- signal
784
- });
788
+ /** Start a profile → CDP ws endpoint. Errors: 4002/4003/4005/4006/4008. */
789
+ startProfile(id, body = {}) {
790
+ return this.request("POST", `/v1/profiles/${enc(id)}/start`, body);
785
791
  }
786
- getRecording(recordingId, signal) {
787
- return this.request("GET", `/v1/recordings/${enc(recordingId)}`, void 0, {
788
- signal
789
- });
792
+ stopProfile(id) {
793
+ return this.request("POST", `/v1/profiles/${enc(id)}/stop`, {});
794
+ }
795
+ /** Drop the profile's lease, the operator's way out.
796
+ *
797
+ * An agent releases with its handle; this call needs none. The daemon stops
798
+ * the browser first when one is still up, then lets the lease go, so a dead
799
+ * session cannot park the profile until its TTL runs out. `released: false`
800
+ * is an honest no-op: there was nothing to let go. */
801
+ releaseProfile(id) {
802
+ return this.request("POST", `/v1/profiles/${enc(id)}/release`, {});
803
+ }
804
+ /** Ask for a profile that is open on another machine of this account.
805
+ *
806
+ * Closes nothing here and nothing there: the control plane revokes the holder's
807
+ * right, the holder closes its own browser at its next heartbeat. Start only
808
+ * after this answered `free`. Errors: 4012 when it is held elsewhere. */
809
+ takeoverProfile(id) {
810
+ return this.request("POST", `/v1/profiles/${enc(id)}/takeover`, {});
811
+ }
812
+ // ── Bulk (PRD §D2) ───────────────────────────────────────────────────────
813
+ bulkCreateProfiles(body) {
814
+ return this.request("POST", "/v1/bulk/profiles", body);
790
815
  }
791
816
  /**
792
- * Purge a recording's RAW material; the row stays forever
793
- * (`raw_purged_at`). 409 `job_running` while a render of it is open.
817
+ * Start a batch. `headless` is the ONE visibility decision for the run; omitted
818
+ * means headless, the right default for an unattended client. The daemon stops at
819
+ * the machine's capacity limit and reports how far it got (`started` /
820
+ * `remaining` / `stopped_reason`) instead of failing every remaining id.
794
821
  */
795
- deleteRecordingRaw(recordingId, signal) {
796
- return this.request("DELETE", `/v1/recordings/${enc(recordingId)}`, void 0, {
797
- signal
798
- });
822
+ bulkStart(ids, headless) {
823
+ return this.request("POST", "/v1/bulk/start", { ids, headless });
799
824
  }
800
- /** A NEW video from this recording (the same body the `video` tool takes). */
801
- renderRecording(recordingId, body = {}, signal) {
802
- return this.request("POST", `/v1/recordings/${enc(recordingId)}/renders`, body, {
803
- signal
804
- });
825
+ bulkStop(ids) {
826
+ return this.request("POST", "/v1/bulk/stop", { ids });
805
827
  }
806
- // ── Codecs ────────────────────────────────────────────────────────────────
807
- /** What this machine can encode with. The hardware list is PROBED, not read
808
- * off the binary: `h264_nvenc` is always listed and fails without a driver. */
809
- getCodecs(signal) {
810
- return this.request("GET", "/v1/codecs", void 0, { signal });
828
+ bulkDelete(ids) {
829
+ return this.request("POST", "/v1/bulk/delete", { ids });
811
830
  }
812
- /** Turn Cisco's OpenH264 on or off. The hardware encoders are not switchable. */
813
- setCodecEnabled(codec, enabled, signal) {
814
- return this.request("PUT", `/v1/codecs/${enc(codec)}`, { enabled }, { signal });
831
+ bulkAssignProxy(ids, proxyId) {
832
+ return this.request("POST", "/v1/bulk/assign-proxy", { ids, proxy_id: proxyId });
815
833
  }
816
- // -- Sharing ----------------------------------------------------------------
817
- /** Make a finished video public. Measures every file, asks the control plane
818
- * for permission, uploads, and has the result confirmed. */
819
- share(videoId, body = {}, signal) {
820
- return this.request("POST", `/v1/videos/${enc(videoId)}/share`, body, {
821
- signal
822
- });
834
+ /**
835
+ * Give every id exactly this set of extensions.
836
+ *
837
+ * It REPLACES rather than adds, the same shape as assigning a proxy: picking a
838
+ * set for a batch states what those profiles carry. An empty list is a
839
+ * legitimate "none". An id the library does not hold refuses the whole call.
840
+ */
841
+ bulkAssignExtensions(ids, extRefs) {
842
+ return this.request("POST", "/v1/bulk/assign-extensions", { ids, ext_refs: extRefs });
823
843
  }
824
- /** The share ORDER as this machine knows it. How many people watched and
825
- * whether the link still lives is read from the control plane, not from
826
- * here: a number carried along would be wrong the moment another machine
827
- * revoked. */
828
- shareStatus(videoId, signal) {
829
- return this.request(
830
- "GET",
831
- `/v1/videos/${enc(videoId)}/share`,
832
- void 0,
833
- { signal }
834
- );
844
+ // ── Live detector audit ─────────────────────────────────────────────────────
845
+ //
846
+ // Drives the profile's OWN running browser through ~15 third-party fingerprint
847
+ // sites and keeps what each one saw. The profile must be RUNNING; the run takes
848
+ // minutes, so `startAudit` returns immediately and you poll `getAudit`.
849
+ /** Queue a run → the status right after queueing. `409` if not running / already running. */
850
+ startAudit(profileId) {
851
+ return this.request("POST", `/v1/profiles/${enc(profileId)}/audit`);
835
852
  }
836
- /** Revoke the link. The files fall at once; the order stays until the control
837
- * plane confirms. */
838
- revokeShare(videoId, signal) {
839
- return this.request("DELETE", `/v1/videos/${enc(videoId)}/share`, void 0, {
853
+ /** State plus the last stored report. */
854
+ getAudit(profileId, signal) {
855
+ return this.request("GET", `/v1/profiles/${enc(profileId)}/audit`, void 0, {
840
856
  signal
841
857
  });
842
858
  }
843
- // -- Media ------------------------------------------------------------------
844
- /** Files a preset points at: background image, music bed, subtitle font. */
845
- listMedia(kind, signal) {
846
- return this.request("GET", "/v1/media", void 0, {
847
- query: { kind },
859
+ // ── Exit-address exclusivity ────────────────────────────────────────────────
860
+ /** Which exit addresses this profile owns, and how far the rule reaches.
861
+ *
862
+ * `scope` is `'machine'` while this computer has no account key. That is the
863
+ * ordinary state of a second machine until sync is unlocked there once, and it is
864
+ * reported rather than hidden: believing the rule spans three machines when it
865
+ * spans one is worse than knowing. */
866
+ getExitStatus(profileId, signal) {
867
+ return this.request("GET", `/v1/profiles/${enc(profileId)}/exit`, void 0, {
848
868
  signal
849
869
  });
850
870
  }
851
- /** Upload one file. The body IS the file; `name` and `mime` ride in the query. */
852
- uploadMedia(kind, name, mime, bytes, signal) {
853
- return this.request("POST", "/v1/media", bytes, {
854
- query: { kind, name, mime },
855
- signal
856
- });
871
+ // ── Session bundles ─────────────────────────────────────────────────────────
872
+ exportSession(profileId, body) {
873
+ return this.request("POST", `/v1/profiles/${enc(profileId)}/session/export`, body);
857
874
  }
858
- /** Remove a file. Refuses with 409 while a preset still points at it. */
859
- deleteMedia(id, signal) {
860
- return this.request("DELETE", `/v1/media/${enc(id)}`, void 0, { signal });
875
+ importSession(profileId, body) {
876
+ return this.request("POST", `/v1/profiles/${enc(profileId)}/session/import`, body);
861
877
  }
862
- /** The file itself. */
863
- mediaFile(id, signal) {
864
- return this.requestBytes("GET", `/v1/media/${enc(id)}/file`, { signal });
878
+ // ── Trusted input ──────────────────────────────────────────────────────────
879
+ sendInput(profileId, body) {
880
+ return this.request("POST", `/v1/profiles/${enc(profileId)}/input`, body);
865
881
  }
866
- // -- Voice providers --------------------------------------------------------
867
- /** Which speech providers have a key. The key itself never comes back: the
868
- * answer carries a hint, and that is on purpose. */
869
- listTtsProviders(signal) {
870
- return this.request("GET", "/v1/tts-providers", void 0, { signal });
882
+ };
883
+
884
+ // src/client-proxies.ts
885
+ var ProxiesApi = class extends ClientCore {
886
+ // ── Proxies (credentials write-only) ────────────────────────────────────────
887
+ /** The list carries `used_by` per row; `getProxy` below does not. */
888
+ listProxies(signal) {
889
+ return this.request("GET", "/v1/proxies", void 0, { signal });
871
890
  }
872
- /** Store a key, or rename one. Leaving `apiKey` out keeps the stored key.
873
- *
874
- * `apiKey` may also be a secret reference (`{{sb:...}}`), which is what an
875
- * agent gets back after fetching a key from the provider's own site: it is
876
- * not allowed to read the value. A reference needs `profileId`, because the
877
- * value belongs to one profile and there is nothing to open without it.
891
+ getProxy(id, signal) {
892
+ return this.request("GET", `/v1/proxies/${enc(id)}`, void 0, { signal });
893
+ }
894
+ createProxy(body) {
895
+ return this.request("POST", "/v1/proxies", body);
896
+ }
897
+ updateProxy(id, body) {
898
+ return this.request("PATCH", `/v1/proxies/${enc(id)}`, body);
899
+ }
900
+ deleteProxy(id) {
901
+ return this.request("DELETE", `/v1/proxies/${enc(id)}`);
902
+ }
903
+ /** Health + geo check (exit-ip/country/healthy + JA4↔JA4T verdict). */
904
+ checkProxy(id) {
905
+ return this.request("POST", `/v1/proxies/${enc(id)}/check`, {});
906
+ }
907
+ /**
908
+ * Probe a proxy config WITHOUT persisting it: validate before you commit.
909
+ * Pass `id` (and omit the credentials) to probe an existing proxy's stored,
910
+ * write-only credentials in an edit form.
878
911
  */
879
- putTtsProvider(provider, body, signal) {
880
- return this.request(
881
- "PUT",
882
- `/v1/tts-providers/${enc(provider)}`,
883
- { api_key: body.apiKey, label: body.label, profile_id: body.profileId },
884
- { signal }
885
- );
912
+ checkProxyConfig(body) {
913
+ return this.request("POST", "/v1/proxies/check", body);
886
914
  }
887
- /** Call the provider once for real, and record that the key works.
915
+ };
916
+
917
+ // src/client-tasks.ts
918
+ var TasksApi = class extends ClientCore {
919
+ // -- Tasks ------------------------------------------------------------------
920
+ //
921
+ // Fleet-wide by default, and deliberately so: the question this surface exists
922
+ // for is "what is outstanding across my twenty accounts", and a per-profile
923
+ // route would make a client ask it twenty times and sort the answers itself.
924
+ //
925
+ // None of these starts a browser. A task is a row, so reading the whole fleet's
926
+ // list costs nothing from the plan's concurrent-browser allowance. Reserve only
927
+ // the profiles that turn out to have work.
928
+ /**
929
+ * What is still to be done. No filter means every task of every profile.
888
930
  *
889
- * Costs one short sentence of the account's own quota. Until this runs, a
890
- * provider row says `verified_at: null`, which means untested rather than
891
- * broken.
931
+ * Pass `state: 'open'` for what is outstanding; without it, tasks finished
932
+ * within the last day are included too.
892
933
  */
893
- verifyTtsProvider(provider, signal) {
894
- return this.request(
895
- "POST",
896
- `/v1/tts-providers/${enc(provider)}/verify`,
897
- void 0,
898
- { signal }
899
- );
900
- }
901
- /** Forget a provider's key. */
902
- deleteTtsProvider(provider, signal) {
903
- return this.request("DELETE", `/v1/tts-providers/${enc(provider)}`, void 0, {
934
+ listTasks(params, signal) {
935
+ return this.request("GET", "/v1/tasks", void 0, {
936
+ query: { ...params },
904
937
  signal
905
938
  });
906
939
  }
907
- };
908
-
909
- // src/events.ts
910
- var MAX_BACKOFF_MS = 15e3;
911
- function subscribeEvents(params, handlers) {
912
- const controller = new AbortController();
913
- const fetchImpl = params.fetch ?? globalThis.fetch;
914
- let closed = false;
915
- let attempt = 0;
916
- let lastSeq = null;
917
- let everConnected = false;
918
- const setStatus = (s) => handlers.onStatus?.(s);
919
- const resync = (dropped) => handlers.onEvent({ type: "resync", dropped, at: Math.floor(Date.now() / 1e3) });
920
- const deliver = (event) => {
921
- if (typeof event.seq === "number") {
922
- if (lastSeq !== null && event.seq > lastSeq + 1) resync(event.seq - lastSeq - 1);
923
- lastSeq = event.seq;
924
- }
925
- handlers.onEvent(event);
926
- };
927
- async function connect() {
928
- while (!closed) {
929
- setStatus("connecting");
930
- try {
931
- const headers = { Accept: "text/event-stream" };
932
- if (params.token) headers["Authorization"] = `Bearer ${params.token}`;
933
- const res = await fetchImpl(`${params.baseUrl}/v1/events`, {
934
- headers,
935
- signal: controller.signal
936
- });
937
- if (res.status === 401) {
938
- params.onUnauthorized?.();
939
- break;
940
- }
941
- if (!res.ok || !res.body) {
942
- throw new NetworkError(`event stream failed (HTTP ${res.status})`);
943
- }
944
- attempt = 0;
945
- setStatus("open");
946
- lastSeq = null;
947
- if (everConnected) resync(0);
948
- everConnected = true;
949
- await pump(res.body, deliver, controller.signal);
950
- } catch {
951
- if (closed || controller.signal.aborted) break;
952
- }
953
- if (closed) break;
954
- setStatus("connecting");
955
- attempt += 1;
956
- const delay2 = Math.min(MAX_BACKOFF_MS, 500 * 2 ** Math.min(attempt, 5));
957
- await sleep(delay2, controller.signal);
958
- }
959
- setStatus("closed");
940
+ /**
941
+ * Put the same task on one or many profiles, one row each.
942
+ *
943
+ * A task is a LINE, not a document: name the work and leave the material where
944
+ * it lives. `due_at` is a deadline, not a trigger; nothing runs by itself.
945
+ *
946
+ * Profiles that no longer exist come back in `missing` instead of failing the
947
+ * whole call.
948
+ */
949
+ createTasks(body) {
950
+ return this.request("POST", "/v1/tasks", body);
960
951
  }
961
- void connect();
962
- return () => {
963
- closed = true;
964
- controller.abort();
965
- };
966
- }
967
- async function pump(body, onEvent, signal) {
968
- const reader = body.getReader();
969
- const decoder = new TextDecoder();
970
- let buffer = "";
971
- try {
972
- while (!signal.aborted) {
973
- const { value, done } = await reader.read();
974
- if (done) break;
975
- buffer += decoder.decode(value, { stream: true });
976
- let sep;
977
- while ((sep = indexOfFrameEnd(buffer)) !== -1) {
978
- const frame = buffer.slice(0, sep);
979
- buffer = buffer.slice(sep).replace(/^(\r?\n){1,2}/, "");
980
- const event = parseSseFrame(frame);
981
- if (event) onEvent(event);
982
- }
983
- }
984
- } finally {
985
- void reader.cancel().catch(() => {
952
+ /**
953
+ * Call a task off.
954
+ *
955
+ * Only a task nobody has claimed can be cancelled here. One an agent is working
956
+ * on right now is stopped by telling that agent, which is a channel that already
957
+ * reaches it; this surface answers `409` for it rather than pretending.
958
+ */
959
+ cancelTask(id) {
960
+ return this.request("PATCH", `/v1/tasks/${enc(id)}`, { state: "cancelled" });
961
+ }
962
+ /** Remove a task outright. */
963
+ deleteTask(id) {
964
+ return this.request("DELETE", `/v1/tasks/${enc(id)}`);
965
+ }
966
+ // -- Profile memory ---------------------------------------------------------
967
+ /**
968
+ * A profile's durable memory, the `PROFILE.md` at the top of its directory.
969
+ *
970
+ * `content` is `null` while nobody has written one: a profile starts with no
971
+ * file, not with an empty one.
972
+ */
973
+ getProfileMemory(profileId, signal) {
974
+ return this.request("GET", `/v1/profiles/${enc(profileId)}/memory`, void 0, {
975
+ signal
986
976
  });
987
977
  }
988
- }
989
- function indexOfFrameEnd(buffer) {
990
- const lf = buffer.indexOf("\n\n");
991
- const crlf = buffer.indexOf("\r\n\r\n");
992
- if (lf === -1) return crlf;
993
- if (crlf === -1) return lf;
994
- return Math.min(lf, crlf);
995
- }
996
- function parseSseFrame(frame) {
997
- const dataLines = [];
998
- for (const line of frame.split(/\r?\n/)) {
999
- if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
978
+ /**
979
+ * Replace a profile's memory.
980
+ *
981
+ * Stored verbatim: no trimming, no appended newline, no line-ending
982
+ * translation. The last write wins and nothing warns, exactly as with a file
983
+ * two people can open.
984
+ */
985
+ putProfileMemory(profileId, content) {
986
+ return this.request("PUT", `/v1/profiles/${enc(profileId)}/memory`, { content });
1000
987
  }
1001
- if (dataLines.length === 0) return null;
1002
- const payload = dataLines.join("\n");
1003
- if (!payload || payload === "[DONE]") return null;
1004
- try {
1005
- return JSON.parse(payload);
1006
- } catch {
1007
- return null;
988
+ /** Remove a profile's memory. Removing one that is not there is not an error. */
989
+ deleteProfileMemory(profileId) {
990
+ return this.request("DELETE", `/v1/profiles/${enc(profileId)}/memory`);
1008
991
  }
1009
- }
1010
- function eventStream(params) {
1011
- const buffer = [];
1012
- const waiters = [];
1013
- let done = false;
1014
- const unsubscribe = subscribeEvents(params, {
1015
- onEvent: (event) => {
1016
- const waiter = waiters.shift();
1017
- if (waiter) waiter({ value: event, done: false });
1018
- else buffer.push(event);
1019
- }
1020
- });
1021
- const finish = () => {
1022
- if (!done) {
1023
- done = true;
1024
- unsubscribe();
1025
- for (const w of waiters.splice(0)) w({ value: void 0, done: true });
1026
- }
1027
- return { value: void 0, done: true };
1028
- };
1029
- return {
1030
- next() {
1031
- const next = buffer.shift();
1032
- if (next !== void 0) return Promise.resolve({ value: next, done: false });
1033
- if (done) return Promise.resolve({ value: void 0, done: true });
1034
- return new Promise((resolve) => waiters.push(resolve));
1035
- },
1036
- return() {
1037
- return Promise.resolve(finish());
1038
- },
1039
- [Symbol.asyncIterator]() {
1040
- return this;
1041
- }
1042
- };
1043
- }
1044
- function sleep(ms, signal) {
1045
- return new Promise((resolve) => {
1046
- if (signal.aborted) return resolve();
1047
- const t = setTimeout(resolve, ms);
1048
- signal.addEventListener(
1049
- "abort",
1050
- () => {
1051
- clearTimeout(t);
1052
- resolve();
992
+ };
993
+
994
+ // src/client-runs.ts
995
+ var RunsApi = class extends ClientCore {
996
+ // ── Agent runs ─────────────────────────────────────────────────────────────
997
+ /** Runs, newest first. Read-only, all of it. */
998
+ listRuns(params = {}, signal) {
999
+ return this.request("GET", "/v1/runs", void 0, {
1000
+ query: { profile_id: params.profile_id, limit: params.limit, offset: params.offset },
1001
+ signal
1002
+ });
1003
+ }
1004
+ /** One run. */
1005
+ getRun(runId, signal) {
1006
+ return this.request("GET", `/v1/runs/${enc(runId)}`, void 0, { signal });
1007
+ }
1008
+ /** A run's steps, oldest first: the order they happened in. */
1009
+ listRunSteps(runId, params = {}, signal) {
1010
+ return this.request("GET", `/v1/runs/${enc(runId)}/steps`, void 0, {
1011
+ query: { limit: params.limit, offset: params.offset },
1012
+ signal
1013
+ });
1014
+ }
1015
+ /**
1016
+ * The still taken at one step, as JPEG bytes.
1017
+ *
1018
+ * The picture shows a customer's account, so it is decrypted only on the way
1019
+ * out and never cached anywhere shared.
1020
+ */
1021
+ getRunShot(runId, seq, signal) {
1022
+ return this.requestBytes("GET", `/v1/runs/${enc(runId)}/shots/${seq}`, { signal });
1023
+ }
1024
+ /** What every running profile currently shows. */
1025
+ getActivity(signal) {
1026
+ return this.request("GET", "/v1/activity", void 0, { signal });
1027
+ }
1028
+ };
1029
+
1030
+ // src/client-videos.ts
1031
+ var VideosApi = class extends ClientCore {
1032
+ // ── Videos ────────────────────────────────────────────────────────────────
1033
+ /** One gallery page, newest first. Running renders are videos too. */
1034
+ listVideos(params = {}, signal) {
1035
+ return this.request("GET", "/v1/videos", void 0, {
1036
+ query: {
1037
+ q: params.q,
1038
+ profile_id: params.profile_id,
1039
+ run_id: params.run_id,
1040
+ kind: params.kind,
1041
+ preset: params.preset,
1042
+ state: params.state,
1043
+ page: params.page
1053
1044
  },
1054
- { once: true }
1045
+ signal
1046
+ });
1047
+ }
1048
+ /** One video row, progress included while it renders. */
1049
+ getVideo(videoId, signal) {
1050
+ return this.request("GET", `/v1/videos/${enc(videoId)}`, void 0, { signal });
1051
+ }
1052
+ /** Rename. The title also lands in the MP4's metadata on the NEXT render. */
1053
+ renameVideo(videoId, title, signal) {
1054
+ return this.request("PATCH", `/v1/videos/${enc(videoId)}`, { title }, { signal });
1055
+ }
1056
+ /**
1057
+ * Delete a video (`raw: true` also purges its recording's raw material).
1058
+ * A live share link is revoked first; 409 `job_running` while it renders.
1059
+ */
1060
+ deleteVideo(videoId, opts = {}, signal) {
1061
+ return this.request("DELETE", `/v1/videos/${enc(videoId)}`, void 0, {
1062
+ query: { raw: opts.raw },
1063
+ signal
1064
+ });
1065
+ }
1066
+ /** Cancel a queued or rendering video. `done` stays done (409). */
1067
+ cancelVideo(videoId, signal) {
1068
+ return this.request("POST", `/v1/videos/${enc(videoId)}/cancel`, void 0, {
1069
+ signal
1070
+ });
1071
+ }
1072
+ /** Render a failed or cancelled video again: same id, new job. */
1073
+ rerenderVideo(videoId, body = {}, signal) {
1074
+ return this.request("POST", `/v1/videos/${enc(videoId)}/renders`, body, { signal });
1075
+ }
1076
+ /** A short-lived fetchable address for one asset (default the video). */
1077
+ videoAccess(videoId, asset, signal) {
1078
+ return this.request(
1079
+ "POST",
1080
+ `/v1/videos/${enc(videoId)}/access`,
1081
+ asset ? { asset } : {},
1082
+ { signal }
1055
1083
  );
1056
- });
1057
- }
1058
- var CdpConnection = class {
1059
- #ws;
1060
- #nextId = 0;
1061
- #pending = /* @__PURE__ */ new Map();
1062
- #subscribers = /* @__PURE__ */ new Set();
1063
- #closeListeners = /* @__PURE__ */ new Set();
1064
- #closeReason = null;
1065
- constructor(ws) {
1066
- this.#ws = ws;
1067
- ws.on("message", (data) => this.#onMessage(data));
1068
- ws.on("close", () => this.#onClose(new CdpError(-1, "CDP connection closed")));
1069
- ws.on("error", (err) => this.#onClose(new NetworkError("CDP connection error", err)));
1070
1084
  }
1071
- request(method, params, sessionId) {
1072
- if (this.#closeReason) return Promise.reject(this.#closeReason);
1073
- const id = ++this.#nextId;
1074
- const frame = { id, method };
1075
- if (params !== void 0) frame["params"] = params;
1076
- if (sessionId !== void 0) frame["sessionId"] = sessionId;
1077
- return new Promise((resolve, reject) => {
1078
- this.#pending.set(id, { resolve, reject });
1079
- this.#ws.send(JSON.stringify(frame), (err) => {
1080
- if (err) {
1081
- this.#pending.delete(id);
1082
- reject(new NetworkError("failed to send CDP command", err));
1083
- }
1084
- });
1085
+ /** One asset's bytes, through a freshly minted grant. */
1086
+ async downloadVideoFile(videoId, asset = "video", signal) {
1087
+ const grant = await this.videoAccess(videoId, asset, signal);
1088
+ const grantParam = new URL(grant.url, "http://x").searchParams.get("grant") ?? "";
1089
+ return this.requestBytes("GET", `/v1/videos/${enc(videoId)}/file`, {
1090
+ query: { asset, grant: grantParam },
1091
+ signal
1085
1092
  });
1086
1093
  }
1087
- subscribe(cb) {
1088
- this.#subscribers.add(cb);
1089
- return () => this.#subscribers.delete(cb);
1094
+ /** What recordings and videos cost on disk, plus the caps. */
1095
+ videosStorage(signal) {
1096
+ return this.request("GET", "/v1/videos/storage", void 0, { signal });
1090
1097
  }
1091
- /** Register a callback fired once when the connection closes (immediately if
1092
- * it already has); event streams use this to end instead of hanging. */
1093
- onClose(cb) {
1094
- if (this.#closeReason) {
1095
- cb();
1096
- return () => {
1097
- };
1098
- }
1099
- this.#closeListeners.add(cb);
1100
- return () => this.#closeListeners.delete(cb);
1098
+ // ── Recordings ────────────────────────────────────────────────────────────
1099
+ /** Recordings, newest first. The row outlives its raw bytes. */
1100
+ listRecordings(params = {}, signal) {
1101
+ return this.request("GET", "/v1/recordings", void 0, {
1102
+ query: { profile_id: params.profile_id },
1103
+ signal
1104
+ });
1101
1105
  }
1102
- close() {
1103
- this.#ws.close();
1106
+ getRecording(recordingId, signal) {
1107
+ return this.request("GET", `/v1/recordings/${enc(recordingId)}`, void 0, {
1108
+ signal
1109
+ });
1104
1110
  }
1105
- get closed() {
1106
- return this.#closeReason !== null;
1111
+ /**
1112
+ * Purge a recording's RAW material; the row stays forever
1113
+ * (`raw_purged_at`). 409 `job_running` while a render of it is open.
1114
+ */
1115
+ deleteRecordingRaw(recordingId, signal) {
1116
+ return this.request("DELETE", `/v1/recordings/${enc(recordingId)}`, void 0, {
1117
+ signal
1118
+ });
1107
1119
  }
1108
- #onMessage(data) {
1109
- let frame;
1110
- try {
1111
- frame = JSON.parse(rawToString(data));
1112
- } catch {
1113
- return;
1114
- }
1115
- if (typeof frame.id === "number") {
1116
- const pending = this.#pending.get(frame.id);
1117
- if (!pending) return;
1118
- this.#pending.delete(frame.id);
1119
- if (frame.error) pending.reject(new CdpError(frame.error.code, frame.error.message, frame.error.data));
1120
- else pending.resolve(frame.result);
1121
- return;
1122
- }
1123
- if (typeof frame.method === "string") {
1124
- const event = { method: frame.method, params: frame.params ?? {}, sessionId: frame.sessionId };
1125
- for (const cb of [...this.#subscribers]) cb(event);
1126
- }
1120
+ /** A NEW video from this recording (the same body the `video` tool takes). */
1121
+ renderRecording(recordingId, body = {}, signal) {
1122
+ return this.request("POST", `/v1/recordings/${enc(recordingId)}/renders`, body, {
1123
+ signal
1124
+ });
1127
1125
  }
1128
- #onClose(reason) {
1129
- if (this.#closeReason) return;
1130
- this.#closeReason = reason;
1131
- for (const [, pending] of this.#pending) pending.reject(reason);
1132
- this.#pending.clear();
1133
- this.#subscribers.clear();
1134
- for (const cb of [...this.#closeListeners]) cb();
1135
- this.#closeListeners.clear();
1126
+ // ── Codecs ────────────────────────────────────────────────────────────────
1127
+ /** What this machine can encode with. The hardware list is PROBED, not read
1128
+ * off the binary: `h264_nvenc` is always listed and fails without a driver. */
1129
+ getCodecs(signal) {
1130
+ return this.request("GET", "/v1/codecs", void 0, { signal });
1131
+ }
1132
+ /** Turn Cisco's OpenH264 on or off. The hardware encoders are not switchable. */
1133
+ setCodecEnabled(codec, enabled, signal) {
1134
+ return this.request("PUT", `/v1/codecs/${enc(codec)}`, { enabled }, { signal });
1135
+ }
1136
+ // -- Sharing ----------------------------------------------------------------
1137
+ /** Make a finished video public. Measures every file, asks the control plane
1138
+ * for permission, uploads, and has the result confirmed. */
1139
+ share(videoId, body = {}, signal) {
1140
+ return this.request("POST", `/v1/videos/${enc(videoId)}/share`, body, {
1141
+ signal
1142
+ });
1143
+ }
1144
+ /** The share ORDER as this machine knows it. How many people watched and
1145
+ * whether the link still lives is read from the control plane, not from
1146
+ * here: a number carried along would be wrong the moment another machine
1147
+ * revoked. */
1148
+ shareStatus(videoId, signal) {
1149
+ return this.request(
1150
+ "GET",
1151
+ `/v1/videos/${enc(videoId)}/share`,
1152
+ void 0,
1153
+ { signal }
1154
+ );
1155
+ }
1156
+ /** Revoke the link. The files fall at once; the order stays until the control
1157
+ * plane confirms. */
1158
+ revokeShare(videoId, signal) {
1159
+ return this.request("DELETE", `/v1/videos/${enc(videoId)}/share`, void 0, {
1160
+ signal
1161
+ });
1162
+ }
1163
+ // -- Media ------------------------------------------------------------------
1164
+ /** Files a preset points at: background image, music bed, subtitle font. */
1165
+ listMedia(kind, signal) {
1166
+ return this.request("GET", "/v1/media", void 0, {
1167
+ query: { kind },
1168
+ signal
1169
+ });
1170
+ }
1171
+ /** Upload one file. The body IS the file; `name` and `mime` ride in the query. */
1172
+ uploadMedia(kind, name, mime, bytes, signal) {
1173
+ return this.request("POST", "/v1/media", bytes, {
1174
+ query: { kind, name, mime },
1175
+ signal
1176
+ });
1177
+ }
1178
+ /** Remove a file. Refuses with 409 while a preset still points at it. */
1179
+ deleteMedia(id, signal) {
1180
+ return this.request("DELETE", `/v1/media/${enc(id)}`, void 0, { signal });
1181
+ }
1182
+ /** The file itself. */
1183
+ mediaFile(id, signal) {
1184
+ return this.requestBytes("GET", `/v1/media/${enc(id)}/file`, { signal });
1185
+ }
1186
+ // -- Voice providers --------------------------------------------------------
1187
+ /** Which speech providers have a key. The key itself never comes back: the
1188
+ * answer carries a hint, and that is on purpose. */
1189
+ listTtsProviders(signal) {
1190
+ return this.request("GET", "/v1/tts-providers", void 0, { signal });
1191
+ }
1192
+ /** Store a key, or rename one. Leaving `apiKey` out keeps the stored key.
1193
+ *
1194
+ * `apiKey` may also be a secret reference (`{{sb:...}}`), which is what an
1195
+ * agent gets back after fetching a key from the provider's own site: it is
1196
+ * not allowed to read the value. A reference needs `profileId`, because the
1197
+ * value belongs to one profile and there is nothing to open without it.
1198
+ */
1199
+ putTtsProvider(provider, body, signal) {
1200
+ return this.request(
1201
+ "PUT",
1202
+ `/v1/tts-providers/${enc(provider)}`,
1203
+ { api_key: body.apiKey, label: body.label, profile_id: body.profileId },
1204
+ { signal }
1205
+ );
1206
+ }
1207
+ /** Call the provider once for real, and record that the key works.
1208
+ *
1209
+ * Costs one short sentence of the account's own quota. Until this runs, a
1210
+ * provider row says `verified_at: null`, which means untested rather than
1211
+ * broken.
1212
+ */
1213
+ verifyTtsProvider(provider, signal) {
1214
+ return this.request(
1215
+ "POST",
1216
+ `/v1/tts-providers/${enc(provider)}/verify`,
1217
+ void 0,
1218
+ { signal }
1219
+ );
1220
+ }
1221
+ /** Forget a provider's key. */
1222
+ deleteTtsProvider(provider, signal) {
1223
+ return this.request("DELETE", `/v1/tts-providers/${enc(provider)}`, void 0, {
1224
+ signal
1225
+ });
1226
+ }
1227
+ };
1228
+
1229
+ // src/client-tables.ts
1230
+ var TablesApi = class extends ClientCore {
1231
+ /**
1232
+ * The tables a workflow uses, before deleting it: those only it uses (what
1233
+ * `deleteWorkflow(id, { tables: 'trash' })` takes to the bin) and those another
1234
+ * workflow uses too.
1235
+ */
1236
+ workflowDeletePreview(workflowId, signal) {
1237
+ return this.request(
1238
+ "GET",
1239
+ `/v1/workflows/${enc(workflowId)}/delete-preview`,
1240
+ void 0,
1241
+ { signal }
1242
+ );
1136
1243
  }
1137
- };
1138
- function rawToString(data) {
1139
- if (typeof data === "string") return data;
1140
- if (Buffer.isBuffer(data)) return data.toString("utf8");
1141
- if (Array.isArray(data)) return Buffer.concat(data).toString("utf8");
1142
- return Buffer.from(data).toString("utf8");
1143
- }
1144
- var CdpSession = class _CdpSession {
1145
- #conn;
1146
- sessionId;
1147
- #client;
1148
- #profileId;
1149
- constructor(conn, sessionId, client, profileId) {
1150
- this.#conn = conn;
1151
- this.sessionId = sessionId;
1152
- this.#client = client;
1153
- this.#profileId = profileId;
1244
+ /** Every table outside the bin. */
1245
+ listTables(signal) {
1246
+ return this.request("GET", "/v1/tables", void 0, { signal });
1154
1247
  }
1155
- /** Send a raw CDP command and await its result (by id). */
1156
- send(method, params) {
1157
- return this.#conn.request(method, params, this.sessionId);
1248
+ /** A local table with its first columns. */
1249
+ createTable(body) {
1250
+ return this.request("POST", "/v1/tables", body);
1158
1251
  }
1159
- /** Register a callback for a CDP event scoped to this session. Returns unsubscribe. */
1160
- on(method, callback) {
1161
- return this.#conn.subscribe((event) => {
1162
- if (event.method === method && event.sessionId === this.sessionId) callback(event.params);
1163
- });
1252
+ getTable(id, signal) {
1253
+ return this.request("GET", `/v1/tables/${enc(id)}`, void 0, { signal });
1164
1254
  }
1165
- /** `for await (const event of session.events())` over this session's CDP events. */
1166
- events() {
1167
- return scopedEventStream(this.#conn, this.sessionId);
1255
+ /** Rename, describe or change the delete rule. */
1256
+ updateTable(id, body) {
1257
+ return this.request("PATCH", `/v1/tables/${enc(id)}`, body);
1168
1258
  }
1169
- /** Attach to a page target, returning a page-bound child session. */
1170
- async attachToPage(opts = {}) {
1171
- let targetId = opts.targetId;
1172
- if (opts.create) {
1173
- const created = await this.send("Target.createTarget", { url: opts.url ?? "about:blank" });
1174
- targetId = created.targetId;
1175
- } else if (!targetId) {
1176
- const { targetInfos } = await this.send("Target.getTargets");
1177
- const page = targetInfos.find((t) => t.type === "page");
1178
- if (page) {
1179
- targetId = page.targetId;
1180
- } else {
1181
- const created = await this.send("Target.createTarget", { url: opts.url ?? "about:blank" });
1182
- targetId = created.targetId;
1183
- }
1184
- }
1185
- const { sessionId } = await this.send("Target.attachToTarget", { targetId, flatten: true });
1186
- return new _CdpSession(this.#conn, sessionId, this.#client, this.#profileId);
1259
+ /** To the bin for 30 days. */
1260
+ deleteTable(id) {
1261
+ return this.request("DELETE", `/v1/tables/${enc(id)}`);
1187
1262
  }
1188
- /** Navigate the page; optionally wait for load. Never calls `Runtime.enable`. */
1189
- async navigate(url, opts = {}) {
1190
- const res = await this.send("Page.navigate", { url });
1191
- if (res.errorText) throw new CdpError(-1, `navigation failed: ${res.errorText}`);
1192
- if (opts.wait !== false) await this.#waitForLoad(opts.timeout ?? 3e4);
1193
- return res;
1263
+ /** One page of rows in order, with the total. */
1264
+ listTableRows(id, params, signal) {
1265
+ const { where, ...rest } = params ?? {};
1266
+ return this.request("GET", `/v1/tables/${enc(id)}/rows`, void 0, {
1267
+ query: { ...rest, ...where ? { where: JSON.stringify(where) } : {} },
1268
+ signal
1269
+ });
1194
1270
  }
1195
- /** Evaluate a JS expression in the page (or an isolated world) and return its value. */
1196
- async evaluate(expression, opts = {}) {
1197
- const params = {
1198
- expression,
1199
- awaitPromise: opts.awaitPromise ?? true,
1200
- returnByValue: opts.returnByValue ?? true
1201
- };
1202
- if (opts.isolated) params["contextId"] = await this.createIsolatedWorld();
1203
- const res = await this.send("Runtime.evaluate", params);
1204
- if (res.exceptionDetails) {
1205
- throw new CdpError(-1, res.exceptionDetails.text || res.exceptionDetails.exception?.description || "evaluate exception");
1206
- }
1207
- return res.result.value;
1271
+ /** How many rows the table has outside the bin. */
1272
+ countTableRows(id, signal) {
1273
+ return this.request("GET", `/v1/tables/${enc(id)}/rows`, void 0, {
1274
+ query: { count_only: true },
1275
+ signal
1276
+ });
1277
+ }
1278
+ /** Ops in order, one result each. */
1279
+ editTable(id, body) {
1280
+ return this.request("POST", `/v1/tables/${enc(id)}/edit`, body);
1208
1281
  }
1209
1282
  /**
1210
- * Create an isolated world for the (main) frame and return its execution context id.
1211
- *
1212
- * The id belongs to the document that is loaded right now. After a navigation it
1213
- * names a context that no longer exists, so ask again instead of keeping it:
1214
- * Chrome hands back the same world for the same name, so asking costs one round
1215
- * trip.
1283
+ * Bring a file into a table: CSV (encoding and delimiter recognised), XLSX,
1284
+ * XLS or ODS, at most 10 MB, its first row naming the columns. A column
1285
+ * matches by name; a name the table lacks becomes a new column. The rows stay
1286
+ * invisible until the import is complete.
1216
1287
  */
1217
- async createIsolatedWorld(frameId, worldName = "__sb") {
1218
- let frame = frameId;
1219
- if (!frame) {
1220
- const { frameTree } = await this.send("Page.getFrameTree");
1221
- frame = frameTree.frame.id;
1222
- }
1223
- const { executionContextId } = await this.send("Page.createIsolatedWorld", {
1224
- frameId: frame,
1225
- worldName,
1226
- grantUniveralAccess: false
1288
+ importTable(id, file, params) {
1289
+ return this.request("POST", `/v1/tables/${enc(id)}/import`, file, {
1290
+ query: { ...params }
1227
1291
  });
1228
- return executionContextId;
1229
1292
  }
1230
- // ── Humanized trusted input (routes through the daemon) ────────────────────
1231
- humanizeMove(x, y, opts = {}) {
1232
- return this.#input({ action: "move", x, y, width: opts.width });
1293
+ /** The whole table as a file to save: CSV (UTF-8 with a byte order mark) or XLSX. */
1294
+ exportTable(id, format = "csv", signal) {
1295
+ return this.requestBytes("GET", `/v1/tables/${enc(id)}/export`, { query: { format }, signal });
1233
1296
  }
1234
- humanizeClick(x, y, opts = {}) {
1235
- return this.#input({
1236
- action: "click",
1237
- x,
1238
- y,
1239
- button: opts.button ?? "left",
1240
- click_count: opts.clickCount ?? 1,
1241
- width: opts.width
1242
- });
1297
+ /** The tables in the bin. */
1298
+ listTableTrash(signal) {
1299
+ return this.request("GET", "/v1/tables/trash", void 0, { signal });
1300
+ }
1301
+ /** Take tables back out of the bin. */
1302
+ restoreTables(ids) {
1303
+ return this.request("POST", "/v1/tables/trash/restore", { ids });
1304
+ }
1305
+ /** The Google accounts connected for linked sheets. Never a token. */
1306
+ async listGoogleConnections(signal) {
1307
+ const r = await this.request("GET", "/v1/connections/google", void 0, { signal });
1308
+ return r.connections;
1243
1309
  }
1244
1310
  /**
1245
- * B5: humanized move to the CENTRE of an element box, with the box width fed to the
1246
- * daemon's Fitts-law approach so the pointer lands *within* the element (not on a
1247
- * fixed corner). Pass a `getBoundingClientRect()`-shaped box.
1311
+ * Start connecting a Google account: open `auth_url` in the default browser;
1312
+ * the connection appears once the person agreed there.
1248
1313
  */
1249
- humanizeMoveToElement(box) {
1250
- return this.humanizeMove(box.x + box.width / 2, box.y + box.height / 2, { width: box.width });
1314
+ connectGoogle(body = {}) {
1315
+ return this.request("POST", "/v1/connections/google", body);
1251
1316
  }
1252
- /** B5: humanized click at the centre of an element box (see {@link humanizeMoveToElement}). */
1253
- humanizeClickElement(box, opts = {}) {
1254
- return this.humanizeClick(box.x + box.width / 2, box.y + box.height / 2, { ...opts, width: box.width });
1317
+ /** Disconnect and revoke at Google; a connection a table still uses stays as revoked. */
1318
+ async disconnectGoogle(id) {
1319
+ const r = await this.request("DELETE", `/v1/connections/google/${enc(id)}`);
1320
+ return r.connections;
1255
1321
  }
1256
- humanizeType(text) {
1257
- return this.#input({ action: "type", text });
1322
+ /**
1323
+ * Link a table to a sheet: open `auth_url` in the default browser, where Google
1324
+ * shows its own file picker; the link appears once the person picked the sheet.
1325
+ */
1326
+ linkSheet(connectionId, body) {
1327
+ return this.request("POST", `/v1/connections/google/${enc(connectionId)}/link`, body);
1258
1328
  }
1259
- humanizeScroll(x, y, opts = {}) {
1260
- return this.#input({ action: "scroll", x, y, delta_x: opts.deltaX ?? 0, delta_y: opts.deltaY ?? 0 });
1329
+ };
1330
+
1331
+ // src/client-workflows.ts
1332
+ var WorkflowsApi = class extends ClientCore {
1333
+ // -- Definitions --------------------------------------------------------------
1334
+ /** Every workflow, by name. */
1335
+ listWorkflows(signal) {
1336
+ return this.request("GET", "/v1/workflows", void 0, { signal });
1337
+ }
1338
+ /** An empty workflow. Its draft starts at revision 0. */
1339
+ createWorkflow(body) {
1340
+ return this.request("POST", "/v1/workflows", body);
1341
+ }
1342
+ /** A workflow in full: summary, draft document and the findings of the checks. */
1343
+ getWorkflow(id, params, signal) {
1344
+ return this.request("GET", `/v1/workflows/${enc(id)}`, void 0, {
1345
+ query: { ...params },
1346
+ signal
1347
+ });
1261
1348
  }
1262
- /** Close the underlying CDP connection. */
1263
- close() {
1264
- this.#conn.close();
1349
+ /** What a field of one node may point at: the steps that run before it and the workflow's scopes. */
1350
+ getWorkflowReferenceSources(id, node, signal) {
1351
+ return this.request("GET", `/v1/workflows/${enc(id)}/reference-sources`, void 0, {
1352
+ query: { node },
1353
+ signal
1354
+ });
1265
1355
  }
1266
- #input(body) {
1267
- if (!this.#client || !this.#profileId) {
1268
- return Promise.reject(
1269
- new CdpError(-1, "humanize requires a CDP session created via client.connectCdp(result, profileId)")
1270
- );
1271
- }
1272
- return this.#client.sendInput(this.#profileId, { ...body, humanize: true });
1356
+ renameWorkflow(id, name) {
1357
+ return this.request("PATCH", `/v1/workflows/${enc(id)}`, { name });
1273
1358
  }
1274
- // The poll reads in an isolated world: a page that wraps `document.readyState`
1275
- // sees a read from its own world, with a caller that is none of its scripts.
1276
- // While the new document commits, the world of the old one can vanish between
1277
- // asking for it and reading in it; the browser refuses that read with its own
1278
- // error code, and it is retried. A closed connection still fails at once.
1279
- async #waitForLoad(timeout) {
1280
- const deadline = Date.now() + timeout;
1281
- for (; ; ) {
1282
- const ready = await this.evaluate("document.readyState", { isolated: true }).catch((err) => {
1283
- if (err instanceof CdpError && err.code !== -1) return void 0;
1284
- throw err;
1285
- });
1286
- if (ready === "complete") return;
1287
- if (Date.now() > deadline) throw new CdpError(-1, "navigation wait timed out");
1288
- await delay(50);
1289
- }
1359
+ /** Delete a workflow with its draft, versions and runs. */
1360
+ deleteWorkflow(id, params) {
1361
+ return this.request("DELETE", `/v1/workflows/${enc(id)}`, void 0, {
1362
+ query: { ...params }
1363
+ });
1290
1364
  }
1291
- };
1292
- async function openCdpSession(url, options = {}) {
1293
- const headers = {};
1294
- if (options.token) headers["Authorization"] = `Bearer ${options.token}`;
1295
- const ws = new WebSocket(url, { headers, handshakeTimeout: options.openTimeout ?? 3e4 });
1296
- await waitForOpen(ws);
1297
- const conn = new CdpConnection(ws);
1298
- const root = new CdpSession(conn, void 0, options.client, options.profileId);
1299
- if (options.autoAttach === false) return root;
1300
- return root.attachToPage({ create: options.createTarget ?? false, url: options.targetUrl });
1301
- }
1302
- function waitForOpen(ws) {
1303
- return new Promise((resolve, reject) => {
1304
- const onOpen = () => {
1305
- cleanup();
1306
- resolve();
1307
- };
1308
- const onError = (err) => {
1309
- cleanup();
1310
- reject(new NetworkError("CDP websocket failed to open", err));
1311
- };
1312
- const cleanup = () => {
1313
- ws.off("open", onOpen);
1314
- ws.off("error", onError);
1315
- };
1316
- ws.on("open", onOpen);
1317
- ws.on("error", onError);
1318
- });
1319
- }
1320
- function scopedEventStream(conn, sessionId) {
1321
- const buffer = [];
1322
- const waiters = [];
1323
- let done = false;
1324
- const unsubscribe = conn.subscribe((event) => {
1325
- if (event.sessionId !== sessionId) return;
1326
- const waiter = waiters.shift();
1327
- if (waiter) waiter({ value: event, done: false });
1328
- else buffer.push(event);
1329
- });
1330
- let offClose = () => {
1331
- };
1332
- const finish = () => {
1333
- if (!done) {
1334
- done = true;
1335
- unsubscribe();
1336
- offClose();
1337
- for (const w of waiters.splice(0)) w({ value: void 0, done: true });
1338
- }
1339
- return { value: void 0, done: true };
1340
- };
1341
- offClose = conn.onClose(() => {
1342
- finish();
1343
- });
1344
- return {
1345
- next() {
1346
- const next = buffer.shift();
1347
- if (next !== void 0) return Promise.resolve({ value: next, done: false });
1348
- if (done) return Promise.resolve({ value: void 0, done: true });
1349
- return new Promise((resolve) => waiters.push(resolve));
1350
- },
1351
- return() {
1352
- return Promise.resolve(finish());
1353
- },
1354
- [Symbol.asyncIterator]() {
1355
- return this;
1356
- }
1357
- };
1358
- }
1359
- function delay(ms) {
1360
- return new Promise((resolve) => setTimeout(resolve, ms));
1361
- }
1362
-
1363
- // src/client.ts
1364
- var DEFAULT_CAPACITY = {
1365
- max_concurrent: 64,
1366
- ram_budget_mb: 24576,
1367
- vram_budget_mb: null
1368
- };
1369
- var ScalebrowserClient = class extends ClientCore {
1370
- // ── Profiles ───────────────────────────────────────────────────────────────
1371
- listProfiles(params = {}, signal) {
1372
- return this.request("GET", "/v1/profiles", void 0, {
1373
- query: {
1374
- group: params.group,
1375
- state: params.state,
1376
- q: params.q,
1377
- limit: params.limit,
1378
- offset: params.offset,
1379
- sort: params.sort,
1380
- order: params.order
1381
- },
1365
+ /**
1366
+ * Apply a batch of edit ops to the draft, as a person. The batch applies whole
1367
+ * or not at all; graph findings come back as `diagnostics`, not as a refusal.
1368
+ *
1369
+ * While another machine of the account edits this workflow the daemon answers
1370
+ * 409 with `reason: workflow_edit_locked` and the holder's name.
1371
+ */
1372
+ editWorkflow(id, body) {
1373
+ return this.request("POST", `/v1/workflows/${enc(id)}/edit`, body);
1374
+ }
1375
+ /**
1376
+ * Move a part of the draft into a workflow of its own, published at once. One
1377
+ * "Run Workflow" step stands where the part was, and later steps read its
1378
+ * result. The part needs one line in, one line out, no trigger, both ends of
1379
+ * every pair and no step that writes a variable; otherwise the daemon answers
1380
+ * 422 with `reason: extract_invalid` and the sentence why in `why`.
1381
+ * `base_rev` must be the draft's revision, else 409.
1382
+ */
1383
+ extractWorkflow(id, body) {
1384
+ return this.request("POST", `/v1/workflows/${enc(id)}/extract`, body);
1385
+ }
1386
+ // -- Versions -----------------------------------------------------------------
1387
+ /** The versions, newest first. */
1388
+ listWorkflowVersions(id, signal) {
1389
+ return this.request("GET", `/v1/workflows/${enc(id)}/versions`, void 0, {
1390
+ signal
1391
+ });
1392
+ }
1393
+ /** One version whole, with its graph document; a version of another workflow is 404. */
1394
+ getWorkflowVersion(id, versionId, signal) {
1395
+ return this.request(
1396
+ "GET",
1397
+ `/v1/workflows/${enc(id)}/versions/${enc(versionId)}`,
1398
+ void 0,
1399
+ { signal }
1400
+ );
1401
+ }
1402
+ /**
1403
+ * Save the draft as a version. The same content twice is one version. A draft
1404
+ * with a hard finding (a cycle, an open region) answers 422 with the findings.
1405
+ */
1406
+ saveWorkflowVersion(id, body) {
1407
+ return this.request("POST", `/v1/workflows/${enc(id)}/versions`, body ?? {});
1408
+ }
1409
+ /** Publish a version; `null` withdraws. Schedules, agents and every profile use what is published. */
1410
+ publishWorkflow(id, versionId) {
1411
+ return this.request("POST", `/v1/workflows/${enc(id)}/publish`, {
1412
+ version_id: versionId
1413
+ });
1414
+ }
1415
+ /**
1416
+ * Restore a version: the draft takes its content back as one batch, and that
1417
+ * content is saved as a new version. Publishing it is a step of its own.
1418
+ */
1419
+ revertWorkflow(id, body) {
1420
+ return this.request("POST", `/v1/workflows/${enc(id)}/revert`, body);
1421
+ }
1422
+ /**
1423
+ * Take one batch of the draft back, as a person: its restore ops become a new
1424
+ * batch, which can itself be undone. With the `batch_id` that
1425
+ * `adoptWorkflowVersion` answered it takes an applied repair back. A batch
1426
+ * undone already, or one a later batch changed over (`reason: undo_conflict`),
1427
+ * answers 409; a batch the draft never had, 404.
1428
+ */
1429
+ undoWorkflowBatch(id, batchId) {
1430
+ return this.request("POST", `/v1/workflows/${enc(id)}/batches/${enc(batchId)}/undo`);
1431
+ }
1432
+ /** What changed from one version to another; without `to`, against the draft. */
1433
+ diffWorkflow(id, params, signal) {
1434
+ return this.request("GET", `/v1/workflows/${enc(id)}/diff`, void 0, {
1435
+ query: { ...params },
1436
+ signal
1437
+ });
1438
+ }
1439
+ // -- Variables ----------------------------------------------------------------
1440
+ /** Every variable with its values; a secret value only as `{ withheld }`. */
1441
+ listWorkflowVariables(id, signal) {
1442
+ return this.request("GET", `/v1/workflows/${enc(id)}/variables`, void 0, {
1443
+ signal
1444
+ });
1445
+ }
1446
+ /** Set a value; a `secret` variable's value is stored sealed and never shown again. */
1447
+ setWorkflowVariable(id, varId, body) {
1448
+ return this.request("PUT", `/v1/workflows/${enc(id)}/variables/${enc(varId)}`, body);
1449
+ }
1450
+ /** The stored value in plain, behind the vault password. */
1451
+ revealWorkflowVariable(id, varId, body) {
1452
+ return this.request(
1453
+ "POST",
1454
+ `/v1/workflows/${enc(id)}/variables/${enc(varId)}/reveal`,
1455
+ body
1456
+ );
1457
+ }
1458
+ // -- Catalog ------------------------------------------------------------------
1459
+ /** The newest version of every node type. */
1460
+ listNodeTypes(params, signal) {
1461
+ return this.request("GET", "/v1/node-types", void 0, {
1462
+ query: { ...params },
1382
1463
  signal
1383
1464
  });
1384
1465
  }
1466
+ /** One node type: `click` for its newest version, `click@1` for a given one. */
1467
+ getNodeType(type, signal) {
1468
+ return this.request("GET", `/v1/node-types/${enc(type)}`, void 0, { signal });
1469
+ }
1385
1470
  /**
1386
- * Every id matching `params`, unpaged: what "act on all matches" needs.
1471
+ * The workflow's own settings, shaped like a node type's schema: one property
1472
+ * per setting a run reads, keyed by the dotted path `set_workflow_settings`
1473
+ * takes (`repair.rights`), `x-human-only` where only a person changes it.
1474
+ */
1475
+ getWorkflowSettingsSchema(signal) {
1476
+ return this.request("GET", "/v1/workflow-settings-schema", void 0, { signal });
1477
+ }
1478
+ };
1479
+
1480
+ // src/client-workflow-runs.ts
1481
+ var WorkflowRunsApi = class extends ClientCore {
1482
+ // -- Runs ---------------------------------------------------------------------
1483
+ /**
1484
+ * Start a run. With `wait` the request holds up to that many seconds (at most
1485
+ * 110) and answers with the result of a run that succeeded in that time.
1486
+ * Without a free profile the run waits in the queue, by default for an hour.
1387
1487
  *
1388
- * `listProfiles` is paged, so acting on "everything" from it only ever covers the
1389
- * page you fetched. Asking for ids costs one request and no persona payloads.
1488
+ * A start of the published version (the default) where nothing is published
1489
+ * answers 409 with `reason: no_published_version`; `{ version: 'draft' }` runs
1490
+ * the draft. A copy from a link nobody trusted yet answers 409 with
1491
+ * `reason: code_trust_required`. A `run_on` no single machine carries answers
1492
+ * 422 with `reason: run_on_unknown`; a draft test run naming another machine
1493
+ * answers 422 with `reason: run_on_local_only`.
1390
1494
  */
1391
- listProfileIds(params = {}, signal) {
1392
- return this.request("GET", "/v1/profiles/ids", void 0, {
1393
- query: {
1394
- group: params.group,
1395
- state: params.state,
1396
- q: params.q,
1397
- sort: params.sort,
1398
- order: params.order
1399
- },
1495
+ startWorkflowRun(id, body, params) {
1496
+ return this.request("POST", `/v1/workflows/${enc(id)}/runs`, body ?? {}, {
1497
+ query: { ...params }
1498
+ });
1499
+ }
1500
+ /** Runs, newest first. */
1501
+ listWorkflowRuns(params, signal) {
1502
+ return this.request("GET", "/v1/workflow-runs", void 0, {
1503
+ query: { ...params },
1504
+ signal
1505
+ });
1506
+ }
1507
+ /** One run with the state of every node. */
1508
+ getWorkflowRun(runId, params, signal) {
1509
+ return this.request("GET", `/v1/workflow-runs/${enc(runId)}`, void 0, {
1510
+ query: { ...params },
1400
1511
  signal
1401
1512
  });
1402
1513
  }
1514
+ /** The steps of a run with what went in and what came out. */
1515
+ listWorkflowRunSteps(runId, params, signal) {
1516
+ return this.request("GET", `/v1/workflow-runs/${enc(runId)}/steps`, void 0, {
1517
+ query: { ...params },
1518
+ signal
1519
+ });
1520
+ }
1521
+ /** This machine's folder boundary for file blocks. */
1522
+ getWorkflowFileAccess(signal) {
1523
+ return this.request("GET", "/v1/workflow-file-access", void 0, { signal });
1524
+ }
1525
+ /** Switch "only granted folders" on or off for this machine. */
1526
+ setWorkflowFileAccess(onlyGrantedFolders) {
1527
+ return this.request("PUT", "/v1/workflow-file-access", {
1528
+ only_granted_folders: onlyGrantedFolders
1529
+ });
1530
+ }
1531
+ /** Grant a folder (a file's path grants its folder) for reading or writing. */
1532
+ grantWorkflowFolder(path, mode, via) {
1533
+ return this.request("POST", "/v1/workflow-file-access/folders", { path, mode, via });
1534
+ }
1535
+ /** Take a folder off the list. */
1536
+ ungrantWorkflowFolder(path) {
1537
+ return this.request("DELETE", "/v1/workflow-file-access/folders", void 0, {
1538
+ query: { path }
1539
+ });
1540
+ }
1541
+ /** The fast mode's domain list, empty by default. */
1542
+ listWorkflowFastDomains(signal) {
1543
+ return this.request("GET", "/v1/workflow-fast-domains", void 0, { signal });
1544
+ }
1545
+ /** Put a domain on the fast mode's list, or switch it on again after it switched itself off. */
1546
+ putWorkflowFastDomain(domain) {
1547
+ return this.request("PUT", `/v1/workflow-fast-domains/${enc(domain)}`);
1548
+ }
1549
+ /** Take a domain off the fast mode's list. */
1550
+ deleteWorkflowFastDomain(domain) {
1551
+ return this.request("DELETE", `/v1/workflow-fast-domains/${enc(domain)}`);
1552
+ }
1403
1553
  /**
1404
- * How many profiles match `params`, split by runtime state. One request, and
1405
- * its size does not grow with the fleet.
1406
- *
1407
- * `listProfiles` is paged and answers a bare array, so counting what came back
1408
- * counts the page. `listProfileIds` carries the same total but ships an
1409
- * identifier per profile to do it, and cannot split by state at all.
1410
- *
1411
- * `params.state` is ignored here on purpose: a caller labelling every state is
1412
- * sitting in one of them.
1554
+ * What this machine can do for a planned start that wakes it: sleep model, wake timers,
1555
+ * the wake task, autostart and sign-in, each with its reason. A fix runs at the machine,
1556
+ * from the app's card, because Windows asks for an administrator's confirmation.
1413
1557
  */
1414
- countProfiles(params = {}, signal) {
1415
- return this.request("GET", "/v1/profiles/count", void 0, {
1416
- query: { group: params.group, q: params.q },
1558
+ getWorkflowReadiness(signal) {
1559
+ return this.request("GET", "/v1/workflow-readiness", void 0, { signal });
1560
+ }
1561
+ /** The approvals of this machine waiting for an answer, oldest first. */
1562
+ async listWorkflowApprovals(signal) {
1563
+ const out = await this.request("GET", "/v1/workflow-approvals", void 0, {
1417
1564
  signal
1418
1565
  });
1566
+ return out.approvals;
1419
1567
  }
1420
- getProfile(id, signal) {
1421
- return this.request("GET", `/v1/profiles/${enc(id)}`, void 0, { signal });
1568
+ /** Decide an approval from the app; the first valid decision counts. */
1569
+ decideWorkflowApproval(id, body) {
1570
+ return this.request("POST", `/v1/workflow-approvals/${enc(id)}/decide`, body);
1422
1571
  }
1423
- createProfile(body) {
1424
- return this.request("POST", "/v1/profiles", body);
1572
+ /**
1573
+ * The account's approval webhook. Your system answers an announced approval with an
1574
+ * API key that carries `approvals:decide`, at the `answer_url` the announcement names.
1575
+ */
1576
+ getApprovalWebhook(signal) {
1577
+ return this.request("GET", "/v1/workflow-approval-webhook", void 0, { signal });
1425
1578
  }
1426
- updateProfile(id, body) {
1427
- return this.request("PATCH", `/v1/profiles/${enc(id)}`, body);
1579
+ /** Set the approval webhook for every machine of the account; `url: null` clears it. */
1580
+ setApprovalWebhook(body) {
1581
+ return this.request("PUT", "/v1/workflow-approval-webhook", body);
1428
1582
  }
1429
- deleteProfile(id) {
1430
- return this.request("DELETE", `/v1/profiles/${enc(id)}`);
1583
+ /** The files a run keeps (screenshots, PDFs), without their bytes. */
1584
+ listWorkflowRunFiles(runId, signal) {
1585
+ return this.request("GET", `/v1/workflow-runs/${enc(runId)}/files`, void 0, {
1586
+ signal
1587
+ });
1431
1588
  }
1432
- /** Start a profile → CDP ws endpoint. Errors: 4002/4003/4005/4006/4008. */
1433
- startProfile(id, body = {}) {
1434
- return this.request("POST", `/v1/profiles/${enc(id)}/start`, body);
1589
+ /** One file a run keeps, as bytes to save. */
1590
+ getWorkflowRunFile(runId, fileId, signal) {
1591
+ return this.requestBytes("GET", `/v1/workflow-runs/${enc(runId)}/files/${enc(fileId)}`, { signal });
1435
1592
  }
1436
- stopProfile(id) {
1437
- return this.request("POST", `/v1/profiles/${enc(id)}/stop`, {});
1593
+ /** Cancel a run that has not ended. An ended run answers 409. */
1594
+ cancelWorkflowRun(runId) {
1595
+ return this.request("POST", `/v1/workflow-runs/${enc(runId)}/cancel`);
1438
1596
  }
1439
- /** Drop the profile's lease, the operator's way out.
1440
- *
1441
- * An agent releases with its handle; this call needs none. The daemon stops
1442
- * the browser first when one is still up, then lets the lease go, so a dead
1443
- * session cannot park the profile until its TTL runs out. `released: false`
1444
- * is an honest no-op: there was nothing to let go. */
1445
- releaseProfile(id) {
1446
- return this.request("POST", `/v1/profiles/${enc(id)}/release`, {});
1597
+ /**
1598
+ * Cancel a start that went to the machine its profile lives on, by the
1599
+ * `forward_ref` its start answered. Not yet fetched there, it ends
1600
+ * `cancelled` at once; fetched, it stays `sent` until that machine reports
1601
+ * its end. An ended one answers as it ended.
1602
+ */
1603
+ cancelWorkflowForward(forwardRef) {
1604
+ return this.request("POST", `/v1/workflow-forwards/${enc(forwardRef)}/cancel`);
1447
1605
  }
1448
- /** Ask for a profile that is open on another machine of this account.
1449
- *
1450
- * Closes nothing here and nothing there: the control plane revokes the holder's
1451
- * right, the holder closes its own browser at its next heartbeat. Start only
1452
- * after this answered `free`. Errors: 4012 when it is held elsewhere. */
1453
- takeoverProfile(id) {
1454
- return this.request("POST", `/v1/profiles/${enc(id)}/takeover`, {});
1606
+ /**
1607
+ * Answer a halted run: an effect that may have landed stopped it. `done` counts
1608
+ * it as sent and the run goes on without sending again, `retry` sends once
1609
+ * more, `abort` ends the run. A run that does not stand halted answers 409.
1610
+ * With `row_key`, answer one row to check instead: `done`, `retry` (the next
1611
+ * run takes the row again) or `ignore`.
1612
+ */
1613
+ decideWorkflowRun(runId, body) {
1614
+ return this.request("POST", `/v1/workflow-runs/${enc(runId)}/decide`, body);
1455
1615
  }
1456
- // ── Bulk (PRD §D2) ───────────────────────────────────────────────────────
1457
- bulkCreateProfiles(body) {
1458
- return this.request("POST", "/v1/bulk/profiles", body);
1616
+ /** What "run from step X" of a run would do: seeded, stale and coming nodes. */
1617
+ previewWorkflowRunFork(runId, body) {
1618
+ return this.request("POST", `/v1/workflow-runs/${enc(runId)}/fork-preview`, body);
1459
1619
  }
1460
1620
  /**
1461
- * Start a batch. `headless` is the ONE visibility decision for the run; omitted
1462
- * means headless, the right default for an unattended client. The daemon stops at
1463
- * the machine's capacity limit and reports how far it got (`started` /
1464
- * `remaining` / `stopped_reason`) instead of failing every remaining id.
1621
+ * Start "run from step X" of a run: the same trigger's run, the outputs before
1622
+ * the node taken from the source, nothing of them run again.
1465
1623
  */
1466
- bulkStart(ids, headless) {
1467
- return this.request("POST", "/v1/bulk/start", { ids, headless });
1624
+ forkWorkflowRun(runId, body) {
1625
+ return this.request("POST", `/v1/workflow-runs/${enc(runId)}/fork`, body);
1468
1626
  }
1469
- bulkStop(ids) {
1470
- return this.request("POST", "/v1/bulk/stop", { ids });
1627
+ /** This machine's ended runs per workflow, profile or day, with the success rate. */
1628
+ getWorkflowUsage(params, signal) {
1629
+ return this.request("GET", "/v1/workflow-usage", void 0, {
1630
+ query: { ...params },
1631
+ signal
1632
+ });
1471
1633
  }
1472
- bulkDelete(ids) {
1473
- return this.request("POST", "/v1/bulk/delete", { ids });
1634
+ /** The runs that wait, each with why: queued ones with their place in the dispatch order. */
1635
+ getWorkflowQueue(signal) {
1636
+ return this.request("GET", "/v1/workflow-queue", void 0, { signal });
1474
1637
  }
1475
- bulkAssignProxy(ids, proxyId) {
1476
- return this.request("POST", "/v1/bulk/assign-proxy", { ids, proxy_id: proxyId });
1638
+ /**
1639
+ * Planned, queued and skipped starts of "Schedule Window" triggers, oldest
1640
+ * moment first, each with its reason.
1641
+ */
1642
+ listWorkflowFires(params = {}, signal) {
1643
+ return this.request("GET", "/v1/workflow-fires", void 0, {
1644
+ query: { ...params },
1645
+ signal
1646
+ });
1647
+ }
1648
+ // -- Switches -----------------------------------------------------------------
1649
+ /** Every switch that was ever flipped; a switch without a row is on. */
1650
+ listWorkflowSwitches(signal) {
1651
+ return this.request("GET", "/v1/workflow-switches", void 0, { signal });
1477
1652
  }
1478
1653
  /**
1479
- * Give every id exactly this set of extensions.
1654
+ * Flip a switch. A profile switch turned off ends that profile's begun runs at
1655
+ * once; its queued runs wait until it is on again.
1656
+ */
1657
+ setWorkflowSwitch(body) {
1658
+ return this.request("PUT", "/v1/workflow-switches", body);
1659
+ }
1660
+ /**
1661
+ * Stop everything: the account switch off and every begun run ended. Nothing
1662
+ * starts until the account switch is on again.
1663
+ */
1664
+ emergencyStopWorkflows() {
1665
+ return this.request("POST", "/v1/workflow-switches/emergency-stop");
1666
+ }
1667
+ };
1668
+
1669
+ // src/client-workflow-ai.ts
1670
+ var WorkflowAiApi = class extends ClientCore {
1671
+ /** A repair's fixes, newest first; with `workflowId` those of one workflow. */
1672
+ listWorkflowRepairs(workflowId, signal) {
1673
+ return this.request("GET", "/v1/workflow-repairs", void 0, {
1674
+ signal,
1675
+ query: workflowId ? { workflow_id: workflowId } : void 0
1676
+ });
1677
+ }
1678
+ /** The repair's key figures per version and node; 7 days unless `days` (1 to 365) says otherwise. */
1679
+ getWorkflowRepairMetrics(workflowId, days, signal) {
1680
+ return this.request("GET", "/v1/workflow-repair-metrics", void 0, {
1681
+ signal,
1682
+ query: days === void 0 ? { workflow_id: workflowId } : { workflow_id: workflowId, days: String(days) }
1683
+ });
1684
+ }
1685
+ /** One fix with the attempts that made or used it. */
1686
+ getWorkflowRepair(id, signal) {
1687
+ return this.request("GET", `/v1/workflow-repairs/${enc(id)}`, void 0, { signal });
1688
+ }
1689
+ /**
1690
+ * Take a published fix back: a version `rollback` is published at once, the draft is left
1691
+ * as it is; an element changed since the repair stays and is named.
1692
+ */
1693
+ revertWorkflowRepair(id) {
1694
+ return this.request("POST", `/v1/workflow-repairs/${enc(id)}/revert`);
1695
+ }
1696
+ /**
1697
+ * Take a repair's version over into the draft as one batch of the repair (one undo takes it
1698
+ * back); a node a person changed since the repair's base is left and named.
1699
+ */
1700
+ adoptWorkflowVersion(workflowId, versionId) {
1701
+ return this.request("POST", `/v1/workflows/${enc(workflowId)}/adopt`, {
1702
+ version_id: versionId
1703
+ });
1704
+ }
1705
+ /** Every model source with this machine's health, by label; never a key. */
1706
+ listAiSources(signal) {
1707
+ return this.request("GET", "/v1/ai-sources", void 0, { signal });
1708
+ }
1709
+ /** Add a model source; its key is written once and never read back. */
1710
+ createAiSource(body) {
1711
+ return this.request("POST", "/v1/ai-sources", body);
1712
+ }
1713
+ /** Change a model source or replace its key. */
1714
+ updateAiSource(id, body) {
1715
+ return this.request("PATCH", `/v1/ai-sources/${enc(id)}`, body);
1716
+ }
1717
+ /** Remove a model source. */
1718
+ deleteAiSource(id) {
1719
+ return this.request("DELETE", `/v1/ai-sources/${enc(id)}`);
1720
+ }
1721
+ /** Check an API source now: its health, and what the model takes when it
1722
+ * answers (a few small paid calls). A coding agent is not checked here yet. */
1723
+ checkAiSource(id) {
1724
+ return this.request("POST", `/v1/ai-sources/${enc(id)}/check`);
1725
+ }
1726
+ /** The account's model settings: default order and subscription limits. */
1727
+ getAiSettings(signal) {
1728
+ return this.request("GET", "/v1/ai-settings", void 0, { signal });
1729
+ }
1730
+ /** Set the default order (existing sources only) and the subscription limits. */
1731
+ putAiSettings(body) {
1732
+ return this.request("PUT", "/v1/ai-settings", body);
1733
+ }
1734
+ /** The model calls of runs, newest first. */
1735
+ listAiInvocations(params = {}, signal) {
1736
+ return this.request("GET", "/v1/ai-invocations", void 0, {
1737
+ signal,
1738
+ query: { ...params }
1739
+ });
1740
+ }
1741
+ };
1742
+
1743
+ // src/client-workflow-sync.ts
1744
+ var WorkflowSyncApi = class extends ClientCore {
1745
+ /**
1746
+ * Every published workflow with a "Webhook" trigger, with the address to enter in
1747
+ * Zapier, Make or your own system, and the counts of held and expired calls.
1748
+ */
1749
+ async workflowHooks(signal) {
1750
+ const out = await this.request("GET", "/v1/workflow-hooks", void 0, { signal });
1751
+ return out.hooks;
1752
+ }
1753
+ /** A new address for the workflow; the old one stops taking calls. */
1754
+ rotateWorkflowHook(workflowId) {
1755
+ return this.request("POST", `/v1/workflow-hooks/${enc(workflowId)}/rotate`);
1756
+ }
1757
+ /**
1758
+ * This machine and every machine that is home to a profile, with the home of
1759
+ * each profile: the binding a start names in `run_on`. Empty without an account.
1760
+ */
1761
+ workflowMachines(signal) {
1762
+ return this.request("GET", "/v1/workflow-sync/machines", void 0, { signal });
1763
+ }
1764
+ };
1765
+
1766
+ // src/client-workflow-shares.ts
1767
+ var WorkflowSharesApi = class extends ClientCore {
1768
+ /**
1769
+ * Share a workflow by link. The link carries the published version, else the
1770
+ * newest saved one, or the version named in `versionId`; the draft is never
1771
+ * shared. Tables, variable values and secrets stay on this machine.
1480
1772
  *
1481
- * It REPLACES rather than adds, the same shape as assigning a proxy: picking a
1482
- * set for a batch states what those profiles carry. An empty list is a
1483
- * legitimate "none". An id the library does not hold refuses the whole call.
1773
+ * A refusal answers with a `reason`: `share_needs_version` (nothing is saved
1774
+ * yet), `share_unavailable` (no account is connected), `share_refused` or
1775
+ * `share_offline` (the account's server said no, or could not be reached).
1484
1776
  */
1485
- bulkAssignExtensions(ids, extRefs) {
1486
- return this.request("POST", "/v1/bulk/assign-extensions", { ids, ext_refs: extRefs });
1777
+ shareWorkflow(workflowId, opts = {}) {
1778
+ return this.request("POST", "/v1/workflow-shares", {
1779
+ workflow_id: workflowId,
1780
+ ...opts.versionId !== void 0 ? { version_id: opts.versionId } : {}
1781
+ });
1487
1782
  }
1488
- // ── Groups & presets ───────────────────────────────────────────────────────
1489
- listGroups(signal) {
1490
- return this.request("GET", "/v1/groups", void 0, { signal });
1783
+ /**
1784
+ * Open a link into a copy in this account. The copy keeps its sender's name
1785
+ * and asks a person's trust before its first start. A link that opens
1786
+ * nothing answers 400 with `reason: workflow_share_bad_link`; a revoked one
1787
+ * 404 with `workflow_share_revoked`; one from a newer Scalebrowser 422 with
1788
+ * `share_newer_format`.
1789
+ */
1790
+ importSharedWorkflow(link) {
1791
+ return this.request("POST", "/v1/workflow-shares/import", { link });
1491
1792
  }
1492
- getGroup(id, signal) {
1493
- return this.request("GET", `/v1/groups/${enc(id)}`, void 0, { signal });
1793
+ /**
1794
+ * Stop a link from making new copies. Copies already made stay. Named for the
1795
+ * workflow because `revokeShare` already withdraws a shared video.
1796
+ */
1797
+ revokeWorkflowShare(shareId) {
1798
+ return this.request("POST", `/v1/workflow-shares/${enc(shareId)}/revoke`);
1494
1799
  }
1495
- createGroup(body) {
1496
- return this.request("POST", "/v1/groups", body);
1800
+ /** The ready-made starting points. */
1801
+ workflowTemplates(signal) {
1802
+ return this.request("GET", "/v1/workflow-templates", void 0, { signal });
1497
1803
  }
1498
- updateGroup(id, body) {
1499
- return this.request("PATCH", `/v1/groups/${enc(id)}`, body);
1804
+ /** Make a workflow from a template; `name` replaces the template's own. */
1805
+ instantiateTemplate(templateId, opts = {}) {
1806
+ return this.request(
1807
+ "POST",
1808
+ `/v1/workflow-templates/${enc(templateId)}/instantiate`,
1809
+ opts.name !== void 0 ? { name: opts.name } : {}
1810
+ );
1500
1811
  }
1501
- deleteGroup(id) {
1502
- return this.request("DELETE", `/v1/groups/${enc(id)}`);
1812
+ };
1813
+
1814
+ // src/client-workflow-agency.ts
1815
+ var WorkflowAgencyApi = class extends ClientCore {
1816
+ /**
1817
+ * On a client app: what is shared with the agency, and the end-to-end mode
1818
+ * (`brief_mode`), which the agency switches and this call only shows. Before an
1819
+ * activation `activated` is `false` and `agency` is `null`.
1820
+ */
1821
+ wlSharing(signal) {
1822
+ return this.request("GET", "/v1/wl/sharing", void 0, { signal });
1503
1823
  }
1504
- listPresets(signal) {
1505
- return this.request("GET", "/v1/presets", void 0, { signal });
1824
+ /**
1825
+ * On a client app: change what is shared with the agency. A scope left out
1826
+ * stays as it is; turning one off withdraws it from then on, and what the
1827
+ * agency already saw stays seen. Answers the state after the change.
1828
+ *
1829
+ * Before the app is connected to an agency this answers 409 with
1830
+ * `reason: wl_not_activated`.
1831
+ */
1832
+ setWlSharing(body) {
1833
+ return this.request("PUT", "/v1/wl/sharing", body);
1506
1834
  }
1507
- getPreset(id, signal) {
1508
- return this.request("GET", `/v1/presets/${enc(id)}`, void 0, { signal });
1835
+ /** On an agency: its end customers, with the mode each one's machine reported last. */
1836
+ async agencyClients(signal) {
1837
+ const out = await this.request("GET", "/v1/agency/clients", void 0, { signal });
1838
+ return out.clients;
1509
1839
  }
1510
- createPreset(body) {
1511
- return this.request("POST", "/v1/presets", body);
1840
+ /**
1841
+ * On an agency: hand the published version of each workflow to a client. The
1842
+ * client's app adopts each one as its own published copy. A workflow that was
1843
+ * never published answers 409; publish it first.
1844
+ */
1845
+ setAgencyDistribution(clientId, workflowIds) {
1846
+ return this.request("PUT", "/v1/agency/distribution", {
1847
+ client_id: clientId,
1848
+ workflow_ids: workflowIds
1849
+ });
1850
+ }
1851
+ /**
1852
+ * On an agency: switch the end-to-end mode of one client. Only the agency can;
1853
+ * the client's own app shows the mode and cannot change it.
1854
+ */
1855
+ setClientBriefMode(clientId, mode) {
1856
+ return this.request(
1857
+ "PUT",
1858
+ `/v1/agency/clients/${enc(clientId)}/brief-mode`,
1859
+ { mode }
1860
+ );
1861
+ }
1862
+ /**
1863
+ * On an agency: set the model key its clients' apps use. Write-only: the
1864
+ * daemon keeps it and sends it, sealed, to every active client, and nothing
1865
+ * reads it back. Saving a key again sends it again. With `kind`, each client
1866
+ * turns it into a model source marked `agency`.
1867
+ */
1868
+ setAgencyAiKey(key, options = {}) {
1869
+ const body = { key };
1870
+ if (options.kind !== void 0) body.kind = options.kind;
1871
+ if (options.model !== void 0) body.model = options.model;
1872
+ if (options.baseUrl !== void 0) body.base_url = options.baseUrl;
1873
+ return this.request("PUT", "/v1/agency/ai-key", body);
1874
+ }
1875
+ };
1876
+
1877
+ // src/events.ts
1878
+ var MAX_BACKOFF_MS = 15e3;
1879
+ function subscribeEvents(params, handlers) {
1880
+ const controller = new AbortController();
1881
+ const fetchImpl = params.fetch ?? globalThis.fetch;
1882
+ let closed = false;
1883
+ let attempt = 0;
1884
+ let lastSeq = null;
1885
+ let everConnected = false;
1886
+ const setStatus = (s) => handlers.onStatus?.(s);
1887
+ const resync = (dropped) => handlers.onEvent({ type: "resync", dropped, at: Math.floor(Date.now() / 1e3) });
1888
+ const deliver = (event) => {
1889
+ if (typeof event.seq === "number") {
1890
+ if (lastSeq !== null && event.seq > lastSeq + 1) resync(event.seq - lastSeq - 1);
1891
+ lastSeq = event.seq;
1892
+ }
1893
+ handlers.onEvent(event);
1894
+ };
1895
+ async function connect() {
1896
+ while (!closed) {
1897
+ setStatus("connecting");
1898
+ try {
1899
+ const headers = { Accept: "text/event-stream" };
1900
+ if (params.token) headers["Authorization"] = `Bearer ${params.token}`;
1901
+ const res = await fetchImpl(`${params.baseUrl}/v1/events`, {
1902
+ headers,
1903
+ signal: controller.signal
1904
+ });
1905
+ if (res.status === 401) {
1906
+ params.onUnauthorized?.();
1907
+ break;
1908
+ }
1909
+ if (!res.ok || !res.body) {
1910
+ throw new NetworkError(`event stream failed (HTTP ${res.status})`);
1911
+ }
1912
+ attempt = 0;
1913
+ setStatus("open");
1914
+ lastSeq = null;
1915
+ if (everConnected) resync(0);
1916
+ everConnected = true;
1917
+ await pump(res.body, deliver, controller.signal);
1918
+ } catch {
1919
+ if (closed || controller.signal.aborted) break;
1920
+ }
1921
+ if (closed) break;
1922
+ setStatus("connecting");
1923
+ attempt += 1;
1924
+ const delay2 = Math.min(MAX_BACKOFF_MS, 500 * 2 ** Math.min(attempt, 5));
1925
+ await sleep(delay2, controller.signal);
1926
+ }
1927
+ setStatus("closed");
1512
1928
  }
1513
- updatePreset(id, body) {
1514
- return this.request("PATCH", `/v1/presets/${enc(id)}`, body);
1929
+ void connect();
1930
+ return () => {
1931
+ closed = true;
1932
+ controller.abort();
1933
+ };
1934
+ }
1935
+ async function pump(body, onEvent, signal) {
1936
+ const reader = body.getReader();
1937
+ const decoder = new TextDecoder();
1938
+ let buffer = "";
1939
+ try {
1940
+ while (!signal.aborted) {
1941
+ const { value, done } = await reader.read();
1942
+ if (done) break;
1943
+ buffer += decoder.decode(value, { stream: true });
1944
+ let sep;
1945
+ while ((sep = indexOfFrameEnd(buffer)) !== -1) {
1946
+ const frame = buffer.slice(0, sep);
1947
+ buffer = buffer.slice(sep).replace(/^(\r?\n){1,2}/, "");
1948
+ const event = parseSseFrame(frame);
1949
+ if (event) onEvent(event);
1950
+ }
1951
+ }
1952
+ } finally {
1953
+ void reader.cancel().catch(() => {
1954
+ });
1515
1955
  }
1516
- deletePreset(id) {
1517
- return this.request("DELETE", `/v1/presets/${enc(id)}`);
1956
+ }
1957
+ function indexOfFrameEnd(buffer) {
1958
+ const lf = buffer.indexOf("\n\n");
1959
+ const crlf = buffer.indexOf("\r\n\r\n");
1960
+ if (lf === -1) return crlf;
1961
+ if (crlf === -1) return lf;
1962
+ return Math.min(lf, crlf);
1963
+ }
1964
+ function parseSseFrame(frame) {
1965
+ const dataLines = [];
1966
+ for (const line of frame.split(/\r?\n/)) {
1967
+ if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
1518
1968
  }
1519
- // ── Video presets ──────────────────────────────────────────────────────────
1520
- listVideoPresets(signal) {
1521
- return this.request("GET", "/v1/video-presets", void 0, { signal });
1969
+ if (dataLines.length === 0) return null;
1970
+ const payload = dataLines.join("\n");
1971
+ if (!payload || payload === "[DONE]") return null;
1972
+ try {
1973
+ return JSON.parse(payload);
1974
+ } catch {
1975
+ return null;
1522
1976
  }
1523
- getVideoPreset(id, signal) {
1524
- return this.request("GET", `/v1/video-presets/${enc(id)}`, void 0, { signal });
1977
+ }
1978
+ function eventStream(params) {
1979
+ const buffer = [];
1980
+ const waiters = [];
1981
+ let done = false;
1982
+ const unsubscribe = subscribeEvents(params, {
1983
+ onEvent: (event) => {
1984
+ const waiter = waiters.shift();
1985
+ if (waiter) waiter({ value: event, done: false });
1986
+ else buffer.push(event);
1987
+ }
1988
+ });
1989
+ const finish = () => {
1990
+ if (!done) {
1991
+ done = true;
1992
+ unsubscribe();
1993
+ for (const w of waiters.splice(0)) w({ value: void 0, done: true });
1994
+ }
1995
+ return { value: void 0, done: true };
1996
+ };
1997
+ return {
1998
+ next() {
1999
+ const next = buffer.shift();
2000
+ if (next !== void 0) return Promise.resolve({ value: next, done: false });
2001
+ if (done) return Promise.resolve({ value: void 0, done: true });
2002
+ return new Promise((resolve) => waiters.push(resolve));
2003
+ },
2004
+ return() {
2005
+ return Promise.resolve(finish());
2006
+ },
2007
+ [Symbol.asyncIterator]() {
2008
+ return this;
2009
+ }
2010
+ };
2011
+ }
2012
+ function sleep(ms, signal) {
2013
+ return new Promise((resolve) => {
2014
+ if (signal.aborted) return resolve();
2015
+ const t = setTimeout(resolve, ms);
2016
+ signal.addEventListener(
2017
+ "abort",
2018
+ () => {
2019
+ clearTimeout(t);
2020
+ resolve();
2021
+ },
2022
+ { once: true }
2023
+ );
2024
+ });
2025
+ }
2026
+
2027
+ // src/cdp-connection.ts
2028
+ var CdpConnection = class {
2029
+ #ws;
2030
+ #nextId = 0;
2031
+ #pending = /* @__PURE__ */ new Map();
2032
+ #subscribers = /* @__PURE__ */ new Set();
2033
+ #closeListeners = /* @__PURE__ */ new Set();
2034
+ #closeReason = null;
2035
+ constructor(ws) {
2036
+ this.#ws = ws;
2037
+ ws.on("message", (data) => this.#onMessage(data));
2038
+ ws.on("close", () => this.#onClose(new CdpError(-1, "CDP connection closed")));
2039
+ ws.on("error", (err) => this.#onClose(new NetworkError("CDP connection error", err)));
1525
2040
  }
1526
- /** The daemon validates `config` strictly and names the failing field path. */
1527
- createVideoPreset(body) {
1528
- return this.request("POST", "/v1/video-presets", body);
2041
+ request(method, params, sessionId) {
2042
+ if (this.#closeReason) return Promise.reject(this.#closeReason);
2043
+ const id = ++this.#nextId;
2044
+ const frame = { id, method };
2045
+ if (params !== void 0) frame["params"] = params;
2046
+ if (sessionId !== void 0) frame["sessionId"] = sessionId;
2047
+ return new Promise((resolve, reject) => {
2048
+ this.#pending.set(id, { resolve, reject });
2049
+ this.#ws.send(JSON.stringify(frame), (err) => {
2050
+ if (err) {
2051
+ this.#pending.delete(id);
2052
+ reject(new NetworkError("failed to send CDP command", err));
2053
+ }
2054
+ });
2055
+ });
1529
2056
  }
1530
- /** A built-in preset refuses edits. Duplicate it and edit the copy. */
1531
- updateVideoPreset(id, body) {
1532
- return this.request("PATCH", `/v1/video-presets/${enc(id)}`, body);
2057
+ subscribe(cb) {
2058
+ this.#subscribers.add(cb);
2059
+ return () => this.#subscribers.delete(cb);
1533
2060
  }
1534
- /** The only way to make a built-in preset yours: copy, then edit the copy. */
1535
- duplicateVideoPreset(id, body = {}) {
1536
- return this.request("POST", `/v1/video-presets/${enc(id)}/duplicate`, body);
2061
+ /** Register a callback fired once when the connection closes (immediately if
2062
+ * it already has); event streams use this to end instead of hanging. */
2063
+ onClose(cb) {
2064
+ if (this.#closeReason) {
2065
+ cb();
2066
+ return () => {
2067
+ };
2068
+ }
2069
+ this.#closeListeners.add(cb);
2070
+ return () => this.#closeListeners.delete(cb);
1537
2071
  }
1538
- deleteVideoPreset(id) {
1539
- return this.request("DELETE", `/v1/video-presets/${enc(id)}`);
2072
+ close() {
2073
+ this.#ws.close();
1540
2074
  }
1541
- /** One still of the sample film under an UNSAVED config, drawn by the daemon's own renderer.
1542
- * `moment` wins over `frame`; both absent means the daemon's default moment. */
1543
- previewVideoPreset(body, signal) {
1544
- return this.request("POST", "/v1/video-presets/preview", body, { signal });
2075
+ get closed() {
2076
+ return this.#closeReason !== null;
1545
2077
  }
1546
- /** A clip of the sample film, blocking until it is encoded (the editor uses the SSE form of the
1547
- * same route; a script waits). Fetch the bytes with `downloadVideoPresetClip`. */
1548
- previewVideoPresetClip(body, signal) {
1549
- return this.request("POST", "/v1/video-presets/preview/clip", body, { signal });
2078
+ #onMessage(data) {
2079
+ let frame;
2080
+ try {
2081
+ frame = JSON.parse(rawToString(data));
2082
+ } catch {
2083
+ return;
2084
+ }
2085
+ if (typeof frame.id === "number") {
2086
+ const pending = this.#pending.get(frame.id);
2087
+ if (!pending) return;
2088
+ this.#pending.delete(frame.id);
2089
+ if (frame.error) pending.reject(new CdpError(frame.error.code, frame.error.message, frame.error.data));
2090
+ else pending.resolve(frame.result);
2091
+ return;
2092
+ }
2093
+ if (typeof frame.method === "string") {
2094
+ const event = { method: frame.method, params: frame.params ?? {}, sessionId: frame.sessionId };
2095
+ for (const cb of [...this.#subscribers]) cb(event);
2096
+ }
1550
2097
  }
1551
- /** The clip's MP4 bytes. */
1552
- downloadVideoPresetClip(id, signal) {
1553
- return this.requestBytes("GET", `/v1/video-presets/preview/clip/${enc(id)}`, { signal });
2098
+ #onClose(reason) {
2099
+ if (this.#closeReason) return;
2100
+ this.#closeReason = reason;
2101
+ for (const [, pending] of this.#pending) pending.reject(reason);
2102
+ this.#pending.clear();
2103
+ this.#subscribers.clear();
2104
+ for (const cb of [...this.#closeListeners]) cb();
2105
+ this.#closeListeners.clear();
1554
2106
  }
1555
- /**
1556
- * The values a preset's `constraints` may take. Ask rather than hardcode: the
1557
- * region list comes from the daemon's embedded persona model, so a pinned copy
1558
- * drifts the moment that model changes and every create 400s.
1559
- */
1560
- getPersonaConstraints(signal) {
1561
- return this.request(
1562
- "GET",
1563
- "/v1/persona/constraints",
1564
- void 0,
1565
- { signal }
1566
- );
2107
+ };
2108
+ function rawToString(data) {
2109
+ if (typeof data === "string") return data;
2110
+ if (Buffer.isBuffer(data)) return data.toString("utf8");
2111
+ if (Array.isArray(data)) return Buffer.concat(data).toString("utf8");
2112
+ return Buffer.from(data).toString("utf8");
2113
+ }
2114
+
2115
+ // src/cdp.ts
2116
+ var CdpSession = class _CdpSession {
2117
+ #conn;
2118
+ sessionId;
2119
+ #client;
2120
+ #profileId;
2121
+ constructor(conn, sessionId, client, profileId) {
2122
+ this.#conn = conn;
2123
+ this.sessionId = sessionId;
2124
+ this.#client = client;
2125
+ this.#profileId = profileId;
1567
2126
  }
1568
- // ── Proxies (credentials write-only) ────────────────────────────────────────
1569
- /** The list carries `used_by` per row; `getProxy` below does not. */
1570
- listProxies(signal) {
1571
- return this.request("GET", "/v1/proxies", void 0, { signal });
2127
+ /** Send a raw CDP command and await its result (by id). */
2128
+ send(method, params) {
2129
+ return this.#conn.request(method, params, this.sessionId);
1572
2130
  }
1573
- getProxy(id, signal) {
1574
- return this.request("GET", `/v1/proxies/${enc(id)}`, void 0, { signal });
2131
+ /** Register a callback for a CDP event scoped to this session. Returns unsubscribe. */
2132
+ on(method, callback) {
2133
+ return this.#conn.subscribe((event) => {
2134
+ if (event.method === method && event.sessionId === this.sessionId) callback(event.params);
2135
+ });
1575
2136
  }
1576
- createProxy(body) {
1577
- return this.request("POST", "/v1/proxies", body);
2137
+ /** `for await (const event of session.events())` over this session's CDP events. */
2138
+ events() {
2139
+ return scopedEventStream(this.#conn, this.sessionId);
1578
2140
  }
1579
- updateProxy(id, body) {
1580
- return this.request("PATCH", `/v1/proxies/${enc(id)}`, body);
2141
+ /** Attach to a page target, returning a page-bound child session. */
2142
+ async attachToPage(opts = {}) {
2143
+ let targetId = opts.targetId;
2144
+ if (opts.create) {
2145
+ const created = await this.send("Target.createTarget", { url: opts.url ?? "about:blank" });
2146
+ targetId = created.targetId;
2147
+ } else if (!targetId) {
2148
+ const { targetInfos } = await this.send("Target.getTargets");
2149
+ const page = targetInfos.find((t) => t.type === "page");
2150
+ if (page) {
2151
+ targetId = page.targetId;
2152
+ } else {
2153
+ const created = await this.send("Target.createTarget", { url: opts.url ?? "about:blank" });
2154
+ targetId = created.targetId;
2155
+ }
2156
+ }
2157
+ const { sessionId } = await this.send("Target.attachToTarget", { targetId, flatten: true });
2158
+ return new _CdpSession(this.#conn, sessionId, this.#client, this.#profileId);
1581
2159
  }
1582
- deleteProxy(id) {
1583
- return this.request("DELETE", `/v1/proxies/${enc(id)}`);
2160
+ /** Navigate the page; optionally wait for load. Never calls `Runtime.enable`. */
2161
+ async navigate(url, opts = {}) {
2162
+ const res = await this.send("Page.navigate", { url });
2163
+ if (res.errorText) throw new CdpError(-1, `navigation failed: ${res.errorText}`);
2164
+ if (opts.wait !== false) await this.#waitForLoad(opts.timeout ?? 3e4);
2165
+ return res;
1584
2166
  }
1585
- /** Health + geo check (exit-ip/country/healthy + JA4↔JA4T verdict). */
1586
- checkProxy(id) {
1587
- return this.request("POST", `/v1/proxies/${enc(id)}/check`, {});
2167
+ /** Evaluate a JS expression in the page (or an isolated world) and return its value. */
2168
+ async evaluate(expression, opts = {}) {
2169
+ const params = {
2170
+ expression,
2171
+ awaitPromise: opts.awaitPromise ?? true,
2172
+ returnByValue: opts.returnByValue ?? true
2173
+ };
2174
+ if (opts.isolated) params["contextId"] = await this.createIsolatedWorld();
2175
+ const res = await this.send("Runtime.evaluate", params);
2176
+ if (res.exceptionDetails) {
2177
+ throw new CdpError(-1, res.exceptionDetails.text || res.exceptionDetails.exception?.description || "evaluate exception");
2178
+ }
2179
+ return res.result.value;
1588
2180
  }
1589
2181
  /**
1590
- * Probe a proxy config WITHOUT persisting it: validate before you commit.
1591
- * Pass `id` (and omit the credentials) to probe an existing proxy's stored,
1592
- * write-only credentials in an edit form.
2182
+ * Create an isolated world for the (main) frame and return its execution context id.
2183
+ *
2184
+ * The id belongs to the document that is loaded right now. After a navigation it
2185
+ * names a context that no longer exists, so ask again instead of keeping it:
2186
+ * Chrome hands back the same world for the same name, so asking costs one round
2187
+ * trip.
1593
2188
  */
1594
- checkProxyConfig(body) {
1595
- return this.request("POST", "/v1/proxies/check", body);
2189
+ async createIsolatedWorld(frameId, worldName = "__sb") {
2190
+ let frame = frameId;
2191
+ if (!frame) {
2192
+ const { frameTree } = await this.send("Page.getFrameTree");
2193
+ frame = frameTree.frame.id;
2194
+ }
2195
+ const { executionContextId } = await this.send("Page.createIsolatedWorld", {
2196
+ frameId: frame,
2197
+ worldName,
2198
+ grantUniveralAccess: false
2199
+ });
2200
+ return executionContextId;
1596
2201
  }
1597
- // ── Live detector audit ─────────────────────────────────────────────────────
1598
- //
1599
- // Drives the profile's OWN running browser through ~15 third-party fingerprint
1600
- // sites and keeps what each one saw. The profile must be RUNNING; the run takes
1601
- // minutes, so `startAudit` returns immediately and you poll `getAudit`.
1602
- /** Queue a run → the status right after queueing. `409` if not running / already running. */
1603
- startAudit(profileId) {
1604
- return this.request("POST", `/v1/profiles/${enc(profileId)}/audit`);
2202
+ // ── Humanized trusted input (routes through the daemon) ────────────────────
2203
+ humanizeMove(x, y, opts = {}) {
2204
+ return this.#input({ action: "move", x, y, width: opts.width });
1605
2205
  }
1606
- /** State plus the last stored report. */
1607
- getAudit(profileId, signal) {
1608
- return this.request("GET", `/v1/profiles/${enc(profileId)}/audit`, void 0, {
1609
- signal
2206
+ humanizeClick(x, y, opts = {}) {
2207
+ return this.#input({
2208
+ action: "click",
2209
+ x,
2210
+ y,
2211
+ button: opts.button ?? "left",
2212
+ click_count: opts.clickCount ?? 1,
2213
+ width: opts.width
1610
2214
  });
1611
2215
  }
1612
- // ── Exit-address exclusivity ────────────────────────────────────────────────
1613
- /** Which exit addresses this profile owns, and how far the rule reaches.
1614
- *
1615
- * `scope` is `'machine'` while this computer has no account key. That is the
1616
- * ordinary state of a second machine until sync is unlocked there once, and it is
1617
- * reported rather than hidden: believing the rule spans three machines when it
1618
- * spans one is worse than knowing. */
1619
- getExitStatus(profileId, signal) {
1620
- return this.request("GET", `/v1/profiles/${enc(profileId)}/exit`, void 0, {
1621
- signal
1622
- });
2216
+ /**
2217
+ * B5: humanized move to the CENTRE of an element box, with the box width fed to the
2218
+ * daemon's Fitts-law approach so the pointer lands *within* the element (not on a
2219
+ * fixed corner). Pass a `getBoundingClientRect()`-shaped box.
2220
+ */
2221
+ humanizeMoveToElement(box) {
2222
+ return this.humanizeMove(box.x + box.width / 2, box.y + box.height / 2, { width: box.width });
1623
2223
  }
1624
- // ── Session bundles ─────────────────────────────────────────────────────────
1625
- exportSession(profileId, body) {
1626
- return this.request("POST", `/v1/profiles/${enc(profileId)}/session/export`, body);
2224
+ /** B5: humanized click at the centre of an element box (see {@link humanizeMoveToElement}). */
2225
+ humanizeClickElement(box, opts = {}) {
2226
+ return this.humanizeClick(box.x + box.width / 2, box.y + box.height / 2, { ...opts, width: box.width });
1627
2227
  }
1628
- importSession(profileId, body) {
1629
- return this.request("POST", `/v1/profiles/${enc(profileId)}/session/import`, body);
2228
+ humanizeType(text) {
2229
+ return this.#input({ action: "type", text });
1630
2230
  }
1631
- // ── Trusted input ──────────────────────────────────────────────────────────
1632
- sendInput(profileId, body) {
1633
- return this.request("POST", `/v1/profiles/${enc(profileId)}/input`, body);
2231
+ humanizeScroll(x, y, opts = {}) {
2232
+ return this.#input({ action: "scroll", x, y, delta_x: opts.deltaX ?? 0, delta_y: opts.deltaY ?? 0 });
2233
+ }
2234
+ /** Close the underlying CDP connection. */
2235
+ close() {
2236
+ this.#conn.close();
2237
+ }
2238
+ #input(body) {
2239
+ if (!this.#client || !this.#profileId) {
2240
+ return Promise.reject(
2241
+ new CdpError(-1, "humanize requires a CDP session created via client.connectCdp(result, profileId)")
2242
+ );
2243
+ }
2244
+ return this.#client.sendInput(this.#profileId, { ...body, humanize: true });
2245
+ }
2246
+ // The poll reads in an isolated world: a page that wraps `document.readyState`
2247
+ // sees a read from its own world, with a caller that is none of its scripts.
2248
+ // While the new document commits, the world of the old one can vanish between
2249
+ // asking for it and reading in it; the browser refuses that read with its own
2250
+ // error code, and it is retried. A closed connection still fails at once.
2251
+ async #waitForLoad(timeout) {
2252
+ const deadline = Date.now() + timeout;
2253
+ for (; ; ) {
2254
+ const ready = await this.evaluate("document.readyState", { isolated: true }).catch((err) => {
2255
+ if (err instanceof CdpError && err.code !== -1) return void 0;
2256
+ throw err;
2257
+ });
2258
+ if (ready === "complete") return;
2259
+ if (Date.now() > deadline) throw new CdpError(-1, "navigation wait timed out");
2260
+ await delay(50);
2261
+ }
1634
2262
  }
2263
+ };
2264
+ async function openCdpSession(url, options = {}) {
2265
+ const headers = {};
2266
+ if (options.token) headers["Authorization"] = `Bearer ${options.token}`;
2267
+ const ws = new WebSocket(url, { headers, handshakeTimeout: options.openTimeout ?? 3e4 });
2268
+ await waitForOpen(ws);
2269
+ const conn = new CdpConnection(ws);
2270
+ const root = new CdpSession(conn, void 0, options.client, options.profileId);
2271
+ if (options.autoAttach === false) return root;
2272
+ return root.attachToPage({ create: options.createTarget ?? false, url: options.targetUrl });
2273
+ }
2274
+ function waitForOpen(ws) {
2275
+ return new Promise((resolve, reject) => {
2276
+ const onOpen = () => {
2277
+ cleanup();
2278
+ resolve();
2279
+ };
2280
+ const onError = (err) => {
2281
+ cleanup();
2282
+ reject(new NetworkError("CDP websocket failed to open", err));
2283
+ };
2284
+ const cleanup = () => {
2285
+ ws.off("open", onOpen);
2286
+ ws.off("error", onError);
2287
+ };
2288
+ ws.on("open", onOpen);
2289
+ ws.on("error", onError);
2290
+ });
2291
+ }
2292
+ function scopedEventStream(conn, sessionId) {
2293
+ const buffer = [];
2294
+ const waiters = [];
2295
+ let done = false;
2296
+ const unsubscribe = conn.subscribe((event) => {
2297
+ if (event.sessionId !== sessionId) return;
2298
+ const waiter = waiters.shift();
2299
+ if (waiter) waiter({ value: event, done: false });
2300
+ else buffer.push(event);
2301
+ });
2302
+ let offClose = () => {
2303
+ };
2304
+ const finish = () => {
2305
+ if (!done) {
2306
+ done = true;
2307
+ unsubscribe();
2308
+ offClose();
2309
+ for (const w of waiters.splice(0)) w({ value: void 0, done: true });
2310
+ }
2311
+ return { value: void 0, done: true };
2312
+ };
2313
+ offClose = conn.onClose(() => {
2314
+ finish();
2315
+ });
2316
+ return {
2317
+ next() {
2318
+ const next = buffer.shift();
2319
+ if (next !== void 0) return Promise.resolve({ value: next, done: false });
2320
+ if (done) return Promise.resolve({ value: void 0, done: true });
2321
+ return new Promise((resolve) => waiters.push(resolve));
2322
+ },
2323
+ return() {
2324
+ return Promise.resolve(finish());
2325
+ },
2326
+ [Symbol.asyncIterator]() {
2327
+ return this;
2328
+ }
2329
+ };
2330
+ }
2331
+ function delay(ms) {
2332
+ return new Promise((resolve) => setTimeout(resolve, ms));
2333
+ }
2334
+
2335
+ // src/client.ts
2336
+ var DEFAULT_CAPACITY = {
2337
+ max_concurrent: 64,
2338
+ ram_budget_mb: 24576,
2339
+ vram_budget_mb: null
2340
+ };
2341
+ var ScalebrowserClient = class extends ClientCore {
1635
2342
  // ── Metrics (with graceful fallback) ────────────────────────────────────────
1636
2343
  async getMetrics(signal) {
1637
2344
  try {
@@ -1755,21 +2462,33 @@ function deriveMetricsFromProfiles(profiles) {
1755
2462
  };
1756
2463
  }
1757
2464
  applyMixins(ScalebrowserClient, [
2465
+ ProfilesApi,
2466
+ PresetsApi,
2467
+ ProxiesApi,
1758
2468
  IdentityApi,
1759
2469
  RunsApi,
1760
2470
  VideosApi,
1761
2471
  ControlApi,
1762
2472
  ExtensionsApi,
1763
- TasksApi
2473
+ TasksApi,
2474
+ WorkflowsApi,
2475
+ WorkflowRunsApi,
2476
+ WorkflowAiApi,
2477
+ WorkflowSyncApi,
2478
+ WorkflowSharesApi,
2479
+ WorkflowAgencyApi,
2480
+ TablesApi
1764
2481
  ]);
1765
2482
 
2483
+ // src/types-proxies.ts
2484
+ var PROXY_KINDS = ["http", "socks5", "gateway"];
2485
+ var ROTATIONS = ["sticky", "rotating"];
2486
+
1766
2487
  // src/types.ts
1767
2488
  var RUNTIME_STATES = ["stopped", "starting", "running", "crashed"];
1768
2489
  var GEO_MODES = ["strict_expected", "follow_exit", "off"];
1769
- var PROXY_KINDS = ["http", "socks5", "gateway"];
1770
- var ROTATIONS = ["sticky", "rotating"];
1771
2490
 
1772
2491
  // src/index.ts
1773
- var VERSION = "1.2.0";
2492
+ var VERSION = "1.3.0";
1774
2493
 
1775
2494
  export { ApiError, CdpError, CdpSession, DEFAULT_CAPACITY, ErrorCode, GEO_MODES, NetworkError, PROXY_KINDS, ROTATIONS, RUNTIME_STATES, ScalebrowserClient, ScalebrowserError, VERSION, deriveMetricsFromProfiles, errorMessage, eventStream, openCdpSession, parseSseFrame, subscribeEvents };