@limrun/ui 0.14.0-rc.5 → 0.14.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/README.md +9 -26
- package/dist/components/remote-control.d.ts +36 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1351 -1196
- package/package.json +10 -51
- package/src/components/remote-control.tsx +414 -2
- package/src/index.ts +1 -1
- package/vite.config.ts +0 -9
- package/dist/app-store-relay/index.cjs +0 -9
- package/dist/app-store-relay/index.d.ts +0 -204
- package/dist/app-store-relay/index.js +0 -677
- package/dist/app-store-relay/react.cjs +0 -1
- package/dist/app-store-relay/react.d.ts +0 -15
- package/dist/app-store-relay/react.js +0 -69
- package/dist/apple-credentials/index.cjs +0 -1
- package/dist/apple-credentials/index.d.ts +0 -110
- package/dist/apple-credentials/index.js +0 -291
- package/dist/browser-storage-BnO-Ma-e.mjs +0 -219
- package/dist/browser-storage-DLxIqZk5.js +0 -1
- package/dist/client-C9z_sgOf.js +0 -1
- package/dist/client-CFDB4mX5.mjs +0 -191
- package/dist/core/device-install/apple/client.d.ts +0 -18
- package/dist/core/device-install/apple/crypto.d.ts +0 -20
- package/dist/core/device-install/apple/gsa-srp.d.ts +0 -26
- package/dist/core/device-install/apple/index.d.ts +0 -5
- package/dist/core/device-install/apple/provisioning.d.ts +0 -237
- package/dist/core/device-install/apple/relay.d.ts +0 -53
- package/dist/core/device-install/index.d.ts +0 -4
- package/dist/core/device-install/operations/index.d.ts +0 -6
- package/dist/core/device-install/operations/limbuild-client.d.ts +0 -43
- package/dist/core/device-install/operations/operations.d.ts +0 -47
- package/dist/core/device-install/operations/relay-client.d.ts +0 -26
- package/dist/core/device-install/operations/relay-protocol.d.ts +0 -27
- package/dist/core/device-install/operations/usbmux.d.ts +0 -32
- package/dist/core/device-install/operations/webusb.d.ts +0 -21
- package/dist/core/device-install/storage/browser-storage.d.ts +0 -56
- package/dist/core/device-install/storage/index.d.ts +0 -2
- package/dist/core/device-install/storage/secret-store.d.ts +0 -124
- package/dist/core/device-install/types.d.ts +0 -57
- package/dist/core/errors.d.ts +0 -1
- package/dist/core/play-publish/google.d.ts +0 -13
- package/dist/core/play-publish/publish.d.ts +0 -34
- package/dist/device-build/index.cjs +0 -1
- package/dist/device-build/index.d.ts +0 -4
- package/dist/device-build/index.js +0 -85
- package/dist/device-build/react.cjs +0 -1
- package/dist/device-build/react.d.ts +0 -21
- package/dist/device-build/react.js +0 -68
- package/dist/device-build/signing.d.ts +0 -20
- package/dist/device-install/demo/demo.d.ts +0 -0
- package/dist/device-install/index.cjs +0 -1
- package/dist/device-install/index.d.ts +0 -18
- package/dist/device-install/index.js +0 -594
- package/dist/device-install/react.cjs +0 -1
- package/dist/device-install/react.d.ts +0 -24
- package/dist/device-install/react.js +0 -105
- package/dist/errors-BSkqS7U-.js +0 -1
- package/dist/errors-gy8yQJos.mjs +0 -6
- package/dist/gsa-srp-Bme3JLEi.mjs +0 -266
- package/dist/gsa-srp-CCIfUz7Y.js +0 -1
- package/dist/index-ChhtuQ7e.js +0 -22
- package/dist/index-YDu8K9a4.mjs +0 -11279
- package/dist/limbuild-client-BagK_FWE.mjs +0 -83
- package/dist/limbuild-client-CVDwq54z.js +0 -1
- package/dist/play-publish/index.cjs +0 -1
- package/dist/play-publish/index.d.ts +0 -2
- package/dist/play-publish/index.js +0 -109
- package/dist/play-publish/react.cjs +0 -1
- package/dist/play-publish/react.d.ts +0 -31
- package/dist/play-publish/react.js +0 -91
- package/dist/provisioning-CDPvxd1h.mjs +0 -270
- package/dist/provisioning-CbC4h--7.js +0 -1
- package/src/app-store-relay/index.test.ts +0 -60
- package/src/app-store-relay/index.ts +0 -846
- package/src/app-store-relay/react.ts +0 -131
- package/src/apple-credentials/index.test.ts +0 -180
- package/src/apple-credentials/index.ts +0 -388
- package/src/core/device-install/apple/client.ts +0 -150
- package/src/core/device-install/apple/crypto.ts +0 -205
- package/src/core/device-install/apple/gsa-srp.ts +0 -123
- package/src/core/device-install/apple/index.ts +0 -5
- package/src/core/device-install/apple/provisioning.test.ts +0 -114
- package/src/core/device-install/apple/provisioning.ts +0 -419
- package/src/core/device-install/apple/relay.ts +0 -202
- package/src/core/device-install/index.ts +0 -4
- package/src/core/device-install/operations/index.ts +0 -6
- package/src/core/device-install/operations/limbuild-client.ts +0 -144
- package/src/core/device-install/operations/operations.ts +0 -239
- package/src/core/device-install/operations/relay-client.ts +0 -286
- package/src/core/device-install/operations/relay-protocol.ts +0 -71
- package/src/core/device-install/operations/usbmux.ts +0 -270
- package/src/core/device-install/operations/webusb-dom.d.ts +0 -54
- package/src/core/device-install/operations/webusb.ts +0 -110
- package/src/core/device-install/storage/browser-storage.test.ts +0 -65
- package/src/core/device-install/storage/browser-storage.ts +0 -325
- package/src/core/device-install/storage/index.ts +0 -2
- package/src/core/device-install/storage/secret-store.ts +0 -332
- package/src/core/device-install/types.ts +0 -69
- package/src/core/errors.ts +0 -3
- package/src/core/play-publish/google.ts +0 -113
- package/src/core/play-publish/publish.test.ts +0 -68
- package/src/core/play-publish/publish.ts +0 -89
- package/src/device-build/index.ts +0 -42
- package/src/device-build/react.ts +0 -130
- package/src/device-build/signing.ts +0 -94
- package/src/device-install/README.md +0 -423
- package/src/device-install/demo/demo.css +0 -493
- package/src/device-install/demo/demo.tsx +0 -1061
- package/src/device-install/demo/index.html +0 -12
- package/src/device-install/index.ts +0 -50
- package/src/device-install/react.ts +0 -195
- package/src/play-publish/README.md +0 -66
- package/src/play-publish/index.ts +0 -12
- package/src/play-publish/react.ts +0 -208
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import type { BuildLogLine, DeviceInstallBuildStatus } from '../types';
|
|
2
|
-
|
|
3
|
-
export type LimbuildInfo = {
|
|
4
|
-
homeDir?: string;
|
|
5
|
-
lastBuildConfig?: {
|
|
6
|
-
bundleId?: string;
|
|
7
|
-
sdk?: string;
|
|
8
|
-
};
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export type StartSignedDeviceBuildOptions = {
|
|
12
|
-
limbuildApiUrl: string;
|
|
13
|
-
token?: string;
|
|
14
|
-
certificateP12Base64: string;
|
|
15
|
-
certificatePassword?: string;
|
|
16
|
-
provisioningProfileBase64: string;
|
|
17
|
-
signedUploadUrl?: string;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export type BuildLogEventsOptions = {
|
|
21
|
-
limbuildApiUrl: string;
|
|
22
|
-
execId: string;
|
|
23
|
-
token?: string;
|
|
24
|
-
onLine: (line: BuildLogLine) => void;
|
|
25
|
-
onStatus: (status: DeviceInstallBuildStatus) => void;
|
|
26
|
-
onError?: (error: Error) => void;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export type IOSOTAInstall = {
|
|
30
|
-
installUrl: string;
|
|
31
|
-
landingUrl: string;
|
|
32
|
-
manifestUrl: string;
|
|
33
|
-
ipaUrl: string;
|
|
34
|
-
bundleId: string;
|
|
35
|
-
displayName: string;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export type GetIOSOTAInstallOptions = {
|
|
39
|
-
limbuildApiUrl: string;
|
|
40
|
-
execId: string;
|
|
41
|
-
token?: string;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export async function fetchLimbuildInfo(limbuildApiUrl: string, token?: string) {
|
|
45
|
-
const url = new URL(`${limbuildApiUrl}/info`);
|
|
46
|
-
if (token) {
|
|
47
|
-
url.searchParams.set('token', token);
|
|
48
|
-
}
|
|
49
|
-
const response = await fetch(url.toString(), {
|
|
50
|
-
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
|
|
51
|
-
});
|
|
52
|
-
if (!response.ok) {
|
|
53
|
-
const body = await response.text();
|
|
54
|
-
throw new Error(`Info request failed: HTTP ${response.status} ${body}`);
|
|
55
|
-
}
|
|
56
|
-
return (await response.json()) as LimbuildInfo;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export async function startSignedDeviceBuild({
|
|
60
|
-
limbuildApiUrl,
|
|
61
|
-
token,
|
|
62
|
-
certificateP12Base64,
|
|
63
|
-
certificatePassword,
|
|
64
|
-
provisioningProfileBase64,
|
|
65
|
-
signedUploadUrl,
|
|
66
|
-
}: StartSignedDeviceBuildOptions) {
|
|
67
|
-
const url = new URL(`${limbuildApiUrl}/exec`);
|
|
68
|
-
if (token) {
|
|
69
|
-
url.searchParams.set('token', token);
|
|
70
|
-
}
|
|
71
|
-
const response = await fetch(url.toString(), {
|
|
72
|
-
method: 'POST',
|
|
73
|
-
headers: {
|
|
74
|
-
'Content-Type': 'application/json',
|
|
75
|
-
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
76
|
-
},
|
|
77
|
-
body: JSON.stringify({
|
|
78
|
-
command: 'xcodebuild',
|
|
79
|
-
xcodebuild: { sdk: 'iphoneos' },
|
|
80
|
-
...(signedUploadUrl ? { signedUploadUrl } : {}),
|
|
81
|
-
signing: {
|
|
82
|
-
certificateP12Base64,
|
|
83
|
-
...(certificatePassword ? { certificatePassword } : {}),
|
|
84
|
-
provisioningProfileBase64,
|
|
85
|
-
},
|
|
86
|
-
}),
|
|
87
|
-
});
|
|
88
|
-
if (!response.ok) {
|
|
89
|
-
const body = await response.text();
|
|
90
|
-
throw new Error(`Build request failed: HTTP ${response.status} ${body}`);
|
|
91
|
-
}
|
|
92
|
-
return (await response.json()) as { execId?: string };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export async function getIOSOTAInstall({ limbuildApiUrl, execId, token }: GetIOSOTAInstallOptions) {
|
|
96
|
-
const url = new URL(`${limbuildApiUrl}/exec/${encodeURIComponent(execId)}/ios/ota`);
|
|
97
|
-
if (token) {
|
|
98
|
-
url.searchParams.set('token', token);
|
|
99
|
-
}
|
|
100
|
-
const response = await fetch(url.toString(), {
|
|
101
|
-
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
|
|
102
|
-
});
|
|
103
|
-
if (!response.ok) {
|
|
104
|
-
const body = await response.text();
|
|
105
|
-
throw new Error(`OTA install metadata failed: HTTP ${response.status} ${body}`);
|
|
106
|
-
}
|
|
107
|
-
return (await response.json()) as IOSOTAInstall;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function watchBuildLogEvents({
|
|
111
|
-
limbuildApiUrl,
|
|
112
|
-
execId,
|
|
113
|
-
token,
|
|
114
|
-
onLine,
|
|
115
|
-
onStatus,
|
|
116
|
-
onError,
|
|
117
|
-
}: BuildLogEventsOptions) {
|
|
118
|
-
const url = new URL(`${limbuildApiUrl}/exec/${execId}/events`);
|
|
119
|
-
if (token) {
|
|
120
|
-
url.searchParams.set('token', token);
|
|
121
|
-
}
|
|
122
|
-
const events = new EventSource(url.toString());
|
|
123
|
-
onStatus('running');
|
|
124
|
-
events.addEventListener('command', (event) => onLine({ type: 'command', data: event.data }));
|
|
125
|
-
events.addEventListener('stdout', (event) => onLine({ type: 'stdout', data: event.data }));
|
|
126
|
-
events.addEventListener('stderr', (event) => onLine({ type: 'stderr', data: event.data }));
|
|
127
|
-
events.addEventListener('exitCode', (event) => {
|
|
128
|
-
const code = parseInt(event.data, 10);
|
|
129
|
-
onStatus(
|
|
130
|
-
code === 0 ? 'succeeded'
|
|
131
|
-
: code < 0 ? 'cancelled'
|
|
132
|
-
: 'failed',
|
|
133
|
-
);
|
|
134
|
-
events.close();
|
|
135
|
-
});
|
|
136
|
-
events.onerror = () => {
|
|
137
|
-
events.close();
|
|
138
|
-
// The stream closed without a terminal exitCode event. Mark the build as
|
|
139
|
-
// failed so consumers don't stay stuck on "running" and can retry.
|
|
140
|
-
onStatus('failed');
|
|
141
|
-
onError?.(new Error('Build log stream closed before completion.'));
|
|
142
|
-
};
|
|
143
|
-
return () => events.close();
|
|
144
|
-
}
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
/// <reference path="./webusb-dom.d.ts" />
|
|
2
|
-
|
|
3
|
-
import type { DeviceHello, DeviceInstallLog, PairRecordPayload } from '../types';
|
|
4
|
-
import { RelayClient } from './relay-client';
|
|
5
|
-
import { closeUsbmuxSession, createUsbmuxSession, type UsbmuxSession } from './usbmux';
|
|
6
|
-
import { claimUsbmux, findUsbmuxCandidates, requestAppleDevice, type UsbmuxCandidate } from './webusb';
|
|
7
|
-
import { errorMessage } from '../../errors';
|
|
8
|
-
|
|
9
|
-
export type DeviceRelayTarget = {
|
|
10
|
-
device: USBDevice;
|
|
11
|
-
candidate: UsbmuxCandidate;
|
|
12
|
-
session?: UsbmuxSession;
|
|
13
|
-
claimedInterfaceNumber?: number;
|
|
14
|
-
hello: DeviceHello;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export type RequestUSBAccessOptions = {
|
|
18
|
-
log: DeviceInstallLog;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type StartPairingRelayOptions = {
|
|
22
|
-
registryApiUrl: string;
|
|
23
|
-
token?: string;
|
|
24
|
-
organizationId?: string;
|
|
25
|
-
log: DeviceInstallLog;
|
|
26
|
-
target: DeviceRelayTarget;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export type InstallSource =
|
|
30
|
-
| { assetId: string; assetName?: never; downloadUrl?: never }
|
|
31
|
-
| { assetId?: never; assetName: string; downloadUrl?: never }
|
|
32
|
-
| { assetId?: never; assetName?: never; downloadUrl: string };
|
|
33
|
-
|
|
34
|
-
export type StartInstallRelayOptions = StartPairingRelayOptions & {
|
|
35
|
-
pairRecord: PairRecordPayload;
|
|
36
|
-
installSource: InstallSource;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export async function requestUSBAccess({ log }: RequestUSBAccessOptions) {
|
|
40
|
-
log('Selecting USB device');
|
|
41
|
-
const device = await requestAppleDevice();
|
|
42
|
-
const target = makeDeviceRelayTarget(device);
|
|
43
|
-
log(
|
|
44
|
-
'Selected USB device',
|
|
45
|
-
`${device.manufacturerName ?? ''} ${device.productName ?? ''} ${device.serialNumber ?? ''}`.trim(),
|
|
46
|
-
);
|
|
47
|
-
return target;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function startPairingRelay({
|
|
51
|
-
registryApiUrl,
|
|
52
|
-
token,
|
|
53
|
-
organizationId,
|
|
54
|
-
log,
|
|
55
|
-
target,
|
|
56
|
-
}: StartPairingRelayOptions) {
|
|
57
|
-
const deviceRelayUrl = deviceRelayWebSocketUrl(registryApiUrl, token, organizationId);
|
|
58
|
-
let relay: RelayClient | undefined;
|
|
59
|
-
try {
|
|
60
|
-
relay = await connectRelay(deviceRelayUrl, target, log);
|
|
61
|
-
const pairRecord = await relay.startPairing();
|
|
62
|
-
return { relay, pairRecord, target };
|
|
63
|
-
} catch (error) {
|
|
64
|
-
relay?.close();
|
|
65
|
-
throw error;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export async function startInstallRelay({
|
|
70
|
-
registryApiUrl,
|
|
71
|
-
token,
|
|
72
|
-
organizationId,
|
|
73
|
-
log,
|
|
74
|
-
target,
|
|
75
|
-
pairRecord,
|
|
76
|
-
installSource,
|
|
77
|
-
}: StartInstallRelayOptions) {
|
|
78
|
-
const deviceRelayUrl = deviceRelayWebSocketUrl(registryApiUrl, token, organizationId);
|
|
79
|
-
let relay: RelayClient | undefined;
|
|
80
|
-
try {
|
|
81
|
-
relay = await connectRelay(deviceRelayUrl, target, log);
|
|
82
|
-
await relay.startInstall(pairRecord, installSource);
|
|
83
|
-
return relay;
|
|
84
|
-
} catch (error) {
|
|
85
|
-
relay?.close();
|
|
86
|
-
throw error;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export async function closeDeviceRelayTarget(target: DeviceRelayTarget | undefined, log?: DeviceInstallLog) {
|
|
91
|
-
if (!target) return;
|
|
92
|
-
if (target.session) {
|
|
93
|
-
closeUsbmuxSession(target.session);
|
|
94
|
-
target.session = undefined;
|
|
95
|
-
}
|
|
96
|
-
if (target.claimedInterfaceNumber !== undefined) {
|
|
97
|
-
try {
|
|
98
|
-
await target.device.releaseInterface(target.claimedInterfaceNumber);
|
|
99
|
-
log?.('Released usbmux interface');
|
|
100
|
-
} catch (error) {
|
|
101
|
-
log?.('USB interface release failed', errorMessage(error));
|
|
102
|
-
} finally {
|
|
103
|
-
target.claimedInterfaceNumber = undefined;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (target.device.opened) {
|
|
107
|
-
try {
|
|
108
|
-
await target.device.close();
|
|
109
|
-
log?.('Closed USB device');
|
|
110
|
-
} catch (error) {
|
|
111
|
-
log?.('USB device close failed', errorMessage(error));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function connectRelay(deviceRelayUrl: string, target: DeviceRelayTarget, log: DeviceInstallLog) {
|
|
117
|
-
await ensureUsbmuxSession(target, log);
|
|
118
|
-
const relay = new RelayClient(deviceRelayUrl, target.session!, target.hello, log);
|
|
119
|
-
await relay.connect();
|
|
120
|
-
return relay;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function ensureUsbmuxSession(target: DeviceRelayTarget, log: DeviceInstallLog) {
|
|
124
|
-
if (target.session) return;
|
|
125
|
-
try {
|
|
126
|
-
target.candidate = await claimBestUsbmuxCandidate(target, log);
|
|
127
|
-
target.session = await createUsbmuxSession(target.device, target.candidate);
|
|
128
|
-
log('Created usbmux session');
|
|
129
|
-
} catch (error) {
|
|
130
|
-
await closeDeviceRelayTarget(target, log);
|
|
131
|
-
throw error;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function makeDeviceRelayTarget(device: USBDevice): DeviceRelayTarget {
|
|
136
|
-
return {
|
|
137
|
-
device,
|
|
138
|
-
candidate: pickUsbmuxCandidate(device),
|
|
139
|
-
hello: {
|
|
140
|
-
serialNumber: device.serialNumber,
|
|
141
|
-
productName: device.productName,
|
|
142
|
-
manufacturerName: device.manufacturerName,
|
|
143
|
-
productId: device.productId,
|
|
144
|
-
vendorId: device.vendorId,
|
|
145
|
-
},
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async function claimBestUsbmuxCandidate(target: DeviceRelayTarget, log: DeviceInstallLog) {
|
|
150
|
-
const candidates = orderedUsbmuxCandidates(target.device);
|
|
151
|
-
if (candidates.length === 0) throw new Error('No Apple usbmux interface found.');
|
|
152
|
-
let lastError: unknown;
|
|
153
|
-
for (const candidate of candidates) {
|
|
154
|
-
for (const attempt of [1, 2]) {
|
|
155
|
-
try {
|
|
156
|
-
if (!target.device.opened) {
|
|
157
|
-
await target.device.open();
|
|
158
|
-
}
|
|
159
|
-
log(
|
|
160
|
-
'Claiming usbmux interface',
|
|
161
|
-
`configuration ${candidate.configurationValue}, interface ${candidate.interfaceNumber}, alternate ${candidate.alternateSetting}, attempt ${attempt}`,
|
|
162
|
-
);
|
|
163
|
-
await claimUsbmux(target.device, candidate);
|
|
164
|
-
target.claimedInterfaceNumber = candidate.interfaceNumber;
|
|
165
|
-
log(
|
|
166
|
-
'Claimed usbmux interface',
|
|
167
|
-
`configuration ${candidate.configurationValue}, interface ${candidate.interfaceNumber}`,
|
|
168
|
-
);
|
|
169
|
-
return candidate;
|
|
170
|
-
} catch (error) {
|
|
171
|
-
lastError = error;
|
|
172
|
-
log(
|
|
173
|
-
'USB interface claim failed',
|
|
174
|
-
`configuration ${candidate.configurationValue}, interface ${
|
|
175
|
-
candidate.interfaceNumber
|
|
176
|
-
}: ${errorMessage(error)}`,
|
|
177
|
-
);
|
|
178
|
-
await resetUSBDevice(target);
|
|
179
|
-
await sleep(250);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
const detail = lastError ? ` (${errorMessage(lastError)})` : '';
|
|
184
|
-
throw new Error(
|
|
185
|
-
'Could not get exclusive USB access to the iPhone' +
|
|
186
|
-
`${detail}. Another app on this computer is likely connected to it. ` +
|
|
187
|
-
'Close other browser tabs that use this device, quit iPhone Mirroring, Xcode, eject iPhone in Finder windows showing the phone and try again.',
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function pickUsbmuxCandidate(device: USBDevice) {
|
|
192
|
-
const candidate = orderedUsbmuxCandidates(device)[0];
|
|
193
|
-
if (!candidate) throw new Error('No Apple usbmux interface found.');
|
|
194
|
-
return candidate;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function orderedUsbmuxCandidates(device: USBDevice) {
|
|
198
|
-
const candidates = findUsbmuxCandidates(device);
|
|
199
|
-
const activeConfigurationValue = device.configuration?.configurationValue;
|
|
200
|
-
return [
|
|
201
|
-
...candidates.filter((item) => item.configurationValue === activeConfigurationValue),
|
|
202
|
-
...candidates.filter((item) => item.configurationValue !== activeConfigurationValue),
|
|
203
|
-
];
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function deviceRelayWebSocketUrl(registryApiUrl: string, token?: string, organizationId?: string) {
|
|
207
|
-
const url = new URL(registryApiUrl);
|
|
208
|
-
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
209
|
-
url.pathname = `${url.pathname.replace(/\/$/, '')}/ios/device/ws`;
|
|
210
|
-
if (token) {
|
|
211
|
-
url.searchParams.set('token', token);
|
|
212
|
-
}
|
|
213
|
-
if (organizationId) {
|
|
214
|
-
url.searchParams.set('organization', organizationId);
|
|
215
|
-
}
|
|
216
|
-
return url.toString();
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async function resetUSBDevice(target: DeviceRelayTarget) {
|
|
220
|
-
if (target.claimedInterfaceNumber !== undefined) {
|
|
221
|
-
try {
|
|
222
|
-
await target.device.releaseInterface(target.claimedInterfaceNumber);
|
|
223
|
-
} catch {
|
|
224
|
-
// Best effort: claim failures often mean there is nothing we own to release.
|
|
225
|
-
}
|
|
226
|
-
target.claimedInterfaceNumber = undefined;
|
|
227
|
-
}
|
|
228
|
-
if (target.device.opened) {
|
|
229
|
-
try {
|
|
230
|
-
await target.device.close();
|
|
231
|
-
} catch {
|
|
232
|
-
// Best effort before reopening for the next candidate/attempt.
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function sleep(ms: number) {
|
|
238
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
239
|
-
}
|
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
import type { DeviceHello, DeviceInstallLog, PairRecordPayload } from '../types';
|
|
2
|
-
import { decodeFrame, decodeJson, encodeFrame, encodeJson, RelayMessageType } from './relay-protocol';
|
|
3
|
-
import type { InstallSource } from './operations';
|
|
4
|
-
import {
|
|
5
|
-
openStream,
|
|
6
|
-
receiveStreamData,
|
|
7
|
-
sendStreamData,
|
|
8
|
-
type UsbmuxSession,
|
|
9
|
-
type UsbmuxStream,
|
|
10
|
-
} from './usbmux';
|
|
11
|
-
|
|
12
|
-
type OpenStreamPayload = {
|
|
13
|
-
port: number;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
type ProgressPayload = {
|
|
17
|
-
message: string;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export class RelayClient {
|
|
21
|
-
private socket?: WebSocket;
|
|
22
|
-
private streams = new Map<number, UsbmuxStream>();
|
|
23
|
-
private frameQueue = Promise.resolve();
|
|
24
|
-
private closed = false;
|
|
25
|
-
private pairRecordWaiter?: {
|
|
26
|
-
resolve: (record: PairRecordPayload) => void;
|
|
27
|
-
reject: (error: Error) => void;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
constructor(
|
|
31
|
-
private readonly webSocketUrl: string,
|
|
32
|
-
private readonly session: UsbmuxSession,
|
|
33
|
-
private readonly deviceHello: DeviceHello,
|
|
34
|
-
private readonly log: DeviceInstallLog,
|
|
35
|
-
) {}
|
|
36
|
-
|
|
37
|
-
async connect() {
|
|
38
|
-
const socket = new WebSocket(this.webSocketUrl);
|
|
39
|
-
socket.binaryType = 'arraybuffer';
|
|
40
|
-
this.socket = socket;
|
|
41
|
-
socket.onclose = () => {
|
|
42
|
-
this.closed = true;
|
|
43
|
-
this.log('Relay socket closed');
|
|
44
|
-
// The server often sends PairRecordReady and then closes immediately. In
|
|
45
|
-
// Chromium, onclose can be delivered before the queued onmessage handler
|
|
46
|
-
// has finished processing that final frame. Drain the frame queue before
|
|
47
|
-
// rejecting the waiter so a successful pair is not reported as
|
|
48
|
-
// "Relay socket closed".
|
|
49
|
-
void this.frameQueue.finally(() => {
|
|
50
|
-
if (this.pairRecordWaiter) {
|
|
51
|
-
this.pairRecordWaiter.reject(new Error('Relay socket closed'));
|
|
52
|
-
this.pairRecordWaiter = undefined;
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
};
|
|
56
|
-
await new Promise<void>((resolve, reject) => {
|
|
57
|
-
socket.onopen = () => resolve();
|
|
58
|
-
socket.onerror = () => reject(new Error('WebSocket connection failed'));
|
|
59
|
-
});
|
|
60
|
-
socket.onmessage = (event) => {
|
|
61
|
-
const data = event.data instanceof ArrayBuffer ? new Uint8Array(event.data) : new Uint8Array();
|
|
62
|
-
this.enqueueFrame(data);
|
|
63
|
-
};
|
|
64
|
-
await this.send({
|
|
65
|
-
type: RelayMessageType.DeviceHello,
|
|
66
|
-
requestId: 0,
|
|
67
|
-
streamId: 0,
|
|
68
|
-
payload: encodeJson(this.deviceHello),
|
|
69
|
-
});
|
|
70
|
-
this.log('Connected WebSocket relay');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async startPairing() {
|
|
74
|
-
const recordPromise = new Promise<PairRecordPayload>((resolve, reject) => {
|
|
75
|
-
this.pairRecordWaiter = { resolve, reject };
|
|
76
|
-
});
|
|
77
|
-
await this.send({
|
|
78
|
-
type: RelayMessageType.StartPairing,
|
|
79
|
-
requestId: 0,
|
|
80
|
-
streamId: 0,
|
|
81
|
-
payload: encodeJson({}),
|
|
82
|
-
});
|
|
83
|
-
this.log('Pairing requested');
|
|
84
|
-
return recordPromise;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async startInstall(pairRecord: PairRecordPayload, installSource: InstallSource) {
|
|
88
|
-
await this.send({
|
|
89
|
-
type: RelayMessageType.StartInstall,
|
|
90
|
-
requestId: 0,
|
|
91
|
-
streamId: 0,
|
|
92
|
-
payload: encodeJson({ ...pairRecord, ...installSource }),
|
|
93
|
-
});
|
|
94
|
-
this.log('Installation requested');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
close() {
|
|
98
|
-
this.closed = true;
|
|
99
|
-
this.socket?.close();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
private enqueueFrame(data: Uint8Array) {
|
|
103
|
-
this.frameQueue = this.frameQueue
|
|
104
|
-
.then(() => this.handleFrame(decodeFrame(data)))
|
|
105
|
-
.catch((error) => {
|
|
106
|
-
this.log('Relay frame handling failed', error instanceof Error ? error.message : String(error));
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
private async handleFrame(frame: ReturnType<typeof decodeFrame>) {
|
|
111
|
-
switch (frame.type) {
|
|
112
|
-
case RelayMessageType.OpenStream:
|
|
113
|
-
await this.handleOpenStream(
|
|
114
|
-
frame.requestId,
|
|
115
|
-
frame.streamId,
|
|
116
|
-
decodeJson<OpenStreamPayload>(frame.payload).port,
|
|
117
|
-
);
|
|
118
|
-
break;
|
|
119
|
-
case RelayMessageType.StreamData: {
|
|
120
|
-
const stream = this.streams.get(frame.streamId);
|
|
121
|
-
if (!stream) throw new Error(`Unknown stream ${frame.streamId}`);
|
|
122
|
-
await sendStreamData(stream, frame.payload);
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
case RelayMessageType.StreamClose:
|
|
126
|
-
this.streams.delete(frame.streamId);
|
|
127
|
-
break;
|
|
128
|
-
case RelayMessageType.InstallProgress:
|
|
129
|
-
this.log(formatInstallProgress(decodeJson<ProgressPayload>(frame.payload).message));
|
|
130
|
-
break;
|
|
131
|
-
case RelayMessageType.Error:
|
|
132
|
-
this.handleError(frame.payload);
|
|
133
|
-
break;
|
|
134
|
-
case RelayMessageType.PairRecordReady:
|
|
135
|
-
this.handlePairRecordReady(decodeJson<PairRecordPayload>(frame.payload));
|
|
136
|
-
break;
|
|
137
|
-
case RelayMessageType.Ping:
|
|
138
|
-
await this.send({
|
|
139
|
-
type: RelayMessageType.Pong,
|
|
140
|
-
requestId: frame.requestId,
|
|
141
|
-
streamId: 0,
|
|
142
|
-
payload: new Uint8Array(),
|
|
143
|
-
});
|
|
144
|
-
break;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
private handleError(payload: Uint8Array) {
|
|
149
|
-
const message = decodeServerError(payload);
|
|
150
|
-
this.log('Server error', message);
|
|
151
|
-
if (this.pairRecordWaiter) {
|
|
152
|
-
this.pairRecordWaiter.reject(new Error(message));
|
|
153
|
-
this.pairRecordWaiter = undefined;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
private handlePairRecordReady(record: PairRecordPayload) {
|
|
158
|
-
this.log('Pair record received', record.udid);
|
|
159
|
-
this.pairRecordWaiter?.resolve(record);
|
|
160
|
-
this.pairRecordWaiter = undefined;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
private async handleOpenStream(requestId: number, streamId: number, port: number) {
|
|
164
|
-
try {
|
|
165
|
-
const stream = await openStream(this.session, port);
|
|
166
|
-
this.streams.set(streamId, stream);
|
|
167
|
-
await this.send({
|
|
168
|
-
type: RelayMessageType.OpenResult,
|
|
169
|
-
requestId,
|
|
170
|
-
streamId,
|
|
171
|
-
payload: encodeJson({ ok: true }),
|
|
172
|
-
});
|
|
173
|
-
this.log(`Opened device stream ${streamId} to port ${port}`);
|
|
174
|
-
void this.pumpDeviceToServer(streamId, stream);
|
|
175
|
-
} catch (error) {
|
|
176
|
-
this.log(
|
|
177
|
-
`Open device stream ${streamId} failed`,
|
|
178
|
-
error instanceof Error ? error.message : String(error),
|
|
179
|
-
);
|
|
180
|
-
await this.send({
|
|
181
|
-
type: RelayMessageType.OpenResult,
|
|
182
|
-
requestId,
|
|
183
|
-
streamId,
|
|
184
|
-
payload: encodeJson({
|
|
185
|
-
ok: false,
|
|
186
|
-
error: error instanceof Error ? error.message : String(error),
|
|
187
|
-
}),
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
private async pumpDeviceToServer(streamId: number, stream: UsbmuxStream) {
|
|
193
|
-
try {
|
|
194
|
-
for (;;) {
|
|
195
|
-
const data = await receiveStreamData(stream);
|
|
196
|
-
if (this.closed) return;
|
|
197
|
-
await this.send({
|
|
198
|
-
type: RelayMessageType.StreamData,
|
|
199
|
-
requestId: 0,
|
|
200
|
-
streamId,
|
|
201
|
-
payload: data,
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
} catch (error) {
|
|
205
|
-
this.log(`Device stream ${streamId} closed`, error instanceof Error ? error.message : String(error));
|
|
206
|
-
await this.send({
|
|
207
|
-
type: RelayMessageType.StreamClose,
|
|
208
|
-
requestId: 0,
|
|
209
|
-
streamId,
|
|
210
|
-
payload: encodeJson({
|
|
211
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
212
|
-
}),
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
private async send(frame: Parameters<typeof encodeFrame>[0]) {
|
|
218
|
-
if (this.closed) return;
|
|
219
|
-
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
220
|
-
this.closed = true;
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
this.socket.send(encodeFrame(frame));
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function decodeServerError(payload: Uint8Array) {
|
|
228
|
-
const text = new TextDecoder().decode(payload);
|
|
229
|
-
try {
|
|
230
|
-
const parsed = JSON.parse(text) as { error?: string };
|
|
231
|
-
return userFacingServerError(parsed.error ?? text);
|
|
232
|
-
} catch {
|
|
233
|
-
return userFacingServerError(text);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function userFacingServerError(message: string) {
|
|
238
|
-
return message.replace(/libimobiledevice/g, '').trim();
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function formatInstallProgress(message: string) {
|
|
242
|
-
const prefix = 'install status: ';
|
|
243
|
-
if (!message.startsWith(prefix)) {
|
|
244
|
-
return message;
|
|
245
|
-
}
|
|
246
|
-
const plist = message.slice(prefix.length);
|
|
247
|
-
// The relay message is untrusted device input. Parsing it with DOMParser is
|
|
248
|
-
// both an XSS DOM sink and an XML entity-expansion (billion-laughs) vector,
|
|
249
|
-
// so extract only the fields we render via plain string scanning instead of
|
|
250
|
-
// building a DOM.
|
|
251
|
-
// Surface the real failure reason (e.g. ApplicationVerificationFailed) rather
|
|
252
|
-
// than collapsing it to an opaque numeric code downstream.
|
|
253
|
-
const error = readPlistValue(plist, 'Error');
|
|
254
|
-
if (error) {
|
|
255
|
-
const description = readPlistValue(plist, 'ErrorDescription');
|
|
256
|
-
return `Install error: ${error}${description ? ` — ${description}` : ''}`;
|
|
257
|
-
}
|
|
258
|
-
const status = readPlistValue(plist, 'Status') ?? 'Unknown';
|
|
259
|
-
const percent = readPlistValue(plist, 'PercentComplete');
|
|
260
|
-
return `Install progress: ${percent ? `${percent}% ` : ''}${status}`;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Returns the <string>/<integer>/<real> value immediately following
|
|
264
|
-
// <key>NAME</key> in an Apple plist fragment, without an XML/DOM parser.
|
|
265
|
-
function readPlistValue(plist: string, key: string): string | undefined {
|
|
266
|
-
const pattern = new RegExp(
|
|
267
|
-
`<key>\\s*${escapeRegExp(
|
|
268
|
-
key,
|
|
269
|
-
)}\\s*</key>\\s*<(?:string|integer|real)>([\\s\\S]*?)</(?:string|integer|real)>`,
|
|
270
|
-
);
|
|
271
|
-
const match = pattern.exec(plist);
|
|
272
|
-
return match ? decodeBasicXmlEntities(match[1].trim()) : undefined;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function escapeRegExp(value: string) {
|
|
276
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function decodeBasicXmlEntities(value: string) {
|
|
280
|
-
return value
|
|
281
|
-
.replace(/</g, '<')
|
|
282
|
-
.replace(/>/g, '>')
|
|
283
|
-
.replace(/"/g, '"')
|
|
284
|
-
.replace(/'/g, "'")
|
|
285
|
-
.replace(/&/g, '&');
|
|
286
|
-
}
|