@limrun/api 0.5.2 → 0.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +27 -103
  2. package/client.d.mts +7 -6
  3. package/client.d.mts.map +1 -1
  4. package/client.d.ts +7 -6
  5. package/client.d.ts.map +1 -1
  6. package/client.js +14 -8
  7. package/client.js.map +1 -1
  8. package/client.mjs +13 -7
  9. package/client.mjs.map +1 -1
  10. package/index.d.mts +0 -1
  11. package/index.d.ts +0 -1
  12. package/index.js +0 -2
  13. package/index.js.map +1 -1
  14. package/index.mjs +0 -1
  15. package/package.json +1 -4
  16. package/resources/android-instances-helpers.d.mts +4 -28
  17. package/resources/android-instances-helpers.d.mts.map +1 -1
  18. package/resources/android-instances-helpers.d.ts +4 -28
  19. package/resources/android-instances-helpers.d.ts.map +1 -1
  20. package/resources/android-instances-helpers.js +18 -124
  21. package/resources/android-instances-helpers.js.map +1 -1
  22. package/resources/android-instances-helpers.mjs +16 -120
  23. package/resources/android-instances-helpers.mjs.map +1 -1
  24. package/resources/android-instances.d.mts +4 -7
  25. package/resources/android-instances.d.mts.map +1 -1
  26. package/resources/android-instances.d.ts +4 -7
  27. package/resources/android-instances.d.ts.map +1 -1
  28. package/resources/android-instances.js.map +1 -1
  29. package/resources/android-instances.mjs.map +1 -1
  30. package/resources/index.d.mts +2 -1
  31. package/resources/index.d.mts.map +1 -1
  32. package/resources/index.d.ts +2 -1
  33. package/resources/index.d.ts.map +1 -1
  34. package/resources/index.js +2 -2
  35. package/resources/index.js.map +1 -1
  36. package/resources/index.mjs +1 -1
  37. package/resources/index.mjs.map +1 -1
  38. package/resources/instance-client.d.mts +57 -0
  39. package/resources/instance-client.d.mts.map +1 -0
  40. package/resources/instance-client.d.ts +57 -0
  41. package/resources/instance-client.d.ts.map +1 -0
  42. package/resources/instance-client.js +221 -0
  43. package/resources/instance-client.js.map +1 -0
  44. package/resources/instance-client.mjs +218 -0
  45. package/resources/instance-client.mjs.map +1 -0
  46. package/resources/tunnel.d.mts +25 -0
  47. package/resources/tunnel.d.mts.map +1 -0
  48. package/resources/tunnel.d.ts +25 -0
  49. package/resources/tunnel.d.ts.map +1 -0
  50. package/resources/tunnel.js +102 -0
  51. package/resources/tunnel.js.map +1 -0
  52. package/resources/tunnel.mjs +98 -0
  53. package/resources/tunnel.mjs.map +1 -0
  54. package/src/client.ts +19 -15
  55. package/src/index.ts +0 -2
  56. package/src/resources/android-instances-helpers.ts +25 -157
  57. package/src/resources/android-instances.ts +3 -8
  58. package/src/resources/index.ts +2 -2
  59. package/src/resources/instance-client.ts +350 -0
  60. package/src/resources/tunnel.ts +126 -0
  61. package/src/version.ts +1 -1
  62. package/version.d.mts +1 -1
  63. package/version.d.ts +1 -1
  64. package/version.js +1 -1
  65. package/version.mjs +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel.d.mts","sourceRoot":"","sources":["../src/resources/tunnel.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,CA6FjB"}
@@ -0,0 +1,25 @@
1
+ export interface Tunnel {
2
+ address: {
3
+ address: string;
4
+ port: number;
5
+ };
6
+ close: () => void;
7
+ }
8
+ /**
9
+ * Starts a one-shot TCP → WebSocket proxy.
10
+ *
11
+ * The function creates a local TCP server that listens on an ephemeral port on
12
+ * 127.0.0.1. As soon as the **first** TCP client connects the server stops
13
+ * accepting further connections and forwards all traffic between that client
14
+ * and `remoteURL` through an authenticated WebSocket. If you need to proxy
15
+ * more than one TCP connection, call `startTcpTunnel` again to create a new
16
+ * proxy instance.
17
+ *
18
+ * @param remoteURL Remote WebSocket endpoint (e.g. wss://example.com/instance)
19
+ * @param token Bearer token sent as `Authorization` header
20
+ * @param hostname Optional IP address to listen on. Default is 127.0.0.1
21
+ * @param port Optional port number to listen on. Default is to ask Node.js
22
+ * to find an available non-privileged port.
23
+ */
24
+ export declare function startTcpTunnel(remoteURL: string, token: string, hostname: string, port: number): Promise<Tunnel>;
25
+ //# sourceMappingURL=tunnel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../src/resources/tunnel.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,CA6FjB"}
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startTcpTunnel = startTcpTunnel;
4
+ const tslib_1 = require("../internal/tslib.js");
5
+ const net = tslib_1.__importStar(require("net"));
6
+ const ws_1 = require("ws");
7
+ /**
8
+ * Starts a one-shot TCP → WebSocket proxy.
9
+ *
10
+ * The function creates a local TCP server that listens on an ephemeral port on
11
+ * 127.0.0.1. As soon as the **first** TCP client connects the server stops
12
+ * accepting further connections and forwards all traffic between that client
13
+ * and `remoteURL` through an authenticated WebSocket. If you need to proxy
14
+ * more than one TCP connection, call `startTcpTunnel` again to create a new
15
+ * proxy instance.
16
+ *
17
+ * @param remoteURL Remote WebSocket endpoint (e.g. wss://example.com/instance)
18
+ * @param token Bearer token sent as `Authorization` header
19
+ * @param hostname Optional IP address to listen on. Default is 127.0.0.1
20
+ * @param port Optional port number to listen on. Default is to ask Node.js
21
+ * to find an available non-privileged port.
22
+ */
23
+ async function startTcpTunnel(remoteURL, token, hostname, port) {
24
+ return new Promise((resolve, reject) => {
25
+ const server = net.createServer();
26
+ let ws;
27
+ let pingInterval;
28
+ // close helper
29
+ const close = () => {
30
+ if (pingInterval) {
31
+ clearInterval(pingInterval);
32
+ pingInterval = undefined;
33
+ }
34
+ if (ws && ws.readyState === ws_1.WebSocket.OPEN) {
35
+ ws.close(1000, 'close');
36
+ }
37
+ if (server.listening) {
38
+ server.close();
39
+ }
40
+ };
41
+ // No AbortController support – proxy can be closed via the returned handle
42
+ // TCP server error
43
+ server.once('error', (err) => {
44
+ close();
45
+ reject(new Error(`TCP server error: ${err.message}`));
46
+ });
47
+ // Listening
48
+ server.once('listening', () => {
49
+ const address = server.address();
50
+ if (!address || typeof address === 'string') {
51
+ close();
52
+ return reject(new Error('Failed to obtain listening address'));
53
+ }
54
+ resolve({ address, close });
55
+ });
56
+ // On first TCP connection
57
+ server.on('connection', (tcpSocket) => {
58
+ // Single-connection proxy
59
+ server.close();
60
+ ws = new ws_1.WebSocket(remoteURL, {
61
+ headers: { Authorization: `Bearer ${token}` },
62
+ perMessageDeflate: false,
63
+ });
64
+ // WebSocket error
65
+ ws.once('error', (err) => {
66
+ console.error('WebSocket error:', err);
67
+ tcpSocket.destroy();
68
+ close();
69
+ });
70
+ ws.once('open', () => {
71
+ const socket = ws; // non-undefined after open
72
+ pingInterval = setInterval(() => {
73
+ if (socket.readyState === ws_1.WebSocket.OPEN) {
74
+ socket.ping();
75
+ }
76
+ }, 30000);
77
+ // TCP → WS
78
+ tcpSocket.on('data', (chunk) => {
79
+ if (socket.readyState === ws_1.WebSocket.OPEN) {
80
+ socket.send(chunk);
81
+ }
82
+ });
83
+ // WS → TCP
84
+ socket.on('message', (data) => {
85
+ if (!tcpSocket.destroyed) {
86
+ tcpSocket.write(data);
87
+ }
88
+ });
89
+ });
90
+ // Mutual close
91
+ tcpSocket.on('close', close);
92
+ tcpSocket.on('error', (err) => {
93
+ console.error('TCP socket error:', err);
94
+ close();
95
+ });
96
+ ws.on('close', () => tcpSocket.destroy());
97
+ });
98
+ // Start listening
99
+ server.listen(port, hostname);
100
+ });
101
+ }
102
+ //# sourceMappingURL=tunnel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel.js","sourceRoot":"","sources":["../src/resources/tunnel.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,98 @@
1
+ import * as net from 'net';
2
+ import { WebSocket } from 'ws';
3
+ /**
4
+ * Starts a one-shot TCP → WebSocket proxy.
5
+ *
6
+ * The function creates a local TCP server that listens on an ephemeral port on
7
+ * 127.0.0.1. As soon as the **first** TCP client connects the server stops
8
+ * accepting further connections and forwards all traffic between that client
9
+ * and `remoteURL` through an authenticated WebSocket. If you need to proxy
10
+ * more than one TCP connection, call `startTcpTunnel` again to create a new
11
+ * proxy instance.
12
+ *
13
+ * @param remoteURL Remote WebSocket endpoint (e.g. wss://example.com/instance)
14
+ * @param token Bearer token sent as `Authorization` header
15
+ * @param hostname Optional IP address to listen on. Default is 127.0.0.1
16
+ * @param port Optional port number to listen on. Default is to ask Node.js
17
+ * to find an available non-privileged port.
18
+ */
19
+ export async function startTcpTunnel(remoteURL, token, hostname, port) {
20
+ return new Promise((resolve, reject) => {
21
+ const server = net.createServer();
22
+ let ws;
23
+ let pingInterval;
24
+ // close helper
25
+ const close = () => {
26
+ if (pingInterval) {
27
+ clearInterval(pingInterval);
28
+ pingInterval = undefined;
29
+ }
30
+ if (ws && ws.readyState === WebSocket.OPEN) {
31
+ ws.close(1000, 'close');
32
+ }
33
+ if (server.listening) {
34
+ server.close();
35
+ }
36
+ };
37
+ // No AbortController support – proxy can be closed via the returned handle
38
+ // TCP server error
39
+ server.once('error', (err) => {
40
+ close();
41
+ reject(new Error(`TCP server error: ${err.message}`));
42
+ });
43
+ // Listening
44
+ server.once('listening', () => {
45
+ const address = server.address();
46
+ if (!address || typeof address === 'string') {
47
+ close();
48
+ return reject(new Error('Failed to obtain listening address'));
49
+ }
50
+ resolve({ address, close });
51
+ });
52
+ // On first TCP connection
53
+ server.on('connection', (tcpSocket) => {
54
+ // Single-connection proxy
55
+ server.close();
56
+ ws = new WebSocket(remoteURL, {
57
+ headers: { Authorization: `Bearer ${token}` },
58
+ perMessageDeflate: false,
59
+ });
60
+ // WebSocket error
61
+ ws.once('error', (err) => {
62
+ console.error('WebSocket error:', err);
63
+ tcpSocket.destroy();
64
+ close();
65
+ });
66
+ ws.once('open', () => {
67
+ const socket = ws; // non-undefined after open
68
+ pingInterval = setInterval(() => {
69
+ if (socket.readyState === WebSocket.OPEN) {
70
+ socket.ping();
71
+ }
72
+ }, 30000);
73
+ // TCP → WS
74
+ tcpSocket.on('data', (chunk) => {
75
+ if (socket.readyState === WebSocket.OPEN) {
76
+ socket.send(chunk);
77
+ }
78
+ });
79
+ // WS → TCP
80
+ socket.on('message', (data) => {
81
+ if (!tcpSocket.destroyed) {
82
+ tcpSocket.write(data);
83
+ }
84
+ });
85
+ });
86
+ // Mutual close
87
+ tcpSocket.on('close', close);
88
+ tcpSocket.on('error', (err) => {
89
+ console.error('TCP socket error:', err);
90
+ close();
91
+ });
92
+ ws.on('close', () => tcpSocket.destroy());
93
+ });
94
+ // Start listening
95
+ server.listen(port, hostname);
96
+ });
97
+ }
98
+ //# sourceMappingURL=tunnel.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel.mjs","sourceRoot":"","sources":["../src/resources/tunnel.ts"],"names":[],"mappings":"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"}
package/src/client.ts CHANGED
@@ -21,17 +21,16 @@ import {
21
21
  AndroidInstanceCreateParams,
22
22
  AndroidInstanceListParams,
23
23
  AndroidInstanceListResponse,
24
- AndroidInstanceState,
25
- AndroidInstances,
26
24
  } from './resources/android-instances';
27
25
  import {
28
26
  Asset,
29
27
  AssetGetOrCreateParams,
30
28
  AssetGetOrCreateResponse,
31
- AssetGetParams,
32
29
  AssetListParams,
33
30
  AssetListResponse,
31
+ AssetGetParams,
34
32
  } from './resources/assets';
33
+ import { AndroidInstances } from './resources/android-instances-helpers';
35
34
  import { Assets, AssetGetOrUploadParams, AssetGetOrUploadResponse } from './resources/assets-helpers';
36
35
  import { type Fetch } from './internal/builtin-types';
37
36
  import { HeadersLike, NullableHeaders, buildHeaders } from './internal/headers';
@@ -50,7 +49,7 @@ export interface ClientOptions {
50
49
  /**
51
50
  * Defaults to process.env['LIM_TOKEN'].
52
51
  */
53
- apiKey?: string | undefined;
52
+ apiKey?: string | null | undefined;
54
53
 
55
54
  /**
56
55
  * Override the default base URL for the API, e.g., "https://api.example.com/v2/"
@@ -125,7 +124,7 @@ export interface ClientOptions {
125
124
  * API Client for interfacing with the Limrun API.
126
125
  */
127
126
  export class Limrun {
128
- apiKey: string;
127
+ apiKey: string | null;
129
128
 
130
129
  baseURL: string;
131
130
  maxRetries: number;
@@ -142,7 +141,7 @@ export class Limrun {
142
141
  /**
143
142
  * API Client for interfacing with the Limrun API.
144
143
  *
145
- * @param {string | undefined} [opts.apiKey=process.env['LIM_TOKEN'] ?? undefined]
144
+ * @param {string | null | undefined} [opts.apiKey=process.env['LIM_TOKEN'] ?? null]
146
145
  * @param {string} [opts.baseURL=process.env['LIMRUN_BASE_URL'] ?? https://api.limrun.com] - Override the default base URL for the API.
147
146
  * @param {number} [opts.timeout=1 minute] - The maximum amount of time (in milliseconds) the client will wait for a response before timing out.
148
147
  * @param {MergedRequestInit} [opts.fetchOptions] - Additional `RequestInit` options to be passed to `fetch` calls.
@@ -153,15 +152,9 @@ export class Limrun {
153
152
  */
154
153
  constructor({
155
154
  baseURL = readEnv('LIMRUN_BASE_URL'),
156
- apiKey = readEnv('LIM_TOKEN'),
155
+ apiKey = readEnv('LIM_TOKEN') ?? null,
157
156
  ...opts
158
157
  }: ClientOptions = {}) {
159
- if (apiKey === undefined) {
160
- throw new Errors.LimrunError(
161
- "The LIM_TOKEN environment variable is missing or empty; either provide it, or instantiate the Limrun client with an apiKey option, like new Limrun({ apiKey: 'My API Key' }).",
162
- );
163
- }
164
-
165
158
  const options: ClientOptions = {
166
159
  apiKey,
167
160
  ...opts,
@@ -219,10 +212,22 @@ export class Limrun {
219
212
  }
220
213
 
221
214
  protected validateHeaders({ values, nulls }: NullableHeaders) {
222
- return;
215
+ if (this.apiKey && values.get('authorization')) {
216
+ return;
217
+ }
218
+ if (nulls.has('authorization')) {
219
+ return;
220
+ }
221
+
222
+ throw new Error(
223
+ 'Could not resolve authentication method. Expected the apiKey to be set. Or for the "Authorization" headers to be explicitly omitted',
224
+ );
223
225
  }
224
226
 
225
227
  protected async authHeaders(opts: FinalRequestOptions): Promise<NullableHeaders | undefined> {
228
+ if (this.apiKey == null) {
229
+ return undefined;
230
+ }
226
231
  return buildHeaders([{ Authorization: `Bearer ${this.apiKey}` }]);
227
232
  }
228
233
 
@@ -743,7 +748,6 @@ export declare namespace Limrun {
743
748
  export {
744
749
  AndroidInstances as AndroidInstances,
745
750
  type AndroidInstance as AndroidInstance,
746
- type AndroidInstanceState as AndroidInstanceState,
747
751
  type AndroidInstanceListResponse as AndroidInstanceListResponse,
748
752
  type AndroidInstanceCreateParams as AndroidInstanceCreateParams,
749
753
  type AndroidInstanceListParams as AndroidInstanceListParams,
package/src/index.ts CHANGED
@@ -20,5 +20,3 @@ export {
20
20
  PermissionDeniedError,
21
21
  UnprocessableEntityError,
22
22
  } from './core/error';
23
-
24
- export * from './resources/android-instances-helpers';
@@ -1,159 +1,27 @@
1
- import { exec } from 'child_process';
2
- import * as net from 'net';
3
- import { WebSocket } from 'ws';
4
-
5
- import { AndroidInstance } from './android-instances';
6
-
7
- /**
8
- * Opens a WebSocket TCP proxy for the ADB port and connects the local adb
9
- * client to it.
10
- */
11
- export const startAdbTunnel = async (
12
- androidInstance: AndroidInstance,
13
- hostname?: string,
14
- port?: number,
15
- ): Promise<Proxy> => {
16
- if (!androidInstance.status.adbWebSocketUrl) {
17
- throw new Error('ADB WebSocket URL is not available');
18
- }
19
- const { address, close } = await startTcpProxy(
20
- androidInstance.status.adbWebSocketUrl,
21
- androidInstance.status.token,
22
- hostname ?? '127.0.0.1',
23
- port ?? 0,
24
- );
25
- try {
26
- await new Promise<void>((resolve, reject) => {
27
- exec(`adb connect ${address.address}:${address.port}`, (err) => {
28
- if (err) return reject(err);
29
- resolve();
30
- });
31
- });
32
- } catch (err) {
33
- close();
34
- throw err;
1
+ import { RequestOptions } from '../internal/request-options';
2
+ import {
3
+ AndroidInstance,
4
+ AndroidInstanceCreateParams,
5
+ AndroidInstances as GeneratedAndroidInstances,
6
+ } from './android-instances';
7
+
8
+ export class AndroidInstances extends GeneratedAndroidInstances {
9
+ async getOrCreate(params: AndroidInstanceCreateParams, options?: RequestOptions): Promise<AndroidInstance> {
10
+ if (!params.metadata || !params.metadata.labels || Object.keys(params.metadata.labels).length === 0) {
11
+ return Promise.reject(new Error('At least one label is required for getOrCreate operation'));
12
+ }
13
+ const instances = await super.list(
14
+ {
15
+ labelSelector: Object.entries(params.metadata.labels)
16
+ .map(([key, value]) => `${key}=${value}`)
17
+ .join(','),
18
+ state: 'ready',
19
+ },
20
+ options,
21
+ );
22
+ if (instances && instances.length > 0) {
23
+ return instances[0]!;
24
+ }
25
+ return super.create(params, options);
35
26
  }
36
- return { address, close };
37
- };
38
-
39
- /** Returned by `startTcpProxy` – holds the chosen localhost port and a close callback. */
40
- export interface Proxy {
41
- address: net.AddressInfo;
42
- close: () => void;
43
- }
44
-
45
- /**
46
- * Starts a one-shot TCP → WebSocket proxy.
47
- *
48
- * The function creates a local TCP server that listens on an ephemeral port on
49
- * 127.0.0.1. As soon as the **first** TCP client connects the server stops
50
- * accepting further connections and forwards all traffic between that client
51
- * and `remoteURL` through an authenticated WebSocket. If you need to proxy
52
- * more than one TCP connection, call `startTcpProxy` again to create a new
53
- * proxy instance.
54
- *
55
- * @param remoteURL Remote WebSocket endpoint (e.g. wss://example.com/instance)
56
- * @param token Bearer token sent as `Authorization` header
57
- * @param hostname Optional IP address to listen on. Default is 127.0.0.1
58
- * @param port Optional port number to listen on. Default is to ask Node.js
59
- * to find an available non-privileged port.
60
- */
61
- export async function startTcpProxy(
62
- remoteURL: string,
63
- token: string,
64
- hostname: string,
65
- port: number,
66
- ): Promise<Proxy> {
67
- return new Promise((resolve, reject) => {
68
- const server = net.createServer();
69
-
70
- let ws: WebSocket | undefined;
71
- let pingInterval: NodeJS.Timeout | undefined;
72
-
73
- // close helper
74
- const close = () => {
75
- if (pingInterval) {
76
- clearInterval(pingInterval);
77
- pingInterval = undefined;
78
- }
79
- if (ws && ws.readyState === WebSocket.OPEN) {
80
- ws.close(1000, 'close');
81
- }
82
- if (server.listening) {
83
- server.close();
84
- }
85
- };
86
-
87
- // No AbortController support – proxy can be closed via the returned handle
88
-
89
- // TCP server error
90
- server.once('error', (err) => {
91
- close();
92
- reject(new Error(`TCP server error: ${err.message}`));
93
- });
94
-
95
- // Listening
96
- server.once('listening', () => {
97
- const address = server.address();
98
- if (!address || typeof address === 'string') {
99
- close();
100
- return reject(new Error('Failed to obtain listening address'));
101
- }
102
- resolve({ address, close });
103
- });
104
-
105
- // On first TCP connection
106
- server.on('connection', (tcpSocket) => {
107
- // Single-connection proxy
108
- server.close();
109
-
110
- ws = new WebSocket(remoteURL, {
111
- headers: { Authorization: `Bearer ${token}` },
112
- perMessageDeflate: false,
113
- });
114
-
115
- // WebSocket error
116
- ws.once('error', (err) => {
117
- console.error('WebSocket error:', err);
118
- tcpSocket.destroy();
119
- close();
120
- });
121
-
122
- ws.once('open', () => {
123
- const socket = ws as WebSocket; // non-undefined after open
124
-
125
- pingInterval = setInterval(() => {
126
- if (socket.readyState === WebSocket.OPEN) {
127
- (socket as any).ping();
128
- }
129
- }, 30_000);
130
-
131
- // TCP → WS
132
- tcpSocket.on('data', (chunk) => {
133
- if (socket.readyState === WebSocket.OPEN) {
134
- socket.send(chunk);
135
- }
136
- });
137
-
138
- // WS → TCP
139
- socket.on('message', (data) => {
140
- if (!tcpSocket.destroyed) {
141
- tcpSocket.write(data as Buffer);
142
- }
143
- });
144
- });
145
-
146
- // Mutual close
147
- tcpSocket.on('close', close);
148
- tcpSocket.on('error', (err) => {
149
- console.error('TCP socket error:', err);
150
- close();
151
- });
152
-
153
- ws.on('close', () => tcpSocket.destroy());
154
- });
155
-
156
- // Start listening
157
- server.listen(port, hostname);
158
- });
159
27
  }
@@ -1,7 +1,6 @@
1
1
  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
3
  import { APIResource } from '../core/resource';
4
- import * as AndroidInstancesAPI from './android-instances';
5
4
  import { APIPromise } from '../core/api-promise';
6
5
  import { buildHeaders } from '../internal/headers';
7
6
  import { RequestOptions } from '../internal/request-options';
@@ -77,8 +76,7 @@ export namespace AndroidInstance {
77
76
 
78
77
  /**
79
78
  * The region where the instance will be created. If not given, will be decided
80
- * based on which region the client is closest to, scheduling clues and
81
- * availability.
79
+ * based on scheduling clues and availability.
82
80
  */
83
81
  region: string;
84
82
 
@@ -92,7 +90,7 @@ export namespace AndroidInstance {
92
90
  export interface Status {
93
91
  token: string;
94
92
 
95
- state: AndroidInstancesAPI.AndroidInstanceState;
93
+ state: 'unknown' | 'creating' | 'ready' | 'terminated';
96
94
 
97
95
  adbWebSocketUrl?: string;
98
96
 
@@ -100,8 +98,6 @@ export namespace AndroidInstance {
100
98
  }
101
99
  }
102
100
 
103
- export type AndroidInstanceState = 'unknown' | 'creating' | 'ready' | 'terminated';
104
-
105
101
  export type AndroidInstanceListResponse = Array<AndroidInstance>;
106
102
 
107
103
  export interface AndroidInstanceCreateParams {
@@ -187,13 +183,12 @@ export interface AndroidInstanceListParams {
187
183
  /**
188
184
  * State filter to apply to Android instances to return.
189
185
  */
190
- state?: AndroidInstanceState;
186
+ state?: 'unknown' | 'creating' | 'ready' | 'terminated';
191
187
  }
192
188
 
193
189
  export declare namespace AndroidInstances {
194
190
  export {
195
191
  type AndroidInstance as AndroidInstance,
196
- type AndroidInstanceState as AndroidInstanceState,
197
192
  type AndroidInstanceListResponse as AndroidInstanceListResponse,
198
193
  type AndroidInstanceCreateParams as AndroidInstanceCreateParams,
199
194
  type AndroidInstanceListParams as AndroidInstanceListParams,
@@ -1,9 +1,7 @@
1
1
  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
3
  export {
4
- AndroidInstances,
5
4
  type AndroidInstance,
6
- type AndroidInstanceState,
7
5
  type AndroidInstanceListResponse,
8
6
  type AndroidInstanceCreateParams,
9
7
  type AndroidInstanceListParams,
@@ -17,4 +15,6 @@ export {
17
15
  type AssetGetOrCreateParams,
18
16
  } from './assets';
19
17
 
18
+ export { AndroidInstances } from './android-instances-helpers';
19
+
20
20
  export { Assets, AssetGetOrUploadParams, AssetGetOrUploadResponse } from './assets-helpers';