@scalebrowser/sdk 1.1.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/CHANGELOG.md +80 -0
- package/README.md +97 -11
- package/dist/index.cjs +1623 -866
- package/dist/index.d.cts +3321 -792
- package/dist/index.d.ts +3321 -792
- package/dist/index.js +1623 -866
- package/package.json +1 -1
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) {
|
|
@@ -582,1036 +625,1720 @@ var IdentityApi = class extends ClientCore {
|
|
|
582
625
|
revealCookies(profileId, body) {
|
|
583
626
|
return this.request("POST", `/v1/profiles/${enc(profileId)}/cookies/reveal`, body);
|
|
584
627
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
// src/client-tasks.ts
|
|
588
|
-
var TasksApi = class extends ClientCore {
|
|
589
|
-
// -- Tasks ------------------------------------------------------------------
|
|
590
|
-
//
|
|
591
|
-
// Fleet-wide by default, and deliberately so: the question this surface exists
|
|
592
|
-
// for is "what is outstanding across my twenty accounts", and a per-profile
|
|
593
|
-
// route would make a client ask it twenty times and sort the answers itself.
|
|
594
|
-
//
|
|
595
|
-
// None of these starts a browser. A task is a row, so reading the whole fleet's
|
|
596
|
-
// list costs nothing from the plan's concurrent-browser allowance. Reserve only
|
|
597
|
-
// the profiles that turn out to have work.
|
|
628
|
+
// ── Recorded requests ──────────────────────────────────────────────────────
|
|
598
629
|
/**
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
630
|
+
* The one door what a recorded request carried leaves through, and it needs the vault
|
|
631
|
+
* password: the full address with its query, every header value (`Cookie`,
|
|
632
|
+
* `Authorization`) and, on request, the body. The agent's `net_capture` answers by
|
|
633
|
+
* shape only. `seq` is the number it listed.
|
|
603
634
|
*/
|
|
604
|
-
|
|
605
|
-
return this.request("
|
|
606
|
-
query: { ...params },
|
|
607
|
-
signal
|
|
608
|
-
});
|
|
635
|
+
revealRequest(profileId, seq, body) {
|
|
636
|
+
return this.request("POST", `/v1/profiles/${enc(profileId)}/network/${seq}/reveal`, body);
|
|
609
637
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
* whole call.
|
|
618
|
-
*/
|
|
619
|
-
createTasks(body) {
|
|
620
|
-
return this.request("POST", "/v1/tasks", body);
|
|
638
|
+
};
|
|
639
|
+
|
|
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 });
|
|
621
645
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
*
|
|
625
|
-
* Only a task nobody has claimed can be cancelled here. One an agent is working
|
|
626
|
-
* on right now is stopped by telling that agent, which is a channel that already
|
|
627
|
-
* reaches it; this surface answers `409` for it rather than pretending.
|
|
628
|
-
*/
|
|
629
|
-
cancelTask(id) {
|
|
630
|
-
return this.request("PATCH", `/v1/tasks/${enc(id)}`, { state: "cancelled" });
|
|
646
|
+
getGroup(id, signal) {
|
|
647
|
+
return this.request("GET", `/v1/groups/${enc(id)}`, void 0, { signal });
|
|
631
648
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
return this.request("DELETE", `/v1/tasks/${enc(id)}`);
|
|
649
|
+
createGroup(body) {
|
|
650
|
+
return this.request("POST", "/v1/groups", body);
|
|
635
651
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
* A profile's durable memory, the `PROFILE.md` at the top of its directory.
|
|
639
|
-
*
|
|
640
|
-
* `content` is `null` while nobody has written one: a profile starts with no
|
|
641
|
-
* file, not with an empty one.
|
|
642
|
-
*/
|
|
643
|
-
getProfileMemory(profileId, signal) {
|
|
644
|
-
return this.request("GET", `/v1/profiles/${enc(profileId)}/memory`, void 0, {
|
|
645
|
-
signal
|
|
646
|
-
});
|
|
652
|
+
updateGroup(id, body) {
|
|
653
|
+
return this.request("PATCH", `/v1/groups/${enc(id)}`, body);
|
|
647
654
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
*
|
|
651
|
-
* Stored verbatim: no trimming, no appended newline, no line-ending
|
|
652
|
-
* translation. The last write wins and nothing warns, exactly as with a file
|
|
653
|
-
* two people can open.
|
|
654
|
-
*/
|
|
655
|
-
putProfileMemory(profileId, content) {
|
|
656
|
-
return this.request("PUT", `/v1/profiles/${enc(profileId)}/memory`, { content });
|
|
655
|
+
deleteGroup(id) {
|
|
656
|
+
return this.request("DELETE", `/v1/groups/${enc(id)}`);
|
|
657
657
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
return this.request("DELETE", `/v1/profiles/${enc(profileId)}/memory`);
|
|
658
|
+
listPresets(signal) {
|
|
659
|
+
return this.request("GET", "/v1/presets", void 0, { signal });
|
|
661
660
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
// src/client-runs.ts
|
|
665
|
-
var RunsApi = class extends ClientCore {
|
|
666
|
-
// ── Agent runs ─────────────────────────────────────────────────────────────
|
|
667
|
-
/** Runs, newest first. Read-only, all of it. */
|
|
668
|
-
listRuns(params = {}, signal) {
|
|
669
|
-
return this.request("GET", "/v1/runs", void 0, {
|
|
670
|
-
query: { profile_id: params.profile_id, limit: params.limit, offset: params.offset },
|
|
671
|
-
signal
|
|
672
|
-
});
|
|
661
|
+
getPreset(id, signal) {
|
|
662
|
+
return this.request("GET", `/v1/presets/${enc(id)}`, void 0, { signal });
|
|
673
663
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
return this.request("GET", `/v1/runs/${enc(runId)}`, void 0, { signal });
|
|
664
|
+
createPreset(body) {
|
|
665
|
+
return this.request("POST", "/v1/presets", body);
|
|
677
666
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
667
|
+
updatePreset(id, body) {
|
|
668
|
+
return this.request("PATCH", `/v1/presets/${enc(id)}`, body);
|
|
669
|
+
}
|
|
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 });
|
|
684
708
|
}
|
|
685
709
|
/**
|
|
686
|
-
* The
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
* 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.
|
|
690
713
|
*/
|
|
691
|
-
|
|
692
|
-
return this.
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
714
|
+
getPersonaConstraints(signal) {
|
|
715
|
+
return this.request(
|
|
716
|
+
"GET",
|
|
717
|
+
"/v1/persona/constraints",
|
|
718
|
+
void 0,
|
|
719
|
+
{ signal }
|
|
720
|
+
);
|
|
697
721
|
}
|
|
698
722
|
};
|
|
699
723
|
|
|
700
|
-
// src/client-
|
|
701
|
-
var
|
|
702
|
-
// ──
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
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, {
|
|
706
729
|
query: {
|
|
707
|
-
|
|
708
|
-
profile_id: params.profile_id,
|
|
709
|
-
run_id: params.run_id,
|
|
710
|
-
kind: params.kind,
|
|
711
|
-
preset: params.preset,
|
|
730
|
+
group: params.group,
|
|
712
731
|
state: params.state,
|
|
713
|
-
|
|
732
|
+
q: params.q,
|
|
733
|
+
limit: params.limit,
|
|
734
|
+
offset: params.offset,
|
|
735
|
+
sort: params.sort,
|
|
736
|
+
order: params.order
|
|
714
737
|
},
|
|
715
738
|
signal
|
|
716
739
|
});
|
|
717
740
|
}
|
|
718
|
-
/** One video row, progress included while it renders. */
|
|
719
|
-
getVideo(videoId, signal) {
|
|
720
|
-
return this.request("GET", `/v1/videos/${enc(videoId)}`, void 0, { signal });
|
|
721
|
-
}
|
|
722
|
-
/** Rename. The title also lands in the MP4's metadata on the NEXT render. */
|
|
723
|
-
renameVideo(videoId, title, signal) {
|
|
724
|
-
return this.request("PATCH", `/v1/videos/${enc(videoId)}`, { title }, { signal });
|
|
725
|
-
}
|
|
726
741
|
/**
|
|
727
|
-
*
|
|
728
|
-
*
|
|
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.
|
|
729
746
|
*/
|
|
730
|
-
|
|
731
|
-
return this.request("
|
|
732
|
-
query: {
|
|
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
|
+
},
|
|
733
756
|
signal
|
|
734
757
|
});
|
|
735
758
|
}
|
|
736
|
-
/**
|
|
737
|
-
|
|
738
|
-
|
|
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 },
|
|
739
773
|
signal
|
|
740
774
|
});
|
|
741
775
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
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 });
|
|
745
778
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
return this.request(
|
|
749
|
-
"POST",
|
|
750
|
-
`/v1/videos/${enc(videoId)}/access`,
|
|
751
|
-
asset ? { asset } : {},
|
|
752
|
-
{ signal }
|
|
753
|
-
);
|
|
779
|
+
createProfile(body) {
|
|
780
|
+
return this.request("POST", "/v1/profiles", body);
|
|
754
781
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
const grant = await this.videoAccess(videoId, asset, signal);
|
|
758
|
-
const grantParam = new URL(grant.url, "http://x").searchParams.get("grant") ?? "";
|
|
759
|
-
return this.requestBytes("GET", `/v1/videos/${enc(videoId)}/file`, {
|
|
760
|
-
query: { asset, grant: grantParam },
|
|
761
|
-
signal
|
|
762
|
-
});
|
|
782
|
+
updateProfile(id, body) {
|
|
783
|
+
return this.request("PATCH", `/v1/profiles/${enc(id)}`, body);
|
|
763
784
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
return this.request("GET", "/v1/videos/storage", void 0, { signal });
|
|
785
|
+
deleteProfile(id) {
|
|
786
|
+
return this.request("DELETE", `/v1/profiles/${enc(id)}`);
|
|
767
787
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
return this.request("GET", "/v1/recordings", void 0, {
|
|
772
|
-
query: { profile_id: params.profile_id },
|
|
773
|
-
signal
|
|
774
|
-
});
|
|
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);
|
|
775
791
|
}
|
|
776
|
-
|
|
777
|
-
return this.request("
|
|
778
|
-
|
|
779
|
-
|
|
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);
|
|
780
815
|
}
|
|
781
816
|
/**
|
|
782
|
-
*
|
|
783
|
-
*
|
|
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.
|
|
784
821
|
*/
|
|
785
|
-
|
|
786
|
-
return this.request("
|
|
787
|
-
signal
|
|
788
|
-
});
|
|
822
|
+
bulkStart(ids, headless) {
|
|
823
|
+
return this.request("POST", "/v1/bulk/start", { ids, headless });
|
|
789
824
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
return this.request("POST", `/v1/recordings/${enc(recordingId)}/renders`, body, {
|
|
793
|
-
signal
|
|
794
|
-
});
|
|
825
|
+
bulkStop(ids) {
|
|
826
|
+
return this.request("POST", "/v1/bulk/stop", { ids });
|
|
795
827
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
* off the binary: `h264_nvenc` is always listed and fails without a driver. */
|
|
799
|
-
getCodecs(signal) {
|
|
800
|
-
return this.request("GET", "/v1/codecs", void 0, { signal });
|
|
828
|
+
bulkDelete(ids) {
|
|
829
|
+
return this.request("POST", "/v1/bulk/delete", { ids });
|
|
801
830
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
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 });
|
|
805
833
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
*
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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 });
|
|
813
843
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
void 0,
|
|
823
|
-
{ signal }
|
|
824
|
-
);
|
|
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`);
|
|
825
852
|
}
|
|
826
|
-
/**
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
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, {
|
|
830
856
|
signal
|
|
831
857
|
});
|
|
832
858
|
}
|
|
833
|
-
//
|
|
834
|
-
/**
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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, {
|
|
838
868
|
signal
|
|
839
869
|
});
|
|
840
870
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
return this.request("POST",
|
|
844
|
-
query: { kind, name, mime },
|
|
845
|
-
signal
|
|
846
|
-
});
|
|
871
|
+
// ── Session bundles ─────────────────────────────────────────────────────────
|
|
872
|
+
exportSession(profileId, body) {
|
|
873
|
+
return this.request("POST", `/v1/profiles/${enc(profileId)}/session/export`, body);
|
|
847
874
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
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);
|
|
851
877
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
return this.
|
|
878
|
+
// ── Trusted input ──────────────────────────────────────────────────────────
|
|
879
|
+
sendInput(profileId, body) {
|
|
880
|
+
return this.request("POST", `/v1/profiles/${enc(profileId)}/input`, body);
|
|
855
881
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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 });
|
|
861
890
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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.
|
|
868
911
|
*/
|
|
869
|
-
|
|
870
|
-
return this.request(
|
|
871
|
-
"PUT",
|
|
872
|
-
`/v1/tts-providers/${enc(provider)}`,
|
|
873
|
-
{ api_key: body.apiKey, label: body.label, profile_id: body.profileId },
|
|
874
|
-
{ signal }
|
|
875
|
-
);
|
|
912
|
+
checkProxyConfig(body) {
|
|
913
|
+
return this.request("POST", "/v1/proxies/check", body);
|
|
876
914
|
}
|
|
877
|
-
|
|
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.
|
|
878
930
|
*
|
|
879
|
-
*
|
|
880
|
-
*
|
|
881
|
-
* broken.
|
|
931
|
+
* Pass `state: 'open'` for what is outstanding; without it, tasks finished
|
|
932
|
+
* within the last day are included too.
|
|
882
933
|
*/
|
|
883
|
-
|
|
884
|
-
return this.request(
|
|
885
|
-
|
|
886
|
-
`/v1/tts-providers/${enc(provider)}/verify`,
|
|
887
|
-
void 0,
|
|
888
|
-
{ signal }
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
/** Forget a provider's key. */
|
|
892
|
-
deleteTtsProvider(provider, signal) {
|
|
893
|
-
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 },
|
|
894
937
|
signal
|
|
895
938
|
});
|
|
896
939
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
const setStatus = (s) => handlers.onStatus?.(s);
|
|
909
|
-
const resync = (dropped) => handlers.onEvent({ type: "resync", dropped, at: Math.floor(Date.now() / 1e3) });
|
|
910
|
-
const deliver = (event) => {
|
|
911
|
-
if (typeof event.seq === "number") {
|
|
912
|
-
if (lastSeq !== null && event.seq > lastSeq + 1) resync(event.seq - lastSeq - 1);
|
|
913
|
-
lastSeq = event.seq;
|
|
914
|
-
}
|
|
915
|
-
handlers.onEvent(event);
|
|
916
|
-
};
|
|
917
|
-
async function connect() {
|
|
918
|
-
while (!closed) {
|
|
919
|
-
setStatus("connecting");
|
|
920
|
-
try {
|
|
921
|
-
const headers = { Accept: "text/event-stream" };
|
|
922
|
-
if (params.token) headers["Authorization"] = `Bearer ${params.token}`;
|
|
923
|
-
const res = await fetchImpl(`${params.baseUrl}/v1/events`, {
|
|
924
|
-
headers,
|
|
925
|
-
signal: controller.signal
|
|
926
|
-
});
|
|
927
|
-
if (res.status === 401) {
|
|
928
|
-
params.onUnauthorized?.();
|
|
929
|
-
break;
|
|
930
|
-
}
|
|
931
|
-
if (!res.ok || !res.body) {
|
|
932
|
-
throw new NetworkError(`event stream failed (HTTP ${res.status})`);
|
|
933
|
-
}
|
|
934
|
-
attempt = 0;
|
|
935
|
-
setStatus("open");
|
|
936
|
-
lastSeq = null;
|
|
937
|
-
if (everConnected) resync(0);
|
|
938
|
-
everConnected = true;
|
|
939
|
-
await pump(res.body, deliver, controller.signal);
|
|
940
|
-
} catch {
|
|
941
|
-
if (closed || controller.signal.aborted) break;
|
|
942
|
-
}
|
|
943
|
-
if (closed) break;
|
|
944
|
-
setStatus("connecting");
|
|
945
|
-
attempt += 1;
|
|
946
|
-
const delay2 = Math.min(MAX_BACKOFF_MS, 500 * 2 ** Math.min(attempt, 5));
|
|
947
|
-
await sleep(delay2, controller.signal);
|
|
948
|
-
}
|
|
949
|
-
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);
|
|
950
951
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
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
|
|
976
976
|
});
|
|
977
977
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
const dataLines = [];
|
|
988
|
-
for (const line of frame.split(/\r?\n/)) {
|
|
989
|
-
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 });
|
|
990
987
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
try {
|
|
995
|
-
return JSON.parse(payload);
|
|
996
|
-
} catch {
|
|
997
|
-
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`);
|
|
998
991
|
}
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
return {
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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
|
|
1043
1044
|
},
|
|
1044
|
-
|
|
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 }
|
|
1045
1083
|
);
|
|
1046
|
-
});
|
|
1047
|
-
}
|
|
1048
|
-
var CdpConnection = class {
|
|
1049
|
-
#ws;
|
|
1050
|
-
#nextId = 0;
|
|
1051
|
-
#pending = /* @__PURE__ */ new Map();
|
|
1052
|
-
#subscribers = /* @__PURE__ */ new Set();
|
|
1053
|
-
#closeListeners = /* @__PURE__ */ new Set();
|
|
1054
|
-
#closeReason = null;
|
|
1055
|
-
constructor(ws) {
|
|
1056
|
-
this.#ws = ws;
|
|
1057
|
-
ws.on("message", (data) => this.#onMessage(data));
|
|
1058
|
-
ws.on("close", () => this.#onClose(new CdpError(-1, "CDP connection closed")));
|
|
1059
|
-
ws.on("error", (err) => this.#onClose(new NetworkError("CDP connection error", err)));
|
|
1060
1084
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
const
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
this.#pending.set(id, { resolve, reject });
|
|
1069
|
-
this.#ws.send(JSON.stringify(frame), (err) => {
|
|
1070
|
-
if (err) {
|
|
1071
|
-
this.#pending.delete(id);
|
|
1072
|
-
reject(new NetworkError("failed to send CDP command", err));
|
|
1073
|
-
}
|
|
1074
|
-
});
|
|
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
|
|
1075
1092
|
});
|
|
1076
1093
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
return (
|
|
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 });
|
|
1080
1097
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
}
|
|
1089
|
-
this.#closeListeners.add(cb);
|
|
1090
|
-
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
|
+
});
|
|
1091
1105
|
}
|
|
1092
|
-
|
|
1093
|
-
this
|
|
1106
|
+
getRecording(recordingId, signal) {
|
|
1107
|
+
return this.request("GET", `/v1/recordings/${enc(recordingId)}`, void 0, {
|
|
1108
|
+
signal
|
|
1109
|
+
});
|
|
1094
1110
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
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
|
+
});
|
|
1097
1119
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
+
});
|
|
1125
|
+
}
|
|
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
|
+
);
|
|
1117
1220
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
this
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
this.#subscribers.clear();
|
|
1124
|
-
for (const cb of [...this.#closeListeners]) cb();
|
|
1125
|
-
this.#closeListeners.clear();
|
|
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
|
+
});
|
|
1126
1226
|
}
|
|
1127
1227
|
};
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
this.#client = client;
|
|
1144
|
-
this.#profileId = profileId;
|
|
1145
|
-
}
|
|
1146
|
-
/** Send a raw CDP command and await its result (by id). */
|
|
1147
|
-
send(method, params) {
|
|
1148
|
-
return this.#conn.request(method, params, this.sessionId);
|
|
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
|
+
);
|
|
1149
1243
|
}
|
|
1150
|
-
/**
|
|
1151
|
-
|
|
1152
|
-
return this
|
|
1153
|
-
if (event.method === method && event.sessionId === this.sessionId) callback(event.params);
|
|
1154
|
-
});
|
|
1244
|
+
/** Every table outside the bin. */
|
|
1245
|
+
listTables(signal) {
|
|
1246
|
+
return this.request("GET", "/v1/tables", void 0, { signal });
|
|
1155
1247
|
}
|
|
1156
|
-
/**
|
|
1157
|
-
|
|
1158
|
-
return
|
|
1248
|
+
/** A local table with its first columns. */
|
|
1249
|
+
createTable(body) {
|
|
1250
|
+
return this.request("POST", "/v1/tables", body);
|
|
1159
1251
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
let targetId = opts.targetId;
|
|
1163
|
-
if (opts.create) {
|
|
1164
|
-
const created = await this.send("Target.createTarget", { url: opts.url ?? "about:blank" });
|
|
1165
|
-
targetId = created.targetId;
|
|
1166
|
-
} else if (!targetId) {
|
|
1167
|
-
const { targetInfos } = await this.send("Target.getTargets");
|
|
1168
|
-
const page = targetInfos.find((t) => t.type === "page");
|
|
1169
|
-
if (page) {
|
|
1170
|
-
targetId = page.targetId;
|
|
1171
|
-
} else {
|
|
1172
|
-
const created = await this.send("Target.createTarget", { url: opts.url ?? "about:blank" });
|
|
1173
|
-
targetId = created.targetId;
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
const { sessionId } = await this.send("Target.attachToTarget", { targetId, flatten: true });
|
|
1177
|
-
return new _CdpSession(this.#conn, sessionId, this.#client, this.#profileId);
|
|
1252
|
+
getTable(id, signal) {
|
|
1253
|
+
return this.request("GET", `/v1/tables/${enc(id)}`, void 0, { signal });
|
|
1178
1254
|
}
|
|
1179
|
-
/**
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
if (res.errorText) throw new CdpError(-1, `navigation failed: ${res.errorText}`);
|
|
1183
|
-
if (opts.wait !== false) await this.#waitForLoad(opts.timeout ?? 3e4);
|
|
1184
|
-
return res;
|
|
1255
|
+
/** Rename, describe or change the delete rule. */
|
|
1256
|
+
updateTable(id, body) {
|
|
1257
|
+
return this.request("PATCH", `/v1/tables/${enc(id)}`, body);
|
|
1185
1258
|
}
|
|
1186
|
-
/**
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
expression,
|
|
1190
|
-
awaitPromise: opts.awaitPromise ?? true,
|
|
1191
|
-
returnByValue: opts.returnByValue ?? true
|
|
1192
|
-
};
|
|
1193
|
-
if (opts.isolated) params["contextId"] = await this.#isolatedContext();
|
|
1194
|
-
const res = await this.send("Runtime.evaluate", params);
|
|
1195
|
-
if (res.exceptionDetails) {
|
|
1196
|
-
throw new CdpError(-1, res.exceptionDetails.text || res.exceptionDetails.exception?.description || "evaluate exception");
|
|
1197
|
-
}
|
|
1198
|
-
return res.result.value;
|
|
1259
|
+
/** To the bin for 30 days. */
|
|
1260
|
+
deleteTable(id) {
|
|
1261
|
+
return this.request("DELETE", `/v1/tables/${enc(id)}`);
|
|
1199
1262
|
}
|
|
1200
|
-
/**
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
}
|
|
1207
|
-
const { executionContextId } = await this.send("Page.createIsolatedWorld", {
|
|
1208
|
-
frameId: frame,
|
|
1209
|
-
worldName,
|
|
1210
|
-
grantUniveralAccess: false
|
|
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
|
|
1211
1269
|
});
|
|
1212
|
-
return executionContextId;
|
|
1213
|
-
}
|
|
1214
|
-
// ── Humanized trusted input (routes through the daemon) ────────────────────
|
|
1215
|
-
humanizeMove(x, y, opts = {}) {
|
|
1216
|
-
return this.#input({ action: "move", x, y, width: opts.width });
|
|
1217
1270
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
button: opts.button ?? "left",
|
|
1224
|
-
click_count: opts.clickCount ?? 1,
|
|
1225
|
-
width: opts.width
|
|
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
|
|
1226
1276
|
});
|
|
1227
1277
|
}
|
|
1278
|
+
/** Ops in order, one result each. */
|
|
1279
|
+
editTable(id, body) {
|
|
1280
|
+
return this.request("POST", `/v1/tables/${enc(id)}/edit`, body);
|
|
1281
|
+
}
|
|
1228
1282
|
/**
|
|
1229
|
-
*
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
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.
|
|
1232
1287
|
*/
|
|
1233
|
-
|
|
1234
|
-
return this.
|
|
1288
|
+
importTable(id, file, params) {
|
|
1289
|
+
return this.request("POST", `/v1/tables/${enc(id)}/import`, file, {
|
|
1290
|
+
query: { ...params }
|
|
1291
|
+
});
|
|
1235
1292
|
}
|
|
1236
|
-
/**
|
|
1237
|
-
|
|
1238
|
-
return this.
|
|
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 });
|
|
1239
1296
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1297
|
+
/** The tables in the bin. */
|
|
1298
|
+
listTableTrash(signal) {
|
|
1299
|
+
return this.request("GET", "/v1/tables/trash", void 0, { signal });
|
|
1242
1300
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1301
|
+
/** Take tables back out of the bin. */
|
|
1302
|
+
restoreTables(ids) {
|
|
1303
|
+
return this.request("POST", "/v1/tables/trash/restore", { ids });
|
|
1245
1304
|
}
|
|
1246
|
-
/**
|
|
1247
|
-
|
|
1248
|
-
this
|
|
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;
|
|
1249
1309
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
return this.#client.sendInput(this.#profileId, { ...body, humanize: true });
|
|
1310
|
+
/**
|
|
1311
|
+
* Start connecting a Google account: open `auth_url` in the default browser;
|
|
1312
|
+
* the connection appears once the person agreed there.
|
|
1313
|
+
*/
|
|
1314
|
+
connectGoogle(body = {}) {
|
|
1315
|
+
return this.request("POST", "/v1/connections/google", body);
|
|
1257
1316
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
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;
|
|
1261
1321
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
await delay(50);
|
|
1269
|
-
}
|
|
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);
|
|
1270
1328
|
}
|
|
1271
1329
|
};
|
|
1272
|
-
async function openCdpSession(url, options = {}) {
|
|
1273
|
-
const headers = {};
|
|
1274
|
-
if (options.token) headers["Authorization"] = `Bearer ${options.token}`;
|
|
1275
|
-
const ws = new WebSocket(url, { headers, handshakeTimeout: options.openTimeout ?? 3e4 });
|
|
1276
|
-
await waitForOpen(ws);
|
|
1277
|
-
const conn = new CdpConnection(ws);
|
|
1278
|
-
const root = new CdpSession(conn, void 0, options.client, options.profileId);
|
|
1279
|
-
if (options.autoAttach === false) return root;
|
|
1280
|
-
return root.attachToPage({ create: options.createTarget ?? false, url: options.targetUrl });
|
|
1281
|
-
}
|
|
1282
|
-
function waitForOpen(ws) {
|
|
1283
|
-
return new Promise((resolve, reject) => {
|
|
1284
|
-
const onOpen = () => {
|
|
1285
|
-
cleanup();
|
|
1286
|
-
resolve();
|
|
1287
|
-
};
|
|
1288
|
-
const onError = (err) => {
|
|
1289
|
-
cleanup();
|
|
1290
|
-
reject(new NetworkError("CDP websocket failed to open", err));
|
|
1291
|
-
};
|
|
1292
|
-
const cleanup = () => {
|
|
1293
|
-
ws.off("open", onOpen);
|
|
1294
|
-
ws.off("error", onError);
|
|
1295
|
-
};
|
|
1296
|
-
ws.on("open", onOpen);
|
|
1297
|
-
ws.on("error", onError);
|
|
1298
|
-
});
|
|
1299
|
-
}
|
|
1300
|
-
function scopedEventStream(conn, sessionId) {
|
|
1301
|
-
const buffer = [];
|
|
1302
|
-
const waiters = [];
|
|
1303
|
-
let done = false;
|
|
1304
|
-
const unsubscribe = conn.subscribe((event) => {
|
|
1305
|
-
if (event.sessionId !== sessionId) return;
|
|
1306
|
-
const waiter = waiters.shift();
|
|
1307
|
-
if (waiter) waiter({ value: event, done: false });
|
|
1308
|
-
else buffer.push(event);
|
|
1309
|
-
});
|
|
1310
|
-
let offClose = () => {
|
|
1311
|
-
};
|
|
1312
|
-
const finish = () => {
|
|
1313
|
-
if (!done) {
|
|
1314
|
-
done = true;
|
|
1315
|
-
unsubscribe();
|
|
1316
|
-
offClose();
|
|
1317
|
-
for (const w of waiters.splice(0)) w({ value: void 0, done: true });
|
|
1318
|
-
}
|
|
1319
|
-
return { value: void 0, done: true };
|
|
1320
|
-
};
|
|
1321
|
-
offClose = conn.onClose(() => {
|
|
1322
|
-
finish();
|
|
1323
|
-
});
|
|
1324
|
-
return {
|
|
1325
|
-
next() {
|
|
1326
|
-
const next = buffer.shift();
|
|
1327
|
-
if (next !== void 0) return Promise.resolve({ value: next, done: false });
|
|
1328
|
-
if (done) return Promise.resolve({ value: void 0, done: true });
|
|
1329
|
-
return new Promise((resolve) => waiters.push(resolve));
|
|
1330
|
-
},
|
|
1331
|
-
return() {
|
|
1332
|
-
return Promise.resolve(finish());
|
|
1333
|
-
},
|
|
1334
|
-
[Symbol.asyncIterator]() {
|
|
1335
|
-
return this;
|
|
1336
|
-
}
|
|
1337
|
-
};
|
|
1338
|
-
}
|
|
1339
|
-
function delay(ms) {
|
|
1340
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1341
|
-
}
|
|
1342
1330
|
|
|
1343
|
-
// src/client.ts
|
|
1344
|
-
var
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
};
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
return this.request("
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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
|
+
});
|
|
1348
|
+
}
|
|
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
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
renameWorkflow(id, name) {
|
|
1357
|
+
return this.request("PATCH", `/v1/workflows/${enc(id)}`, { name });
|
|
1358
|
+
}
|
|
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
|
+
});
|
|
1364
|
+
}
|
|
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, {
|
|
1362
1390
|
signal
|
|
1363
1391
|
});
|
|
1364
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
|
+
}
|
|
1365
1402
|
/**
|
|
1366
|
-
*
|
|
1367
|
-
*
|
|
1368
|
-
* `listProfiles` is paged, so acting on "everything" from it only ever covers the
|
|
1369
|
-
* page you fetched. Asking for ids costs one request and no persona payloads.
|
|
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.
|
|
1370
1405
|
*/
|
|
1371
|
-
|
|
1372
|
-
return this.request("
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
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 },
|
|
1380
1436
|
signal
|
|
1381
1437
|
});
|
|
1382
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 },
|
|
1463
|
+
signal
|
|
1464
|
+
});
|
|
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
|
+
}
|
|
1383
1470
|
/**
|
|
1384
|
-
*
|
|
1385
|
-
*
|
|
1386
|
-
*
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
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.
|
|
1390
1487
|
*
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
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`.
|
|
1393
1494
|
*/
|
|
1394
|
-
|
|
1395
|
-
return this.request("
|
|
1396
|
-
query: {
|
|
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 },
|
|
1397
1504
|
signal
|
|
1398
1505
|
});
|
|
1399
1506
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
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 },
|
|
1511
|
+
signal
|
|
1512
|
+
});
|
|
1402
1513
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
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
|
+
});
|
|
1405
1520
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1521
|
+
/** This machine's folder boundary for file blocks. */
|
|
1522
|
+
getWorkflowFileAccess(signal) {
|
|
1523
|
+
return this.request("GET", "/v1/workflow-file-access", void 0, { signal });
|
|
1408
1524
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
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
|
+
});
|
|
1411
1530
|
}
|
|
1412
|
-
/**
|
|
1413
|
-
|
|
1414
|
-
return this.request("POST",
|
|
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 });
|
|
1415
1534
|
}
|
|
1416
|
-
|
|
1417
|
-
|
|
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
|
+
});
|
|
1418
1540
|
}
|
|
1419
|
-
/**
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
* the browser first when one is still up, then lets the lease go, so a dead
|
|
1423
|
-
* session cannot park the profile until its TTL runs out. `released: false`
|
|
1424
|
-
* is an honest no-op: there was nothing to let go. */
|
|
1425
|
-
releaseProfile(id) {
|
|
1426
|
-
return this.request("POST", `/v1/profiles/${enc(id)}/release`, {});
|
|
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 });
|
|
1427
1544
|
}
|
|
1428
|
-
/**
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
* right, the holder closes its own browser at its next heartbeat. Start only
|
|
1432
|
-
* after this answered `free`. Errors: 4012 when it is held elsewhere. */
|
|
1433
|
-
takeoverProfile(id) {
|
|
1434
|
-
return this.request("POST", `/v1/profiles/${enc(id)}/takeover`, {});
|
|
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)}`);
|
|
1435
1548
|
}
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
return this.request("
|
|
1549
|
+
/** Take a domain off the fast mode's list. */
|
|
1550
|
+
deleteWorkflowFastDomain(domain) {
|
|
1551
|
+
return this.request("DELETE", `/v1/workflow-fast-domains/${enc(domain)}`);
|
|
1439
1552
|
}
|
|
1440
1553
|
/**
|
|
1441
|
-
*
|
|
1442
|
-
*
|
|
1443
|
-
* the
|
|
1444
|
-
* `remaining` / `stopped_reason`) instead of failing every remaining id.
|
|
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.
|
|
1445
1557
|
*/
|
|
1446
|
-
|
|
1447
|
-
return this.request("
|
|
1558
|
+
getWorkflowReadiness(signal) {
|
|
1559
|
+
return this.request("GET", "/v1/workflow-readiness", void 0, { signal });
|
|
1448
1560
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
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, {
|
|
1564
|
+
signal
|
|
1565
|
+
});
|
|
1566
|
+
return out.approvals;
|
|
1451
1567
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
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);
|
|
1454
1571
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
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 });
|
|
1578
|
+
}
|
|
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);
|
|
1582
|
+
}
|
|
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
|
+
});
|
|
1588
|
+
}
|
|
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 });
|
|
1592
|
+
}
|
|
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`);
|
|
1457
1596
|
}
|
|
1458
1597
|
/**
|
|
1459
|
-
*
|
|
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`);
|
|
1605
|
+
}
|
|
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);
|
|
1615
|
+
}
|
|
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);
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
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.
|
|
1623
|
+
*/
|
|
1624
|
+
forkWorkflowRun(runId, body) {
|
|
1625
|
+
return this.request("POST", `/v1/workflow-runs/${enc(runId)}/fork`, body);
|
|
1626
|
+
}
|
|
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
|
+
});
|
|
1633
|
+
}
|
|
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 });
|
|
1637
|
+
}
|
|
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 });
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
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.
|
|
1460
1772
|
*
|
|
1461
|
-
*
|
|
1462
|
-
*
|
|
1463
|
-
*
|
|
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).
|
|
1464
1776
|
*/
|
|
1465
|
-
|
|
1466
|
-
return this.request("POST", "/v1/
|
|
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
|
+
});
|
|
1467
1782
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
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 });
|
|
1471
1792
|
}
|
|
1472
|
-
|
|
1473
|
-
|
|
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`);
|
|
1474
1799
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1800
|
+
/** The ready-made starting points. */
|
|
1801
|
+
workflowTemplates(signal) {
|
|
1802
|
+
return this.request("GET", "/v1/workflow-templates", void 0, { signal });
|
|
1477
1803
|
}
|
|
1478
|
-
|
|
1479
|
-
|
|
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
|
+
);
|
|
1480
1811
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
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 });
|
|
1483
1823
|
}
|
|
1484
|
-
|
|
1485
|
-
|
|
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);
|
|
1486
1834
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
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;
|
|
1489
1839
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
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");
|
|
1492
1928
|
}
|
|
1493
|
-
|
|
1494
|
-
|
|
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
|
+
});
|
|
1495
1955
|
}
|
|
1496
|
-
|
|
1497
|
-
|
|
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(/^ /, ""));
|
|
1498
1968
|
}
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
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;
|
|
1502
1976
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
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)));
|
|
1505
2040
|
}
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
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
|
+
});
|
|
1509
2056
|
}
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
return this.
|
|
2057
|
+
subscribe(cb) {
|
|
2058
|
+
this.#subscribers.add(cb);
|
|
2059
|
+
return () => this.#subscribers.delete(cb);
|
|
1513
2060
|
}
|
|
1514
|
-
/**
|
|
1515
|
-
|
|
1516
|
-
|
|
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);
|
|
1517
2071
|
}
|
|
1518
|
-
|
|
1519
|
-
|
|
2072
|
+
close() {
|
|
2073
|
+
this.#ws.close();
|
|
1520
2074
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
previewVideoPreset(body, signal) {
|
|
1524
|
-
return this.request("POST", "/v1/video-presets/preview", body, { signal });
|
|
2075
|
+
get closed() {
|
|
2076
|
+
return this.#closeReason !== null;
|
|
1525
2077
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
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
|
+
}
|
|
1530
2097
|
}
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
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();
|
|
1534
2106
|
}
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
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;
|
|
1547
2126
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
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);
|
|
1552
2130
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
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
|
+
});
|
|
1555
2136
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
2137
|
+
/** `for await (const event of session.events())` over this session's CDP events. */
|
|
2138
|
+
events() {
|
|
2139
|
+
return scopedEventStream(this.#conn, this.sessionId);
|
|
1558
2140
|
}
|
|
1559
|
-
|
|
1560
|
-
|
|
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);
|
|
1561
2159
|
}
|
|
1562
|
-
|
|
1563
|
-
|
|
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;
|
|
1564
2166
|
}
|
|
1565
|
-
/**
|
|
1566
|
-
|
|
1567
|
-
|
|
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;
|
|
1568
2180
|
}
|
|
1569
2181
|
/**
|
|
1570
|
-
*
|
|
1571
|
-
*
|
|
1572
|
-
*
|
|
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.
|
|
1573
2188
|
*/
|
|
1574
|
-
|
|
1575
|
-
|
|
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;
|
|
1576
2201
|
}
|
|
1577
|
-
// ──
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
// sites and keeps what each one saw. The profile must be RUNNING; the run takes
|
|
1581
|
-
// minutes, so `startAudit` returns immediately and you poll `getAudit`.
|
|
1582
|
-
/** Queue a run → the status right after queueing. `409` if not running / already running. */
|
|
1583
|
-
startAudit(profileId) {
|
|
1584
|
-
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 });
|
|
1585
2205
|
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
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
|
|
1590
2214
|
});
|
|
1591
2215
|
}
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
*
|
|
1595
|
-
*
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
getExitStatus(profileId, signal) {
|
|
1600
|
-
return this.request("GET", `/v1/profiles/${enc(profileId)}/exit`, void 0, {
|
|
1601
|
-
signal
|
|
1602
|
-
});
|
|
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 });
|
|
1603
2223
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
return this.
|
|
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 });
|
|
1607
2227
|
}
|
|
1608
|
-
|
|
1609
|
-
return this
|
|
2228
|
+
humanizeType(text) {
|
|
2229
|
+
return this.#input({ action: "type", text });
|
|
1610
2230
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
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
|
+
}
|
|
1614
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 {
|
|
1615
2342
|
// ── Metrics (with graceful fallback) ────────────────────────────────────────
|
|
1616
2343
|
async getMetrics(signal) {
|
|
1617
2344
|
try {
|
|
@@ -1659,6 +2386,24 @@ var ScalebrowserClient = class extends ClientCore {
|
|
|
1659
2386
|
async setExitRule(enabled, signal) {
|
|
1660
2387
|
return this.request("PUT", "/v1/exit-rule", { enabled }, { signal });
|
|
1661
2388
|
}
|
|
2389
|
+
/**
|
|
2390
|
+
* Which optional MCP tool groups agents on this machine see.
|
|
2391
|
+
*
|
|
2392
|
+
* Agents see the basic tools by default; video, tasks, profile management,
|
|
2393
|
+
* the captcha helpers, the extras and the developer tools are one switch each.
|
|
2394
|
+
*/
|
|
2395
|
+
async getAgentTools(signal) {
|
|
2396
|
+
return this.request("GET", "/v1/agent-tools", void 0, { signal });
|
|
2397
|
+
}
|
|
2398
|
+
/**
|
|
2399
|
+
* Turn one tool group on or off for every agent on this machine.
|
|
2400
|
+
*
|
|
2401
|
+
* It applies to the next tool list an agent asks for. A session that listed
|
|
2402
|
+
* its tools before keeps that list until it reconnects.
|
|
2403
|
+
*/
|
|
2404
|
+
async setAgentToolGroup(group, enabled, signal) {
|
|
2405
|
+
return this.request("PUT", "/v1/agent-tools", { group, enabled }, { signal });
|
|
2406
|
+
}
|
|
1662
2407
|
// ── Events (SSE) ───────────────────────────────────────────────────────────
|
|
1663
2408
|
/** Subscribe to lifecycle events. Returns an unsubscribe function. */
|
|
1664
2409
|
subscribeEvents(handlers) {
|
|
@@ -1717,21 +2462,33 @@ function deriveMetricsFromProfiles(profiles) {
|
|
|
1717
2462
|
};
|
|
1718
2463
|
}
|
|
1719
2464
|
applyMixins(ScalebrowserClient, [
|
|
2465
|
+
ProfilesApi,
|
|
2466
|
+
PresetsApi,
|
|
2467
|
+
ProxiesApi,
|
|
1720
2468
|
IdentityApi,
|
|
1721
2469
|
RunsApi,
|
|
1722
2470
|
VideosApi,
|
|
1723
2471
|
ControlApi,
|
|
1724
2472
|
ExtensionsApi,
|
|
1725
|
-
TasksApi
|
|
2473
|
+
TasksApi,
|
|
2474
|
+
WorkflowsApi,
|
|
2475
|
+
WorkflowRunsApi,
|
|
2476
|
+
WorkflowAiApi,
|
|
2477
|
+
WorkflowSyncApi,
|
|
2478
|
+
WorkflowSharesApi,
|
|
2479
|
+
WorkflowAgencyApi,
|
|
2480
|
+
TablesApi
|
|
1726
2481
|
]);
|
|
1727
2482
|
|
|
2483
|
+
// src/types-proxies.ts
|
|
2484
|
+
var PROXY_KINDS = ["http", "socks5", "gateway"];
|
|
2485
|
+
var ROTATIONS = ["sticky", "rotating"];
|
|
2486
|
+
|
|
1728
2487
|
// src/types.ts
|
|
1729
2488
|
var RUNTIME_STATES = ["stopped", "starting", "running", "crashed"];
|
|
1730
2489
|
var GEO_MODES = ["strict_expected", "follow_exit", "off"];
|
|
1731
|
-
var PROXY_KINDS = ["http", "socks5", "gateway"];
|
|
1732
|
-
var ROTATIONS = ["sticky", "rotating"];
|
|
1733
2490
|
|
|
1734
2491
|
// src/index.ts
|
|
1735
|
-
var VERSION = "1.
|
|
2492
|
+
var VERSION = "1.3.0";
|
|
1736
2493
|
|
|
1737
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 };
|