@limrun/api 0.48.3 → 0.49.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.
Files changed (184) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/client.d.mts +6 -3
  3. package/client.d.mts.map +1 -1
  4. package/client.d.ts +6 -3
  5. package/client.d.ts.map +1 -1
  6. package/client.js +4 -4
  7. package/client.js.map +1 -1
  8. package/client.mjs +2 -2
  9. package/client.mjs.map +1 -1
  10. package/destination-tunnel-dialer.d.mts +30 -2
  11. package/destination-tunnel-dialer.d.mts.map +1 -1
  12. package/destination-tunnel-dialer.d.ts +30 -2
  13. package/destination-tunnel-dialer.d.ts.map +1 -1
  14. package/destination-tunnel-dialer.js +349 -65
  15. package/destination-tunnel-dialer.js.map +1 -1
  16. package/destination-tunnel-dialer.mjs +345 -64
  17. package/destination-tunnel-dialer.mjs.map +1 -1
  18. package/destination-tunnel-har.d.mts +23 -0
  19. package/destination-tunnel-har.d.mts.map +1 -0
  20. package/destination-tunnel-har.d.ts +23 -0
  21. package/destination-tunnel-har.d.ts.map +1 -0
  22. package/destination-tunnel-har.js +164 -0
  23. package/destination-tunnel-har.js.map +1 -0
  24. package/destination-tunnel-har.mjs +160 -0
  25. package/destination-tunnel-har.mjs.map +1 -0
  26. package/destination-tunnel-inspection.d.mts +71 -0
  27. package/destination-tunnel-inspection.d.mts.map +1 -0
  28. package/destination-tunnel-inspection.d.ts +71 -0
  29. package/destination-tunnel-inspection.d.ts.map +1 -0
  30. package/destination-tunnel-inspection.js +267 -0
  31. package/destination-tunnel-inspection.js.map +1 -0
  32. package/destination-tunnel-inspection.mjs +261 -0
  33. package/destination-tunnel-inspection.mjs.map +1 -0
  34. package/destination-tunnel.d.mts +119 -14
  35. package/destination-tunnel.d.mts.map +1 -1
  36. package/destination-tunnel.d.ts +119 -14
  37. package/destination-tunnel.d.ts.map +1 -1
  38. package/destination-tunnel.js +407 -98
  39. package/destination-tunnel.js.map +1 -1
  40. package/destination-tunnel.mjs +374 -73
  41. package/destination-tunnel.mjs.map +1 -1
  42. package/exec-client.d.mts +1 -0
  43. package/exec-client.d.mts.map +1 -1
  44. package/exec-client.d.ts +1 -0
  45. package/exec-client.d.ts.map +1 -1
  46. package/exec-client.js.map +1 -1
  47. package/exec-client.mjs.map +1 -1
  48. package/index.d.mts +6 -0
  49. package/index.d.mts.map +1 -1
  50. package/index.d.ts +6 -0
  51. package/index.d.ts.map +1 -1
  52. package/index.js +23 -2
  53. package/index.js.map +1 -1
  54. package/index.mjs +5 -0
  55. package/index.mjs.map +1 -1
  56. package/instance-client.d.mts +105 -1
  57. package/instance-client.d.mts.map +1 -1
  58. package/instance-client.d.ts +105 -1
  59. package/instance-client.d.ts.map +1 -1
  60. package/instance-client.js +83 -1
  61. package/instance-client.js.map +1 -1
  62. package/instance-client.mjs +83 -1
  63. package/instance-client.mjs.map +1 -1
  64. package/internal/destination-tunnel-management.d.mts +4 -3
  65. package/internal/destination-tunnel-management.d.mts.map +1 -1
  66. package/internal/destination-tunnel-management.d.ts +4 -3
  67. package/internal/destination-tunnel-management.d.ts.map +1 -1
  68. package/internal/destination-tunnel-management.js +64 -46
  69. package/internal/destination-tunnel-management.js.map +1 -1
  70. package/internal/destination-tunnel-management.mjs +55 -37
  71. package/internal/destination-tunnel-management.mjs.map +1 -1
  72. package/internal/destination-tunnel-url.d.mts +2 -0
  73. package/internal/destination-tunnel-url.d.mts.map +1 -1
  74. package/internal/destination-tunnel-url.d.ts +2 -0
  75. package/internal/destination-tunnel-url.d.ts.map +1 -1
  76. package/internal/destination-tunnel-url.js +45 -5
  77. package/internal/destination-tunnel-url.js.map +1 -1
  78. package/internal/destination-tunnel-url.mjs +44 -5
  79. package/internal/destination-tunnel-url.mjs.map +1 -1
  80. package/internal/destination-tunnel-wire-reader.d.mts +23 -0
  81. package/internal/destination-tunnel-wire-reader.d.mts.map +1 -0
  82. package/internal/destination-tunnel-wire-reader.d.ts +23 -0
  83. package/internal/destination-tunnel-wire-reader.d.ts.map +1 -0
  84. package/internal/destination-tunnel-wire-reader.js +110 -0
  85. package/internal/destination-tunnel-wire-reader.js.map +1 -0
  86. package/internal/destination-tunnel-wire-reader.mjs +93 -0
  87. package/internal/destination-tunnel-wire-reader.mjs.map +1 -0
  88. package/internal/persist-option.d.mts +14 -0
  89. package/internal/persist-option.d.mts.map +1 -0
  90. package/internal/persist-option.d.ts +14 -0
  91. package/internal/persist-option.d.ts.map +1 -0
  92. package/internal/persist-option.js +18 -0
  93. package/internal/persist-option.js.map +1 -0
  94. package/internal/persist-option.mjs +15 -0
  95. package/internal/persist-option.mjs.map +1 -0
  96. package/internal/proxy-transport.d.mts +27 -0
  97. package/internal/proxy-transport.d.mts.map +1 -1
  98. package/internal/proxy-transport.d.ts +27 -0
  99. package/internal/proxy-transport.d.ts.map +1 -1
  100. package/internal/proxy-transport.js +206 -0
  101. package/internal/proxy-transport.js.map +1 -1
  102. package/internal/proxy-transport.mjs +205 -0
  103. package/internal/proxy-transport.mjs.map +1 -1
  104. package/internal/session-stream.d.mts +39 -0
  105. package/internal/session-stream.d.mts.map +1 -0
  106. package/internal/session-stream.d.ts +39 -0
  107. package/internal/session-stream.d.ts.map +1 -0
  108. package/internal/session-stream.js +144 -0
  109. package/internal/session-stream.js.map +1 -0
  110. package/internal/session-stream.mjs +139 -0
  111. package/internal/session-stream.mjs.map +1 -0
  112. package/internal/tslib.js +4 -4
  113. package/ios-client.d.mts +61 -4
  114. package/ios-client.d.mts.map +1 -1
  115. package/ios-client.d.ts +61 -4
  116. package/ios-client.d.ts.map +1 -1
  117. package/ios-client.js +48 -2
  118. package/ios-client.js.map +1 -1
  119. package/ios-client.mjs +48 -2
  120. package/ios-client.mjs.map +1 -1
  121. package/package.json +22 -1
  122. package/resources/android-instances-helpers.d.mts +25 -0
  123. package/resources/android-instances-helpers.d.mts.map +1 -0
  124. package/resources/android-instances-helpers.d.ts +25 -0
  125. package/resources/android-instances-helpers.d.ts.map +1 -0
  126. package/resources/android-instances-helpers.js +37 -0
  127. package/resources/android-instances-helpers.js.map +1 -0
  128. package/resources/android-instances-helpers.mjs +33 -0
  129. package/resources/android-instances-helpers.mjs.map +1 -0
  130. package/resources/index.d.mts +5 -2
  131. package/resources/index.d.mts.map +1 -1
  132. package/resources/index.d.ts +5 -2
  133. package/resources/index.d.ts.map +1 -1
  134. package/resources/index.js +5 -5
  135. package/resources/index.js.map +1 -1
  136. package/resources/index.mjs +2 -2
  137. package/resources/index.mjs.map +1 -1
  138. package/resources/ios-instances-helpers.d.mts +25 -0
  139. package/resources/ios-instances-helpers.d.mts.map +1 -0
  140. package/resources/ios-instances-helpers.d.ts +25 -0
  141. package/resources/ios-instances-helpers.d.ts.map +1 -0
  142. package/resources/ios-instances-helpers.js +37 -0
  143. package/resources/ios-instances-helpers.js.map +1 -0
  144. package/resources/ios-instances-helpers.mjs +33 -0
  145. package/resources/ios-instances-helpers.mjs.map +1 -0
  146. package/resources/session-artifacts.d.mts +70 -0
  147. package/resources/session-artifacts.d.mts.map +1 -0
  148. package/resources/session-artifacts.d.ts +70 -0
  149. package/resources/session-artifacts.d.ts.map +1 -0
  150. package/resources/session-artifacts.js +8 -0
  151. package/resources/session-artifacts.js.map +1 -0
  152. package/resources/session-artifacts.mjs +7 -0
  153. package/resources/session-artifacts.mjs.map +1 -0
  154. package/resources/xcode-instances-helpers.d.mts +9 -0
  155. package/resources/xcode-instances-helpers.d.mts.map +1 -1
  156. package/resources/xcode-instances-helpers.d.ts +9 -0
  157. package/resources/xcode-instances-helpers.d.ts.map +1 -1
  158. package/resources/xcode-instances-helpers.js.map +1 -1
  159. package/resources/xcode-instances-helpers.mjs.map +1 -1
  160. package/src/client.ts +4 -2
  161. package/src/destination-tunnel-dialer.ts +403 -64
  162. package/src/destination-tunnel-har.ts +200 -0
  163. package/src/destination-tunnel-inspection.ts +367 -0
  164. package/src/destination-tunnel.ts +510 -88
  165. package/src/exec-client.ts +1 -0
  166. package/src/index.ts +52 -0
  167. package/src/instance-client.ts +271 -5
  168. package/src/internal/destination-tunnel-management.ts +67 -43
  169. package/src/internal/destination-tunnel-url.ts +61 -5
  170. package/src/internal/destination-tunnel-wire-reader.ts +109 -0
  171. package/src/internal/persist-option.ts +26 -0
  172. package/src/internal/proxy-transport.ts +248 -0
  173. package/src/internal/session-stream.ts +157 -0
  174. package/src/ios-client.ts +127 -8
  175. package/src/resources/android-instances-helpers.ts +45 -0
  176. package/src/resources/index.ts +9 -2
  177. package/src/resources/ios-instances-helpers.ts +45 -0
  178. package/src/resources/session-artifacts.ts +95 -0
  179. package/src/resources/xcode-instances-helpers.ts +9 -0
  180. package/src/version.ts +1 -1
  181. package/version.d.mts +1 -1
  182. package/version.d.ts +1 -1
  183. package/version.js +1 -1
  184. package/version.mjs +1 -1
@@ -96,6 +96,7 @@ export type XcodeBuildExecRequest = {
96
96
  apiKeyId: string;
97
97
  apiIssuerId: string;
98
98
  apiPrivateKeyBase64: string;
99
+ entitlements?: Record<string, string>;
99
100
  };
100
101
  /** Wire name retained for compatibility with the limbuild exec API. */
101
102
  testflight?: AppStoreUploadConfig;
package/src/index.ts CHANGED
@@ -8,6 +8,50 @@ export { Limrun, type ClientOptions } from './client';
8
8
  export { PagePromise } from './core/pagination';
9
9
  export * from './instance-client';
10
10
  export * as Ios from './ios-client';
11
+ export {
12
+ isTerminalDestinationTunnelError,
13
+ type DestinationTcpTunnel,
14
+ type DestinationTcpTunnelOptions,
15
+ } from './destination-tunnel-dialer';
16
+ export {
17
+ decodeDestinationTunnelInspectionSSEEvent,
18
+ deriveDestinationTunnelInspectionURL,
19
+ startDestinationTunnelInspectionStream,
20
+ type DestinationTunnelInspectionBodyEvent,
21
+ type DestinationTunnelInspectionComplete,
22
+ type DestinationTunnelInspectionErrorCallback,
23
+ type DestinationTunnelInspectionEvent,
24
+ type DestinationTunnelInspectionEventCallback,
25
+ type DestinationTunnelInspectionExtension,
26
+ type DestinationTunnelInspectionGap,
27
+ type DestinationTunnelInspectionMetadataEvent,
28
+ type DestinationTunnelInspectionStream,
29
+ type DestinationTunnelInspectionStreamOptions,
30
+ } from './destination-tunnel-inspection';
31
+ export {
32
+ DestinationTunnelHARAssembler,
33
+ type DestinationTunnelHAREntry,
34
+ type DestinationTunnelHARExtension,
35
+ } from './destination-tunnel-har';
36
+ export {
37
+ validateDestinationTunnelSelectors,
38
+ normalizeDestinationTunnelInspection,
39
+ disabledDestinationTunnelInspection,
40
+ destinationTunnelConfigHash,
41
+ DestinationTunnelSelectorError,
42
+ DESTINATION_TUNNEL_DEFAULT_MAX_BODY_BYTES,
43
+ DESTINATION_TUNNEL_DEFAULT_TTL_SECONDS,
44
+ DESTINATION_TUNNEL_DEFAULT_WINDOW,
45
+ DESTINATION_TUNNEL_MAX_BODY_BYTES,
46
+ DESTINATION_TUNNEL_MAX_TTL_SECONDS,
47
+ type DestinationTunnelInspectionConfig,
48
+ type DestinationTunnelTransportRequest,
49
+ type DestinationTunnelTransportResult,
50
+ type DestinationTunnelSelectors,
51
+ type DestinationTunnelSelectorReport,
52
+ type DestinationTunnelSelectorErrorCode,
53
+ type DestinationTunnelBindReport,
54
+ } from './destination-tunnel';
11
55
  export {
12
56
  startHttpProxy,
13
57
  startForwardHttpProxy,
@@ -87,6 +131,14 @@ export {
87
131
  type GradleBuildLog,
88
132
  } from './resources/gradle-instances-helpers';
89
133
  export { type AssetUploadOptions } from './resources/daemon-client-shared';
134
+ export {
135
+ type SessionArtifact,
136
+ type SessionArtifactKind,
137
+ type SessionLogLine,
138
+ type SessionEvent,
139
+ type SessionEventType,
140
+ } from './resources/session-artifacts';
141
+ export { SessionStreamLostError } from './internal/session-stream';
90
142
  export {
91
143
  LIMRUN_DIR,
92
144
  TRY_IMPORT_LINE,
@@ -11,10 +11,45 @@ import { nodeProxyTransport } from './internal/proxy-transport';
11
11
  import { startTcpTunnel, isNonRetryableError } from './tunnel';
12
12
  import type { Tunnel } from './tunnel';
13
13
  import { syncFolder, type FolderSyncOptions, type SyncFolderResult } from './folder-sync';
14
+ import { startDestinationTcpTunnel, type DestinationTcpTunnel } from './destination-tunnel-dialer';
15
+ import type { DestinationTunnelInspectionConfig, DestinationTunnelSelectors } from './destination-tunnel';
16
+ import type {
17
+ DestinationTunnelInspectionErrorCallback,
18
+ DestinationTunnelInspectionEventCallback,
19
+ } from './destination-tunnel-inspection';
20
+ import {
21
+ getDestinationTunnelStatus,
22
+ stopDestinationTunnel,
23
+ type DestinationTunnelStatus,
24
+ } from './internal/destination-tunnel-management';
25
+ import { deriveDestinationTunnelURL } from './internal/destination-tunnel-url';
26
+ import { persistFields, type PersistOption } from './internal/persist-option';
27
+ import { streamSessionEntries } from './internal/session-stream';
28
+ import type { SessionLogLine, SessionEvent } from './resources/session-artifacts';
29
+
30
+ export { type PersistOption } from './internal/persist-option';
14
31
 
15
32
  const ANDROID_RECORDING_PATH = '/data/local/tmp/recordings/video_recording.mp4';
16
33
  const ANDROID_SIGNALING_PATH = '/ws';
17
34
 
35
+ /** Transparent destination tunnel from the Android instance to this machine. */
36
+ export type DestinationTunnel = DestinationTcpTunnel;
37
+ export type DestinationTunnelOptions = {
38
+ /** Exact endpoint and domain selector values interpreted by the Android tunnel server. */
39
+ selectors: DestinationTunnelSelectors;
40
+ /** Inspection settings negotiated with the Android tunnel server. */
41
+ inspection?: Partial<DestinationTunnelInspectionConfig>;
42
+ /** Called for each validated inspection metadata or body event. */
43
+ onInspectionEvent?: DestinationTunnelInspectionEventCallback;
44
+ /** Called when the independent inspection stream fails or reconnects. */
45
+ onInspectionError?: DestinationTunnelInspectionErrorCallback;
46
+ /** Per-flow receive window in bytes. Defaults to 1 MiB. */
47
+ window?: number;
48
+ /** Controls tunnel logging verbosity. Defaults to the instance client's log level. */
49
+ logLevel?: LogLevel;
50
+ };
51
+ export type { DestinationTunnelStatus } from './internal/destination-tunnel-management';
52
+
18
53
  /**
19
54
  * Connection state of the instance client
20
55
  */
@@ -134,6 +169,12 @@ export type InstanceClient = {
134
169
  * @param packageName Package name of the app to terminate
135
170
  */
136
171
  terminateApp: (packageName: string) => Promise<void>;
172
+ /**
173
+ * List the launchable apps installed on the instance, each with its package
174
+ * name (as `bundleId`), display name, install type (`user` or `system`),
175
+ * and, when available, a small base64 PNG icon.
176
+ */
177
+ listApps: () => Promise<InstalledApp[]>;
137
178
  /**
138
179
  * Watch an app's exit/crash without launching it through {@link launchApp},
139
180
  * e.g. for apps that are already running or will be opened via a deeplink
@@ -246,8 +287,11 @@ export type InstanceClient = {
246
287
  * Start recording device video. Use stopRecording() to finish the recording.
247
288
  * When provided, `quality` must be one of `5`, `6`, `7`, `8`, `9`, or `10`.
248
289
  * The server default is `5`.
290
+ * With `persist`, the completed recording is uploaded to Limrun's bucket
291
+ * when the recording stops or the instance terminates; list it with
292
+ * `androidInstances.listRecordings`.
249
293
  */
250
- startRecording: (options?: { quality?: RecordingQuality }) => Promise<void>;
294
+ startRecording: (options?: { quality?: RecordingQuality; persist?: PersistOption }) => Promise<void>;
251
295
  /**
252
296
  * Stop the active server-side recording.
253
297
  * If `saveTo.presignedUrl` is provided, the server uploads the completed file there before resolving.
@@ -255,6 +299,48 @@ export type InstanceClient = {
255
299
  * Returns a download URL for the completed recording.
256
300
  */
257
301
  stopRecording: (saveTo: { presignedUrl?: string; localPath?: string }) => Promise<string>;
302
+ /**
303
+ * Start capturing an app's logcat output (one JSONL object per line) for
304
+ * the given package name. One app log capture runs at a time; the server
305
+ * rejects a second start. With `persist`, the capture is uploaded to
306
+ * Limrun's bucket when it stops or the instance terminates; list it with
307
+ * `androidInstances.listAppLogs`.
308
+ */
309
+ startAppLogCapture: (options: { bundleId: string; persist?: PersistOption }) => Promise<void>;
310
+ /** Stop the active app log capture. */
311
+ stopAppLogCapture: () => Promise<void>;
312
+ /**
313
+ * Start capturing the coalesced event log of user and agent actions (one
314
+ * JSONL object per line). One event capture runs at a time; the server
315
+ * rejects a second start. With `persist`, the capture is uploaded to
316
+ * Limrun's bucket when it stops or the instance terminates; list it with
317
+ * `androidInstances.listEvents`.
318
+ */
319
+ startEventCapture: (options?: { persist?: PersistOption }) => Promise<void>;
320
+ /** Stop the active event capture. */
321
+ stopEventCapture: () => Promise<void>;
322
+ /**
323
+ * Tail the live app log stream fed by {@link startAppLogCapture}. On
324
+ * connect the instance replays its recent buffer (up to 1000 lines), then
325
+ * delivers lines as they are captured; a dropped connection reconnects
326
+ * and resumes exactly where it left off, so every line is delivered once.
327
+ * The stream is independent of this client's websocket and survives
328
+ * {@link disconnect}; `onError` fires at most once, after the stream has
329
+ * kept failing for several minutes (e.g. the instance was deleted), and
330
+ * the stream is closed. Returns a function that closes the stream.
331
+ */
332
+ streamAppLogCapture: (handlers: {
333
+ onLine: (line: SessionLogLine) => void;
334
+ onError?: (error: Error) => void;
335
+ }) => () => void;
336
+ /**
337
+ * Tail the live coalesced event log fed by {@link startEventCapture}.
338
+ * Same delivery and lifecycle semantics as {@link streamAppLogCapture}.
339
+ */
340
+ streamEventCapture: (handlers: {
341
+ onEvent: (event: SessionEvent) => void;
342
+ onError?: (error: Error) => void;
343
+ }) => () => void;
258
344
  /** Send an application-level keepAlive message on the control websocket. */
259
345
  keepAlive: () => void;
260
346
  /**
@@ -267,6 +353,22 @@ export type InstanceClient = {
267
353
  * Returns the local TCP port and a cleanup function.
268
354
  */
269
355
  startAdbTunnel: () => Promise<Tunnel>;
356
+
357
+ /**
358
+ * Transparently route declared Android TCP destinations through this
359
+ * machine. Exact `localhost` routes are also reachable on-device as
360
+ * `10.0.2.2:<port>`, following the emulator convention.
361
+ *
362
+ * The caller owns the returned tunnel and must close it. Disconnecting this
363
+ * instance client does not close the tunnel.
364
+ */
365
+ startTunnel: (options: DestinationTunnelOptions) => Promise<DestinationTunnel>;
366
+
367
+ /** Get the active destination tunnel and most recent terminal failure. */
368
+ getTunnelStatus: () => Promise<DestinationTunnelStatus>;
369
+
370
+ /** Stop the active destination tunnel only when its ID matches `tunnelId`. */
371
+ stopTunnel: (tunnelId: string) => Promise<void>;
270
372
  /**
271
373
  * Send an asset URL to the instance. The instance will download the asset
272
374
  * and process it (currently APK install is supported). Resolves on success,
@@ -517,6 +619,18 @@ export type LaunchAppResult = {
517
619
  packageName: string;
518
620
  };
519
621
 
622
+ /**
623
+ * One entry of {@link InstanceClient.listApps}. The package name is carried
624
+ * as `bundleId` so the shape matches iOS's listApps.
625
+ */
626
+ export type InstalledApp = {
627
+ bundleId: string;
628
+ name: string;
629
+ installType: string;
630
+ /** App icon as a base64-encoded 64px PNG; absent when the runtime could not extract one. */
631
+ icon?: string;
632
+ };
633
+
520
634
  /** Handle for an app watch registered via {@link InstanceClient.watchApp}. */
521
635
  export type AppWatch = {
522
636
  /** Identifier of this watch on the server. */
@@ -703,6 +817,13 @@ type WatchAppResultMessage = {
703
817
  error?: CommandError;
704
818
  };
705
819
 
820
+ type ListAppsResultMessage = {
821
+ type: 'listAppsResult';
822
+ id: string;
823
+ payload?: { apps?: InstalledApp[] };
824
+ error?: CommandError;
825
+ };
826
+
706
827
  type UnwatchAppResultMessage = {
707
828
  type: 'unwatchAppResult';
708
829
  id: string;
@@ -770,6 +891,34 @@ type StopVideoRecordingResultMessage = {
770
891
  error?: CommandError;
771
892
  };
772
893
 
894
+ type StartAppLogCaptureResultMessage = {
895
+ type: 'startAppLogCaptureResult';
896
+ id: string;
897
+ payload?: EmptyCommandResult;
898
+ error?: CommandError;
899
+ };
900
+
901
+ type StopAppLogCaptureResultMessage = {
902
+ type: 'stopAppLogCaptureResult';
903
+ id: string;
904
+ payload?: EmptyCommandResult;
905
+ error?: CommandError;
906
+ };
907
+
908
+ type StartEventCaptureResultMessage = {
909
+ type: 'startEventCaptureResult';
910
+ id: string;
911
+ payload?: EmptyCommandResult;
912
+ error?: CommandError;
913
+ };
914
+
915
+ type StopEventCaptureResultMessage = {
916
+ type: 'stopEventCaptureResult';
917
+ id: string;
918
+ payload?: EmptyCommandResult;
919
+ error?: CommandError;
920
+ };
921
+
773
922
  type KnownCommandResultMessage =
774
923
  | ScreenshotResultMessage
775
924
  | GetElementTreeResultMessage
@@ -784,12 +933,17 @@ type KnownCommandResultMessage =
784
933
  | TerminateAppResultMessage
785
934
  | WatchAppResultMessage
786
935
  | UnwatchAppResultMessage
936
+ | ListAppsResultMessage
787
937
  | PlayOnMicrophoneResultMessage
788
938
  | CameraControlResultMessage
789
939
  | AdbShellResultMessage
790
940
  | SetWifiBandwidthResultMessage
791
941
  | StartVideoRecordingResultMessage
792
- | StopVideoRecordingResultMessage;
942
+ | StopVideoRecordingResultMessage
943
+ | StartAppLogCaptureResultMessage
944
+ | StopAppLogCaptureResultMessage
945
+ | StartEventCaptureResultMessage
946
+ | StopEventCaptureResultMessage;
793
947
 
794
948
  type ServerMessage =
795
949
  | ScreenshotResponse
@@ -812,12 +966,17 @@ type CommandRequestMap = {
812
966
  terminateApp: { packageName: string };
813
967
  watchApp: { packageName: string; execId: string };
814
968
  unwatchApp: { execId: string };
969
+ listApps: {};
815
970
  playOnMicrophone: { path: string; once?: boolean };
816
971
  cameraControl: { action: 'setSource'; source: 'video'; arg: string; loop?: boolean } | { action: 'reset' };
817
972
  adbShell: { command: string; timeoutMs?: number };
818
973
  setWifiBandwidth: WifiBandwidthOptions;
819
- startRecording: { quality?: RecordingQuality };
974
+ startRecording: { quality?: RecordingQuality; persist?: boolean; ttlSeconds?: number };
820
975
  stopRecording: { upload?: { presignedUrl: string } };
976
+ startAppLogCapture: { bundleId: string; persist?: boolean; ttlSeconds?: number };
977
+ stopAppLogCapture: {};
978
+ startEventCapture: { persist?: boolean; ttlSeconds?: number };
979
+ stopEventCapture: {};
821
980
  };
822
981
 
823
982
  type CommandResultMap = {
@@ -834,12 +993,17 @@ type CommandResultMap = {
834
993
  terminateApp: { packageName?: string };
835
994
  watchApp: { packageName?: string };
836
995
  unwatchApp: EmptyCommandResult;
996
+ listApps: { apps?: InstalledApp[] };
837
997
  playOnMicrophone: PlayOnMicrophoneResult;
838
998
  cameraControl: EmptyCommandResult;
839
999
  adbShell: AdbShellResultMessage;
840
1000
  setWifiBandwidth: EmptyCommandResult;
841
1001
  startRecording: EmptyCommandResult;
842
1002
  stopRecording: EmptyCommandResult;
1003
+ startAppLogCapture: EmptyCommandResult;
1004
+ stopAppLogCapture: EmptyCommandResult;
1005
+ startEventCapture: EmptyCommandResult;
1006
+ stopEventCapture: EmptyCommandResult;
843
1007
  };
844
1008
 
845
1009
  type PendingRequest<T> = {
@@ -1015,12 +1179,17 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1015
1179
  case 'terminateAppResult':
1016
1180
  case 'watchAppResult':
1017
1181
  case 'unwatchAppResult':
1182
+ case 'listAppsResult':
1018
1183
  case 'playOnMicrophoneResult':
1019
1184
  case 'cameraControlResult':
1020
1185
  case 'adbShellResult':
1021
1186
  case 'setWifiBandwidthResult':
1022
1187
  case 'startRecordingResult':
1023
1188
  case 'stopRecordingResult':
1189
+ case 'startAppLogCaptureResult':
1190
+ case 'stopAppLogCaptureResult':
1191
+ case 'startEventCaptureResult':
1192
+ case 'stopEventCaptureResult':
1024
1193
  return 'id' in message && typeof message.id === 'string';
1025
1194
  default:
1026
1195
  return false;
@@ -1287,6 +1456,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1287
1456
  openUrl,
1288
1457
  launchApp,
1289
1458
  terminateApp,
1459
+ listApps,
1290
1460
  watchApp,
1291
1461
  playOnMicrophone,
1292
1462
  pushFile,
@@ -1297,9 +1467,18 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1297
1467
  setWifiBandwidth,
1298
1468
  startRecording,
1299
1469
  stopRecording,
1470
+ startAppLogCapture,
1471
+ stopAppLogCapture,
1472
+ startEventCapture,
1473
+ stopEventCapture,
1474
+ streamAppLogCapture,
1475
+ streamEventCapture,
1300
1476
  keepAlive,
1301
1477
  disconnect,
1302
1478
  startAdbTunnel,
1479
+ startTunnel,
1480
+ getTunnelStatus,
1481
+ stopTunnel,
1303
1482
  sendAsset,
1304
1483
  syncApp,
1305
1484
  getConnectionState,
@@ -1435,6 +1614,11 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1435
1614
  await sendRequest('terminateApp', { packageName });
1436
1615
  };
1437
1616
 
1617
+ const listApps = async (): Promise<InstalledApp[]> => {
1618
+ const result = await sendRequest('listApps', {});
1619
+ return result.apps ?? [];
1620
+ };
1621
+
1438
1622
  const watchApp = async (packageName: string, onExit: LaunchAppExitCallback): Promise<AppWatch> => {
1439
1623
  const { execId } = await withExitCallback('watch', onExit, (execId) =>
1440
1624
  sendRequest('watchApp', { packageName, execId }),
@@ -1551,8 +1735,13 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1551
1735
  await sendRequest('setWifiBandwidth', request);
1552
1736
  };
1553
1737
 
1554
- const startRecording = async (recordingOptions?: { quality?: RecordingQuality }): Promise<void> => {
1555
- const request: CommandRequestMap['startRecording'] = {};
1738
+ const startRecording = async (recordingOptions?: {
1739
+ quality?: RecordingQuality;
1740
+ persist?: PersistOption;
1741
+ }): Promise<void> => {
1742
+ const request: CommandRequestMap['startRecording'] = {
1743
+ ...persistFields(recordingOptions?.persist),
1744
+ };
1556
1745
  if (recordingOptions?.quality !== undefined) {
1557
1746
  if (
1558
1747
  !Number.isInteger(recordingOptions.quality) ||
@@ -1579,6 +1768,53 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1579
1768
  return downloadUrl;
1580
1769
  };
1581
1770
 
1771
+ const startAppLogCapture = async (captureOptions: {
1772
+ bundleId: string;
1773
+ persist?: PersistOption;
1774
+ }): Promise<void> => {
1775
+ if (!captureOptions.bundleId) {
1776
+ throw new Error('bundleId must be a non-empty string');
1777
+ }
1778
+ await sendRequest('startAppLogCapture', {
1779
+ bundleId: captureOptions.bundleId,
1780
+ ...persistFields(captureOptions.persist),
1781
+ });
1782
+ };
1783
+
1784
+ const stopAppLogCapture = async (): Promise<void> => {
1785
+ await sendRequest('stopAppLogCapture', {});
1786
+ };
1787
+
1788
+ const startEventCapture = async (captureOptions?: { persist?: PersistOption }): Promise<void> => {
1789
+ await sendRequest('startEventCapture', { ...persistFields(captureOptions?.persist) });
1790
+ };
1791
+
1792
+ const stopEventCapture = async (): Promise<void> => {
1793
+ await sendRequest('stopEventCapture', {});
1794
+ };
1795
+
1796
+ const streamAppLogCapture = (handlers: {
1797
+ onLine: (line: SessionLogLine) => void;
1798
+ onError?: (error: Error) => void;
1799
+ }): (() => void) =>
1800
+ streamSessionEntries<SessionLogLine>({
1801
+ url: `${options.apiUrl}/session/applogs/events`,
1802
+ token: options.token,
1803
+ onEntry: handlers.onLine,
1804
+ onError: handlers.onError,
1805
+ });
1806
+
1807
+ const streamEventCapture = (handlers: {
1808
+ onEvent: (event: SessionEvent) => void;
1809
+ onError?: (error: Error) => void;
1810
+ }): (() => void) =>
1811
+ streamSessionEntries<SessionEvent>({
1812
+ url: `${options.apiUrl}/session/events/events`,
1813
+ token: options.token,
1814
+ onEntry: handlers.onEvent,
1815
+ onError: handlers.onError,
1816
+ });
1817
+
1582
1818
  const keepAlive = (): void => {
1583
1819
  if (!ws || ws.readyState !== WebSocket.OPEN) {
1584
1820
  return;
@@ -1643,6 +1879,36 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1643
1879
  return tunnel;
1644
1880
  };
1645
1881
 
1882
+ const requireAdbUrl = (): string => {
1883
+ if (!options.adbUrl) {
1884
+ throw new Error('adbUrl is required to manage a destination tunnel.');
1885
+ }
1886
+ return options.adbUrl;
1887
+ };
1888
+
1889
+ const startTunnel = async (tunnelOptions: DestinationTunnelOptions): Promise<DestinationTunnel> => {
1890
+ return startDestinationTcpTunnel(deriveDestinationTunnelURL(requireAdbUrl()), options.token, {
1891
+ selectors: tunnelOptions.selectors,
1892
+ inspection: {
1893
+ enabled: true,
1894
+ captureBodies: false,
1895
+ ...(tunnelOptions.inspection ?? {}),
1896
+ },
1897
+ ...(tunnelOptions.onInspectionEvent ? { onInspectionEvent: tunnelOptions.onInspectionEvent } : {}),
1898
+ ...(tunnelOptions.onInspectionError ? { onInspectionError: tunnelOptions.onInspectionError } : {}),
1899
+ ...(tunnelOptions.window === undefined ? {} : { window: tunnelOptions.window }),
1900
+ logLevel: tunnelOptions.logLevel ?? logLevel,
1901
+ });
1902
+ };
1903
+
1904
+ const getTunnelStatus = async (): Promise<DestinationTunnelStatus> => {
1905
+ return getDestinationTunnelStatus(requireAdbUrl(), options.token);
1906
+ };
1907
+
1908
+ const stopTunnel = async (tunnelId: string): Promise<void> => {
1909
+ await stopDestinationTunnel(requireAdbUrl(), options.token, tunnelId);
1910
+ };
1911
+
1646
1912
  const sendAsset = async (url: string, timeoutMs?: number): Promise<void> => {
1647
1913
  if (!ws || ws.readyState !== WebSocket.OPEN) {
1648
1914
  return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
@@ -1,12 +1,19 @@
1
1
  import { nodeProxyTransport } from './proxy-transport';
2
2
  import { deriveDestinationTunnelStatusURL, deriveDestinationTunnelStopURL } from './destination-tunnel-url';
3
- import { validateDestinationTunnelRoutes, type DestinationTunnelRoute } from '../destination-tunnel';
3
+ import { readArray, readNonEmptyString, readRecord, readString } from './destination-tunnel-wire-reader';
4
+ import {
5
+ normalizeDestinationTunnelInspection,
6
+ type DestinationTunnelBindReport,
7
+ type DestinationTunnelInspectionConfig,
8
+ type DestinationTunnelSelectorReport,
9
+ } from '../destination-tunnel';
4
10
 
5
11
  export interface DestinationTunnelStatus {
6
12
  active?: {
7
13
  tunnelId: string;
8
14
  state: 'starting' | 'ready' | 'stopping';
9
- routes: DestinationTunnelRoute[];
15
+ selectors: DestinationTunnelSelectorReport[];
16
+ inspection: DestinationTunnelInspectionConfig;
10
17
  };
11
18
  lastFailure?: {
12
19
  tunnelId: string;
@@ -15,7 +22,7 @@ export interface DestinationTunnelStatus {
15
22
  lastDialFailure?: {
16
23
  tunnelId: string;
17
24
  connectionId: number;
18
- routeId: string;
25
+ selectorId: string;
19
26
  reason: string;
20
27
  osCode?: string;
21
28
  };
@@ -74,19 +81,67 @@ function readActiveTunnel(value: unknown): NonNullable<DestinationTunnelStatus['
74
81
  return {
75
82
  tunnelId: readNonEmptyString(active, 'tunnelId'),
76
83
  state,
77
- routes: validateDestinationTunnelRoutes(readArray(active, 'routes').map(readTunnelRoute)),
84
+ selectors: readArray(active, 'selectors').map((selector, index) =>
85
+ readSelectorReport(selector, `selector-${index + 1}`),
86
+ ),
87
+ inspection: readInspection(active),
78
88
  };
79
89
  }
80
90
 
81
- function readTunnelRoute(value: unknown): DestinationTunnelRoute {
82
- const route = readRecord(value, 'tunnel route');
83
- const port = route['port'];
84
- if (typeof port !== 'number' || !Number.isInteger(port) || port < 1 || port > 65_535) {
85
- throw new Error('tunnel route port must be an integer from 1 to 65535');
91
+ function readInspection(active: Record<string, unknown>): DestinationTunnelInspectionConfig {
92
+ const inspection = readRecord(active['inspection'], 'inspection');
93
+ const enabled = inspection['enabled'];
94
+ const captureBodies = inspection['captureBodies'];
95
+ const persist = inspection['persist'];
96
+ if (typeof enabled !== 'boolean' || typeof captureBodies !== 'boolean' || typeof persist !== 'boolean') {
97
+ throw new Error('inspection enabled, captureBodies, and persist must be booleans');
98
+ }
99
+ const maxBodyBytes = inspection['maxBodyBytes'];
100
+ const ttlSeconds = inspection['ttlSeconds'];
101
+ if (typeof maxBodyBytes !== 'number' || typeof ttlSeconds !== 'number') {
102
+ throw new Error('inspection maxBodyBytes and ttlSeconds must be numbers');
103
+ }
104
+ return normalizeDestinationTunnelInspection({
105
+ enabled,
106
+ captureBodies,
107
+ maxBodyBytes,
108
+ persist,
109
+ ttlSeconds,
110
+ });
111
+ }
112
+
113
+ function readSelectorReport(value: unknown, expectedId: string): DestinationTunnelSelectorReport {
114
+ const report = readRecord(value, 'tunnel selector');
115
+ const id = readNonEmptyString(report, 'id');
116
+ if (id !== expectedId) {
117
+ throw new Error(`invalid tunnel selector id ${id}`);
118
+ }
119
+ const kind = readString(report, 'kind');
120
+ if (kind !== 'route' && kind !== 'domain') {
121
+ throw new Error(`invalid tunnel selector kind ${kind}`);
122
+ }
123
+ return {
124
+ id,
125
+ kind,
126
+ value: readNonEmptyString(report, 'value'),
127
+ ...(report['binds'] === undefined ? {} : { binds: readArray(report, 'binds').map(readBindReport) }),
128
+ };
129
+ }
130
+
131
+ function readBindReport(value: unknown): DestinationTunnelBindReport {
132
+ const bind = readRecord(value, 'tunnel bind report');
133
+ const status = readString(bind, 'status');
134
+ if (status !== 'ok' && status !== 'conflict' && status !== 'error') {
135
+ throw new Error(`invalid tunnel bind status ${status}`);
136
+ }
137
+ const osCode = bind['osCode'];
138
+ if (osCode !== undefined && typeof osCode !== 'string') {
139
+ throw new Error('tunnel bind osCode must be a string');
86
140
  }
87
141
  return {
88
- host: readNonEmptyString(route, 'host'),
89
- port,
142
+ address: readNonEmptyString(bind, 'address'),
143
+ status,
144
+ ...(osCode === undefined ? {} : { osCode }),
90
145
  };
91
146
  }
92
147
 
@@ -116,39 +171,8 @@ function readTunnelDialFailure(value: unknown): NonNullable<DestinationTunnelSta
116
171
  return {
117
172
  tunnelId: readNonEmptyString(failure, 'tunnelId'),
118
173
  connectionId,
119
- routeId: readNonEmptyString(failure, 'routeId'),
174
+ selectorId: readNonEmptyString(failure, 'selectorId'),
120
175
  reason: readNonEmptyString(failure, 'reason'),
121
176
  ...(osCode === undefined ? {} : { osCode }),
122
177
  };
123
178
  }
124
-
125
- function readRecord(value: unknown, name: string): Record<string, unknown> {
126
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
127
- throw new Error(`${name} must be an object`);
128
- }
129
- return value as Record<string, unknown>;
130
- }
131
-
132
- function readArray(record: Record<string, unknown>, key: string): unknown[] {
133
- const value = record[key];
134
- if (!Array.isArray(value)) {
135
- throw new Error(`${key} must be an array`);
136
- }
137
- return value;
138
- }
139
-
140
- function readString(record: Record<string, unknown>, key: string): string {
141
- const value = record[key];
142
- if (typeof value !== 'string') {
143
- throw new Error(`${key} must be a string`);
144
- }
145
- return value;
146
- }
147
-
148
- function readNonEmptyString(record: Record<string, unknown>, key: string): string {
149
- const value = readString(record, key);
150
- if (value.length === 0) {
151
- throw new Error(`${key} must not be empty`);
152
- }
153
- return value;
154
- }