@scalebrowser/sdk 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -25,7 +25,13 @@ var ErrorCode = {
25
25
  * Distinct from ConcurrencyLimit (4008 = the plan is full, buy more or free any
26
26
  * browser): the caller's move here costs nothing. Close it on that machine, or
27
27
  * take it over from the app running there. */
28
- ProfileInUse: 4012
28
+ ProfileInUse: 4012,
29
+ /** The exit address this start drew belongs to another profile (DEC-017).
30
+ * Distinct from ProxyGeo (4002 = wrong country or wrong kind of exit, so change
31
+ * the proxy): here the proxy is fine and the address is simply spoken for. Wait
32
+ * for a term to run out, let the daemon draw again, or turn exclusivity off for
33
+ * this profile. */
34
+ ExitAddressTaken: 4013
29
35
  };
30
36
  var CODE_LABELS = {
31
37
  4001: "Not found",
@@ -37,7 +43,8 @@ var CODE_LABELS = {
37
43
  4008: "Concurrent-browser limit reached (subscription-wide)",
38
44
  4010: "Unauthorized",
39
45
  4011: "No account connected to this machine",
40
- 4012: "This profile is open on another machine"
46
+ 4012: "This profile is open on another machine",
47
+ 4013: "This exit address belongs to another profile"
41
48
  };
42
49
  var ScalebrowserError = class extends Error {
43
50
  constructor(message) {
@@ -248,7 +255,7 @@ var ControlApi = class extends ClientCore {
248
255
  /**
249
256
  * Hand the daemon a file so `interact action=upload` has something to attach.
250
257
  *
251
- * The bytes are bound to the PROFILE, not to a reservation: set a file up
258
+ * The bytes are bound to the PROFILE, not to a lease: set a file up
252
259
  * before leasing, and a lease released mid-task does not destroy it.
253
260
  */
254
261
  putArtifact(profileId, bytes, name) {
@@ -566,6 +573,83 @@ var IdentityApi = class extends ClientCore {
566
573
  }
567
574
  };
568
575
 
576
+ // src/client-tasks.ts
577
+ var TasksApi = class extends ClientCore {
578
+ // -- Tasks ------------------------------------------------------------------
579
+ //
580
+ // Fleet-wide by default, and deliberately so: the question this surface exists
581
+ // for is "what is outstanding across my twenty accounts", and a per-profile
582
+ // route would make a client ask it twenty times and sort the answers itself.
583
+ //
584
+ // None of these starts a browser. A task is a row, so reading the whole fleet's
585
+ // list costs nothing from the plan's concurrent-browser allowance. Reserve only
586
+ // the profiles that turn out to have work.
587
+ /**
588
+ * What is still to be done. No filter means every task of every profile.
589
+ *
590
+ * Pass `state: 'open'` for what is outstanding; without it, tasks finished
591
+ * within the last day are included too.
592
+ */
593
+ listTasks(params, signal) {
594
+ return this.request("GET", "/v1/tasks", void 0, {
595
+ query: { ...params },
596
+ signal
597
+ });
598
+ }
599
+ /**
600
+ * Put the same task on one or many profiles, one row each.
601
+ *
602
+ * A task is a LINE, not a document: name the work and leave the material where
603
+ * it lives. `due_at` is a deadline, not a trigger; nothing runs by itself.
604
+ *
605
+ * Profiles that no longer exist come back in `missing` instead of failing the
606
+ * whole call.
607
+ */
608
+ createTasks(body) {
609
+ return this.request("POST", "/v1/tasks", body);
610
+ }
611
+ /**
612
+ * Call a task off.
613
+ *
614
+ * Only a task nobody has claimed can be cancelled here. One an agent is working
615
+ * on right now is stopped by telling that agent, which is a channel that already
616
+ * reaches it; this surface answers `409` for it rather than pretending.
617
+ */
618
+ cancelTask(id) {
619
+ return this.request("PATCH", `/v1/tasks/${enc(id)}`, { state: "cancelled" });
620
+ }
621
+ /** Remove a task outright. */
622
+ deleteTask(id) {
623
+ return this.request("DELETE", `/v1/tasks/${enc(id)}`);
624
+ }
625
+ // -- Profile memory ---------------------------------------------------------
626
+ /**
627
+ * A profile's durable memory, the `PROFILE.md` at the top of its directory.
628
+ *
629
+ * `content` is `null` while nobody has written one: a profile starts with no
630
+ * file, not with an empty one.
631
+ */
632
+ getProfileMemory(profileId, signal) {
633
+ return this.request("GET", `/v1/profiles/${enc(profileId)}/memory`, void 0, {
634
+ signal
635
+ });
636
+ }
637
+ /**
638
+ * Replace a profile's memory.
639
+ *
640
+ * Stored verbatim: no trimming, no appended newline, no line-ending
641
+ * translation. The last write wins and nothing warns, exactly as with a file
642
+ * two people can open.
643
+ */
644
+ putProfileMemory(profileId, content) {
645
+ return this.request("PUT", `/v1/profiles/${enc(profileId)}/memory`, { content });
646
+ }
647
+ /** Remove a profile's memory. Removing one that is not there is not an error. */
648
+ deleteProfileMemory(profileId) {
649
+ return this.request("DELETE", `/v1/profiles/${enc(profileId)}/memory`);
650
+ }
651
+ };
652
+
569
653
  // src/client-runs.ts
570
654
  var RunsApi = class extends ClientCore {
571
655
  // ── Agent runs ─────────────────────────────────────────────────────────────
@@ -602,6 +686,205 @@ var RunsApi = class extends ClientCore {
602
686
  }
603
687
  };
604
688
 
689
+ // src/client-videos.ts
690
+ var VideosApi = class extends ClientCore {
691
+ // ── Videos ────────────────────────────────────────────────────────────────
692
+ /** One gallery page, newest first. Running renders are videos too. */
693
+ listVideos(params = {}, signal) {
694
+ return this.request("GET", "/v1/videos", void 0, {
695
+ query: {
696
+ q: params.q,
697
+ profile_id: params.profile_id,
698
+ run_id: params.run_id,
699
+ kind: params.kind,
700
+ preset: params.preset,
701
+ state: params.state,
702
+ page: params.page
703
+ },
704
+ signal
705
+ });
706
+ }
707
+ /** One video row, progress included while it renders. */
708
+ getVideo(videoId, signal) {
709
+ return this.request("GET", `/v1/videos/${enc(videoId)}`, void 0, { signal });
710
+ }
711
+ /** Rename. The title also lands in the MP4's metadata on the NEXT render. */
712
+ renameVideo(videoId, title, signal) {
713
+ return this.request("PATCH", `/v1/videos/${enc(videoId)}`, { title }, { signal });
714
+ }
715
+ /**
716
+ * Delete a video (`raw: true` also purges its recording's raw material).
717
+ * A live share link is revoked first; 409 `job_running` while it renders.
718
+ */
719
+ deleteVideo(videoId, opts = {}, signal) {
720
+ return this.request("DELETE", `/v1/videos/${enc(videoId)}`, void 0, {
721
+ query: { raw: opts.raw },
722
+ signal
723
+ });
724
+ }
725
+ /** Cancel a queued or rendering video. `done` stays done (409). */
726
+ cancelVideo(videoId, signal) {
727
+ return this.request("POST", `/v1/videos/${enc(videoId)}/cancel`, void 0, {
728
+ signal
729
+ });
730
+ }
731
+ /** Render a failed or cancelled video again: same id, new job. */
732
+ rerenderVideo(videoId, body = {}, signal) {
733
+ return this.request("POST", `/v1/videos/${enc(videoId)}/renders`, body, { signal });
734
+ }
735
+ /** A short-lived fetchable address for one asset (default the video). */
736
+ videoAccess(videoId, asset, signal) {
737
+ return this.request(
738
+ "POST",
739
+ `/v1/videos/${enc(videoId)}/access`,
740
+ asset ? { asset } : {},
741
+ { signal }
742
+ );
743
+ }
744
+ /** One asset's bytes, through a freshly minted grant. */
745
+ async downloadVideoFile(videoId, asset = "video", signal) {
746
+ const grant = await this.videoAccess(videoId, asset, signal);
747
+ const grantParam = new URL(grant.url, "http://x").searchParams.get("grant") ?? "";
748
+ return this.requestBytes("GET", `/v1/videos/${enc(videoId)}/file`, {
749
+ query: { asset, grant: grantParam },
750
+ signal
751
+ });
752
+ }
753
+ /** What recordings and videos cost on disk, plus the caps. */
754
+ videosStorage(signal) {
755
+ return this.request("GET", "/v1/videos/storage", void 0, { signal });
756
+ }
757
+ // ── Recordings ────────────────────────────────────────────────────────────
758
+ /** Recordings, newest first. The row outlives its raw bytes. */
759
+ listRecordings(params = {}, signal) {
760
+ return this.request("GET", "/v1/recordings", void 0, {
761
+ query: { profile_id: params.profile_id },
762
+ signal
763
+ });
764
+ }
765
+ getRecording(recordingId, signal) {
766
+ return this.request("GET", `/v1/recordings/${enc(recordingId)}`, void 0, {
767
+ signal
768
+ });
769
+ }
770
+ /**
771
+ * Purge a recording's RAW material; the row stays forever
772
+ * (`raw_purged_at`). 409 `job_running` while a render of it is open.
773
+ */
774
+ deleteRecordingRaw(recordingId, signal) {
775
+ return this.request("DELETE", `/v1/recordings/${enc(recordingId)}`, void 0, {
776
+ signal
777
+ });
778
+ }
779
+ /** A NEW video from this recording (the same body the `video` tool takes). */
780
+ renderRecording(recordingId, body = {}, signal) {
781
+ return this.request("POST", `/v1/recordings/${enc(recordingId)}/renders`, body, {
782
+ signal
783
+ });
784
+ }
785
+ // ── Codecs ────────────────────────────────────────────────────────────────
786
+ /** What this machine can encode with. The hardware list is PROBED, not read
787
+ * off the binary: `h264_nvenc` is always listed and fails without a driver. */
788
+ getCodecs(signal) {
789
+ return this.request("GET", "/v1/codecs", void 0, { signal });
790
+ }
791
+ /** Turn Cisco's OpenH264 on or off. The hardware encoders are not switchable. */
792
+ setCodecEnabled(codec, enabled, signal) {
793
+ return this.request("PUT", `/v1/codecs/${enc(codec)}`, { enabled }, { signal });
794
+ }
795
+ // -- Sharing ----------------------------------------------------------------
796
+ /** Make a finished video public. Measures every file, asks the control plane
797
+ * for permission, uploads, and has the result confirmed. */
798
+ share(videoId, body = {}, signal) {
799
+ return this.request("POST", `/v1/videos/${enc(videoId)}/share`, body, {
800
+ signal
801
+ });
802
+ }
803
+ /** The share ORDER as this machine knows it. How many people watched and
804
+ * whether the link still lives is read from the control plane, not from
805
+ * here: a number carried along would be wrong the moment another machine
806
+ * revoked. */
807
+ shareStatus(videoId, signal) {
808
+ return this.request(
809
+ "GET",
810
+ `/v1/videos/${enc(videoId)}/share`,
811
+ void 0,
812
+ { signal }
813
+ );
814
+ }
815
+ /** Revoke the link. The files fall at once; the order stays until the control
816
+ * plane confirms. */
817
+ revokeShare(videoId, signal) {
818
+ return this.request("DELETE", `/v1/videos/${enc(videoId)}/share`, void 0, {
819
+ signal
820
+ });
821
+ }
822
+ // -- Media ------------------------------------------------------------------
823
+ /** Files a preset points at: background image, music bed, subtitle font. */
824
+ listMedia(kind, signal) {
825
+ return this.request("GET", "/v1/media", void 0, {
826
+ query: { kind },
827
+ signal
828
+ });
829
+ }
830
+ /** Upload one file. The body IS the file; `name` and `mime` ride in the query. */
831
+ uploadMedia(kind, name, mime, bytes, signal) {
832
+ return this.request("POST", "/v1/media", bytes, {
833
+ query: { kind, name, mime },
834
+ signal
835
+ });
836
+ }
837
+ /** Remove a file. Refuses with 409 while a preset still points at it. */
838
+ deleteMedia(id, signal) {
839
+ return this.request("DELETE", `/v1/media/${enc(id)}`, void 0, { signal });
840
+ }
841
+ /** The file itself. */
842
+ mediaFile(id, signal) {
843
+ return this.requestBytes("GET", `/v1/media/${enc(id)}/file`, { signal });
844
+ }
845
+ // -- Voice providers --------------------------------------------------------
846
+ /** Which speech providers have a key. The key itself never comes back: the
847
+ * answer carries a hint, and that is on purpose. */
848
+ listTtsProviders(signal) {
849
+ return this.request("GET", "/v1/tts-providers", void 0, { signal });
850
+ }
851
+ /** Store a key, or rename one. Leaving `apiKey` out keeps the stored key.
852
+ *
853
+ * `apiKey` may also be a secret reference (`{{sb:...}}`), which is what an
854
+ * agent gets back after fetching a key from the provider's own site: it is
855
+ * not allowed to read the value. A reference needs `profileId`, because the
856
+ * value belongs to one profile and there is nothing to open without it.
857
+ */
858
+ putTtsProvider(provider, body, signal) {
859
+ return this.request(
860
+ "PUT",
861
+ `/v1/tts-providers/${enc(provider)}`,
862
+ { api_key: body.apiKey, label: body.label, profile_id: body.profileId },
863
+ { signal }
864
+ );
865
+ }
866
+ /** Call the provider once for real, and record that the key works.
867
+ *
868
+ * Costs one short sentence of the account's own quota. Until this runs, a
869
+ * provider row says `verified_at: null`, which means untested rather than
870
+ * broken.
871
+ */
872
+ verifyTtsProvider(provider, signal) {
873
+ return this.request(
874
+ "POST",
875
+ `/v1/tts-providers/${enc(provider)}/verify`,
876
+ void 0,
877
+ { signal }
878
+ );
879
+ }
880
+ /** Forget a provider's key. */
881
+ deleteTtsProvider(provider, signal) {
882
+ return this.request("DELETE", `/v1/tts-providers/${enc(provider)}`, void 0, {
883
+ signal
884
+ });
885
+ }
886
+ };
887
+
605
888
  // src/events.ts
606
889
  var MAX_BACKOFF_MS = 15e3;
607
890
  function subscribeEvents(params, handlers) {
@@ -1105,6 +1388,15 @@ var ScalebrowserClient = class extends ClientCore {
1105
1388
  stopProfile(id) {
1106
1389
  return this.request("POST", `/v1/profiles/${enc(id)}/stop`, {});
1107
1390
  }
1391
+ /** Drop the profile's lease, the operator's way out.
1392
+ *
1393
+ * An agent releases with its handle; this call needs none. The daemon stops
1394
+ * the browser first when one is still up, then lets the lease go, so a dead
1395
+ * session cannot park the profile until its TTL runs out. `released: false`
1396
+ * is an honest no-op: there was nothing to let go. */
1397
+ releaseProfile(id) {
1398
+ return this.request("POST", `/v1/profiles/${enc(id)}/release`, {});
1399
+ }
1108
1400
  /** Ask for a profile that is open on another machine of this account.
1109
1401
  *
1110
1402
  * Closes nothing here and nothing there: the control plane revokes the holder's
@@ -1176,6 +1468,42 @@ var ScalebrowserClient = class extends ClientCore {
1176
1468
  deletePreset(id) {
1177
1469
  return this.request("DELETE", `/v1/presets/${enc(id)}`);
1178
1470
  }
1471
+ // ── Video presets ──────────────────────────────────────────────────────────
1472
+ listVideoPresets(signal) {
1473
+ return this.request("GET", "/v1/video-presets", void 0, { signal });
1474
+ }
1475
+ getVideoPreset(id, signal) {
1476
+ return this.request("GET", `/v1/video-presets/${enc(id)}`, void 0, { signal });
1477
+ }
1478
+ /** The daemon validates `config` strictly and names the failing field path. */
1479
+ createVideoPreset(body) {
1480
+ return this.request("POST", "/v1/video-presets", body);
1481
+ }
1482
+ /** A built-in preset refuses edits. Duplicate it and edit the copy. */
1483
+ updateVideoPreset(id, body) {
1484
+ return this.request("PATCH", `/v1/video-presets/${enc(id)}`, body);
1485
+ }
1486
+ /** The only way to make a built-in preset yours: copy, then edit the copy. */
1487
+ duplicateVideoPreset(id, body = {}) {
1488
+ return this.request("POST", `/v1/video-presets/${enc(id)}/duplicate`, body);
1489
+ }
1490
+ deleteVideoPreset(id) {
1491
+ return this.request("DELETE", `/v1/video-presets/${enc(id)}`);
1492
+ }
1493
+ /** One still of the sample film under an UNSAVED config, drawn by the daemon's own renderer.
1494
+ * `moment` wins over `frame`; both absent means the daemon's default moment. */
1495
+ previewVideoPreset(body, signal) {
1496
+ return this.request("POST", "/v1/video-presets/preview", body, { signal });
1497
+ }
1498
+ /** A clip of the sample film, blocking until it is encoded (the editor uses the SSE form of the
1499
+ * same route; a script waits). Fetch the bytes with `downloadVideoPresetClip`. */
1500
+ previewVideoPresetClip(body, signal) {
1501
+ return this.request("POST", "/v1/video-presets/preview/clip", body, { signal });
1502
+ }
1503
+ /** The clip's MP4 bytes. */
1504
+ downloadVideoPresetClip(id, signal) {
1505
+ return this.requestBytes("GET", `/v1/video-presets/preview/clip/${enc(id)}`, { signal });
1506
+ }
1179
1507
  /**
1180
1508
  * The values a preset's `constraints` may take. Ask rather than hardcode: the
1181
1509
  * region list comes from the daemon's embedded persona model, so a pinned copy
@@ -1232,6 +1560,18 @@ var ScalebrowserClient = class extends ClientCore {
1232
1560
  signal
1233
1561
  });
1234
1562
  }
1563
+ // ── Exit-address exclusivity (DEC-017) ──────────────────────────────────────
1564
+ /** Which exit addresses this profile owns, and how far the rule reaches.
1565
+ *
1566
+ * `scope` is `'machine'` while this computer has no account key. That is the
1567
+ * ordinary state of a second machine until sync is unlocked there once, and it is
1568
+ * reported rather than hidden: believing the rule spans three machines when it
1569
+ * spans one is worse than knowing. */
1570
+ getExitStatus(profileId, signal) {
1571
+ return this.request("GET", `/v1/profiles/${enc(profileId)}/exit`, void 0, {
1572
+ signal
1573
+ });
1574
+ }
1235
1575
  // ── Session bundles (AC-ST-002/003) ─────────────────────────────────────────
1236
1576
  exportSession(profileId, body) {
1237
1577
  return this.request("POST", `/v1/profiles/${enc(profileId)}/session/export`, body);
@@ -1254,6 +1594,21 @@ var ScalebrowserClient = class extends ClientCore {
1254
1594
  throw err;
1255
1595
  }
1256
1596
  }
1597
+ // ── Remote access (the machine's own switch) ────────────────────────────────
1598
+ /**
1599
+ * Is remote access on for this machine?
1600
+ *
1601
+ * The switch is deliberately local: a chat service reaches this browser only
1602
+ * once somebody at the machine has turned it on. On a headless install there
1603
+ * is no UI to click, which is why it is here as well.
1604
+ */
1605
+ async getRemoteAccess(signal) {
1606
+ return this.request("GET", "/v1/remote", void 0, { signal });
1607
+ }
1608
+ /** Turn remote access on or off for this machine. */
1609
+ async setRemoteAccess(enabled, signal) {
1610
+ return this.request("PUT", "/v1/remote", { enabled }, { signal });
1611
+ }
1257
1612
  // ── Events (SSE) ───────────────────────────────────────────────────────────
1258
1613
  /** Subscribe to lifecycle events. Returns an unsubscribe function. */
1259
1614
  subscribeEvents(handlers) {
@@ -1311,7 +1666,14 @@ function deriveMetricsFromProfiles(profiles) {
1311
1666
  profiles: []
1312
1667
  };
1313
1668
  }
1314
- applyMixins(ScalebrowserClient, [IdentityApi, RunsApi, ControlApi, ExtensionsApi]);
1669
+ applyMixins(ScalebrowserClient, [
1670
+ IdentityApi,
1671
+ RunsApi,
1672
+ VideosApi,
1673
+ ControlApi,
1674
+ ExtensionsApi,
1675
+ TasksApi
1676
+ ]);
1315
1677
 
1316
1678
  // src/types.ts
1317
1679
  var RUNTIME_STATES = ["stopped", "starting", "running", "crashed"];
@@ -1321,6 +1683,6 @@ var PROXY_KINDS = ["http", "socks5", "gateway"];
1321
1683
  var ROTATIONS = ["sticky", "rotating"];
1322
1684
 
1323
1685
  // src/index.ts
1324
- var VERSION = "0.4.1";
1686
+ var VERSION = "0.6.0";
1325
1687
 
1326
1688
  export { ApiError, CdpError, CdpSession, DEFAULT_CAPACITY, ErrorCode, GEO_MODES, HOST_MODES, NetworkError, PROXY_KINDS, ROTATIONS, RUNTIME_STATES, ScalebrowserClient, ScalebrowserError, VERSION, deriveMetricsFromProfiles, errorMessage, eventStream, openCdpSession, parseSseFrame, subscribeEvents };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalebrowser/sdk",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "Official Node/TypeScript SDK for the Scalebrowser daemon — typed REST client + direct-CDP driver plane (nodriver-style, not Playwright).",
5
5
  "type": "module",
6
6
  "license": "MIT",