@limrun/api 0.2.0 → 0.5.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.
- package/CHANGELOG.md +100 -0
- package/README.md +1 -1
- package/client.d.mts +3 -2
- package/client.d.mts.map +1 -1
- package/client.d.ts +3 -2
- package/client.d.ts.map +1 -1
- package/client.js +2 -2
- package/client.js.map +1 -1
- package/client.mjs +1 -1
- package/client.mjs.map +1 -1
- package/index.d.mts +1 -0
- package/index.d.ts +1 -0
- package/index.js +2 -0
- package/index.js.map +1 -1
- package/index.mjs +1 -0
- package/package.json +4 -2
- package/resources/android-instances-helpers.d.mts +30 -0
- package/resources/android-instances-helpers.d.mts.map +1 -0
- package/resources/android-instances-helpers.d.ts +30 -0
- package/resources/android-instances-helpers.d.ts.map +1 -0
- package/resources/android-instances-helpers.js +129 -0
- package/resources/android-instances-helpers.js.map +1 -0
- package/resources/android-instances-helpers.mjs +123 -0
- package/resources/android-instances-helpers.mjs.map +1 -0
- package/resources/assets-helpers.d.mts +22 -0
- package/resources/assets-helpers.d.mts.map +1 -0
- package/resources/assets-helpers.d.ts +22 -0
- package/resources/assets-helpers.d.ts.map +1 -0
- package/resources/assets-helpers.js +41 -0
- package/resources/assets-helpers.js.map +1 -0
- package/resources/assets-helpers.mjs +37 -0
- package/resources/assets-helpers.mjs.map +1 -0
- package/resources/assets.d.mts +12 -2
- package/resources/assets.d.mts.map +1 -1
- package/resources/assets.d.ts +12 -2
- package/resources/assets.d.ts.map +1 -1
- package/resources/index.d.mts +2 -1
- package/resources/index.d.mts.map +1 -1
- package/resources/index.d.ts +2 -1
- package/resources/index.d.ts.map +1 -1
- package/resources/index.js +2 -2
- package/resources/index.js.map +1 -1
- package/resources/index.mjs +1 -1
- package/resources/index.mjs.map +1 -1
- package/src/client.ts +5 -1
- package/src/index.ts +2 -0
- package/src/resources/android-instances-helpers.ts +159 -0
- package/src/resources/assets-helpers.ts +63 -0
- package/src/resources/assets.ts +17 -1
- package/src/resources/index.ts +3 -1
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
|
@@ -0,0 +1,159 @@
|
|
|
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;
|
|
35
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { RequestOptions } from '../internal/request-options';
|
|
2
|
+
import { Assets as GeneratedAssets } from './assets';
|
|
3
|
+
|
|
4
|
+
export interface AssetGetOrUploadParams {
|
|
5
|
+
/**
|
|
6
|
+
* The path to the file to upload.
|
|
7
|
+
*/
|
|
8
|
+
path: string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The name for the asset. Defaults to the name of the file given in the filePath parameter.
|
|
12
|
+
*/
|
|
13
|
+
name?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AssetGetOrUploadResponse {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
signedDownloadUrl: string;
|
|
20
|
+
md5: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class Assets extends GeneratedAssets {
|
|
24
|
+
async getOrUpload(
|
|
25
|
+
body: AssetGetOrUploadParams,
|
|
26
|
+
options?: RequestOptions,
|
|
27
|
+
): Promise<AssetGetOrUploadResponse> {
|
|
28
|
+
const { crypto, fs, path } = globalThis as any;
|
|
29
|
+
const creationResponse = await this.getOrCreate(
|
|
30
|
+
{
|
|
31
|
+
name: body.name ?? path.basename(body.path),
|
|
32
|
+
},
|
|
33
|
+
options,
|
|
34
|
+
);
|
|
35
|
+
const data = await fs.promises.readFile(body.path);
|
|
36
|
+
const md5 = crypto.createHash('md5').update(data).digest('hex');
|
|
37
|
+
if (creationResponse.md5 && creationResponse.md5 === md5) {
|
|
38
|
+
return {
|
|
39
|
+
id: creationResponse.id,
|
|
40
|
+
name: creationResponse.name,
|
|
41
|
+
signedDownloadUrl: creationResponse.signedDownloadUrl,
|
|
42
|
+
md5: creationResponse.md5,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const uploadResponse = await fetch(creationResponse.signedUploadUrl, {
|
|
46
|
+
headers: {
|
|
47
|
+
'Content-Length': data.length.toString(),
|
|
48
|
+
'Content-Type': 'application/octet-stream',
|
|
49
|
+
},
|
|
50
|
+
method: 'PUT',
|
|
51
|
+
body: data,
|
|
52
|
+
});
|
|
53
|
+
if (uploadResponse.status !== 200) {
|
|
54
|
+
throw new Error(`Failed to upload asset: ${uploadResponse.status} ${await uploadResponse.text()}`);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
id: creationResponse.id,
|
|
58
|
+
name: creationResponse.name,
|
|
59
|
+
signedDownloadUrl: creationResponse.signedDownloadUrl,
|
|
60
|
+
md5,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/resources/assets.ts
CHANGED
|
@@ -35,7 +35,7 @@ export class Assets extends APIResource {
|
|
|
35
35
|
* is no corresponding file in the storage so downloading it directly or using it
|
|
36
36
|
* in instances will fail until you use the returned upload URL to submit the file.
|
|
37
37
|
*/
|
|
38
|
-
getOrCreate(body: AssetGetOrCreateParams, options?: RequestOptions): APIPromise<
|
|
38
|
+
getOrCreate(body: AssetGetOrCreateParams, options?: RequestOptions): APIPromise<AssetGetOrCreateResponse> {
|
|
39
39
|
return this._client.put('/v1/assets', { body, ...options });
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -57,6 +57,21 @@ export interface Asset {
|
|
|
57
57
|
|
|
58
58
|
export type AssetListResponse = Array<Asset>;
|
|
59
59
|
|
|
60
|
+
export interface AssetGetOrCreateResponse {
|
|
61
|
+
id: string;
|
|
62
|
+
|
|
63
|
+
name: string;
|
|
64
|
+
|
|
65
|
+
signedDownloadUrl: string;
|
|
66
|
+
|
|
67
|
+
signedUploadUrl: string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Returned only if there is a corresponding file uploaded already.
|
|
71
|
+
*/
|
|
72
|
+
md5?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
60
75
|
export interface AssetListParams {
|
|
61
76
|
/**
|
|
62
77
|
* Toggles whether a download URL should be included in the response
|
|
@@ -99,6 +114,7 @@ export declare namespace Assets {
|
|
|
99
114
|
export {
|
|
100
115
|
type Asset as Asset,
|
|
101
116
|
type AssetListResponse as AssetListResponse,
|
|
117
|
+
type AssetGetOrCreateResponse as AssetGetOrCreateResponse,
|
|
102
118
|
type AssetListParams as AssetListParams,
|
|
103
119
|
type AssetGetParams as AssetGetParams,
|
|
104
120
|
type AssetGetOrCreateParams as AssetGetOrCreateParams,
|
package/src/resources/index.ts
CHANGED
|
@@ -9,10 +9,12 @@ export {
|
|
|
9
9
|
type AndroidInstanceListParams,
|
|
10
10
|
} from './android-instances';
|
|
11
11
|
export {
|
|
12
|
-
Assets,
|
|
13
12
|
type Asset,
|
|
14
13
|
type AssetListResponse,
|
|
14
|
+
type AssetGetOrCreateResponse,
|
|
15
15
|
type AssetListParams,
|
|
16
16
|
type AssetGetParams,
|
|
17
17
|
type AssetGetOrCreateParams,
|
|
18
18
|
} from './assets';
|
|
19
|
+
|
|
20
|
+
export { Assets, AssetGetOrUploadParams, AssetGetOrUploadResponse } from './assets-helpers';
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.5.1'; // x-release-please-version
|
package/version.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.5.1";
|
|
2
2
|
//# sourceMappingURL=version.d.mts.map
|
package/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.5.1";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/version.js
CHANGED
package/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.5.1'; // x-release-please-version
|
|
2
2
|
//# sourceMappingURL=version.mjs.map
|