@limrun/api 0.48.4 → 0.49.1

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 (193) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/build-settings.d.mts.map +1 -1
  3. package/build-settings.d.ts.map +1 -1
  4. package/build-settings.js +4 -5
  5. package/build-settings.js.map +1 -1
  6. package/build-settings.mjs +4 -5
  7. package/build-settings.mjs.map +1 -1
  8. package/client.d.mts +8 -5
  9. package/client.d.mts.map +1 -1
  10. package/client.d.ts +8 -5
  11. package/client.d.ts.map +1 -1
  12. package/client.js +4 -4
  13. package/client.js.map +1 -1
  14. package/client.mjs +2 -2
  15. package/client.mjs.map +1 -1
  16. package/destination-tunnel-dialer.d.mts +30 -2
  17. package/destination-tunnel-dialer.d.mts.map +1 -1
  18. package/destination-tunnel-dialer.d.ts +30 -2
  19. package/destination-tunnel-dialer.d.ts.map +1 -1
  20. package/destination-tunnel-dialer.js +349 -65
  21. package/destination-tunnel-dialer.js.map +1 -1
  22. package/destination-tunnel-dialer.mjs +345 -64
  23. package/destination-tunnel-dialer.mjs.map +1 -1
  24. package/destination-tunnel-har.d.mts +23 -0
  25. package/destination-tunnel-har.d.mts.map +1 -0
  26. package/destination-tunnel-har.d.ts +23 -0
  27. package/destination-tunnel-har.d.ts.map +1 -0
  28. package/destination-tunnel-har.js +164 -0
  29. package/destination-tunnel-har.js.map +1 -0
  30. package/destination-tunnel-har.mjs +160 -0
  31. package/destination-tunnel-har.mjs.map +1 -0
  32. package/destination-tunnel-inspection.d.mts +71 -0
  33. package/destination-tunnel-inspection.d.mts.map +1 -0
  34. package/destination-tunnel-inspection.d.ts +71 -0
  35. package/destination-tunnel-inspection.d.ts.map +1 -0
  36. package/destination-tunnel-inspection.js +267 -0
  37. package/destination-tunnel-inspection.js.map +1 -0
  38. package/destination-tunnel-inspection.mjs +261 -0
  39. package/destination-tunnel-inspection.mjs.map +1 -0
  40. package/destination-tunnel.d.mts +119 -14
  41. package/destination-tunnel.d.mts.map +1 -1
  42. package/destination-tunnel.d.ts +119 -14
  43. package/destination-tunnel.d.ts.map +1 -1
  44. package/destination-tunnel.js +407 -98
  45. package/destination-tunnel.js.map +1 -1
  46. package/destination-tunnel.mjs +374 -73
  47. package/destination-tunnel.mjs.map +1 -1
  48. package/exec-client.d.mts +9 -0
  49. package/exec-client.d.mts.map +1 -1
  50. package/exec-client.d.ts +9 -0
  51. package/exec-client.d.ts.map +1 -1
  52. package/exec-client.js.map +1 -1
  53. package/exec-client.mjs.map +1 -1
  54. package/index.d.mts +8 -1
  55. package/index.d.mts.map +1 -1
  56. package/index.d.ts +8 -1
  57. package/index.d.ts.map +1 -1
  58. package/index.js +26 -2
  59. package/index.js.map +1 -1
  60. package/index.mjs +7 -1
  61. package/index.mjs.map +1 -1
  62. package/instance-client.d.mts +107 -1
  63. package/instance-client.d.mts.map +1 -1
  64. package/instance-client.d.ts +107 -1
  65. package/instance-client.d.ts.map +1 -1
  66. package/instance-client.js +83 -1
  67. package/instance-client.js.map +1 -1
  68. package/instance-client.mjs +83 -1
  69. package/instance-client.mjs.map +1 -1
  70. package/internal/destination-tunnel-management.d.mts +4 -3
  71. package/internal/destination-tunnel-management.d.mts.map +1 -1
  72. package/internal/destination-tunnel-management.d.ts +4 -3
  73. package/internal/destination-tunnel-management.d.ts.map +1 -1
  74. package/internal/destination-tunnel-management.js +64 -46
  75. package/internal/destination-tunnel-management.js.map +1 -1
  76. package/internal/destination-tunnel-management.mjs +55 -37
  77. package/internal/destination-tunnel-management.mjs.map +1 -1
  78. package/internal/destination-tunnel-url.d.mts +2 -0
  79. package/internal/destination-tunnel-url.d.mts.map +1 -1
  80. package/internal/destination-tunnel-url.d.ts +2 -0
  81. package/internal/destination-tunnel-url.d.ts.map +1 -1
  82. package/internal/destination-tunnel-url.js +45 -5
  83. package/internal/destination-tunnel-url.js.map +1 -1
  84. package/internal/destination-tunnel-url.mjs +44 -5
  85. package/internal/destination-tunnel-url.mjs.map +1 -1
  86. package/internal/destination-tunnel-wire-reader.d.mts +23 -0
  87. package/internal/destination-tunnel-wire-reader.d.mts.map +1 -0
  88. package/internal/destination-tunnel-wire-reader.d.ts +23 -0
  89. package/internal/destination-tunnel-wire-reader.d.ts.map +1 -0
  90. package/internal/destination-tunnel-wire-reader.js +110 -0
  91. package/internal/destination-tunnel-wire-reader.js.map +1 -0
  92. package/internal/destination-tunnel-wire-reader.mjs +93 -0
  93. package/internal/destination-tunnel-wire-reader.mjs.map +1 -0
  94. package/internal/persist-option.d.mts +14 -0
  95. package/internal/persist-option.d.mts.map +1 -0
  96. package/internal/persist-option.d.ts +14 -0
  97. package/internal/persist-option.d.ts.map +1 -0
  98. package/internal/persist-option.js +18 -0
  99. package/internal/persist-option.js.map +1 -0
  100. package/internal/persist-option.mjs +15 -0
  101. package/internal/persist-option.mjs.map +1 -0
  102. package/internal/proxy-transport.d.mts +27 -0
  103. package/internal/proxy-transport.d.mts.map +1 -1
  104. package/internal/proxy-transport.d.ts +27 -0
  105. package/internal/proxy-transport.d.ts.map +1 -1
  106. package/internal/proxy-transport.js +206 -0
  107. package/internal/proxy-transport.js.map +1 -1
  108. package/internal/proxy-transport.mjs +205 -0
  109. package/internal/proxy-transport.mjs.map +1 -1
  110. package/internal/session-stream.d.mts +39 -0
  111. package/internal/session-stream.d.mts.map +1 -0
  112. package/internal/session-stream.d.ts +39 -0
  113. package/internal/session-stream.d.ts.map +1 -0
  114. package/internal/session-stream.js +144 -0
  115. package/internal/session-stream.js.map +1 -0
  116. package/internal/session-stream.mjs +139 -0
  117. package/internal/session-stream.mjs.map +1 -0
  118. package/internal/tslib.js +4 -4
  119. package/ios-client.d.mts +63 -4
  120. package/ios-client.d.mts.map +1 -1
  121. package/ios-client.d.ts +63 -4
  122. package/ios-client.d.ts.map +1 -1
  123. package/ios-client.js +48 -2
  124. package/ios-client.js.map +1 -1
  125. package/ios-client.mjs +48 -2
  126. package/ios-client.mjs.map +1 -1
  127. package/package.json +22 -1
  128. package/resources/android-instances-helpers.d.mts +25 -0
  129. package/resources/android-instances-helpers.d.mts.map +1 -0
  130. package/resources/android-instances-helpers.d.ts +25 -0
  131. package/resources/android-instances-helpers.d.ts.map +1 -0
  132. package/resources/android-instances-helpers.js +37 -0
  133. package/resources/android-instances-helpers.js.map +1 -0
  134. package/resources/android-instances-helpers.mjs +33 -0
  135. package/resources/android-instances-helpers.mjs.map +1 -0
  136. package/resources/index.d.mts +6 -3
  137. package/resources/index.d.mts.map +1 -1
  138. package/resources/index.d.ts +6 -3
  139. package/resources/index.d.ts.map +1 -1
  140. package/resources/index.js +6 -5
  141. package/resources/index.js.map +1 -1
  142. package/resources/index.mjs +3 -3
  143. package/resources/index.mjs.map +1 -1
  144. package/resources/ios-instances-helpers.d.mts +25 -0
  145. package/resources/ios-instances-helpers.d.mts.map +1 -0
  146. package/resources/ios-instances-helpers.d.ts +25 -0
  147. package/resources/ios-instances-helpers.d.ts.map +1 -0
  148. package/resources/ios-instances-helpers.js +37 -0
  149. package/resources/ios-instances-helpers.js.map +1 -0
  150. package/resources/ios-instances-helpers.mjs +33 -0
  151. package/resources/ios-instances-helpers.mjs.map +1 -0
  152. package/resources/session-artifacts.d.mts +70 -0
  153. package/resources/session-artifacts.d.mts.map +1 -0
  154. package/resources/session-artifacts.d.ts +70 -0
  155. package/resources/session-artifacts.d.ts.map +1 -0
  156. package/resources/session-artifacts.js +8 -0
  157. package/resources/session-artifacts.js.map +1 -0
  158. package/resources/session-artifacts.mjs +7 -0
  159. package/resources/session-artifacts.mjs.map +1 -0
  160. package/resources/xcode-instances-helpers.d.mts +55 -0
  161. package/resources/xcode-instances-helpers.d.mts.map +1 -1
  162. package/resources/xcode-instances-helpers.d.ts +55 -0
  163. package/resources/xcode-instances-helpers.d.ts.map +1 -1
  164. package/resources/xcode-instances-helpers.js +49 -3
  165. package/resources/xcode-instances-helpers.js.map +1 -1
  166. package/resources/xcode-instances-helpers.mjs +47 -2
  167. package/resources/xcode-instances-helpers.mjs.map +1 -1
  168. package/src/build-settings.ts +4 -5
  169. package/src/client.ts +12 -2
  170. package/src/destination-tunnel-dialer.ts +403 -64
  171. package/src/destination-tunnel-har.ts +200 -0
  172. package/src/destination-tunnel-inspection.ts +367 -0
  173. package/src/destination-tunnel.ts +510 -88
  174. package/src/exec-client.ts +9 -0
  175. package/src/index.ts +58 -0
  176. package/src/instance-client.ts +273 -5
  177. package/src/internal/destination-tunnel-management.ts +67 -43
  178. package/src/internal/destination-tunnel-url.ts +61 -5
  179. package/src/internal/destination-tunnel-wire-reader.ts +109 -0
  180. package/src/internal/persist-option.ts +26 -0
  181. package/src/internal/proxy-transport.ts +248 -0
  182. package/src/internal/session-stream.ts +157 -0
  183. package/src/ios-client.ts +129 -8
  184. package/src/resources/android-instances-helpers.ts +45 -0
  185. package/src/resources/index.ts +14 -2
  186. package/src/resources/ios-instances-helpers.ts +45 -0
  187. package/src/resources/session-artifacts.ts +95 -0
  188. package/src/resources/xcode-instances-helpers.ts +116 -5
  189. package/src/version.ts +1 -1
  190. package/version.d.mts +1 -1
  191. package/version.d.ts +1 -1
  192. package/version.js +1 -1
  193. package/version.mjs +1 -1
@@ -146,6 +146,15 @@ export type WebhookConfig = {
146
146
  * Transfer-Encoding, Connection) are rejected.
147
147
  */
148
148
  headers?: Record<string, string>;
149
+ /**
150
+ * Caller-defined key/value pairs echoed verbatim in the payload's `labels`
151
+ * field, so the receiver can correlate the callback with its own context
152
+ * (a pipeline run, a commit, a ticket) without keeping a lookup table
153
+ * keyed by execId. At most 32 entries; keys and values are printable
154
+ * ASCII of at most 64 characters each, and keys must not be empty. Older
155
+ * build daemons ignore it.
156
+ */
157
+ labels?: Record<string, string>;
149
158
  };
150
159
 
151
160
  /** Android ABIs the gradle daemon accepts; 'all' keeps the project's own configuration. */
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,
@@ -59,8 +103,13 @@ export {
59
103
  type RbeBuildSummary,
60
104
  type Tunnel,
61
105
  RbeUnsupportedError,
106
+ XcodeSelectionUnsupportedError,
62
107
  deriveRbeTunnelUrl,
63
108
  DEFAULT_RBE_TUNNEL_PORT,
109
+ type XcodeInfo,
110
+ type XcodeStatus,
111
+ type XcodeSelectResult,
112
+ type SandboxInfo,
64
113
  type XcodeProjectConfig,
65
114
  type XcodeBuildOptions,
66
115
  type XcodeRunOptions,
@@ -87,6 +136,14 @@ export {
87
136
  type GradleBuildLog,
88
137
  } from './resources/gradle-instances-helpers';
89
138
  export { type AssetUploadOptions } from './resources/daemon-client-shared';
139
+ export {
140
+ type SessionArtifact,
141
+ type SessionArtifactKind,
142
+ type SessionLogLine,
143
+ type SessionEvent,
144
+ type SessionEventType,
145
+ } from './resources/session-artifacts';
146
+ export { SessionStreamLostError } from './internal/session-stream';
90
147
  export {
91
148
  LIMRUN_DIR,
92
149
  TRY_IMPORT_LINE,
@@ -143,3 +200,4 @@ export {
143
200
  PermissionDeniedError,
144
201
  UnprocessableEntityError,
145
202
  } from './core/error';
203
+ export { isDirectInstanceHttpError, type DirectInstanceHttpError } from './internal/direct-instance-errors';
@@ -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,20 @@ 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
+ /** Install time as epoch milliseconds; absent when the runtime cannot tell. */
633
+ installedAt?: number;
634
+ };
635
+
520
636
  /** Handle for an app watch registered via {@link InstanceClient.watchApp}. */
521
637
  export type AppWatch = {
522
638
  /** Identifier of this watch on the server. */
@@ -703,6 +819,13 @@ type WatchAppResultMessage = {
703
819
  error?: CommandError;
704
820
  };
705
821
 
822
+ type ListAppsResultMessage = {
823
+ type: 'listAppsResult';
824
+ id: string;
825
+ payload?: { apps?: InstalledApp[] };
826
+ error?: CommandError;
827
+ };
828
+
706
829
  type UnwatchAppResultMessage = {
707
830
  type: 'unwatchAppResult';
708
831
  id: string;
@@ -770,6 +893,34 @@ type StopVideoRecordingResultMessage = {
770
893
  error?: CommandError;
771
894
  };
772
895
 
896
+ type StartAppLogCaptureResultMessage = {
897
+ type: 'startAppLogCaptureResult';
898
+ id: string;
899
+ payload?: EmptyCommandResult;
900
+ error?: CommandError;
901
+ };
902
+
903
+ type StopAppLogCaptureResultMessage = {
904
+ type: 'stopAppLogCaptureResult';
905
+ id: string;
906
+ payload?: EmptyCommandResult;
907
+ error?: CommandError;
908
+ };
909
+
910
+ type StartEventCaptureResultMessage = {
911
+ type: 'startEventCaptureResult';
912
+ id: string;
913
+ payload?: EmptyCommandResult;
914
+ error?: CommandError;
915
+ };
916
+
917
+ type StopEventCaptureResultMessage = {
918
+ type: 'stopEventCaptureResult';
919
+ id: string;
920
+ payload?: EmptyCommandResult;
921
+ error?: CommandError;
922
+ };
923
+
773
924
  type KnownCommandResultMessage =
774
925
  | ScreenshotResultMessage
775
926
  | GetElementTreeResultMessage
@@ -784,12 +935,17 @@ type KnownCommandResultMessage =
784
935
  | TerminateAppResultMessage
785
936
  | WatchAppResultMessage
786
937
  | UnwatchAppResultMessage
938
+ | ListAppsResultMessage
787
939
  | PlayOnMicrophoneResultMessage
788
940
  | CameraControlResultMessage
789
941
  | AdbShellResultMessage
790
942
  | SetWifiBandwidthResultMessage
791
943
  | StartVideoRecordingResultMessage
792
- | StopVideoRecordingResultMessage;
944
+ | StopVideoRecordingResultMessage
945
+ | StartAppLogCaptureResultMessage
946
+ | StopAppLogCaptureResultMessage
947
+ | StartEventCaptureResultMessage
948
+ | StopEventCaptureResultMessage;
793
949
 
794
950
  type ServerMessage =
795
951
  | ScreenshotResponse
@@ -812,12 +968,17 @@ type CommandRequestMap = {
812
968
  terminateApp: { packageName: string };
813
969
  watchApp: { packageName: string; execId: string };
814
970
  unwatchApp: { execId: string };
971
+ listApps: {};
815
972
  playOnMicrophone: { path: string; once?: boolean };
816
973
  cameraControl: { action: 'setSource'; source: 'video'; arg: string; loop?: boolean } | { action: 'reset' };
817
974
  adbShell: { command: string; timeoutMs?: number };
818
975
  setWifiBandwidth: WifiBandwidthOptions;
819
- startRecording: { quality?: RecordingQuality };
976
+ startRecording: { quality?: RecordingQuality; persist?: boolean; ttlSeconds?: number };
820
977
  stopRecording: { upload?: { presignedUrl: string } };
978
+ startAppLogCapture: { bundleId: string; persist?: boolean; ttlSeconds?: number };
979
+ stopAppLogCapture: {};
980
+ startEventCapture: { persist?: boolean; ttlSeconds?: number };
981
+ stopEventCapture: {};
821
982
  };
822
983
 
823
984
  type CommandResultMap = {
@@ -834,12 +995,17 @@ type CommandResultMap = {
834
995
  terminateApp: { packageName?: string };
835
996
  watchApp: { packageName?: string };
836
997
  unwatchApp: EmptyCommandResult;
998
+ listApps: { apps?: InstalledApp[] };
837
999
  playOnMicrophone: PlayOnMicrophoneResult;
838
1000
  cameraControl: EmptyCommandResult;
839
1001
  adbShell: AdbShellResultMessage;
840
1002
  setWifiBandwidth: EmptyCommandResult;
841
1003
  startRecording: EmptyCommandResult;
842
1004
  stopRecording: EmptyCommandResult;
1005
+ startAppLogCapture: EmptyCommandResult;
1006
+ stopAppLogCapture: EmptyCommandResult;
1007
+ startEventCapture: EmptyCommandResult;
1008
+ stopEventCapture: EmptyCommandResult;
843
1009
  };
844
1010
 
845
1011
  type PendingRequest<T> = {
@@ -1015,12 +1181,17 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1015
1181
  case 'terminateAppResult':
1016
1182
  case 'watchAppResult':
1017
1183
  case 'unwatchAppResult':
1184
+ case 'listAppsResult':
1018
1185
  case 'playOnMicrophoneResult':
1019
1186
  case 'cameraControlResult':
1020
1187
  case 'adbShellResult':
1021
1188
  case 'setWifiBandwidthResult':
1022
1189
  case 'startRecordingResult':
1023
1190
  case 'stopRecordingResult':
1191
+ case 'startAppLogCaptureResult':
1192
+ case 'stopAppLogCaptureResult':
1193
+ case 'startEventCaptureResult':
1194
+ case 'stopEventCaptureResult':
1024
1195
  return 'id' in message && typeof message.id === 'string';
1025
1196
  default:
1026
1197
  return false;
@@ -1287,6 +1458,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1287
1458
  openUrl,
1288
1459
  launchApp,
1289
1460
  terminateApp,
1461
+ listApps,
1290
1462
  watchApp,
1291
1463
  playOnMicrophone,
1292
1464
  pushFile,
@@ -1297,9 +1469,18 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1297
1469
  setWifiBandwidth,
1298
1470
  startRecording,
1299
1471
  stopRecording,
1472
+ startAppLogCapture,
1473
+ stopAppLogCapture,
1474
+ startEventCapture,
1475
+ stopEventCapture,
1476
+ streamAppLogCapture,
1477
+ streamEventCapture,
1300
1478
  keepAlive,
1301
1479
  disconnect,
1302
1480
  startAdbTunnel,
1481
+ startTunnel,
1482
+ getTunnelStatus,
1483
+ stopTunnel,
1303
1484
  sendAsset,
1304
1485
  syncApp,
1305
1486
  getConnectionState,
@@ -1435,6 +1616,11 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1435
1616
  await sendRequest('terminateApp', { packageName });
1436
1617
  };
1437
1618
 
1619
+ const listApps = async (): Promise<InstalledApp[]> => {
1620
+ const result = await sendRequest('listApps', {});
1621
+ return result.apps ?? [];
1622
+ };
1623
+
1438
1624
  const watchApp = async (packageName: string, onExit: LaunchAppExitCallback): Promise<AppWatch> => {
1439
1625
  const { execId } = await withExitCallback('watch', onExit, (execId) =>
1440
1626
  sendRequest('watchApp', { packageName, execId }),
@@ -1551,8 +1737,13 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1551
1737
  await sendRequest('setWifiBandwidth', request);
1552
1738
  };
1553
1739
 
1554
- const startRecording = async (recordingOptions?: { quality?: RecordingQuality }): Promise<void> => {
1555
- const request: CommandRequestMap['startRecording'] = {};
1740
+ const startRecording = async (recordingOptions?: {
1741
+ quality?: RecordingQuality;
1742
+ persist?: PersistOption;
1743
+ }): Promise<void> => {
1744
+ const request: CommandRequestMap['startRecording'] = {
1745
+ ...persistFields(recordingOptions?.persist),
1746
+ };
1556
1747
  if (recordingOptions?.quality !== undefined) {
1557
1748
  if (
1558
1749
  !Number.isInteger(recordingOptions.quality) ||
@@ -1579,6 +1770,53 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1579
1770
  return downloadUrl;
1580
1771
  };
1581
1772
 
1773
+ const startAppLogCapture = async (captureOptions: {
1774
+ bundleId: string;
1775
+ persist?: PersistOption;
1776
+ }): Promise<void> => {
1777
+ if (!captureOptions.bundleId) {
1778
+ throw new Error('bundleId must be a non-empty string');
1779
+ }
1780
+ await sendRequest('startAppLogCapture', {
1781
+ bundleId: captureOptions.bundleId,
1782
+ ...persistFields(captureOptions.persist),
1783
+ });
1784
+ };
1785
+
1786
+ const stopAppLogCapture = async (): Promise<void> => {
1787
+ await sendRequest('stopAppLogCapture', {});
1788
+ };
1789
+
1790
+ const startEventCapture = async (captureOptions?: { persist?: PersistOption }): Promise<void> => {
1791
+ await sendRequest('startEventCapture', { ...persistFields(captureOptions?.persist) });
1792
+ };
1793
+
1794
+ const stopEventCapture = async (): Promise<void> => {
1795
+ await sendRequest('stopEventCapture', {});
1796
+ };
1797
+
1798
+ const streamAppLogCapture = (handlers: {
1799
+ onLine: (line: SessionLogLine) => void;
1800
+ onError?: (error: Error) => void;
1801
+ }): (() => void) =>
1802
+ streamSessionEntries<SessionLogLine>({
1803
+ url: `${options.apiUrl}/session/applogs/events`,
1804
+ token: options.token,
1805
+ onEntry: handlers.onLine,
1806
+ onError: handlers.onError,
1807
+ });
1808
+
1809
+ const streamEventCapture = (handlers: {
1810
+ onEvent: (event: SessionEvent) => void;
1811
+ onError?: (error: Error) => void;
1812
+ }): (() => void) =>
1813
+ streamSessionEntries<SessionEvent>({
1814
+ url: `${options.apiUrl}/session/events/events`,
1815
+ token: options.token,
1816
+ onEntry: handlers.onEvent,
1817
+ onError: handlers.onError,
1818
+ });
1819
+
1582
1820
  const keepAlive = (): void => {
1583
1821
  if (!ws || ws.readyState !== WebSocket.OPEN) {
1584
1822
  return;
@@ -1643,6 +1881,36 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1643
1881
  return tunnel;
1644
1882
  };
1645
1883
 
1884
+ const requireAdbUrl = (): string => {
1885
+ if (!options.adbUrl) {
1886
+ throw new Error('adbUrl is required to manage a destination tunnel.');
1887
+ }
1888
+ return options.adbUrl;
1889
+ };
1890
+
1891
+ const startTunnel = async (tunnelOptions: DestinationTunnelOptions): Promise<DestinationTunnel> => {
1892
+ return startDestinationTcpTunnel(deriveDestinationTunnelURL(requireAdbUrl()), options.token, {
1893
+ selectors: tunnelOptions.selectors,
1894
+ inspection: {
1895
+ enabled: true,
1896
+ captureBodies: false,
1897
+ ...(tunnelOptions.inspection ?? {}),
1898
+ },
1899
+ ...(tunnelOptions.onInspectionEvent ? { onInspectionEvent: tunnelOptions.onInspectionEvent } : {}),
1900
+ ...(tunnelOptions.onInspectionError ? { onInspectionError: tunnelOptions.onInspectionError } : {}),
1901
+ ...(tunnelOptions.window === undefined ? {} : { window: tunnelOptions.window }),
1902
+ logLevel: tunnelOptions.logLevel ?? logLevel,
1903
+ });
1904
+ };
1905
+
1906
+ const getTunnelStatus = async (): Promise<DestinationTunnelStatus> => {
1907
+ return getDestinationTunnelStatus(requireAdbUrl(), options.token);
1908
+ };
1909
+
1910
+ const stopTunnel = async (tunnelId: string): Promise<void> => {
1911
+ await stopDestinationTunnel(requireAdbUrl(), options.token, tunnelId);
1912
+ };
1913
+
1646
1914
  const sendAsset = async (url: string, timeoutMs?: number): Promise<void> => {
1647
1915
  if (!ws || ws.readyState !== WebSocket.OPEN) {
1648
1916
  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
- }