@limrun/ui 0.9.0-rc.1 → 0.9.0-rc.10
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/dist/components/inspect-overlay.d.ts +33 -0
- package/dist/components/remote-control.d.ts +86 -0
- package/dist/core/ax-fetcher.d.ts +49 -0
- package/dist/core/ax-tree.d.ts +99 -0
- package/dist/core/device-install/apple/client.d.ts +1 -0
- package/dist/core/device-install/apple/provisioning.d.ts +42 -31
- package/dist/core/device-install/apple/relay.d.ts +5 -9
- package/dist/core/device-install/storage/browser-storage.d.ts +19 -0
- package/dist/core/device-install/types.d.ts +2 -2
- package/dist/device-install/index.cjs +1 -9
- package/dist/device-install/index.js +76 -210
- package/dist/device-install/react.cjs +1 -1
- package/dist/device-install/react.js +1 -1
- package/dist/device-install-dialog-DJkKg8y6.mjs +443 -0
- package/dist/device-install-dialog-DfRemlSg.js +2 -0
- package/dist/device-install-dialog.css +1 -1
- package/dist/hooks/use-device-install.d.ts +21 -3
- package/dist/index.cjs +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1485 -778
- package/dist/use-device-install-CS201taa.js +31 -0
- package/dist/use-device-install-jz1BMMQx.mjs +13627 -0
- package/package.json +7 -3
- package/src/components/device-install/device-install-dialog.css +82 -1
- package/src/components/device-install/device-install-dialog.tsx +319 -187
- package/src/components/inspect-overlay.css +223 -0
- package/src/components/inspect-overlay.tsx +437 -0
- package/src/components/remote-control.tsx +547 -9
- package/src/core/ax-fetcher.test.ts +418 -0
- package/src/core/ax-fetcher.ts +377 -0
- package/src/core/ax-tree.test.ts +491 -0
- package/src/core/ax-tree.ts +416 -0
- package/src/core/device-install/apple/client.ts +92 -4
- package/src/core/device-install/apple/provisioning.ts +67 -24
- package/src/core/device-install/apple/relay.ts +121 -205
- package/src/core/device-install/operations/limbuild-client.ts +1 -1
- package/src/core/device-install/storage/browser-storage.ts +26 -1
- package/src/core/device-install/types.ts +2 -2
- package/src/demo.tsx +93 -10
- package/src/hooks/use-device-install.ts +766 -67
- package/src/index.ts +19 -1
- package/vitest.config.ts +23 -0
- package/dist/device-install-dialog-CTwVViYY.js +0 -2
- package/dist/device-install-dialog-zzKJu7SM.mjs +0 -328
- package/dist/use-device-install-CgrOKKyi.mjs +0 -13042
- package/dist/use-device-install-DDKRf6IL.js +0 -23
|
@@ -1,23 +1,42 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
closeDeviceRelayTarget,
|
|
4
|
+
createBundleIDRequest,
|
|
5
|
+
createDevelopmentProfileRequest,
|
|
6
|
+
downloadCertificateRequest,
|
|
7
|
+
downloadProfileRequest,
|
|
8
|
+
exportAppleCertificateP12,
|
|
4
9
|
fetchLimbuildInfo,
|
|
10
|
+
findDevelopmentCertificatesRequest,
|
|
11
|
+
findBundleIDRequest,
|
|
12
|
+
findDeviceRequest,
|
|
13
|
+
findDevelopmentProfilesRequest,
|
|
14
|
+
generateAppleSigningKeyAndCSR,
|
|
5
15
|
getPairRecord,
|
|
6
16
|
getLatestSigningAssets,
|
|
17
|
+
getLatestSigningAssetsWithCertificate,
|
|
7
18
|
getReusableAppleSigningAssets,
|
|
8
19
|
listTeamsRequest,
|
|
9
20
|
parseProvisioningProfile,
|
|
21
|
+
parseProvisioningProfileBase64,
|
|
10
22
|
profileContainsDevice,
|
|
23
|
+
profileMatchesBundleID,
|
|
11
24
|
proxyProvisioningRequest,
|
|
25
|
+
putAppleGeneratedSigningAssets,
|
|
12
26
|
putPairRecord,
|
|
13
27
|
putSigningAssets,
|
|
28
|
+
registerDeviceRequest,
|
|
14
29
|
requestUSBAccess as requestDeviceUSBAccess,
|
|
15
30
|
startBrowserOwnedAppleIDLogin,
|
|
16
31
|
startSignedDeviceBuild,
|
|
17
32
|
startInstallRelay,
|
|
18
33
|
startPairingRelay,
|
|
34
|
+
submitDevelopmentCSRRequest,
|
|
19
35
|
watchBuildLogEvents,
|
|
20
36
|
type AppleIDLoginResult,
|
|
37
|
+
type AppleDeveloperPortalDevice,
|
|
38
|
+
type AppleDeveloperPortalAppID,
|
|
39
|
+
type AppleDeveloperPortalResponse,
|
|
21
40
|
type AppleDeveloperPortalTeam,
|
|
22
41
|
type BuildLogLine,
|
|
23
42
|
type DeviceInstallBuildStatus,
|
|
@@ -32,6 +51,13 @@ import type { RelayClient } from '../core/device-install/operations';
|
|
|
32
51
|
|
|
33
52
|
type DeviceInstallStepStatuses = Record<DeviceInstallStep, DeviceInstallStepStatus>;
|
|
34
53
|
|
|
54
|
+
type ReusableAppleCertificate = Pick<
|
|
55
|
+
StoredSigningAssets,
|
|
56
|
+
'certificateID' | 'certificateP12Base64' | 'certificatePassword' | 'teamID'
|
|
57
|
+
> & {
|
|
58
|
+
certificateID: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
35
61
|
export type UseDeviceInstallOptions = {
|
|
36
62
|
apiUrl?: string;
|
|
37
63
|
token?: string;
|
|
@@ -43,26 +69,40 @@ export type UseDeviceInstallResult = {
|
|
|
43
69
|
device?: DeviceInstallDevice;
|
|
44
70
|
hasPairRecord: boolean;
|
|
45
71
|
hasSigningAssets: boolean;
|
|
72
|
+
hasSigningInputs: boolean;
|
|
46
73
|
pairConfirmationRequired: boolean;
|
|
47
74
|
logs: string[];
|
|
48
75
|
buildLogs: BuildLogLine[];
|
|
49
76
|
buildStatus: DeviceInstallBuildStatus;
|
|
50
77
|
appleSigningStatus: DeviceInstallAppleSigningStatus;
|
|
51
78
|
appleTeams: AppleDeveloperPortalTeam[];
|
|
79
|
+
appleDevices: AppleDeveloperPortalDevice[];
|
|
80
|
+
appleAppIDs: AppleDeveloperPortalAppID[];
|
|
81
|
+
applePortalSummary?: ApplePortalSummary;
|
|
52
82
|
selectedAppleTeamID?: string;
|
|
83
|
+
selectedAppleDeviceIDs: string[];
|
|
84
|
+
connectedAppleDeviceRegistered: boolean;
|
|
85
|
+
connectedDeviceInProfile?: boolean;
|
|
86
|
+
hasReusableAppleCertificate: boolean;
|
|
87
|
+
appleBundleID: string;
|
|
53
88
|
buildLogPanelOpen: boolean;
|
|
54
89
|
busyAction?: DeviceInstallBusyAction;
|
|
55
90
|
error?: string;
|
|
56
91
|
canBuild: boolean;
|
|
92
|
+
canPrepareAppleSigningAssets: boolean;
|
|
57
93
|
canRequestUSBAccess: boolean;
|
|
58
94
|
canPairBrowser: boolean;
|
|
59
95
|
canInstall: boolean;
|
|
60
96
|
setSigningFiles: (files: DeviceInstallSigningFiles) => void;
|
|
97
|
+
setAppleBundleID: (bundleID: string) => void;
|
|
98
|
+
setSelectedAppleDeviceIDs: (deviceIDs: string[]) => void;
|
|
61
99
|
setBuildLogPanelOpen: (open: boolean) => void;
|
|
62
100
|
startAppleIDLogin: (input: DeviceInstallAppleIDLoginInput) => Promise<void>;
|
|
63
101
|
submitAppleTwoFactorCode: (code: string) => Promise<void>;
|
|
64
102
|
setSelectedAppleTeamID: (teamID: string | undefined) => void;
|
|
65
|
-
|
|
103
|
+
clearAppleIDLogin: () => void;
|
|
104
|
+
registerConnectedAppleDevice: () => Promise<void>;
|
|
105
|
+
prepareAppleSigningAssets: () => Promise<void>;
|
|
66
106
|
startDeviceBuild: () => Promise<void>;
|
|
67
107
|
requestUSBAccess: () => Promise<void>;
|
|
68
108
|
pairBrowser: () => Promise<void>;
|
|
@@ -76,6 +116,7 @@ export type DeviceInstallAppleSigningStatus =
|
|
|
76
116
|
| 'two-factor-required'
|
|
77
117
|
| 'authenticated'
|
|
78
118
|
| 'preparing-assets'
|
|
119
|
+
| 'assets-ready'
|
|
79
120
|
| 'using-cached-profile'
|
|
80
121
|
| 'error';
|
|
81
122
|
|
|
@@ -96,10 +137,15 @@ export type DeviceInstallAppleIDLoginInput = {
|
|
|
96
137
|
password: string;
|
|
97
138
|
};
|
|
98
139
|
|
|
140
|
+
export type ApplePortalSummary = {
|
|
141
|
+
certificateCount: number;
|
|
142
|
+
profileCount: number;
|
|
143
|
+
};
|
|
144
|
+
|
|
99
145
|
const initialStepStatuses: DeviceInstallStepStatuses = {
|
|
146
|
+
signing: 'idle',
|
|
147
|
+
connect: 'idle',
|
|
100
148
|
build: 'idle',
|
|
101
|
-
usb: 'idle',
|
|
102
|
-
pair: 'idle',
|
|
103
149
|
install: 'idle',
|
|
104
150
|
};
|
|
105
151
|
|
|
@@ -107,19 +153,25 @@ export function useDeviceInstall({
|
|
|
107
153
|
apiUrl,
|
|
108
154
|
token,
|
|
109
155
|
}: UseDeviceInstallOptions): UseDeviceInstallResult {
|
|
110
|
-
const [currentStep, setCurrentStep] = useState<DeviceInstallStep>('
|
|
156
|
+
const [currentStep, setCurrentStep] = useState<DeviceInstallStep>('signing');
|
|
111
157
|
const [stepStatuses, setStepStatuses] = useState<DeviceInstallStepStatuses>(initialStepStatuses);
|
|
112
158
|
const [selectedDevice, setSelectedDevice] = useState<DeviceRelayTarget | undefined>();
|
|
113
159
|
const [pairRecord, setPairRecord] = useState<StoredPairRecord | undefined>();
|
|
114
160
|
const [signingAssets, setSigningAssets] = useState<StoredSigningAssets | undefined>();
|
|
115
161
|
const [logs, setLogs] = useState<string[]>([
|
|
116
|
-
'Ready.
|
|
162
|
+
'Ready. Prepare signing assets, build, connect and pair the iPhone, then install.',
|
|
117
163
|
]);
|
|
118
164
|
const [buildLogs, setBuildLogs] = useState<BuildLogLine[]>([]);
|
|
119
165
|
const [buildStatus, setBuildStatus] = useState<DeviceInstallBuildStatus>('idle');
|
|
120
166
|
const [appleSigningStatus, setAppleSigningStatus] = useState<DeviceInstallAppleSigningStatus>('idle');
|
|
121
167
|
const [appleTeams, setAppleTeams] = useState<AppleDeveloperPortalTeam[]>([]);
|
|
168
|
+
const [appleDevices, setAppleDevices] = useState<AppleDeveloperPortalDevice[]>([]);
|
|
169
|
+
const [appleAppIDs, setAppleAppIDs] = useState<AppleDeveloperPortalAppID[]>([]);
|
|
170
|
+
const [selectedAppleDeviceIDs, setSelectedAppleDeviceIDs] = useState<string[]>([]);
|
|
171
|
+
const [applePortalSummary, setApplePortalSummary] = useState<ApplePortalSummary | undefined>();
|
|
122
172
|
const [selectedAppleTeamID, setSelectedAppleTeamID] = useState<string | undefined>();
|
|
173
|
+
const [appleBundleID, setAppleBundleID] = useState('');
|
|
174
|
+
const [reusableAppleCertificate, setReusableAppleCertificate] = useState<ReusableAppleCertificate | undefined>();
|
|
123
175
|
const [buildLogPanelOpen, setBuildLogPanelOpen] = useState(false);
|
|
124
176
|
const [busyAction, setBusyAction] = useState<DeviceInstallBusyAction | undefined>();
|
|
125
177
|
const [error, setError] = useState<string | undefined>();
|
|
@@ -128,7 +180,7 @@ export function useDeviceInstall({
|
|
|
128
180
|
const relayRef = useRef<RelayClient | undefined>(undefined);
|
|
129
181
|
const selectedDeviceRef = useRef<DeviceRelayTarget | undefined>(undefined);
|
|
130
182
|
const stopBuildWatcherRef = useRef<(() => void) | undefined>(undefined);
|
|
131
|
-
const
|
|
183
|
+
const appleIDLoginRef = useRef<AppleIDLoginResult | undefined>(undefined);
|
|
132
184
|
|
|
133
185
|
const log = useCallback((message: string, detail?: string) => {
|
|
134
186
|
const line = detail ? `${message}\n${detail}` : message;
|
|
@@ -140,9 +192,67 @@ export function useDeviceInstall({
|
|
|
140
192
|
}, []);
|
|
141
193
|
|
|
142
194
|
const setSigningFiles = useCallback((files: DeviceInstallSigningFiles) => {
|
|
143
|
-
setSigningFilesState((current) =>
|
|
195
|
+
setSigningFilesState((current) => {
|
|
196
|
+
const next = { ...current, ...files };
|
|
197
|
+
const ready = !!next.certificateFile && !!next.provisioningProfileFile && !!next.certificatePassword;
|
|
198
|
+
setStepStatus('signing', ready ? 'complete' : 'active');
|
|
199
|
+
if (ready) {
|
|
200
|
+
setAppleSigningStatus('assets-ready');
|
|
201
|
+
setCurrentStep('build');
|
|
202
|
+
}
|
|
203
|
+
return next;
|
|
204
|
+
});
|
|
144
205
|
setSigningAssets(undefined);
|
|
145
|
-
}, []);
|
|
206
|
+
}, [setStepStatus]);
|
|
207
|
+
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
let cancelled = false;
|
|
210
|
+
void getLatestSigningAssets().then((stored) => {
|
|
211
|
+
if (cancelled || !stored) return;
|
|
212
|
+
setSigningAssets(stored);
|
|
213
|
+
setAppleBundleID(stored.bundleID);
|
|
214
|
+
setAppleSigningStatus('using-cached-profile');
|
|
215
|
+
setStepStatus('signing', 'complete');
|
|
216
|
+
setCurrentStep('build');
|
|
217
|
+
log('Using stored signing assets', stored.bundleID);
|
|
218
|
+
});
|
|
219
|
+
return () => {
|
|
220
|
+
cancelled = true;
|
|
221
|
+
};
|
|
222
|
+
}, [log, setStepStatus]);
|
|
223
|
+
|
|
224
|
+
const selectedDeveloperTeamID = useCallback(() => {
|
|
225
|
+
return developerPortalTeamID(appleTeams.find((team) => appleTeamSelectionID(team) === selectedAppleTeamID));
|
|
226
|
+
}, [appleTeams, selectedAppleTeamID]);
|
|
227
|
+
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
const teamID = selectedDeveloperTeamID();
|
|
230
|
+
if (!teamID) {
|
|
231
|
+
setReusableAppleCertificate(undefined);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
let cancelled = false;
|
|
235
|
+
void findReusableAppleCertificate(teamID).then((certificate) => {
|
|
236
|
+
if (!cancelled) {
|
|
237
|
+
setReusableAppleCertificate(certificate);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
return () => {
|
|
241
|
+
cancelled = true;
|
|
242
|
+
};
|
|
243
|
+
}, [selectedDeveloperTeamID]);
|
|
244
|
+
|
|
245
|
+
const connectedAppleDevice = selectedDevice?.hello.serialNumber
|
|
246
|
+
? appleDevices.find((device) => normalizeAppleUDID(device.deviceNumber) === normalizeAppleUDID(selectedDevice.hello.serialNumber))
|
|
247
|
+
: undefined;
|
|
248
|
+
const connectedAppleDeviceRegistered = !!connectedAppleDevice?.deviceId;
|
|
249
|
+
const manualSigningFilesReady =
|
|
250
|
+
!!signingFiles.certificateFile && !!signingFiles.provisioningProfileFile && !!signingFiles.certificatePassword;
|
|
251
|
+
const signingInputsReady = !!signingAssets || manualSigningFilesReady;
|
|
252
|
+
const connectedDeviceInProfile =
|
|
253
|
+
selectedDevice?.hello.serialNumber && signingAssets
|
|
254
|
+
? profileContainsDevice(signingAssets.profile, selectedDevice.hello.serialNumber)
|
|
255
|
+
: undefined;
|
|
146
256
|
|
|
147
257
|
useEffect(() => {
|
|
148
258
|
selectedDeviceRef.current = selectedDevice;
|
|
@@ -157,18 +267,27 @@ export function useDeviceInstall({
|
|
|
157
267
|
useEffect(() => {
|
|
158
268
|
return () => {
|
|
159
269
|
stopBuildWatcherRef.current?.();
|
|
160
|
-
void
|
|
270
|
+
void appleIDLoginRef.current?.close();
|
|
271
|
+
appleIDLoginRef.current = undefined;
|
|
161
272
|
void cleanupDeviceAccess();
|
|
162
273
|
};
|
|
163
274
|
}, [cleanupDeviceAccess]);
|
|
164
275
|
|
|
165
276
|
const resolveSigningAssetsForBuild = useCallback(async () => {
|
|
166
|
-
const
|
|
167
|
-
if (
|
|
277
|
+
const requestedBundleID = appleBundleID.trim();
|
|
278
|
+
if (!requestedBundleID || (signingAssets && profileMatchesBundleID(signingAssets.profile, requestedBundleID))) {
|
|
279
|
+
if (signingAssets) {
|
|
280
|
+
log('Using prepared signing assets', signingAssets.bundleID);
|
|
281
|
+
return signingAssets;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const info = requestedBundleID ? undefined : apiUrl ? await fetchStoredBuildInfo(apiUrl, token).catch(() => undefined) : undefined;
|
|
285
|
+
const bundleID = requestedBundleID || info?.lastBuildConfig?.bundleId;
|
|
286
|
+
if (bundleID) {
|
|
168
287
|
const cached = await getReusableAppleSigningAssets({
|
|
169
|
-
bundleID
|
|
288
|
+
bundleID,
|
|
170
289
|
deviceUDID: selectedDevice?.hello.serialNumber,
|
|
171
|
-
teamID:
|
|
290
|
+
teamID: selectedDeveloperTeamID(),
|
|
172
291
|
});
|
|
173
292
|
if (cached) {
|
|
174
293
|
setAppleSigningStatus('using-cached-profile');
|
|
@@ -213,21 +332,43 @@ export function useDeviceInstall({
|
|
|
213
332
|
setSigningAssets(storedAssets);
|
|
214
333
|
log('Signing assets stored locally', storageBundleId);
|
|
215
334
|
return storedAssets;
|
|
216
|
-
}, [apiUrl, log,
|
|
335
|
+
}, [apiUrl, appleBundleID, log, selectedDeveloperTeamID, selectedDevice?.hello.serialNumber, signingAssets, signingFiles, token]);
|
|
217
336
|
|
|
218
337
|
const startAppleIDLogin = useCallback(
|
|
219
338
|
async ({ accountName, password }: DeviceInstallAppleIDLoginInput) => {
|
|
220
339
|
if (!apiUrl) return;
|
|
221
|
-
setBusyAction('
|
|
340
|
+
setBusyAction('signing');
|
|
222
341
|
setError(undefined);
|
|
342
|
+
setCurrentStep('signing');
|
|
343
|
+
setStepStatus('signing', 'active');
|
|
223
344
|
setAppleSigningStatus('authenticating');
|
|
224
345
|
try {
|
|
225
|
-
await
|
|
346
|
+
await appleIDLoginRef.current?.close().catch(() => undefined);
|
|
226
347
|
const session = await startBrowserOwnedAppleIDLogin({ limbuildApiUrl: apiUrl, token, accountName, password });
|
|
227
|
-
|
|
348
|
+
appleIDLoginRef.current = session;
|
|
228
349
|
if (!session.requiresTwoFactor) {
|
|
229
|
-
await session.finalize();
|
|
230
|
-
await refreshAppleTeams(
|
|
350
|
+
const accountSession = await session.finalize().catch(() => undefined);
|
|
351
|
+
const teamID = await refreshAppleTeams(
|
|
352
|
+
apiUrl,
|
|
353
|
+
session.appleSessionId,
|
|
354
|
+
token,
|
|
355
|
+
setAppleTeams,
|
|
356
|
+
setSelectedAppleTeamID,
|
|
357
|
+
accountSession?.body as AppleDeveloperPortalResponse | undefined,
|
|
358
|
+
);
|
|
359
|
+
await refreshAppleAppIDs(apiUrl, session.appleSessionId, token, teamID, setAppleAppIDs, setAppleBundleID);
|
|
360
|
+
if (teamID) {
|
|
361
|
+
await refreshAppleDevices({
|
|
362
|
+
apiUrl,
|
|
363
|
+
token,
|
|
364
|
+
appleSessionId: session.appleSessionId,
|
|
365
|
+
teamID,
|
|
366
|
+
setAppleDevices,
|
|
367
|
+
setSelectedAppleDeviceIDs,
|
|
368
|
+
log,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
await refreshApplePortalSummary(apiUrl, session.appleSessionId, token, teamID, setApplePortalSummary, log);
|
|
231
372
|
}
|
|
232
373
|
setAppleSigningStatus(session.requiresTwoFactor ? 'two-factor-required' : 'authenticated');
|
|
233
374
|
log(
|
|
@@ -243,22 +384,44 @@ export function useDeviceInstall({
|
|
|
243
384
|
setBusyAction(undefined);
|
|
244
385
|
}
|
|
245
386
|
},
|
|
246
|
-
[apiUrl, log, token],
|
|
387
|
+
[apiUrl, log, setStepStatus, token],
|
|
247
388
|
);
|
|
248
389
|
|
|
249
390
|
const submitAppleTwoFactorCode = useCallback(
|
|
250
391
|
async (code: string) => {
|
|
251
|
-
const session =
|
|
392
|
+
const session = appleIDLoginRef.current;
|
|
252
393
|
if (!session) {
|
|
253
394
|
throw new Error('Start Apple ID login before submitting a two-factor code.');
|
|
254
395
|
}
|
|
255
|
-
setBusyAction('
|
|
396
|
+
setBusyAction('signing');
|
|
256
397
|
setError(undefined);
|
|
398
|
+
setCurrentStep('signing');
|
|
399
|
+
setStepStatus('signing', 'active');
|
|
257
400
|
try {
|
|
258
401
|
await session.finishTwoFactor(code);
|
|
259
|
-
await session.finalize();
|
|
260
402
|
if (apiUrl) {
|
|
261
|
-
await
|
|
403
|
+
const accountSession = await session.finalize().catch(() => undefined);
|
|
404
|
+
const teamID = await refreshAppleTeams(
|
|
405
|
+
apiUrl,
|
|
406
|
+
session.appleSessionId,
|
|
407
|
+
token,
|
|
408
|
+
setAppleTeams,
|
|
409
|
+
setSelectedAppleTeamID,
|
|
410
|
+
accountSession?.body as AppleDeveloperPortalResponse | undefined,
|
|
411
|
+
);
|
|
412
|
+
await refreshAppleAppIDs(apiUrl, session.appleSessionId, token, teamID, setAppleAppIDs, setAppleBundleID);
|
|
413
|
+
if (teamID) {
|
|
414
|
+
await refreshAppleDevices({
|
|
415
|
+
apiUrl,
|
|
416
|
+
token,
|
|
417
|
+
appleSessionId: session.appleSessionId,
|
|
418
|
+
teamID,
|
|
419
|
+
setAppleDevices,
|
|
420
|
+
setSelectedAppleDeviceIDs,
|
|
421
|
+
log,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
await refreshApplePortalSummary(apiUrl, session.appleSessionId, token, teamID, setApplePortalSummary, log);
|
|
262
425
|
}
|
|
263
426
|
setAppleSigningStatus('authenticated');
|
|
264
427
|
log('Apple ID two-factor authentication accepted');
|
|
@@ -271,17 +434,183 @@ export function useDeviceInstall({
|
|
|
271
434
|
setBusyAction(undefined);
|
|
272
435
|
}
|
|
273
436
|
},
|
|
274
|
-
[apiUrl, log, token],
|
|
437
|
+
[apiUrl, log, setStepStatus, token],
|
|
275
438
|
);
|
|
276
439
|
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
440
|
+
const clearAppleIDLogin = useCallback(() => {
|
|
441
|
+
void appleIDLoginRef.current?.close();
|
|
442
|
+
appleIDLoginRef.current = undefined;
|
|
280
443
|
setAppleTeams([]);
|
|
444
|
+
setAppleDevices([]);
|
|
445
|
+
setAppleAppIDs([]);
|
|
446
|
+
setSelectedAppleDeviceIDs([]);
|
|
447
|
+
setApplePortalSummary(undefined);
|
|
281
448
|
setSelectedAppleTeamID(undefined);
|
|
449
|
+
setReusableAppleCertificate(undefined);
|
|
282
450
|
setAppleSigningStatus('idle');
|
|
283
|
-
|
|
284
|
-
|
|
451
|
+
setStepStatus('signing', 'idle');
|
|
452
|
+
log('Apple ID login state cleared');
|
|
453
|
+
}, [log, setStepStatus]);
|
|
454
|
+
|
|
455
|
+
const selectAppleTeam = useCallback(
|
|
456
|
+
(teamID: string | undefined) => {
|
|
457
|
+
setSelectedAppleTeamID(teamID);
|
|
458
|
+
setAppleAppIDs([]);
|
|
459
|
+
setAppleDevices([]);
|
|
460
|
+
setSelectedAppleDeviceIDs([]);
|
|
461
|
+
setApplePortalSummary(undefined);
|
|
462
|
+
setAppleBundleID('');
|
|
463
|
+
setSigningAssets(undefined);
|
|
464
|
+
const session = appleIDLoginRef.current;
|
|
465
|
+
if (!apiUrl || !session || !teamID) return;
|
|
466
|
+
const developerTeamID = developerPortalTeamID(appleTeams.find((team) => appleTeamSelectionID(team) === teamID));
|
|
467
|
+
if (!developerTeamID) return;
|
|
468
|
+
void (async () => {
|
|
469
|
+
try {
|
|
470
|
+
await refreshAppleAppIDs(apiUrl, session.appleSessionId, token, developerTeamID, setAppleAppIDs, setAppleBundleID);
|
|
471
|
+
await refreshApplePortalSummary(apiUrl, session.appleSessionId, token, developerTeamID, setApplePortalSummary, log);
|
|
472
|
+
await refreshAppleDevices({
|
|
473
|
+
apiUrl,
|
|
474
|
+
token,
|
|
475
|
+
appleSessionId: session.appleSessionId,
|
|
476
|
+
teamID: developerTeamID,
|
|
477
|
+
connectedUDID: selectedDeviceRef.current?.hello.serialNumber,
|
|
478
|
+
setAppleDevices,
|
|
479
|
+
setSelectedAppleDeviceIDs,
|
|
480
|
+
log,
|
|
481
|
+
});
|
|
482
|
+
} catch (caught) {
|
|
483
|
+
const message = errorMessage(caught);
|
|
484
|
+
setError(message);
|
|
485
|
+
log('Apple team refresh failed', message);
|
|
486
|
+
}
|
|
487
|
+
})();
|
|
488
|
+
},
|
|
489
|
+
[apiUrl, appleTeams, log, token],
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
const registerConnectedAppleDevice = useCallback(async () => {
|
|
493
|
+
const teamID = selectedDeveloperTeamID();
|
|
494
|
+
if (!apiUrl || !appleIDLoginRef.current || !selectedDevice?.hello.serialNumber || !teamID) return;
|
|
495
|
+
setBusyAction('signing');
|
|
496
|
+
setError(undefined);
|
|
497
|
+
try {
|
|
498
|
+
const normalizedUDID = normalizeAppleUDID(selectedDevice.hello.serialNumber);
|
|
499
|
+
const created = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
|
|
500
|
+
apiUrl,
|
|
501
|
+
appleIDLoginRef.current.appleSessionId,
|
|
502
|
+
registerDeviceRequest({
|
|
503
|
+
deviceUDID: normalizedUDID,
|
|
504
|
+
teamID,
|
|
505
|
+
name: selectedDevice.hello.productName ?? 'Limrun iPhone',
|
|
506
|
+
}),
|
|
507
|
+
token,
|
|
508
|
+
);
|
|
509
|
+
assertApplePortalResponseOK(created.body, 'Apple device registration');
|
|
510
|
+
await refreshAppleDevices({
|
|
511
|
+
apiUrl,
|
|
512
|
+
token,
|
|
513
|
+
appleSessionId: appleIDLoginRef.current.appleSessionId,
|
|
514
|
+
teamID,
|
|
515
|
+
connectedUDID: selectedDevice.hello.serialNumber,
|
|
516
|
+
setAppleDevices,
|
|
517
|
+
setSelectedAppleDeviceIDs,
|
|
518
|
+
log,
|
|
519
|
+
});
|
|
520
|
+
log('Connected iPhone registered with Apple Developer', normalizedUDID);
|
|
521
|
+
} catch (caught) {
|
|
522
|
+
const message = errorMessage(caught);
|
|
523
|
+
setError(message);
|
|
524
|
+
log('Apple device registration failed', message);
|
|
525
|
+
} finally {
|
|
526
|
+
setBusyAction(undefined);
|
|
527
|
+
}
|
|
528
|
+
}, [apiUrl, log, selectedDeveloperTeamID, selectedDevice?.hello.productName, selectedDevice?.hello.serialNumber, token]);
|
|
529
|
+
|
|
530
|
+
const prepareAppleSigningAssets = useCallback(async () => {
|
|
531
|
+
if (!apiUrl || !appleIDLoginRef.current) return;
|
|
532
|
+
const bundleID = appleBundleID.trim();
|
|
533
|
+
if (!bundleID) {
|
|
534
|
+
throw new Error('Enter a bundle ID before preparing signing assets.');
|
|
535
|
+
}
|
|
536
|
+
if (!selectedAppleTeamID) {
|
|
537
|
+
throw new Error('Select an Apple Developer team before preparing signing assets.');
|
|
538
|
+
}
|
|
539
|
+
const teamID = selectedDeveloperTeamID();
|
|
540
|
+
if (!teamID) {
|
|
541
|
+
throw new Error('Selected Apple team does not include a Developer Portal team ID.');
|
|
542
|
+
}
|
|
543
|
+
if (selectedAppleDeviceIDs.length === 0) {
|
|
544
|
+
throw new Error('Select at least one Apple Developer device before preparing signing assets.');
|
|
545
|
+
}
|
|
546
|
+
if (!reusableAppleCertificate && !signingFiles.certificatePassword) {
|
|
547
|
+
throw new Error('Enter a .p12 password before preparing signing assets.');
|
|
548
|
+
}
|
|
549
|
+
const selectedPortalDevice = appleDevices.find(
|
|
550
|
+
(device) => !!device.deviceNumber && selectedAppleDeviceIDs.includes(device.deviceId ?? ''),
|
|
551
|
+
);
|
|
552
|
+
const signingDeviceUDID = selectedDevice?.hello.serialNumber ?? selectedPortalDevice?.deviceNumber;
|
|
553
|
+
setBusyAction('signing');
|
|
554
|
+
setError(undefined);
|
|
555
|
+
setCurrentStep('signing');
|
|
556
|
+
setStepStatus('signing', 'active');
|
|
557
|
+
setAppleSigningStatus('preparing-assets');
|
|
558
|
+
try {
|
|
559
|
+
const cached = await getReusableAppleSigningAssets({
|
|
560
|
+
bundleID,
|
|
561
|
+
deviceUDID: signingDeviceUDID,
|
|
562
|
+
teamID,
|
|
563
|
+
});
|
|
564
|
+
if (cached) {
|
|
565
|
+
setSigningAssets(cached);
|
|
566
|
+
setAppleSigningStatus('assets-ready');
|
|
567
|
+
setStepStatus('signing', 'complete');
|
|
568
|
+
setCurrentStep('build');
|
|
569
|
+
log('Using cached Apple signing assets', bundleID);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const assets = await prepareAppleSigningAssetsForDevice({
|
|
573
|
+
apiUrl,
|
|
574
|
+
token,
|
|
575
|
+
appleSessionId: appleIDLoginRef.current.appleSessionId,
|
|
576
|
+
teamID,
|
|
577
|
+
bundleID,
|
|
578
|
+
deviceUDID: signingDeviceUDID,
|
|
579
|
+
deviceIDs: selectedAppleDeviceIDs,
|
|
580
|
+
certificatePassword: signingFiles.certificatePassword,
|
|
581
|
+
reusableCertificate: reusableAppleCertificate,
|
|
582
|
+
});
|
|
583
|
+
setSigningAssets(assets);
|
|
584
|
+
setAppleSigningStatus('assets-ready');
|
|
585
|
+
setStepStatus('signing', 'complete');
|
|
586
|
+
setCurrentStep('build');
|
|
587
|
+
log(
|
|
588
|
+
'Apple signing assets stored locally',
|
|
589
|
+
signingDeviceUDID ? `${bundleID} for ${signingDeviceUDID}` : `${bundleID} for selected Apple devices`,
|
|
590
|
+
);
|
|
591
|
+
} catch (caught) {
|
|
592
|
+
const message = errorMessage(caught);
|
|
593
|
+
setError(message);
|
|
594
|
+
setAppleSigningStatus('error');
|
|
595
|
+
log('Apple signing asset preparation failed', message);
|
|
596
|
+
} finally {
|
|
597
|
+
setBusyAction(undefined);
|
|
598
|
+
}
|
|
599
|
+
}, [
|
|
600
|
+
apiUrl,
|
|
601
|
+
appleBundleID,
|
|
602
|
+
appleDevices,
|
|
603
|
+
appleTeams,
|
|
604
|
+
log,
|
|
605
|
+
selectedAppleTeamID,
|
|
606
|
+
selectedAppleDeviceIDs,
|
|
607
|
+
selectedDeveloperTeamID,
|
|
608
|
+
selectedDevice?.hello.serialNumber,
|
|
609
|
+
setStepStatus,
|
|
610
|
+
reusableAppleCertificate,
|
|
611
|
+
signingFiles.certificatePassword,
|
|
612
|
+
token,
|
|
613
|
+
]);
|
|
285
614
|
|
|
286
615
|
const startDeviceBuild = useCallback(async () => {
|
|
287
616
|
if (!apiUrl) return;
|
|
@@ -316,7 +645,8 @@ export function useDeviceInstall({
|
|
|
316
645
|
setBuildStatus(status);
|
|
317
646
|
if (status === 'succeeded') {
|
|
318
647
|
setStepStatus('build', 'complete');
|
|
319
|
-
|
|
648
|
+
setStepStatus('connect', 'active');
|
|
649
|
+
setCurrentStep('connect');
|
|
320
650
|
} else if (status === 'failed' || status === 'cancelled') {
|
|
321
651
|
setStepStatus('build', 'error');
|
|
322
652
|
}
|
|
@@ -341,42 +671,61 @@ export function useDeviceInstall({
|
|
|
341
671
|
const requestUSBAccess = useCallback(async () => {
|
|
342
672
|
setBusyAction('usb');
|
|
343
673
|
setError(undefined);
|
|
344
|
-
setCurrentStep('
|
|
345
|
-
setStepStatus('
|
|
674
|
+
setCurrentStep('connect');
|
|
675
|
+
setStepStatus('connect', 'active');
|
|
676
|
+
let target: DeviceRelayTarget | undefined;
|
|
346
677
|
try {
|
|
347
678
|
await cleanupDeviceAccess();
|
|
348
|
-
|
|
349
|
-
setSelectedDevice(target);
|
|
679
|
+
target = await requestDeviceUSBAccess({ log });
|
|
350
680
|
setPairConfirmationRequired(false);
|
|
351
681
|
const storedPairRecord = await getPairRecord(target.hello.serialNumber);
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (!profileContainsDevice(storedSigningAssets.profile, target.hello.serialNumber)) {
|
|
682
|
+
const activeSigningAssets = signingAssets ?? (manualSigningFilesReady ? undefined : await getLatestSigningAssets());
|
|
683
|
+
if (activeSigningAssets) {
|
|
684
|
+
if (!profileContainsDevice(activeSigningAssets.profile, target.hello.serialNumber)) {
|
|
356
685
|
throw new Error('Stored provisioning profile does not include the selected iPhone.');
|
|
357
686
|
}
|
|
358
|
-
setSigningAssets(
|
|
687
|
+
setSigningAssets(activeSigningAssets);
|
|
359
688
|
}
|
|
360
|
-
|
|
361
|
-
|
|
689
|
+
if (apiUrl && appleIDLoginRef.current) {
|
|
690
|
+
const teamID = selectedDeveloperTeamID();
|
|
691
|
+
if (teamID) {
|
|
692
|
+
await refreshAppleDevices({
|
|
693
|
+
apiUrl,
|
|
694
|
+
token,
|
|
695
|
+
appleSessionId: appleIDLoginRef.current.appleSessionId,
|
|
696
|
+
teamID,
|
|
697
|
+
connectedUDID: target.hello.serialNumber,
|
|
698
|
+
setAppleDevices,
|
|
699
|
+
setSelectedAppleDeviceIDs,
|
|
700
|
+
log,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
setSelectedDevice(target);
|
|
705
|
+
setPairRecord(storedPairRecord);
|
|
706
|
+
setStepStatus('connect', storedPairRecord ? 'complete' : 'active');
|
|
707
|
+
setCurrentStep(storedPairRecord ? 'install' : 'connect');
|
|
362
708
|
log(storedPairRecord ? 'Pair record found' : 'No pair record found', target.hello.serialNumber);
|
|
363
709
|
} catch (caught) {
|
|
710
|
+
await closeDeviceRelayTarget(target, log);
|
|
711
|
+
setSelectedDevice(undefined);
|
|
712
|
+
setPairRecord(undefined);
|
|
364
713
|
const message = errorMessage(caught);
|
|
365
714
|
setError(message);
|
|
366
|
-
setStepStatus('
|
|
715
|
+
setStepStatus('connect', 'error');
|
|
367
716
|
log('USB access failed', message);
|
|
368
717
|
} finally {
|
|
369
718
|
setBusyAction(undefined);
|
|
370
719
|
}
|
|
371
|
-
}, [cleanupDeviceAccess, log, setStepStatus]);
|
|
720
|
+
}, [apiUrl, cleanupDeviceAccess, log, manualSigningFilesReady, selectedDeveloperTeamID, setStepStatus, signingAssets, token]);
|
|
372
721
|
|
|
373
722
|
const pairBrowser = useCallback(async () => {
|
|
374
723
|
if (!apiUrl || !selectedDevice) return;
|
|
375
724
|
setBusyAction('pair');
|
|
376
725
|
setError(undefined);
|
|
377
726
|
setPairConfirmationRequired(false);
|
|
378
|
-
setCurrentStep('
|
|
379
|
-
setStepStatus('
|
|
727
|
+
setCurrentStep('connect');
|
|
728
|
+
setStepStatus('connect', 'active');
|
|
380
729
|
try {
|
|
381
730
|
await cleanupDeviceAccess();
|
|
382
731
|
const result = await startPairingRelay({
|
|
@@ -392,7 +741,7 @@ export function useDeviceInstall({
|
|
|
392
741
|
await closeDeviceRelayTarget(selectedDevice, log);
|
|
393
742
|
setPairRecord(stored);
|
|
394
743
|
setPairConfirmationRequired(false);
|
|
395
|
-
setStepStatus('
|
|
744
|
+
setStepStatus('connect', 'complete');
|
|
396
745
|
setCurrentStep('install');
|
|
397
746
|
log('Device paired', 'The pair record was stored locally in this browser.');
|
|
398
747
|
} catch (caught) {
|
|
@@ -400,7 +749,7 @@ export function useDeviceInstall({
|
|
|
400
749
|
const message = errorMessage(caught);
|
|
401
750
|
setPairConfirmationRequired(true);
|
|
402
751
|
setError('Unlock the iPhone, tap Trust, then confirm the pair record.');
|
|
403
|
-
setStepStatus('
|
|
752
|
+
setStepStatus('connect', 'error');
|
|
404
753
|
log('Device pairing failed', message);
|
|
405
754
|
} finally {
|
|
406
755
|
setBusyAction(undefined);
|
|
@@ -446,26 +795,50 @@ export function useDeviceInstall({
|
|
|
446
795
|
device: selectedDevice?.hello,
|
|
447
796
|
hasPairRecord: !!pairRecord,
|
|
448
797
|
hasSigningAssets: !!signingAssets,
|
|
798
|
+
hasSigningInputs: signingInputsReady,
|
|
449
799
|
pairConfirmationRequired,
|
|
450
800
|
logs,
|
|
451
801
|
buildLogs,
|
|
452
802
|
buildStatus,
|
|
453
803
|
appleSigningStatus,
|
|
454
804
|
appleTeams,
|
|
805
|
+
appleDevices,
|
|
806
|
+
appleAppIDs,
|
|
807
|
+
applePortalSummary,
|
|
455
808
|
selectedAppleTeamID,
|
|
809
|
+
selectedAppleDeviceIDs,
|
|
810
|
+
connectedAppleDeviceRegistered,
|
|
811
|
+
connectedDeviceInProfile,
|
|
812
|
+
hasReusableAppleCertificate: !!reusableAppleCertificate,
|
|
813
|
+
appleBundleID,
|
|
456
814
|
buildLogPanelOpen,
|
|
457
815
|
busyAction,
|
|
458
816
|
error,
|
|
459
|
-
canBuild:
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
817
|
+
canBuild:
|
|
818
|
+
!!apiUrl &&
|
|
819
|
+
!busyAction &&
|
|
820
|
+
signingInputsReady,
|
|
821
|
+
canPrepareAppleSigningAssets:
|
|
822
|
+
!!apiUrl &&
|
|
823
|
+
!busyAction &&
|
|
824
|
+
!!appleIDLoginRef.current &&
|
|
825
|
+
!!appleBundleID.trim() &&
|
|
826
|
+
!!selectedDeveloperTeamID() &&
|
|
827
|
+
selectedAppleDeviceIDs.length > 0 &&
|
|
828
|
+
(!!reusableAppleCertificate || !!signingFiles.certificatePassword),
|
|
829
|
+
canRequestUSBAccess: !busyAction && buildStatus === 'succeeded',
|
|
830
|
+
canPairBrowser: !!apiUrl && !busyAction && buildStatus === 'succeeded' && !!selectedDevice,
|
|
831
|
+
canInstall: !!apiUrl && !busyAction && buildStatus === 'succeeded' && !!selectedDevice && !!pairRecord,
|
|
463
832
|
setSigningFiles,
|
|
833
|
+
setAppleBundleID,
|
|
834
|
+
setSelectedAppleDeviceIDs,
|
|
464
835
|
setBuildLogPanelOpen,
|
|
465
836
|
startAppleIDLogin,
|
|
466
837
|
submitAppleTwoFactorCode,
|
|
467
|
-
setSelectedAppleTeamID,
|
|
468
|
-
|
|
838
|
+
setSelectedAppleTeamID: selectAppleTeam,
|
|
839
|
+
clearAppleIDLogin,
|
|
840
|
+
registerConnectedAppleDevice,
|
|
841
|
+
prepareAppleSigningAssets,
|
|
469
842
|
startDeviceBuild,
|
|
470
843
|
requestUSBAccess,
|
|
471
844
|
pairBrowser,
|
|
@@ -478,35 +851,361 @@ async function fetchStoredBuildInfo(apiUrl: string, token?: string) {
|
|
|
478
851
|
return fetchLimbuildInfo(apiUrl, token);
|
|
479
852
|
}
|
|
480
853
|
|
|
854
|
+
async function findReusableAppleCertificate(teamID: string): Promise<ReusableAppleCertificate | undefined> {
|
|
855
|
+
const stored = await getLatestSigningAssetsWithCertificate(teamID);
|
|
856
|
+
if (!stored?.certificateID || !stored.certificateP12Base64 || !stored.certificatePassword) {
|
|
857
|
+
return undefined;
|
|
858
|
+
}
|
|
859
|
+
return {
|
|
860
|
+
certificateID: stored.certificateID,
|
|
861
|
+
certificateP12Base64: stored.certificateP12Base64,
|
|
862
|
+
certificatePassword: stored.certificatePassword,
|
|
863
|
+
teamID: stored.teamID,
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
481
867
|
async function refreshAppleTeams(
|
|
482
868
|
apiUrl: string,
|
|
483
869
|
appleSessionId: string,
|
|
484
870
|
token: string | undefined,
|
|
485
871
|
setAppleTeams: (teams: AppleDeveloperPortalTeam[]) => void,
|
|
486
872
|
setSelectedAppleTeamID: (teamID: string | undefined) => void,
|
|
873
|
+
accountSession?: AppleDeveloperPortalResponse,
|
|
487
874
|
) {
|
|
488
|
-
const response = await proxyProvisioningRequest<
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
provider?: AppleDeveloperPortalTeam;
|
|
492
|
-
}>(apiUrl, appleSessionId, listTeamsRequest(), token);
|
|
493
|
-
const teams = [
|
|
875
|
+
const response = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(apiUrl, appleSessionId, listTeamsRequest(), token);
|
|
876
|
+
assertApplePortalResponseOK(response.body, 'Apple Developer team list');
|
|
877
|
+
const teams = uniqueAppleTeams([
|
|
494
878
|
...(response.body?.teams ?? []),
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
];
|
|
879
|
+
]);
|
|
880
|
+
void accountSession;
|
|
498
881
|
setAppleTeams(teams);
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
882
|
+
const firstDeveloperTeam = teams.find((team) => developerPortalTeamID(team));
|
|
883
|
+
const firstSelectionID = appleTeamSelectionID(firstDeveloperTeam ?? teams[0]);
|
|
884
|
+
if (firstSelectionID) {
|
|
885
|
+
setSelectedAppleTeamID(firstSelectionID);
|
|
886
|
+
}
|
|
887
|
+
if (teams.length === 0) {
|
|
888
|
+
throw new Error('Apple Developer account did not return any teams or providers.');
|
|
889
|
+
}
|
|
890
|
+
return teams.map(developerPortalTeamID).find((teamID) => !!teamID);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function uniqueAppleTeams(teams: AppleDeveloperPortalTeam[]) {
|
|
894
|
+
const seen = new Set<string>();
|
|
895
|
+
const result: AppleDeveloperPortalTeam[] = [];
|
|
896
|
+
for (const team of teams) {
|
|
897
|
+
const id = appleTeamSelectionID(team);
|
|
898
|
+
const key = id ?? team.name ?? JSON.stringify(team);
|
|
899
|
+
if (seen.has(key)) continue;
|
|
900
|
+
seen.add(key);
|
|
901
|
+
result.push(team);
|
|
902
|
+
}
|
|
903
|
+
return result;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
async function refreshAppleAppIDs(
|
|
907
|
+
apiUrl: string,
|
|
908
|
+
appleSessionId: string,
|
|
909
|
+
token: string | undefined,
|
|
910
|
+
teamID: string | undefined,
|
|
911
|
+
setAppleAppIDs: (appIDs: AppleDeveloperPortalAppID[]) => void,
|
|
912
|
+
setAppleBundleID: (bundleID: string) => void,
|
|
913
|
+
) {
|
|
914
|
+
if (!teamID) return;
|
|
915
|
+
const response = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
|
|
916
|
+
apiUrl,
|
|
917
|
+
appleSessionId,
|
|
918
|
+
findBundleIDRequest({ bundleID: '', teamID }),
|
|
919
|
+
token,
|
|
920
|
+
);
|
|
921
|
+
assertApplePortalResponseOK(response.body, 'Apple bundle ID list');
|
|
922
|
+
const appIDs = response.body?.appIds ?? [];
|
|
923
|
+
setAppleAppIDs(appIDs);
|
|
924
|
+
const firstBundleID = bundleIDFromAppleAppID(appIDs[0]);
|
|
925
|
+
if (firstBundleID) {
|
|
926
|
+
setAppleBundleID(firstBundleID);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const info = await fetchStoredBuildInfo(apiUrl, token).catch(() => undefined);
|
|
930
|
+
if (info?.lastBuildConfig?.bundleId) {
|
|
931
|
+
setAppleBundleID(info.lastBuildConfig.bundleId);
|
|
502
932
|
}
|
|
503
933
|
}
|
|
504
934
|
|
|
505
|
-
function
|
|
935
|
+
async function refreshApplePortalSummary(
|
|
936
|
+
apiUrl: string,
|
|
937
|
+
appleSessionId: string,
|
|
938
|
+
token: string | undefined,
|
|
939
|
+
teamID: string | undefined,
|
|
940
|
+
setApplePortalSummary: (summary: ApplePortalSummary | undefined) => void,
|
|
941
|
+
log: (message: string, detail?: string) => void,
|
|
942
|
+
) {
|
|
943
|
+
const [certificates, profiles] = await Promise.all([
|
|
944
|
+
proxyProvisioningRequest<AppleDeveloperPortalResponse>(
|
|
945
|
+
apiUrl,
|
|
946
|
+
appleSessionId,
|
|
947
|
+
findDevelopmentCertificatesRequest(teamID ?? ''),
|
|
948
|
+
token,
|
|
949
|
+
),
|
|
950
|
+
proxyProvisioningRequest<AppleDeveloperPortalResponse>(
|
|
951
|
+
apiUrl,
|
|
952
|
+
appleSessionId,
|
|
953
|
+
findDevelopmentProfilesRequest({ bundleID: '', teamID: teamID ?? '' }),
|
|
954
|
+
token,
|
|
955
|
+
),
|
|
956
|
+
]);
|
|
957
|
+
assertApplePortalResponseOK(certificates.body, 'Apple Developer certificate list');
|
|
958
|
+
assertApplePortalResponseOK(profiles.body, 'Apple Developer profile list');
|
|
959
|
+
const summary = {
|
|
960
|
+
certificateCount: certificates.body?.certRequests?.length ?? 0,
|
|
961
|
+
profileCount: profiles.body?.provisioningProfiles?.length ?? 0,
|
|
962
|
+
};
|
|
963
|
+
setApplePortalSummary(summary);
|
|
964
|
+
log(
|
|
965
|
+
'Apple Developer resources fetched',
|
|
966
|
+
`${summary.certificateCount} certificates, ${summary.profileCount} provisioning profiles`,
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
async function refreshAppleDevices({
|
|
971
|
+
apiUrl,
|
|
972
|
+
token,
|
|
973
|
+
appleSessionId,
|
|
974
|
+
teamID,
|
|
975
|
+
connectedUDID,
|
|
976
|
+
setAppleDevices,
|
|
977
|
+
setSelectedAppleDeviceIDs,
|
|
978
|
+
log,
|
|
979
|
+
}: {
|
|
980
|
+
apiUrl: string;
|
|
981
|
+
token?: string;
|
|
982
|
+
appleSessionId: string;
|
|
983
|
+
teamID: string;
|
|
984
|
+
connectedUDID?: string;
|
|
985
|
+
setAppleDevices: (devices: AppleDeveloperPortalDevice[]) => void;
|
|
986
|
+
setSelectedAppleDeviceIDs: (deviceIDs: string[]) => void;
|
|
987
|
+
log: (message: string, detail?: string) => void;
|
|
988
|
+
}) {
|
|
989
|
+
const response = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
|
|
990
|
+
apiUrl,
|
|
991
|
+
appleSessionId,
|
|
992
|
+
findDeviceRequest({ deviceUDID: connectedUDID ?? '', teamID }),
|
|
993
|
+
token,
|
|
994
|
+
);
|
|
995
|
+
assertApplePortalResponseOK(response.body, 'Apple device list');
|
|
996
|
+
const devices = response.body?.devices ?? [];
|
|
997
|
+
setAppleDevices(devices);
|
|
998
|
+
const firstDeviceID = devices.find((device) => !!device.deviceId)?.deviceId;
|
|
999
|
+
if (!connectedUDID) {
|
|
1000
|
+
setSelectedAppleDeviceIDs(firstDeviceID ? [firstDeviceID] : []);
|
|
1001
|
+
log('Apple Developer devices fetched', `${devices.length} devices`);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
const connected = devices.find((device) => normalizeAppleUDID(device.deviceNumber) === normalizeAppleUDID(connectedUDID));
|
|
1005
|
+
if (connected?.deviceId) {
|
|
1006
|
+
setSelectedAppleDeviceIDs([connected.deviceId]);
|
|
1007
|
+
log('Connected iPhone found in Apple Developer devices', connected.name ?? connected.deviceNumber);
|
|
1008
|
+
} else {
|
|
1009
|
+
setSelectedAppleDeviceIDs(firstDeviceID ? [firstDeviceID] : []);
|
|
1010
|
+
log('Connected iPhone is not registered with Apple Developer', connectedUDID);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
async function prepareAppleSigningAssetsForDevice({
|
|
1015
|
+
apiUrl,
|
|
1016
|
+
token,
|
|
1017
|
+
appleSessionId,
|
|
1018
|
+
teamID,
|
|
1019
|
+
bundleID,
|
|
1020
|
+
deviceUDID,
|
|
1021
|
+
certificatePassword,
|
|
1022
|
+
deviceIDs,
|
|
1023
|
+
reusableCertificate,
|
|
1024
|
+
}: {
|
|
1025
|
+
apiUrl: string;
|
|
1026
|
+
token?: string;
|
|
1027
|
+
appleSessionId: string;
|
|
1028
|
+
teamID: string;
|
|
1029
|
+
bundleID: string;
|
|
1030
|
+
deviceUDID?: string;
|
|
1031
|
+
deviceIDs: string[];
|
|
1032
|
+
certificatePassword?: string;
|
|
1033
|
+
reusableCertificate?: ReusableAppleCertificate;
|
|
1034
|
+
}) {
|
|
1035
|
+
const normalizedUDID = deviceUDID?.replace(/-/g, '').replace(/[^a-fA-F0-9]/g, '') ?? '';
|
|
1036
|
+
const appIDID = await findOrCreateAppleBundleID({
|
|
1037
|
+
apiUrl,
|
|
1038
|
+
token,
|
|
1039
|
+
appleSessionId,
|
|
1040
|
+
teamID,
|
|
1041
|
+
bundleID,
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
let certificateID = reusableCertificate?.certificateID;
|
|
1045
|
+
let certificateP12Base64 = reusableCertificate?.certificateP12Base64;
|
|
1046
|
+
let storedCertificatePassword = reusableCertificate?.certificatePassword;
|
|
1047
|
+
if (!certificateID || !certificateP12Base64 || !storedCertificatePassword) {
|
|
1048
|
+
if (!certificatePassword) {
|
|
1049
|
+
throw new Error('Enter a .p12 password before preparing signing assets.');
|
|
1050
|
+
}
|
|
1051
|
+
const keyMaterial = await generateAppleSigningKeyAndCSR({
|
|
1052
|
+
commonName: `Limrun ${bundleID}`,
|
|
1053
|
+
});
|
|
1054
|
+
const certificateResponse = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
|
|
1055
|
+
apiUrl,
|
|
1056
|
+
appleSessionId,
|
|
1057
|
+
submitDevelopmentCSRRequest({ csrPEM: keyMaterial.csrPEM, teamID }),
|
|
1058
|
+
token,
|
|
1059
|
+
);
|
|
1060
|
+
assertApplePortalResponseOK(certificateResponse.body, 'Apple Development certificate creation');
|
|
1061
|
+
certificateID =
|
|
1062
|
+
stringField(certificateResponse.body?.certRequest, 'certificateId') ??
|
|
1063
|
+
stringField(certificateResponse.body?.certRequest, 'certRequestId') ??
|
|
1064
|
+
stringField(certificateResponse.body, 'certificateId') ??
|
|
1065
|
+
stringField(certificateResponse.body, 'certRequestId');
|
|
1066
|
+
if (!certificateID) {
|
|
1067
|
+
throw new Error('Apple certificate creation did not return a certificate ID.');
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const downloadedCertificate = await proxyProvisioningRequest(
|
|
1071
|
+
apiUrl,
|
|
1072
|
+
appleSessionId,
|
|
1073
|
+
downloadCertificateRequest(certificateID, teamID),
|
|
1074
|
+
token,
|
|
1075
|
+
);
|
|
1076
|
+
if (downloadedCertificate.status < 200 || downloadedCertificate.status >= 300 || !downloadedCertificate.rawBodyBase64) {
|
|
1077
|
+
throw new Error(`Apple certificate download failed: HTTP ${downloadedCertificate.status}`);
|
|
1078
|
+
}
|
|
1079
|
+
certificateP12Base64 = exportAppleCertificateP12({
|
|
1080
|
+
privateKeyPKCS8Base64: keyMaterial.privateKeyPKCS8Base64,
|
|
1081
|
+
certificateBase64: downloadedCertificate.rawBodyBase64,
|
|
1082
|
+
password: certificatePassword,
|
|
1083
|
+
friendlyName: `Apple Development ${bundleID}`,
|
|
1084
|
+
});
|
|
1085
|
+
storedCertificatePassword = certificatePassword;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const profileName = `Limrun ${bundleID}`;
|
|
1089
|
+
const profileResponse = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
|
|
1090
|
+
apiUrl,
|
|
1091
|
+
appleSessionId,
|
|
1092
|
+
createDevelopmentProfileRequest({
|
|
1093
|
+
bundleID,
|
|
1094
|
+
teamID,
|
|
1095
|
+
appIDID,
|
|
1096
|
+
certificateID,
|
|
1097
|
+
deviceIDs,
|
|
1098
|
+
name: profileName,
|
|
1099
|
+
}),
|
|
1100
|
+
token,
|
|
1101
|
+
);
|
|
1102
|
+
assertApplePortalResponseOK(profileResponse.body, 'Apple provisioning profile creation');
|
|
1103
|
+
const profileID =
|
|
1104
|
+
stringField(profileResponse.body?.provisioningProfile, 'provisioningProfileId') ??
|
|
1105
|
+
stringField(profileResponse.body, 'provisioningProfileId');
|
|
1106
|
+
if (!profileID) {
|
|
1107
|
+
throw new Error('Apple provisioning profile creation did not return a profile ID.');
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const downloadedProfile = await proxyProvisioningRequest(
|
|
1111
|
+
apiUrl,
|
|
1112
|
+
appleSessionId,
|
|
1113
|
+
downloadProfileRequest(profileID, teamID),
|
|
1114
|
+
token,
|
|
1115
|
+
);
|
|
1116
|
+
if (downloadedProfile.status < 200 || downloadedProfile.status >= 300 || !downloadedProfile.rawBodyBase64) {
|
|
1117
|
+
throw new Error(`Apple provisioning profile download failed: HTTP ${downloadedProfile.status}`);
|
|
1118
|
+
}
|
|
1119
|
+
const provisioningProfileBase64 = downloadedProfile.rawBodyBase64;
|
|
1120
|
+
const profile = parseProvisioningProfileBase64(provisioningProfileBase64);
|
|
1121
|
+
|
|
1122
|
+
return putAppleGeneratedSigningAssets({
|
|
1123
|
+
bundleID,
|
|
1124
|
+
deviceUDID: normalizedUDID || undefined,
|
|
1125
|
+
teamID,
|
|
1126
|
+
certificateID,
|
|
1127
|
+
certificateP12Base64,
|
|
1128
|
+
certificatePassword: storedCertificatePassword,
|
|
1129
|
+
provisioningProfileBase64,
|
|
1130
|
+
profile,
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
async function findOrCreateAppleBundleID({
|
|
1135
|
+
apiUrl,
|
|
1136
|
+
token,
|
|
1137
|
+
appleSessionId,
|
|
1138
|
+
teamID,
|
|
1139
|
+
bundleID,
|
|
1140
|
+
}: {
|
|
1141
|
+
apiUrl: string;
|
|
1142
|
+
token?: string;
|
|
1143
|
+
appleSessionId: string;
|
|
1144
|
+
teamID: string;
|
|
1145
|
+
bundleID: string;
|
|
1146
|
+
}) {
|
|
1147
|
+
const existing = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
|
|
1148
|
+
apiUrl,
|
|
1149
|
+
appleSessionId,
|
|
1150
|
+
findBundleIDRequest({ bundleID, teamID }),
|
|
1151
|
+
token,
|
|
1152
|
+
);
|
|
1153
|
+
assertApplePortalResponseOK(existing.body, 'Apple bundle ID lookup');
|
|
1154
|
+
const found = existing.body?.appIds?.find((app) => stringField(app, 'identifier') === bundleID || stringField(app, 'bundleId') === bundleID);
|
|
1155
|
+
const foundID = stringField(found, 'appIdId') ?? stringField(found, 'appId');
|
|
1156
|
+
if (foundID) return foundID;
|
|
1157
|
+
|
|
1158
|
+
const created = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
|
|
1159
|
+
apiUrl,
|
|
1160
|
+
appleSessionId,
|
|
1161
|
+
createBundleIDRequest({ bundleID, teamID, name: bundleID }),
|
|
1162
|
+
token,
|
|
1163
|
+
);
|
|
1164
|
+
assertApplePortalResponseOK(created.body, 'Apple bundle ID creation');
|
|
1165
|
+
const createdID =
|
|
1166
|
+
stringField(created.body?.appId, 'appIdId') ??
|
|
1167
|
+
stringField(created.body?.appId, 'appId') ??
|
|
1168
|
+
stringField(created.body, 'appIdId') ??
|
|
1169
|
+
stringField(created.body, 'appId');
|
|
1170
|
+
if (!createdID) {
|
|
1171
|
+
throw new Error('Apple bundle ID creation did not return an App ID.');
|
|
1172
|
+
}
|
|
1173
|
+
return createdID;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function assertApplePortalResponseOK(response: AppleDeveloperPortalResponse | undefined, label: string) {
|
|
1177
|
+
if (!response) {
|
|
1178
|
+
throw new Error(`${label} returned an empty response.`);
|
|
1179
|
+
}
|
|
1180
|
+
if (response.resultCode !== undefined && response.resultCode !== 0) {
|
|
1181
|
+
throw new Error(`${label} failed: ${response.userString ?? response.resultString ?? response.resultCode}`);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function stringField(record: Record<string, unknown> | undefined, key: string) {
|
|
1186
|
+
const value = record?.[key];
|
|
1187
|
+
if (typeof value === 'string') return value;
|
|
1188
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
1189
|
+
return undefined;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function normalizeAppleUDID(udid?: string) {
|
|
1193
|
+
return (udid ?? '').replace(/-/g, '').replace(/[^a-fA-F0-9]/g, '').toUpperCase();
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function appleTeamSelectionID(team?: AppleDeveloperPortalTeam) {
|
|
506
1197
|
const value = team?.teamId ?? team?.providerId ?? team?.publicProviderId;
|
|
507
1198
|
return value === undefined || value === '' ? undefined : String(value);
|
|
508
1199
|
}
|
|
509
1200
|
|
|
1201
|
+
function developerPortalTeamID(team?: AppleDeveloperPortalTeam) {
|
|
1202
|
+
return team?.teamId && team.teamId !== '' ? team.teamId : undefined;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function bundleIDFromAppleAppID(appID?: AppleDeveloperPortalAppID) {
|
|
1206
|
+
return appID?.identifier || appID?.bundleId || undefined;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
510
1209
|
async function fileToBase64(file: File) {
|
|
511
1210
|
const buffer = await file.arrayBuffer();
|
|
512
1211
|
let binary = '';
|