@limrun/ui 0.7.0 → 0.9.0-rc.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 -0
- package/dist/components/device-install/device-install-dialog.d.ts +5 -0
- package/dist/components/device-install/index.d.ts +2 -0
- package/dist/components/remote-control.d.ts +2 -0
- package/dist/core/device-install/apple/client.d.ts +16 -0
- package/dist/core/device-install/apple/crypto.d.ts +20 -0
- package/dist/core/device-install/apple/gsa-srp.d.ts +26 -0
- package/dist/core/device-install/apple/index.d.ts +5 -0
- package/dist/core/device-install/apple/provisioning.d.ts +150 -0
- package/dist/core/device-install/apple/relay.d.ts +33 -0
- package/dist/core/device-install/index.d.ts +4 -0
- package/dist/core/device-install/operations/index.d.ts +6 -0
- package/dist/core/device-install/operations/limbuild-client.d.ts +28 -0
- package/dist/core/device-install/operations/operations.d.ts +32 -0
- package/dist/core/device-install/operations/relay-client.d.ts +25 -0
- package/dist/core/device-install/operations/relay-protocol.d.ts +27 -0
- package/dist/core/device-install/operations/usbmux.d.ts +32 -0
- package/dist/core/device-install/operations/webusb.d.ts +21 -0
- package/dist/core/device-install/storage/browser-storage.d.ts +25 -0
- package/dist/core/device-install/storage/index.d.ts +1 -0
- package/dist/core/device-install/types.d.ts +48 -0
- package/dist/demo.d.ts +1 -0
- package/dist/device-install/index.cjs +9 -0
- package/dist/device-install/index.d.ts +3 -0
- package/dist/device-install/index.js +212 -0
- package/dist/device-install/react.cjs +1 -0
- package/dist/device-install/react.d.ts +1 -0
- package/dist/device-install/react.js +4 -0
- package/dist/device-install-dialog-CTwVViYY.js +2 -0
- package/dist/device-install-dialog-zzKJu7SM.mjs +328 -0
- package/dist/device-install-dialog.css +1 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/use-device-install.d.ts +55 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +526 -510
- package/dist/use-device-install-CgrOKKyi.mjs +13042 -0
- package/dist/use-device-install-DDKRf6IL.js +23 -0
- package/index.html +199 -0
- package/package.json +54 -41
- package/src/components/device-install/device-install-dialog.css +244 -0
- package/src/components/device-install/device-install-dialog.tsx +363 -0
- package/src/components/device-install/index.ts +2 -0
- package/src/components/remote-control.css +1 -1
- package/src/components/remote-control.tsx +222 -110
- package/src/core/device-install/apple/client.ts +64 -0
- package/src/core/device-install/apple/crypto.ts +202 -0
- package/src/core/device-install/apple/gsa-srp.ts +127 -0
- package/src/core/device-install/apple/index.ts +5 -0
- package/src/core/device-install/apple/provisioning.ts +255 -0
- package/src/core/device-install/apple/relay.ts +305 -0
- package/src/core/device-install/index.ts +4 -0
- package/src/core/device-install/operations/index.ts +6 -0
- package/src/core/device-install/operations/limbuild-client.ts +104 -0
- package/src/core/device-install/operations/operations.ts +217 -0
- package/src/core/device-install/operations/relay-client.ts +255 -0
- package/src/core/device-install/operations/relay-protocol.ts +71 -0
- package/src/core/device-install/operations/usbmux.ts +270 -0
- package/src/core/device-install/operations/webusb-dom.d.ts +54 -0
- package/src/core/device-install/operations/webusb.ts +105 -0
- package/src/core/device-install/storage/browser-storage.ts +238 -0
- package/src/core/device-install/storage/index.ts +1 -0
- package/src/core/device-install/types.ts +65 -0
- package/src/demo.tsx +185 -0
- package/src/device-install/index.ts +3 -0
- package/src/device-install/react.ts +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-device-install.ts +522 -0
- package/src/index.ts +2 -0
- package/tsconfig.json +25 -25
- package/tsconfig.node.json +25 -25
- package/vite.config.ts +6 -2
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
closeDeviceRelayTarget,
|
|
4
|
+
fetchLimbuildInfo,
|
|
5
|
+
getPairRecord,
|
|
6
|
+
getLatestSigningAssets,
|
|
7
|
+
getReusableAppleSigningAssets,
|
|
8
|
+
listTeamsRequest,
|
|
9
|
+
parseProvisioningProfile,
|
|
10
|
+
profileContainsDevice,
|
|
11
|
+
proxyProvisioningRequest,
|
|
12
|
+
putPairRecord,
|
|
13
|
+
putSigningAssets,
|
|
14
|
+
requestUSBAccess as requestDeviceUSBAccess,
|
|
15
|
+
startBrowserOwnedAppleIDLogin,
|
|
16
|
+
startSignedDeviceBuild,
|
|
17
|
+
startInstallRelay,
|
|
18
|
+
startPairingRelay,
|
|
19
|
+
watchBuildLogEvents,
|
|
20
|
+
type AppleIDLoginResult,
|
|
21
|
+
type AppleDeveloperPortalTeam,
|
|
22
|
+
type BuildLogLine,
|
|
23
|
+
type DeviceInstallBuildStatus,
|
|
24
|
+
type DeviceInstallBusyAction,
|
|
25
|
+
type DeviceInstallStep,
|
|
26
|
+
type DeviceInstallStepStatus,
|
|
27
|
+
type DeviceRelayTarget,
|
|
28
|
+
type StoredPairRecord,
|
|
29
|
+
type StoredSigningAssets,
|
|
30
|
+
} from '../core/device-install';
|
|
31
|
+
import type { RelayClient } from '../core/device-install/operations';
|
|
32
|
+
|
|
33
|
+
type DeviceInstallStepStatuses = Record<DeviceInstallStep, DeviceInstallStepStatus>;
|
|
34
|
+
|
|
35
|
+
export type UseDeviceInstallOptions = {
|
|
36
|
+
apiUrl?: string;
|
|
37
|
+
token?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type UseDeviceInstallResult = {
|
|
41
|
+
currentStep: DeviceInstallStep;
|
|
42
|
+
stepStatuses: DeviceInstallStepStatuses;
|
|
43
|
+
device?: DeviceInstallDevice;
|
|
44
|
+
hasPairRecord: boolean;
|
|
45
|
+
hasSigningAssets: boolean;
|
|
46
|
+
pairConfirmationRequired: boolean;
|
|
47
|
+
logs: string[];
|
|
48
|
+
buildLogs: BuildLogLine[];
|
|
49
|
+
buildStatus: DeviceInstallBuildStatus;
|
|
50
|
+
appleSigningStatus: DeviceInstallAppleSigningStatus;
|
|
51
|
+
appleTeams: AppleDeveloperPortalTeam[];
|
|
52
|
+
selectedAppleTeamID?: string;
|
|
53
|
+
buildLogPanelOpen: boolean;
|
|
54
|
+
busyAction?: DeviceInstallBusyAction;
|
|
55
|
+
error?: string;
|
|
56
|
+
canBuild: boolean;
|
|
57
|
+
canRequestUSBAccess: boolean;
|
|
58
|
+
canPairBrowser: boolean;
|
|
59
|
+
canInstall: boolean;
|
|
60
|
+
setSigningFiles: (files: DeviceInstallSigningFiles) => void;
|
|
61
|
+
setBuildLogPanelOpen: (open: boolean) => void;
|
|
62
|
+
startAppleIDLogin: (input: DeviceInstallAppleIDLoginInput) => Promise<void>;
|
|
63
|
+
submitAppleTwoFactorCode: (code: string) => Promise<void>;
|
|
64
|
+
setSelectedAppleTeamID: (teamID: string | undefined) => void;
|
|
65
|
+
clearAppleIDSession: () => Promise<void>;
|
|
66
|
+
startDeviceBuild: () => Promise<void>;
|
|
67
|
+
requestUSBAccess: () => Promise<void>;
|
|
68
|
+
pairBrowser: () => Promise<void>;
|
|
69
|
+
startInstallation: () => Promise<void>;
|
|
70
|
+
stopRelay: () => void;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type DeviceInstallAppleSigningStatus =
|
|
74
|
+
| 'idle'
|
|
75
|
+
| 'authenticating'
|
|
76
|
+
| 'two-factor-required'
|
|
77
|
+
| 'authenticated'
|
|
78
|
+
| 'preparing-assets'
|
|
79
|
+
| 'using-cached-profile'
|
|
80
|
+
| 'error';
|
|
81
|
+
|
|
82
|
+
export type DeviceInstallDevice = {
|
|
83
|
+
serialNumber?: string;
|
|
84
|
+
productName?: string;
|
|
85
|
+
manufacturerName?: string;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type DeviceInstallSigningFiles = {
|
|
89
|
+
certificateFile?: File;
|
|
90
|
+
provisioningProfileFile?: File;
|
|
91
|
+
certificatePassword?: string;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type DeviceInstallAppleIDLoginInput = {
|
|
95
|
+
accountName: string;
|
|
96
|
+
password: string;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const initialStepStatuses: DeviceInstallStepStatuses = {
|
|
100
|
+
build: 'idle',
|
|
101
|
+
usb: 'idle',
|
|
102
|
+
pair: 'idle',
|
|
103
|
+
install: 'idle',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export function useDeviceInstall({
|
|
107
|
+
apiUrl,
|
|
108
|
+
token,
|
|
109
|
+
}: UseDeviceInstallOptions): UseDeviceInstallResult {
|
|
110
|
+
const [currentStep, setCurrentStep] = useState<DeviceInstallStep>('build');
|
|
111
|
+
const [stepStatuses, setStepStatuses] = useState<DeviceInstallStepStatuses>(initialStepStatuses);
|
|
112
|
+
const [selectedDevice, setSelectedDevice] = useState<DeviceRelayTarget | undefined>();
|
|
113
|
+
const [pairRecord, setPairRecord] = useState<StoredPairRecord | undefined>();
|
|
114
|
+
const [signingAssets, setSigningAssets] = useState<StoredSigningAssets | undefined>();
|
|
115
|
+
const [logs, setLogs] = useState<string[]>([
|
|
116
|
+
'Ready. Start a signed device build, allow USB access, pair this browser, then install.',
|
|
117
|
+
]);
|
|
118
|
+
const [buildLogs, setBuildLogs] = useState<BuildLogLine[]>([]);
|
|
119
|
+
const [buildStatus, setBuildStatus] = useState<DeviceInstallBuildStatus>('idle');
|
|
120
|
+
const [appleSigningStatus, setAppleSigningStatus] = useState<DeviceInstallAppleSigningStatus>('idle');
|
|
121
|
+
const [appleTeams, setAppleTeams] = useState<AppleDeveloperPortalTeam[]>([]);
|
|
122
|
+
const [selectedAppleTeamID, setSelectedAppleTeamID] = useState<string | undefined>();
|
|
123
|
+
const [buildLogPanelOpen, setBuildLogPanelOpen] = useState(false);
|
|
124
|
+
const [busyAction, setBusyAction] = useState<DeviceInstallBusyAction | undefined>();
|
|
125
|
+
const [error, setError] = useState<string | undefined>();
|
|
126
|
+
const [pairConfirmationRequired, setPairConfirmationRequired] = useState(false);
|
|
127
|
+
const [signingFiles, setSigningFilesState] = useState<DeviceInstallSigningFiles>({});
|
|
128
|
+
const relayRef = useRef<RelayClient | undefined>(undefined);
|
|
129
|
+
const selectedDeviceRef = useRef<DeviceRelayTarget | undefined>(undefined);
|
|
130
|
+
const stopBuildWatcherRef = useRef<(() => void) | undefined>(undefined);
|
|
131
|
+
const appleIDSessionRef = useRef<AppleIDLoginResult | undefined>(undefined);
|
|
132
|
+
|
|
133
|
+
const log = useCallback((message: string, detail?: string) => {
|
|
134
|
+
const line = detail ? `${message}\n${detail}` : message;
|
|
135
|
+
setLogs((current) => [line, ...current].slice(0, 100));
|
|
136
|
+
}, []);
|
|
137
|
+
|
|
138
|
+
const setStepStatus = useCallback((step: DeviceInstallStep, status: DeviceInstallStepStatus) => {
|
|
139
|
+
setStepStatuses((current) => ({ ...current, [step]: status }));
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
const setSigningFiles = useCallback((files: DeviceInstallSigningFiles) => {
|
|
143
|
+
setSigningFilesState((current) => ({ ...current, ...files }));
|
|
144
|
+
setSigningAssets(undefined);
|
|
145
|
+
}, []);
|
|
146
|
+
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
selectedDeviceRef.current = selectedDevice;
|
|
149
|
+
}, [selectedDevice]);
|
|
150
|
+
|
|
151
|
+
const cleanupDeviceAccess = useCallback(async () => {
|
|
152
|
+
relayRef.current?.close();
|
|
153
|
+
relayRef.current = undefined;
|
|
154
|
+
await closeDeviceRelayTarget(selectedDeviceRef.current, log);
|
|
155
|
+
}, [log]);
|
|
156
|
+
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
return () => {
|
|
159
|
+
stopBuildWatcherRef.current?.();
|
|
160
|
+
void appleIDSessionRef.current?.close();
|
|
161
|
+
void cleanupDeviceAccess();
|
|
162
|
+
};
|
|
163
|
+
}, [cleanupDeviceAccess]);
|
|
164
|
+
|
|
165
|
+
const resolveSigningAssetsForBuild = useCallback(async () => {
|
|
166
|
+
const info = apiUrl ? await fetchStoredBuildInfo(apiUrl, token).catch(() => undefined) : undefined;
|
|
167
|
+
if (info?.lastBuildConfig?.bundleId) {
|
|
168
|
+
const cached = await getReusableAppleSigningAssets({
|
|
169
|
+
bundleID: info.lastBuildConfig.bundleId,
|
|
170
|
+
deviceUDID: selectedDevice?.hello.serialNumber,
|
|
171
|
+
teamID: selectedAppleTeamID,
|
|
172
|
+
});
|
|
173
|
+
if (cached) {
|
|
174
|
+
setAppleSigningStatus('using-cached-profile');
|
|
175
|
+
log('Using cached Apple signing profile', cached.bundleID);
|
|
176
|
+
setSigningAssets(cached);
|
|
177
|
+
return cached;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const stored = await getLatestSigningAssets();
|
|
181
|
+
if (stored) {
|
|
182
|
+
log('Using stored signing assets', stored.bundleID);
|
|
183
|
+
setSigningAssets(stored);
|
|
184
|
+
return stored;
|
|
185
|
+
}
|
|
186
|
+
if (
|
|
187
|
+
!signingFiles.certificateFile ||
|
|
188
|
+
!signingFiles.provisioningProfileFile ||
|
|
189
|
+
!signingFiles.certificatePassword
|
|
190
|
+
) {
|
|
191
|
+
throw new Error('Upload a certificate, provisioning profile, and certificate password.');
|
|
192
|
+
}
|
|
193
|
+
log('Preparing signing assets');
|
|
194
|
+
const [certificateP12Base64, provisioningProfileBase64, profile] = await Promise.all([
|
|
195
|
+
fileToBase64(signingFiles.certificateFile),
|
|
196
|
+
fileToBase64(signingFiles.provisioningProfileFile),
|
|
197
|
+
parseProvisioningProfile(signingFiles.provisioningProfileFile),
|
|
198
|
+
]);
|
|
199
|
+
if (selectedDevice?.hello.serialNumber && !profileContainsDevice(profile, selectedDevice.hello.serialNumber)) {
|
|
200
|
+
throw new Error('Provisioning profile does not include the selected iPhone.');
|
|
201
|
+
}
|
|
202
|
+
const storageBundleId = profile.bundleID ?? profile.applicationIdentifier ?? signingFiles.provisioningProfileFile.name;
|
|
203
|
+
const storedAssets = await putSigningAssets({
|
|
204
|
+
deviceUDID: selectedDevice?.hello.serialNumber,
|
|
205
|
+
bundleID: storageBundleId,
|
|
206
|
+
certificateP12Base64,
|
|
207
|
+
certificateFileName: signingFiles.certificateFile.name,
|
|
208
|
+
certificatePassword: signingFiles.certificatePassword,
|
|
209
|
+
provisioningProfileBase64,
|
|
210
|
+
profileFileName: signingFiles.provisioningProfileFile.name,
|
|
211
|
+
profile,
|
|
212
|
+
});
|
|
213
|
+
setSigningAssets(storedAssets);
|
|
214
|
+
log('Signing assets stored locally', storageBundleId);
|
|
215
|
+
return storedAssets;
|
|
216
|
+
}, [apiUrl, log, selectedAppleTeamID, selectedDevice?.hello.serialNumber, signingFiles, token]);
|
|
217
|
+
|
|
218
|
+
const startAppleIDLogin = useCallback(
|
|
219
|
+
async ({ accountName, password }: DeviceInstallAppleIDLoginInput) => {
|
|
220
|
+
if (!apiUrl) return;
|
|
221
|
+
setBusyAction('build');
|
|
222
|
+
setError(undefined);
|
|
223
|
+
setAppleSigningStatus('authenticating');
|
|
224
|
+
try {
|
|
225
|
+
await appleIDSessionRef.current?.close().catch(() => undefined);
|
|
226
|
+
const session = await startBrowserOwnedAppleIDLogin({ limbuildApiUrl: apiUrl, token, accountName, password });
|
|
227
|
+
appleIDSessionRef.current = session;
|
|
228
|
+
if (!session.requiresTwoFactor) {
|
|
229
|
+
await session.finalize();
|
|
230
|
+
await refreshAppleTeams(apiUrl, session.appleSessionId, token, setAppleTeams, setSelectedAppleTeamID);
|
|
231
|
+
}
|
|
232
|
+
setAppleSigningStatus(session.requiresTwoFactor ? 'two-factor-required' : 'authenticated');
|
|
233
|
+
log(
|
|
234
|
+
session.requiresTwoFactor ? 'Apple ID requires two-factor authentication' : 'Apple ID authenticated',
|
|
235
|
+
accountName,
|
|
236
|
+
);
|
|
237
|
+
} catch (caught) {
|
|
238
|
+
const message = errorMessage(caught);
|
|
239
|
+
setError(message);
|
|
240
|
+
setAppleSigningStatus('error');
|
|
241
|
+
log('Apple ID authentication failed', message);
|
|
242
|
+
} finally {
|
|
243
|
+
setBusyAction(undefined);
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
[apiUrl, log, token],
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const submitAppleTwoFactorCode = useCallback(
|
|
250
|
+
async (code: string) => {
|
|
251
|
+
const session = appleIDSessionRef.current;
|
|
252
|
+
if (!session) {
|
|
253
|
+
throw new Error('Start Apple ID login before submitting a two-factor code.');
|
|
254
|
+
}
|
|
255
|
+
setBusyAction('build');
|
|
256
|
+
setError(undefined);
|
|
257
|
+
try {
|
|
258
|
+
await session.finishTwoFactor(code);
|
|
259
|
+
await session.finalize();
|
|
260
|
+
if (apiUrl) {
|
|
261
|
+
await refreshAppleTeams(apiUrl, session.appleSessionId, token, setAppleTeams, setSelectedAppleTeamID);
|
|
262
|
+
}
|
|
263
|
+
setAppleSigningStatus('authenticated');
|
|
264
|
+
log('Apple ID two-factor authentication accepted');
|
|
265
|
+
} catch (caught) {
|
|
266
|
+
const message = errorMessage(caught);
|
|
267
|
+
setError(message);
|
|
268
|
+
setAppleSigningStatus('error');
|
|
269
|
+
log('Apple ID two-factor authentication failed', message);
|
|
270
|
+
} finally {
|
|
271
|
+
setBusyAction(undefined);
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
[apiUrl, log, token],
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const clearAppleIDSession = useCallback(async () => {
|
|
278
|
+
await appleIDSessionRef.current?.close().catch(() => undefined);
|
|
279
|
+
appleIDSessionRef.current = undefined;
|
|
280
|
+
setAppleTeams([]);
|
|
281
|
+
setSelectedAppleTeamID(undefined);
|
|
282
|
+
setAppleSigningStatus('idle');
|
|
283
|
+
log('Apple ID session cleared');
|
|
284
|
+
}, [log]);
|
|
285
|
+
|
|
286
|
+
const startDeviceBuild = useCallback(async () => {
|
|
287
|
+
if (!apiUrl) return;
|
|
288
|
+
setBusyAction('build');
|
|
289
|
+
setError(undefined);
|
|
290
|
+
setCurrentStep('build');
|
|
291
|
+
setStepStatus('build', 'active');
|
|
292
|
+
setBuildLogPanelOpen(true);
|
|
293
|
+
setBuildLogs([]);
|
|
294
|
+
setBuildStatus('queued');
|
|
295
|
+
stopBuildWatcherRef.current?.();
|
|
296
|
+
try {
|
|
297
|
+
const assets = await resolveSigningAssetsForBuild();
|
|
298
|
+
log('Starting signed device build');
|
|
299
|
+
const result = await startSignedDeviceBuild({
|
|
300
|
+
limbuildApiUrl: apiUrl,
|
|
301
|
+
token,
|
|
302
|
+
certificateP12Base64: assets.certificateP12Base64,
|
|
303
|
+
certificatePassword: assets.certificatePassword,
|
|
304
|
+
provisioningProfileBase64: assets.provisioningProfileBase64,
|
|
305
|
+
});
|
|
306
|
+
if (!result.execId) {
|
|
307
|
+
throw new Error('Build request did not return an exec ID.');
|
|
308
|
+
}
|
|
309
|
+
log('Signed device build started', result.execId);
|
|
310
|
+
stopBuildWatcherRef.current = watchBuildLogEvents({
|
|
311
|
+
limbuildApiUrl: apiUrl,
|
|
312
|
+
execId: result.execId,
|
|
313
|
+
token,
|
|
314
|
+
onLine: (line) => setBuildLogs((current) => [...current, line]),
|
|
315
|
+
onStatus: (status) => {
|
|
316
|
+
setBuildStatus(status);
|
|
317
|
+
if (status === 'succeeded') {
|
|
318
|
+
setStepStatus('build', 'complete');
|
|
319
|
+
setCurrentStep('usb');
|
|
320
|
+
} else if (status === 'failed' || status === 'cancelled') {
|
|
321
|
+
setStepStatus('build', 'error');
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
onError: (caught) => {
|
|
325
|
+
const message = errorMessage(caught);
|
|
326
|
+
setError(message);
|
|
327
|
+
log('Build log stream failed', message);
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
} catch (caught) {
|
|
331
|
+
const message = errorMessage(caught);
|
|
332
|
+
setError(message);
|
|
333
|
+
setBuildStatus('failed');
|
|
334
|
+
setStepStatus('build', 'error');
|
|
335
|
+
log('Signed device build failed', message);
|
|
336
|
+
} finally {
|
|
337
|
+
setBusyAction(undefined);
|
|
338
|
+
}
|
|
339
|
+
}, [apiUrl, log, resolveSigningAssetsForBuild, setStepStatus, token]);
|
|
340
|
+
|
|
341
|
+
const requestUSBAccess = useCallback(async () => {
|
|
342
|
+
setBusyAction('usb');
|
|
343
|
+
setError(undefined);
|
|
344
|
+
setCurrentStep('usb');
|
|
345
|
+
setStepStatus('usb', 'active');
|
|
346
|
+
try {
|
|
347
|
+
await cleanupDeviceAccess();
|
|
348
|
+
const target = await requestDeviceUSBAccess({ log });
|
|
349
|
+
setSelectedDevice(target);
|
|
350
|
+
setPairConfirmationRequired(false);
|
|
351
|
+
const storedPairRecord = await getPairRecord(target.hello.serialNumber);
|
|
352
|
+
setPairRecord(storedPairRecord);
|
|
353
|
+
const storedSigningAssets = await getLatestSigningAssets();
|
|
354
|
+
if (storedSigningAssets) {
|
|
355
|
+
if (!profileContainsDevice(storedSigningAssets.profile, target.hello.serialNumber)) {
|
|
356
|
+
throw new Error('Stored provisioning profile does not include the selected iPhone.');
|
|
357
|
+
}
|
|
358
|
+
setSigningAssets(storedSigningAssets);
|
|
359
|
+
}
|
|
360
|
+
setStepStatus('usb', 'complete');
|
|
361
|
+
setCurrentStep(storedPairRecord ? 'install' : 'pair');
|
|
362
|
+
log(storedPairRecord ? 'Pair record found' : 'No pair record found', target.hello.serialNumber);
|
|
363
|
+
} catch (caught) {
|
|
364
|
+
const message = errorMessage(caught);
|
|
365
|
+
setError(message);
|
|
366
|
+
setStepStatus('usb', 'error');
|
|
367
|
+
log('USB access failed', message);
|
|
368
|
+
} finally {
|
|
369
|
+
setBusyAction(undefined);
|
|
370
|
+
}
|
|
371
|
+
}, [cleanupDeviceAccess, log, setStepStatus]);
|
|
372
|
+
|
|
373
|
+
const pairBrowser = useCallback(async () => {
|
|
374
|
+
if (!apiUrl || !selectedDevice) return;
|
|
375
|
+
setBusyAction('pair');
|
|
376
|
+
setError(undefined);
|
|
377
|
+
setPairConfirmationRequired(false);
|
|
378
|
+
setCurrentStep('pair');
|
|
379
|
+
setStepStatus('pair', 'active');
|
|
380
|
+
try {
|
|
381
|
+
await cleanupDeviceAccess();
|
|
382
|
+
const result = await startPairingRelay({
|
|
383
|
+
limbuildApiUrl: apiUrl,
|
|
384
|
+
token,
|
|
385
|
+
log,
|
|
386
|
+
target: selectedDevice,
|
|
387
|
+
});
|
|
388
|
+
const stored = await putPairRecord(result.pairRecord, {
|
|
389
|
+
productName: selectedDevice.hello.productName,
|
|
390
|
+
});
|
|
391
|
+
result.relay.close();
|
|
392
|
+
await closeDeviceRelayTarget(selectedDevice, log);
|
|
393
|
+
setPairRecord(stored);
|
|
394
|
+
setPairConfirmationRequired(false);
|
|
395
|
+
setStepStatus('pair', 'complete');
|
|
396
|
+
setCurrentStep('install');
|
|
397
|
+
log('Device paired', 'The pair record was stored locally in this browser.');
|
|
398
|
+
} catch (caught) {
|
|
399
|
+
await closeDeviceRelayTarget(selectedDevice, log);
|
|
400
|
+
const message = errorMessage(caught);
|
|
401
|
+
setPairConfirmationRequired(true);
|
|
402
|
+
setError('Unlock the iPhone, tap Trust, then confirm the pair record.');
|
|
403
|
+
setStepStatus('pair', 'error');
|
|
404
|
+
log('Device pairing failed', message);
|
|
405
|
+
} finally {
|
|
406
|
+
setBusyAction(undefined);
|
|
407
|
+
}
|
|
408
|
+
}, [apiUrl, cleanupDeviceAccess, log, selectedDevice, setStepStatus, token]);
|
|
409
|
+
|
|
410
|
+
const startInstallation = useCallback(async () => {
|
|
411
|
+
if (!apiUrl || !selectedDevice || !pairRecord) return;
|
|
412
|
+
setBusyAction('install');
|
|
413
|
+
setError(undefined);
|
|
414
|
+
setCurrentStep('install');
|
|
415
|
+
setStepStatus('install', 'active');
|
|
416
|
+
try {
|
|
417
|
+
await cleanupDeviceAccess();
|
|
418
|
+
relayRef.current = await startInstallRelay({
|
|
419
|
+
limbuildApiUrl: apiUrl,
|
|
420
|
+
token,
|
|
421
|
+
log,
|
|
422
|
+
target: selectedDevice,
|
|
423
|
+
pairRecord,
|
|
424
|
+
});
|
|
425
|
+
setStepStatus('install', 'complete');
|
|
426
|
+
log('Device install started', 'Installation will continue through the connected iPhone.');
|
|
427
|
+
} catch (caught) {
|
|
428
|
+
await closeDeviceRelayTarget(selectedDevice, log);
|
|
429
|
+
const message = errorMessage(caught);
|
|
430
|
+
setError(message);
|
|
431
|
+
setStepStatus('install', 'error');
|
|
432
|
+
log('Device install relay failed', message);
|
|
433
|
+
} finally {
|
|
434
|
+
setBusyAction(undefined);
|
|
435
|
+
}
|
|
436
|
+
}, [apiUrl, cleanupDeviceAccess, log, pairRecord, selectedDevice, setStepStatus, token]);
|
|
437
|
+
|
|
438
|
+
const stopRelay = useCallback(() => {
|
|
439
|
+
void cleanupDeviceAccess();
|
|
440
|
+
log('Device relay stopped');
|
|
441
|
+
}, [cleanupDeviceAccess, log]);
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
currentStep,
|
|
445
|
+
stepStatuses,
|
|
446
|
+
device: selectedDevice?.hello,
|
|
447
|
+
hasPairRecord: !!pairRecord,
|
|
448
|
+
hasSigningAssets: !!signingAssets,
|
|
449
|
+
pairConfirmationRequired,
|
|
450
|
+
logs,
|
|
451
|
+
buildLogs,
|
|
452
|
+
buildStatus,
|
|
453
|
+
appleSigningStatus,
|
|
454
|
+
appleTeams,
|
|
455
|
+
selectedAppleTeamID,
|
|
456
|
+
buildLogPanelOpen,
|
|
457
|
+
busyAction,
|
|
458
|
+
error,
|
|
459
|
+
canBuild: !!apiUrl && !busyAction,
|
|
460
|
+
canRequestUSBAccess: !busyAction && (buildStatus === 'succeeded' || stepStatuses.build === 'complete'),
|
|
461
|
+
canPairBrowser: !!apiUrl && !busyAction && !!selectedDevice,
|
|
462
|
+
canInstall: !!apiUrl && !busyAction && !!selectedDevice && !!pairRecord,
|
|
463
|
+
setSigningFiles,
|
|
464
|
+
setBuildLogPanelOpen,
|
|
465
|
+
startAppleIDLogin,
|
|
466
|
+
submitAppleTwoFactorCode,
|
|
467
|
+
setSelectedAppleTeamID,
|
|
468
|
+
clearAppleIDSession,
|
|
469
|
+
startDeviceBuild,
|
|
470
|
+
requestUSBAccess,
|
|
471
|
+
pairBrowser,
|
|
472
|
+
startInstallation,
|
|
473
|
+
stopRelay,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function fetchStoredBuildInfo(apiUrl: string, token?: string) {
|
|
478
|
+
return fetchLimbuildInfo(apiUrl, token);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function refreshAppleTeams(
|
|
482
|
+
apiUrl: string,
|
|
483
|
+
appleSessionId: string,
|
|
484
|
+
token: string | undefined,
|
|
485
|
+
setAppleTeams: (teams: AppleDeveloperPortalTeam[]) => void,
|
|
486
|
+
setSelectedAppleTeamID: (teamID: string | undefined) => void,
|
|
487
|
+
) {
|
|
488
|
+
const response = await proxyProvisioningRequest<{
|
|
489
|
+
teams?: AppleDeveloperPortalTeam[];
|
|
490
|
+
availableProviders?: AppleDeveloperPortalTeam[];
|
|
491
|
+
provider?: AppleDeveloperPortalTeam;
|
|
492
|
+
}>(apiUrl, appleSessionId, listTeamsRequest(), token);
|
|
493
|
+
const teams = [
|
|
494
|
+
...(response.body?.teams ?? []),
|
|
495
|
+
...(response.body?.availableProviders ?? []),
|
|
496
|
+
...(response.body?.provider ? [response.body.provider] : []),
|
|
497
|
+
];
|
|
498
|
+
setAppleTeams(teams);
|
|
499
|
+
const firstTeamID = teamIDFromPortalTeam(teams[0]);
|
|
500
|
+
if (firstTeamID) {
|
|
501
|
+
setSelectedAppleTeamID(firstTeamID);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function teamIDFromPortalTeam(team?: AppleDeveloperPortalTeam) {
|
|
506
|
+
const value = team?.teamId ?? team?.providerId ?? team?.publicProviderId;
|
|
507
|
+
return value === undefined || value === '' ? undefined : String(value);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function fileToBase64(file: File) {
|
|
511
|
+
const buffer = await file.arrayBuffer();
|
|
512
|
+
let binary = '';
|
|
513
|
+
const bytes = new Uint8Array(buffer);
|
|
514
|
+
for (const byte of bytes) {
|
|
515
|
+
binary += String.fromCharCode(byte);
|
|
516
|
+
}
|
|
517
|
+
return btoa(binary);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function errorMessage(error: unknown) {
|
|
521
|
+
return error instanceof Error ? error.message : String(error);
|
|
522
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { RemoteControl } from './components/remote-control';
|
|
2
2
|
export type { RemoteControlHandle } from './components/remote-control';
|
|
3
|
+
export { DeviceInstallDialog, DeviceInstallRelay } from './components/device-install';
|
|
4
|
+
export { useDeviceInstall } from './hooks/use-device-install';
|
package/tsconfig.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": false,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"noEmit": false,
|
|
13
|
+
"emitDeclarationOnly": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationDir": "dist",
|
|
16
|
+
"jsx": "react-jsx",
|
|
17
|
+
"strict": true,
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"outDir": "dist"
|
|
22
|
+
},
|
|
23
|
+
"include": ["src"],
|
|
24
|
+
"exclude": ["**/*.test.ts", "**/*.test.tsx", "node_modules", "dist"],
|
|
25
|
+
"references": [{ "path": "./tsconfig.node.json" }]
|
|
26
|
+
}
|
package/tsconfig.node.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"composite": true,
|
|
4
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
5
|
+
"target": "ES2022",
|
|
6
|
+
"lib": ["ES2023"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": false,
|
|
16
|
+
"emitDeclarationOnly": true,
|
|
17
|
+
|
|
18
|
+
/* Linting */
|
|
19
|
+
"strict": true,
|
|
20
|
+
"noUnusedLocals": true,
|
|
21
|
+
"noUnusedParameters": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["vite.config.ts"]
|
|
26
|
+
}
|
package/vite.config.ts
CHANGED
|
@@ -9,10 +9,14 @@ export default defineConfig({
|
|
|
9
9
|
plugins: [react(), libInjectCss(), dts({ include: ['src'] })],
|
|
10
10
|
build: {
|
|
11
11
|
lib: {
|
|
12
|
-
entry:
|
|
12
|
+
entry: {
|
|
13
|
+
index: resolve(__dirname, 'src/index.ts'),
|
|
14
|
+
'device-install/index': resolve(__dirname, 'src/device-install/index.ts'),
|
|
15
|
+
'device-install/react': resolve(__dirname, 'src/device-install/react.ts'),
|
|
16
|
+
},
|
|
13
17
|
name: 'LimrunUI',
|
|
14
18
|
formats: ['es', 'cjs'],
|
|
15
|
-
fileName: (format) =>
|
|
19
|
+
fileName: (format, entryName) => `${entryName}.${format === 'es' ? 'js' : 'cjs'}`,
|
|
16
20
|
},
|
|
17
21
|
rollupOptions: {
|
|
18
22
|
external: ['react', 'react-dom', 'react/jsx-runtime'],
|