browserscale-ts 1.4.0 → 1.7.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/client.d.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  import type { Transport } from "@connectrpc/connect";
2
2
  import type { Locator } from "./locator.ts";
3
- import type { DOMResult, DragResult, ElementResult, EvaluateResult, InterceptedRequest, InterceptedResponse, InspectResult, NavigateResult, PageInfo, ScreenshotResult, ReadCanvasResult, SelectOptionResult, WaitResult, IceServer, ReactionInfo } from "./types.ts";
3
+ import type { DOMResult, DragResult, ElementResult, EvaluateResult, InterceptedRequest, InterceptedResponse, InspectResult, NavigateResult, NetworkCaptureOptions, PageInfo, ScreenshotResult, ReadCanvasResult, SelectOptionResult, WaitResult, IceServer, StreamAnswer, ReactionInfo } from "./types.ts";
4
4
  import type { ClickOpts, FillOpts, GetDOMOpts, GetObservationOpts, LoadHTMLOpts, NavigateOpts, ReactionOpts, ScreenshotOpts, ReadCanvasOpts, SelectOpts, WaitOpts } from "./options.ts";
5
5
  import type { HeaderModification, RequestPattern } from "./network.ts";
6
+ import { NetworkCapture, type NetworkExchangeHandler } from "./network-capture.ts";
7
+ import { ScriptFollow, ScriptRun, type ScriptEventHandler, type ScriptResult, type ScriptRunInfo } from "./scripts.ts";
8
+ import { DomMirror, type DomChangeHandler, type DomMirrorOptions, type DomResyncHandler, type DomSnapshot } from "./dom-mirror.ts";
6
9
  import type { CookieParam } from "./cookies.ts";
7
10
  import type { StorageOriginEntry } from "./storage.ts";
11
+ import type { AuthSession } from "./auth-session.ts";
8
12
  /**
9
13
  * CloudBrowser is the SDK-side handle for an active browserscale browser session.
10
14
  *
@@ -680,6 +684,180 @@ export declare class CloudBrowser {
680
684
  modifications?: HeaderModification[];
681
685
  timeoutMs?: number;
682
686
  }): Promise<InterceptedRequest | null>;
687
+ /**
688
+ * Starts capturing the session's network traffic and returns a live view of
689
+ * it.
690
+ *
691
+ * Every request matching `opts.patterns` is reported once it completes, and
692
+ * "every request" is literal: capture sits in the browser process rather than
693
+ * in a page, so cross-process iframes, workers and service workers are
694
+ * included, the headers are the ones actually put on the wire (Cookie and
695
+ * Sec-* included), and each hop of a redirect chain arrives as its own
696
+ * exchange. Requests are never paused, so the page loads at full speed.
697
+ *
698
+ * This resolves as soon as the capture is running; onExchange then fires in
699
+ * the background while you drive the browser. The capture is armed only after
700
+ * the subscription exists, so nothing that happens after this resolves is
701
+ * missed. Call {@link NetworkCapture.stop} when done — it disarms the capture
702
+ * server-side, which an aborted transport alone does not.
703
+ *
704
+ * @param opts - which requests to capture and whether to keep bodies
705
+ * @param onExchange - called per exchange; see {@link NetworkExchangeHandler}
706
+ * for the ordering and blocking rules
707
+ *
708
+ * @returns NetworkCapture handle for stopping the capture and inspecting how
709
+ * it ended
710
+ *
711
+ * @throws UNKNOWN_ERROR - the capture could not be started
712
+ *
713
+ * @example
714
+ * const capture = await browser.captureNetwork(
715
+ * { patterns: ["*\/api/*"], bodies: "text" },
716
+ * (ex) => console.log(ex.statusCode, ex.method, ex.url),
717
+ * );
718
+ * try {
719
+ * await browser.navigate("https://example.com");
720
+ * } finally {
721
+ * await capture.stop();
722
+ * }
723
+ */
724
+ captureNetwork(opts: NetworkCaptureOptions, onExchange: NetworkExchangeHandler): Promise<NetworkCapture>;
725
+ /**
726
+ * Arms a capture without subscribing to it.
727
+ *
728
+ * Use it when the reader lives somewhere else — another tab, or a later
729
+ * {@link createWebSocketBrowser} against the same session. Most callers want
730
+ * {@link CloudBrowser.captureNetwork} instead, which arms and subscribes
731
+ * together. Calling this again replaces the running capture.
732
+ *
733
+ * @param opts - which requests to capture and whether to keep bodies
734
+ *
735
+ * @throws UNKNOWN_ERROR - the capture could not be started
736
+ */
737
+ startNetworkCapture(opts: NetworkCaptureOptions): Promise<void>;
738
+ /**
739
+ * Disarms the session's capture.
740
+ *
741
+ * @returns whether a capture was running
742
+ *
743
+ * @throws UNKNOWN_ERROR - the capture could not be stopped
744
+ */
745
+ stopNetworkCapture(): Promise<boolean>;
746
+ /**
747
+ * Subscribes to the session's capture without arming one, for reading a
748
+ * capture that {@link CloudBrowser.startNetworkCapture} armed elsewhere.
749
+ * Several readers can watch the same capture, each with its own buffer.
750
+ *
751
+ * Stopping the returned view detaches this reader and leaves the capture
752
+ * running, since other readers may still be attached.
753
+ *
754
+ * @param onExchange - called per exchange; see {@link NetworkExchangeHandler}
755
+ * for the ordering and blocking rules
756
+ *
757
+ * @returns NetworkCapture attached to whatever capture is running; onExchange
758
+ * simply never fires when none is
759
+ *
760
+ * @throws UNKNOWN_ERROR - the subscription could not be opened
761
+ */
762
+ streamNetworkExchanges(onExchange: NetworkExchangeHandler): Promise<NetworkCapture>;
763
+ /**
764
+ * Opens the stream and waits for the server to acknowledge the subscription
765
+ * before resolving.
766
+ *
767
+ * Merely calling the streaming method does not wait for the server to start
768
+ * handling it, so arming a capture straight after could outrun the
769
+ * subscription and lose the first exchanges. The response headers arrive once
770
+ * the handler is subscribed, and onHeader reports exactly that — over native
771
+ * gRPC as well as over the WebSocket transport, which forwards the event as
772
+ * its own frame.
773
+ */
774
+ private subscribeNetworkExchanges;
775
+ /**
776
+ * Starts a live copy of a frame's DOM and keeps it up to date.
777
+ *
778
+ * The browser sends the top of the tree once, then reports only what changed
779
+ * in the part you expanded. Everything else costs a child count per batch, no
780
+ * matter how much churns inside it — which is what makes this usable on a
781
+ * page that rewrites a list sixty times a second, where re-fetching the
782
+ * document on a timer is not.
783
+ *
784
+ * Expand and collapse as the user opens and closes nodes; that is what moves
785
+ * the boundary of what gets reported. The returned {@link DomMirror} holds
786
+ * the tree and exposes `expand`, `collapse` and `reveal`.
787
+ *
788
+ * The handler runs after each applied batch. `mirror.root` is a new object
789
+ * whenever anything under it changed and the untouched parts keep their
790
+ * identity, so rendering straight from it with memoized components is cheap.
791
+ *
792
+ * One mirror covers the whole page as ONE tree. An `<iframe>` is an
793
+ * ordinary element whose single child is the document it hosts; expanding it
794
+ * fetches that document and starts mirroring the frame, however deeply
795
+ * nested and whether or not it is cross-origin. Unlike the inlining
796
+ * {@link CloudBrowser.getDOM} does, these regions stay live — and frames
797
+ * nobody opened cost nothing.
798
+ *
799
+ * ```ts
800
+ * const mirror = await browser.mirrorDom({ pierce: true }, () => {
801
+ * render(mirror.root);
802
+ * });
803
+ * await mirror.expand(bodyNode);
804
+ * // ...
805
+ * await mirror.stop();
806
+ * ```
807
+ *
808
+ * @param opts initial depth and whether to pierce shadow roots
809
+ * @param onChange called after every change, including the first snapshot
810
+ * @param onResync called when the copy had to be rebuilt, after the new tree
811
+ * is in place. Rebuilding is automatic; this is for telling the user why
812
+ * their expanded nodes collapsed.
813
+ * @throws UNKNOWN_ERROR - the mirror could not be started
814
+ */
815
+ mirrorDom(opts: DomMirrorOptions, onChange: DomChangeHandler, onResync?: DomResyncHandler): Promise<DomMirror>;
816
+ /**
817
+ * Starts (or restarts) the page's mirror and returns the main document,
818
+ * without subscribing to changes. {@link CloudBrowser.mirrorDom} is what you
819
+ * normally want; this is the raw command.
820
+ */
821
+ startDomMirror(opts?: DomMirrorOptions): Promise<DomSnapshot>;
822
+ /** Stops the page's mirror, every frame of it. Idempotent. */
823
+ stopDomMirror(): Promise<void>;
824
+ /**
825
+ * Fetches a node's children and starts reporting changes inside them.
826
+ * {@link DomMirror.expand} calls this and folds the result into the tree.
827
+ *
828
+ * On an `<iframe>` the one child is the document it hosts, and this call is
829
+ * what starts mirroring that frame.
830
+ */
831
+ getDomChildren(backendNodeId: number, frameId?: string, depth?: number): Promise<{
832
+ children: string;
833
+ seq: number;
834
+ }>;
835
+ /**
836
+ * Stops reporting changes inside a node, and inside any frame below it.
837
+ * {@link DomMirror.collapse} calls this.
838
+ */
839
+ releaseDomSubtree(backendNodeId: number, frameId?: string): Promise<void>;
840
+ /**
841
+ * Returns the chain from the main document down to a node, each ancestor
842
+ * with its own children, crossing into frames where it has to and starting
843
+ * the ones it passes through. {@link DomMirror.reveal} calls this and
844
+ * splices it in.
845
+ */
846
+ revealDomNode(backendNodeId: number, frameId?: string): Promise<{
847
+ path: string;
848
+ seq: number;
849
+ }>;
850
+ /**
851
+ * A frame's mutation counter, incremented on every change the document sees.
852
+ * O(1) in the browser and the change detector to poll if you are not
853
+ * consuming mirror events.
854
+ *
855
+ * Prefer this over {@link CloudBrowser.getDOMHash}, which serializes the
856
+ * whole tree just to hash it. The two answer different questions: a hash
857
+ * compares content, a revision only says whether this document moved since
858
+ * you last asked.
859
+ */
860
+ getDomRevision(frameId?: string): Promise<number>;
683
861
  /**
684
862
  * Returns all cookies currently stored in this session's browser context.
685
863
  *
@@ -780,6 +958,39 @@ export declare class CloudBrowser {
780
958
  * await browser.clearStorage();
781
959
  */
782
960
  clearStorage(origin?: string): Promise<void>;
961
+ /**
962
+ * Exports the signed-in primary account and DBSC sessions of this
963
+ * browser context.
964
+ *
965
+ * State is read in the browser process, so no page needs to be open.
966
+ * Returns undefined when the context has neither a signed-in account
967
+ * nor DBSC sessions.
968
+ *
969
+ * @returns AuthSession, or undefined when there is nothing to export
970
+ *
971
+ * @throws UNKNOWN_ERROR - the auth session could not be read
972
+ *
973
+ * @example
974
+ * const auth = await browser.getAuthSession();
975
+ * if (auth) await fs.writeFile("auth.json", JSON.stringify(auth));
976
+ */
977
+ getAuthSession(): Promise<AuthSession | undefined>;
978
+ /**
979
+ * Imports an auth session so the context comes up signed in (and syncing
980
+ * if syncConsent) with its DBSC sessions restored.
981
+ *
982
+ * Call it before navigating. Pair with setCookies() / setStorage() to
983
+ * restore a full persona.
984
+ *
985
+ * @param session - session as returned by getAuthSession()
986
+ *
987
+ * @throws UNKNOWN_ERROR - the auth session could not be written
988
+ *
989
+ * @example
990
+ * await browser.setAuthSession(saved);
991
+ * await browser.navigate("https://mail.google.com");
992
+ */
993
+ setAuthSession(session: AuthSession): Promise<void>;
783
994
  /**
784
995
  * Hit-tests at the viewport-relative (x, y) and returns the topmost
785
996
  * element under that point.
@@ -967,22 +1178,22 @@ export declare class CloudBrowser {
967
1178
  getStreamConfig(): Promise<IceServer[]>;
968
1179
  /**
969
1180
  * Answers your WebRTC SDP offer and starts streaming the page as a video
970
- * track, returning the SDP answer to set as your peer's remote description.
971
- * The browser is the answerer; you are the offerer (see
1181
+ * track. The browser is the answerer; you are the offerer (see
972
1182
  * {@link getStreamConfig} for the credentials to build the offer).
973
1183
  *
974
1184
  * @param offerSdp - your `RTCPeerConnection`'s SDP offer
975
1185
  *
976
- * @returns the SDP answer to apply as the remote description
1186
+ * @returns the SDP answer to apply as the remote description, plus the
1187
+ * viewport to map input coordinates into
977
1188
  *
978
1189
  * @throws UNKNOWN_ERROR - the offer was empty, TURN is unconfigured, or the
979
1190
  * browser could not negotiate the stream
980
1191
  *
981
1192
  * @example
982
- * const answer = await browser.startStream(offer.sdp);
983
- * await pc.setRemoteDescription({ type: "answer", sdp: answer });
1193
+ * const { answerSdp, viewport } = await browser.startStream(offer.sdp);
1194
+ * await pc.setRemoteDescription({ type: "answer", sdp: answerSdp });
984
1195
  */
985
- startStream(offerSdp: string): Promise<string>;
1196
+ startStream(offerSdp: string): Promise<StreamAnswer>;
986
1197
  /**
987
1198
  * Tears down the live video stream for the session's page. Safe to call even
988
1199
  * if no stream is running.
@@ -1051,6 +1262,138 @@ export declare class CloudBrowser {
1051
1262
  * for (const r of pending) console.log(r.reactionId, r.matchSelector);
1052
1263
  */
1053
1264
  listReactions(): Promise<ReactionInfo[]>;
1265
+ /**
1266
+ * Runs `source` in the session's browser and waits for it to finish.
1267
+ *
1268
+ * The script executes in a V8 isolate inside the browser process, not in the
1269
+ * page, and reaches the same operations this SDK exposes through a `browser`
1270
+ * object it is handed. The difference is cost: each call is a function call in
1271
+ * the browser rather than a network round trip, so work that is chatty by
1272
+ * nature — polling for a selector, walking a list, following pagination —
1273
+ * runs in microseconds per step instead of tens of milliseconds.
1274
+ *
1275
+ * This waits for as long as the script runs, and cannot be bounded: the run
1276
+ * id needed to cancel only arrives with the reply. Use
1277
+ * {@link CloudBrowser.startScript} when the script may outlive the caller's
1278
+ * patience, or {@link CloudBrowser.stopScripts} to abandon what this session
1279
+ * is running.
1280
+ *
1281
+ * @param source - JavaScript to execute; its return value comes back as JSON
1282
+ *
1283
+ * @returns the return value and the script's whole console output. A script
1284
+ * that threw is reported as `success: false`, not as a rejection
1285
+ *
1286
+ * @throws UNKNOWN_ERROR - the script could not be delivered to the browser
1287
+ *
1288
+ * @example
1289
+ * const result = await browser.runScript(`
1290
+ * await browser.navigate("https://example.com");
1291
+ * const items = [];
1292
+ * for (const el of await browser.getDOM().querySelectorAll("h1")) {
1293
+ * items.push(el.textContent);
1294
+ * }
1295
+ * return items;
1296
+ * `);
1297
+ * console.log(result.success, result.result);
1298
+ */
1299
+ runScript(source: string): Promise<ScriptResult>;
1300
+ /**
1301
+ * Launches `source` in the session's browser and resolves as soon as the run
1302
+ * is under way.
1303
+ *
1304
+ * The counterpart to {@link CloudBrowser.runScript}, for scripts that are not
1305
+ * worth waiting on: a watcher that runs for the life of the session, work
1306
+ * that should survive this page. Output arrives at `onEvent` while the caller
1307
+ * gets on with something else, and {@link ScriptRun.wait} collects the
1308
+ * outcome if it is wanted.
1309
+ *
1310
+ * Subscribing has to happen before the launch, because a detached run's
1311
+ * output is not kept anywhere — the browser rejects a start with nobody
1312
+ * listening rather than discard the script's log and result. This call does
1313
+ * both in that order, so nothing the script prints is missed.
1314
+ *
1315
+ * @param source - JavaScript to execute
1316
+ * @param onEvent - called per log line and once for the outcome; see
1317
+ * {@link ScriptEventHandler} for the ordering and blocking rules
1318
+ *
1319
+ * @returns ScriptRun handle for awaiting or cancelling the run
1320
+ *
1321
+ * @throws UNKNOWN_ERROR - the run could not be started
1322
+ *
1323
+ * @example
1324
+ * const run = await browser.startScript(source, (ev) => {
1325
+ * if (ev.log) console.log(ev.log.level, ev.log.message);
1326
+ * });
1327
+ * const outcome = await run.wait();
1328
+ */
1329
+ startScript(source: string, onEvent: ScriptEventHandler): Promise<ScriptRun>;
1330
+ /**
1331
+ * Watches script output in this session without starting anything.
1332
+ *
1333
+ * For the case {@link CloudBrowser.startScript} cannot cover: a run somebody
1334
+ * else launched, or one this page started before it reloaded. Several readers
1335
+ * can watch the same session, each with its own buffer.
1336
+ *
1337
+ * Only output produced from now on arrives — lines printed before the
1338
+ * subscription existed are not kept. A run that has already finished is
1339
+ * therefore invisible here; {@link CloudBrowser.listScriptRuns} is how you
1340
+ * tell that apart from a run that is merely quiet.
1341
+ *
1342
+ * @param runId - run to follow, or `""` to follow every run in the session
1343
+ * @param onEvent - called per event; see {@link ScriptEventHandler} for the
1344
+ * ordering and blocking rules
1345
+ *
1346
+ * @returns ScriptFollow handle for stopping the subscription
1347
+ *
1348
+ * @throws UNKNOWN_ERROR - the subscription could not be opened
1349
+ *
1350
+ * @example
1351
+ * const follow = await browser.followScript(runId, (ev) => {
1352
+ * if (ev.log) console.log(ev.log.message);
1353
+ * });
1354
+ * try {
1355
+ * await follow.wait();
1356
+ * } finally {
1357
+ * await follow.stop();
1358
+ * }
1359
+ */
1360
+ followScript(runId: string, onEvent: ScriptEventHandler): Promise<ScriptFollow>;
1361
+ /**
1362
+ * Cancels runs in this session and reports how many it ended.
1363
+ *
1364
+ * An empty `runId` cancels every run in the session, which is the only form
1365
+ * available to a caller that never learned an id — notably one abandoning a
1366
+ * {@link CloudBrowser.runScript}.
1367
+ *
1368
+ * @param runId - run to cancel, or `""` for all of them
1369
+ *
1370
+ * @returns how many runs were cancelled; 0 when the id named nothing in
1371
+ * flight
1372
+ *
1373
+ * @throws UNKNOWN_ERROR - the cancel could not be delivered
1374
+ *
1375
+ * @example
1376
+ * await browser.stopScripts(""); // abandon everything running
1377
+ */
1378
+ stopScripts(runId: string): Promise<number>;
1379
+ /**
1380
+ * Reports the scripts still running in this session.
1381
+ *
1382
+ * Only runs in flight — a finished run is reported once on the event stream
1383
+ * and then forgotten, so this is not a history. Its use is finding work this
1384
+ * caller did not start: a script a previous page left behind, which
1385
+ * {@link CloudBrowser.stopScripts} needs an id to name.
1386
+ *
1387
+ * @returns one entry per run still executing
1388
+ *
1389
+ * @throws UNKNOWN_ERROR - the session could not be queried
1390
+ *
1391
+ * @example
1392
+ * for (const run of await browser.listScriptRuns()) {
1393
+ * console.log(run.runId, run.runningMs);
1394
+ * }
1395
+ */
1396
+ listScriptRuns(): Promise<ScriptRunInfo[]>;
1054
1397
  /**
1055
1398
  * @internal — a reaction match/action must be a css()/js() locator: node()
1056
1399
  * and at() are rejected (a reaction watches for a condition, like a wait).