@limrun/ui 0.9.0-rc.5 → 0.9.0-rc.7

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