@limrun/ui 0.9.0-rc.1 → 0.9.0-rc.2

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.
@@ -1,23 +1,40 @@
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,
7
17
  getReusableAppleSigningAssets,
8
18
  listTeamsRequest,
9
19
  parseProvisioningProfile,
20
+ parseProvisioningProfileBase64,
10
21
  profileContainsDevice,
11
22
  proxyProvisioningRequest,
23
+ putAppleGeneratedSigningAssets,
12
24
  putPairRecord,
13
25
  putSigningAssets,
26
+ registerDeviceRequest,
14
27
  requestUSBAccess as requestDeviceUSBAccess,
15
28
  startBrowserOwnedAppleIDLogin,
16
29
  startSignedDeviceBuild,
17
30
  startInstallRelay,
18
31
  startPairingRelay,
32
+ submitDevelopmentCSRRequest,
19
33
  watchBuildLogEvents,
20
34
  type AppleIDLoginResult,
35
+ type AppleDeveloperPortalDevice,
36
+ type AppleDeveloperPortalAppID,
37
+ type AppleDeveloperPortalResponse,
21
38
  type AppleDeveloperPortalTeam,
22
39
  type BuildLogLine,
23
40
  type DeviceInstallBuildStatus,
@@ -49,20 +66,31 @@ export type UseDeviceInstallResult = {
49
66
  buildStatus: DeviceInstallBuildStatus;
50
67
  appleSigningStatus: DeviceInstallAppleSigningStatus;
51
68
  appleTeams: AppleDeveloperPortalTeam[];
69
+ appleDevices: AppleDeveloperPortalDevice[];
70
+ appleAppIDs: AppleDeveloperPortalAppID[];
71
+ applePortalSummary?: ApplePortalSummary;
52
72
  selectedAppleTeamID?: string;
73
+ selectedAppleDeviceIDs: string[];
74
+ connectedAppleDeviceRegistered: boolean;
75
+ appleBundleID: string;
53
76
  buildLogPanelOpen: boolean;
54
77
  busyAction?: DeviceInstallBusyAction;
55
78
  error?: string;
56
79
  canBuild: boolean;
80
+ canPrepareAppleSigningAssets: boolean;
57
81
  canRequestUSBAccess: boolean;
58
82
  canPairBrowser: boolean;
59
83
  canInstall: boolean;
60
84
  setSigningFiles: (files: DeviceInstallSigningFiles) => void;
85
+ setAppleBundleID: (bundleID: string) => void;
86
+ setSelectedAppleDeviceIDs: (deviceIDs: string[]) => void;
61
87
  setBuildLogPanelOpen: (open: boolean) => void;
62
88
  startAppleIDLogin: (input: DeviceInstallAppleIDLoginInput) => Promise<void>;
63
89
  submitAppleTwoFactorCode: (code: string) => Promise<void>;
64
90
  setSelectedAppleTeamID: (teamID: string | undefined) => void;
65
- clearAppleIDSession: () => Promise<void>;
91
+ clearAppleIDLogin: () => void;
92
+ registerConnectedAppleDevice: () => Promise<void>;
93
+ prepareAppleSigningAssets: () => Promise<void>;
66
94
  startDeviceBuild: () => Promise<void>;
67
95
  requestUSBAccess: () => Promise<void>;
68
96
  pairBrowser: () => Promise<void>;
@@ -76,6 +104,7 @@ export type DeviceInstallAppleSigningStatus =
76
104
  | 'two-factor-required'
77
105
  | 'authenticated'
78
106
  | 'preparing-assets'
107
+ | 'assets-ready'
79
108
  | 'using-cached-profile'
80
109
  | 'error';
81
110
 
@@ -96,6 +125,11 @@ export type DeviceInstallAppleIDLoginInput = {
96
125
  password: string;
97
126
  };
98
127
 
128
+ export type ApplePortalSummary = {
129
+ certificateCount: number;
130
+ profileCount: number;
131
+ };
132
+
99
133
  const initialStepStatuses: DeviceInstallStepStatuses = {
100
134
  build: 'idle',
101
135
  usb: 'idle',
@@ -119,7 +153,12 @@ export function useDeviceInstall({
119
153
  const [buildStatus, setBuildStatus] = useState<DeviceInstallBuildStatus>('idle');
120
154
  const [appleSigningStatus, setAppleSigningStatus] = useState<DeviceInstallAppleSigningStatus>('idle');
121
155
  const [appleTeams, setAppleTeams] = useState<AppleDeveloperPortalTeam[]>([]);
156
+ const [appleDevices, setAppleDevices] = useState<AppleDeveloperPortalDevice[]>([]);
157
+ const [appleAppIDs, setAppleAppIDs] = useState<AppleDeveloperPortalAppID[]>([]);
158
+ const [selectedAppleDeviceIDs, setSelectedAppleDeviceIDs] = useState<string[]>([]);
159
+ const [applePortalSummary, setApplePortalSummary] = useState<ApplePortalSummary | undefined>();
122
160
  const [selectedAppleTeamID, setSelectedAppleTeamID] = useState<string | undefined>();
161
+ const [appleBundleID, setAppleBundleID] = useState('');
123
162
  const [buildLogPanelOpen, setBuildLogPanelOpen] = useState(false);
124
163
  const [busyAction, setBusyAction] = useState<DeviceInstallBusyAction | undefined>();
125
164
  const [error, setError] = useState<string | undefined>();
@@ -128,7 +167,7 @@ export function useDeviceInstall({
128
167
  const relayRef = useRef<RelayClient | undefined>(undefined);
129
168
  const selectedDeviceRef = useRef<DeviceRelayTarget | undefined>(undefined);
130
169
  const stopBuildWatcherRef = useRef<(() => void) | undefined>(undefined);
131
- const appleIDSessionRef = useRef<AppleIDLoginResult | undefined>(undefined);
170
+ const appleIDLoginRef = useRef<AppleIDLoginResult | undefined>(undefined);
132
171
 
133
172
  const log = useCallback((message: string, detail?: string) => {
134
173
  const line = detail ? `${message}\n${detail}` : message;
@@ -144,6 +183,15 @@ export function useDeviceInstall({
144
183
  setSigningAssets(undefined);
145
184
  }, []);
146
185
 
186
+ const selectedDeveloperTeamID = useCallback(() => {
187
+ return developerPortalTeamID(appleTeams.find((team) => appleTeamSelectionID(team) === selectedAppleTeamID));
188
+ }, [appleTeams, selectedAppleTeamID]);
189
+
190
+ const connectedAppleDevice = selectedDevice?.hello.serialNumber
191
+ ? appleDevices.find((device) => normalizeAppleUDID(device.deviceNumber) === normalizeAppleUDID(selectedDevice.hello.serialNumber))
192
+ : undefined;
193
+ const connectedAppleDeviceRegistered = !!connectedAppleDevice?.deviceId;
194
+
147
195
  useEffect(() => {
148
196
  selectedDeviceRef.current = selectedDevice;
149
197
  }, [selectedDevice]);
@@ -157,16 +205,19 @@ export function useDeviceInstall({
157
205
  useEffect(() => {
158
206
  return () => {
159
207
  stopBuildWatcherRef.current?.();
160
- void appleIDSessionRef.current?.close();
208
+ void appleIDLoginRef.current?.close();
209
+ appleIDLoginRef.current = undefined;
161
210
  void cleanupDeviceAccess();
162
211
  };
163
212
  }, [cleanupDeviceAccess]);
164
213
 
165
214
  const resolveSigningAssetsForBuild = useCallback(async () => {
166
- const info = apiUrl ? await fetchStoredBuildInfo(apiUrl, token).catch(() => undefined) : undefined;
167
- if (info?.lastBuildConfig?.bundleId) {
215
+ const requestedBundleID = appleBundleID.trim();
216
+ const info = requestedBundleID ? undefined : apiUrl ? await fetchStoredBuildInfo(apiUrl, token).catch(() => undefined) : undefined;
217
+ const bundleID = requestedBundleID || info?.lastBuildConfig?.bundleId;
218
+ if (bundleID) {
168
219
  const cached = await getReusableAppleSigningAssets({
169
- bundleID: info.lastBuildConfig.bundleId,
220
+ bundleID,
170
221
  deviceUDID: selectedDevice?.hello.serialNumber,
171
222
  teamID: selectedAppleTeamID,
172
223
  });
@@ -213,7 +264,7 @@ export function useDeviceInstall({
213
264
  setSigningAssets(storedAssets);
214
265
  log('Signing assets stored locally', storageBundleId);
215
266
  return storedAssets;
216
- }, [apiUrl, log, selectedAppleTeamID, selectedDevice?.hello.serialNumber, signingFiles, token]);
267
+ }, [apiUrl, appleBundleID, log, selectedAppleTeamID, selectedDevice?.hello.serialNumber, signingFiles, token]);
217
268
 
218
269
  const startAppleIDLogin = useCallback(
219
270
  async ({ accountName, password }: DeviceInstallAppleIDLoginInput) => {
@@ -222,12 +273,21 @@ export function useDeviceInstall({
222
273
  setError(undefined);
223
274
  setAppleSigningStatus('authenticating');
224
275
  try {
225
- await appleIDSessionRef.current?.close().catch(() => undefined);
276
+ await appleIDLoginRef.current?.close().catch(() => undefined);
226
277
  const session = await startBrowserOwnedAppleIDLogin({ limbuildApiUrl: apiUrl, token, accountName, password });
227
- appleIDSessionRef.current = session;
278
+ appleIDLoginRef.current = session;
228
279
  if (!session.requiresTwoFactor) {
229
- await session.finalize();
230
- await refreshAppleTeams(apiUrl, session.appleSessionId, token, setAppleTeams, setSelectedAppleTeamID);
280
+ const accountSession = await session.finalize().catch(() => undefined);
281
+ const teamID = await refreshAppleTeams(
282
+ apiUrl,
283
+ session.appleSessionId,
284
+ token,
285
+ setAppleTeams,
286
+ setSelectedAppleTeamID,
287
+ accountSession?.body as AppleDeveloperPortalResponse | undefined,
288
+ );
289
+ await refreshAppleAppIDs(apiUrl, session.appleSessionId, token, teamID, setAppleAppIDs, setAppleBundleID);
290
+ await refreshApplePortalSummary(apiUrl, session.appleSessionId, token, teamID, setApplePortalSummary, log);
231
291
  }
232
292
  setAppleSigningStatus(session.requiresTwoFactor ? 'two-factor-required' : 'authenticated');
233
293
  log(
@@ -248,7 +308,7 @@ export function useDeviceInstall({
248
308
 
249
309
  const submitAppleTwoFactorCode = useCallback(
250
310
  async (code: string) => {
251
- const session = appleIDSessionRef.current;
311
+ const session = appleIDLoginRef.current;
252
312
  if (!session) {
253
313
  throw new Error('Start Apple ID login before submitting a two-factor code.');
254
314
  }
@@ -256,9 +316,18 @@ export function useDeviceInstall({
256
316
  setError(undefined);
257
317
  try {
258
318
  await session.finishTwoFactor(code);
259
- await session.finalize();
260
319
  if (apiUrl) {
261
- await refreshAppleTeams(apiUrl, session.appleSessionId, token, setAppleTeams, setSelectedAppleTeamID);
320
+ const accountSession = await session.finalize().catch(() => undefined);
321
+ const teamID = await refreshAppleTeams(
322
+ apiUrl,
323
+ session.appleSessionId,
324
+ token,
325
+ setAppleTeams,
326
+ setSelectedAppleTeamID,
327
+ accountSession?.body as AppleDeveloperPortalResponse | undefined,
328
+ );
329
+ await refreshAppleAppIDs(apiUrl, session.appleSessionId, token, teamID, setAppleAppIDs, setAppleBundleID);
330
+ await refreshApplePortalSummary(apiUrl, session.appleSessionId, token, teamID, setApplePortalSummary, log);
262
331
  }
263
332
  setAppleSigningStatus('authenticated');
264
333
  log('Apple ID two-factor authentication accepted');
@@ -274,15 +343,165 @@ export function useDeviceInstall({
274
343
  [apiUrl, log, token],
275
344
  );
276
345
 
277
- const clearAppleIDSession = useCallback(async () => {
278
- await appleIDSessionRef.current?.close().catch(() => undefined);
279
- appleIDSessionRef.current = undefined;
346
+ const clearAppleIDLogin = useCallback(() => {
347
+ void appleIDLoginRef.current?.close();
348
+ appleIDLoginRef.current = undefined;
280
349
  setAppleTeams([]);
350
+ setAppleDevices([]);
351
+ setAppleAppIDs([]);
352
+ setSelectedAppleDeviceIDs([]);
353
+ setApplePortalSummary(undefined);
281
354
  setSelectedAppleTeamID(undefined);
282
355
  setAppleSigningStatus('idle');
283
- log('Apple ID session cleared');
356
+ log('Apple ID login state cleared');
284
357
  }, [log]);
285
358
 
359
+ const selectAppleTeam = useCallback(
360
+ (teamID: string | undefined) => {
361
+ setSelectedAppleTeamID(teamID);
362
+ setAppleAppIDs([]);
363
+ setAppleDevices([]);
364
+ setSelectedAppleDeviceIDs([]);
365
+ setApplePortalSummary(undefined);
366
+ setAppleBundleID('');
367
+ setSigningAssets(undefined);
368
+ const session = appleIDLoginRef.current;
369
+ if (!apiUrl || !session || !teamID) return;
370
+ const developerTeamID = developerPortalTeamID(appleTeams.find((team) => appleTeamSelectionID(team) === teamID));
371
+ if (!developerTeamID) return;
372
+ void (async () => {
373
+ try {
374
+ await refreshAppleAppIDs(apiUrl, session.appleSessionId, token, developerTeamID, setAppleAppIDs, setAppleBundleID);
375
+ await refreshApplePortalSummary(apiUrl, session.appleSessionId, token, developerTeamID, setApplePortalSummary, log);
376
+ if (selectedDeviceRef.current?.hello.serialNumber) {
377
+ await refreshAppleDevices({
378
+ apiUrl,
379
+ token,
380
+ appleSessionId: session.appleSessionId,
381
+ teamID: developerTeamID,
382
+ connectedUDID: selectedDeviceRef.current.hello.serialNumber,
383
+ setAppleDevices,
384
+ setSelectedAppleDeviceIDs,
385
+ log,
386
+ });
387
+ }
388
+ } catch (caught) {
389
+ const message = errorMessage(caught);
390
+ setError(message);
391
+ log('Apple team refresh failed', message);
392
+ }
393
+ })();
394
+ },
395
+ [apiUrl, appleTeams, log, token],
396
+ );
397
+
398
+ const registerConnectedAppleDevice = useCallback(async () => {
399
+ const teamID = selectedDeveloperTeamID();
400
+ if (!apiUrl || !appleIDLoginRef.current || !selectedDevice?.hello.serialNumber || !teamID) return;
401
+ setBusyAction('build');
402
+ setError(undefined);
403
+ try {
404
+ const normalizedUDID = normalizeAppleUDID(selectedDevice.hello.serialNumber);
405
+ const created = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
406
+ apiUrl,
407
+ appleIDLoginRef.current.appleSessionId,
408
+ registerDeviceRequest({
409
+ deviceUDID: normalizedUDID,
410
+ teamID,
411
+ name: selectedDevice.hello.productName ?? 'Limrun iPhone',
412
+ }),
413
+ token,
414
+ );
415
+ assertApplePortalResponseOK(created.body, 'Apple device registration');
416
+ await refreshAppleDevices({
417
+ apiUrl,
418
+ token,
419
+ appleSessionId: appleIDLoginRef.current.appleSessionId,
420
+ teamID,
421
+ connectedUDID: selectedDevice.hello.serialNumber,
422
+ setAppleDevices,
423
+ setSelectedAppleDeviceIDs,
424
+ log,
425
+ });
426
+ log('Connected iPhone registered with Apple Developer', normalizedUDID);
427
+ } catch (caught) {
428
+ const message = errorMessage(caught);
429
+ setError(message);
430
+ log('Apple device registration failed', message);
431
+ } finally {
432
+ setBusyAction(undefined);
433
+ }
434
+ }, [apiUrl, log, selectedDeveloperTeamID, selectedDevice?.hello.productName, selectedDevice?.hello.serialNumber, token]);
435
+
436
+ const prepareAppleSigningAssets = useCallback(async () => {
437
+ if (!apiUrl || !appleIDLoginRef.current || !selectedDevice?.hello.serialNumber) return;
438
+ const bundleID = appleBundleID.trim();
439
+ if (!bundleID) {
440
+ throw new Error('Enter a bundle ID before preparing signing assets.');
441
+ }
442
+ if (!selectedAppleTeamID) {
443
+ throw new Error('Select an Apple Developer team before preparing signing assets.');
444
+ }
445
+ const teamID = selectedDeveloperTeamID();
446
+ if (!teamID) {
447
+ throw new Error('Selected Apple team does not include a Developer Portal team ID.');
448
+ }
449
+ if (selectedAppleDeviceIDs.length === 0) {
450
+ throw new Error('Select at least one Apple Developer device before preparing signing assets.');
451
+ }
452
+ if (!signingFiles.certificatePassword) {
453
+ throw new Error('Enter a .p12 password before preparing signing assets.');
454
+ }
455
+ setBusyAction('build');
456
+ setError(undefined);
457
+ setAppleSigningStatus('preparing-assets');
458
+ try {
459
+ const cached = await getReusableAppleSigningAssets({
460
+ bundleID,
461
+ deviceUDID: selectedDevice.hello.serialNumber,
462
+ teamID,
463
+ });
464
+ if (cached) {
465
+ setSigningAssets(cached);
466
+ setAppleSigningStatus('assets-ready');
467
+ log('Using cached Apple signing assets', bundleID);
468
+ return;
469
+ }
470
+ const assets = await prepareAppleSigningAssetsForDevice({
471
+ apiUrl,
472
+ token,
473
+ appleSessionId: appleIDLoginRef.current.appleSessionId,
474
+ teamID,
475
+ bundleID,
476
+ deviceUDID: selectedDevice.hello.serialNumber,
477
+ deviceIDs: selectedAppleDeviceIDs,
478
+ certificatePassword: signingFiles.certificatePassword,
479
+ });
480
+ setSigningAssets(assets);
481
+ setAppleSigningStatus('assets-ready');
482
+ log('Apple signing assets stored locally', `${bundleID} for ${selectedDevice.hello.serialNumber}`);
483
+ } catch (caught) {
484
+ const message = errorMessage(caught);
485
+ setError(message);
486
+ setAppleSigningStatus('error');
487
+ log('Apple signing asset preparation failed', message);
488
+ } finally {
489
+ setBusyAction(undefined);
490
+ }
491
+ }, [
492
+ apiUrl,
493
+ appleBundleID,
494
+ appleTeams,
495
+ log,
496
+ selectedAppleTeamID,
497
+ selectedAppleDeviceIDs,
498
+ selectedDeveloperTeamID,
499
+ selectedDevice?.hello.productName,
500
+ selectedDevice?.hello.serialNumber,
501
+ signingFiles.certificatePassword,
502
+ token,
503
+ ]);
504
+
286
505
  const startDeviceBuild = useCallback(async () => {
287
506
  if (!apiUrl) return;
288
507
  setBusyAction('build');
@@ -316,7 +535,7 @@ export function useDeviceInstall({
316
535
  setBuildStatus(status);
317
536
  if (status === 'succeeded') {
318
537
  setStepStatus('build', 'complete');
319
- setCurrentStep('usb');
538
+ setCurrentStep(selectedDeviceRef.current ? (pairRecord ? 'install' : 'pair') : 'usb');
320
539
  } else if (status === 'failed' || status === 'cancelled') {
321
540
  setStepStatus('build', 'error');
322
541
  }
@@ -336,7 +555,7 @@ export function useDeviceInstall({
336
555
  } finally {
337
556
  setBusyAction(undefined);
338
557
  }
339
- }, [apiUrl, log, resolveSigningAssetsForBuild, setStepStatus, token]);
558
+ }, [apiUrl, log, pairRecord, resolveSigningAssetsForBuild, setStepStatus, token]);
340
559
 
341
560
  const requestUSBAccess = useCallback(async () => {
342
561
  setBusyAction('usb');
@@ -357,6 +576,21 @@ export function useDeviceInstall({
357
576
  }
358
577
  setSigningAssets(storedSigningAssets);
359
578
  }
579
+ if (apiUrl && appleIDLoginRef.current) {
580
+ const teamID = selectedDeveloperTeamID();
581
+ if (teamID) {
582
+ await refreshAppleDevices({
583
+ apiUrl,
584
+ token,
585
+ appleSessionId: appleIDLoginRef.current.appleSessionId,
586
+ teamID,
587
+ connectedUDID: target.hello.serialNumber,
588
+ setAppleDevices,
589
+ setSelectedAppleDeviceIDs,
590
+ log,
591
+ });
592
+ }
593
+ }
360
594
  setStepStatus('usb', 'complete');
361
595
  setCurrentStep(storedPairRecord ? 'install' : 'pair');
362
596
  log(storedPairRecord ? 'Pair record found' : 'No pair record found', target.hello.serialNumber);
@@ -368,7 +602,7 @@ export function useDeviceInstall({
368
602
  } finally {
369
603
  setBusyAction(undefined);
370
604
  }
371
- }, [cleanupDeviceAccess, log, setStepStatus]);
605
+ }, [apiUrl, cleanupDeviceAccess, log, selectedDeveloperTeamID, setStepStatus, token]);
372
606
 
373
607
  const pairBrowser = useCallback(async () => {
374
608
  if (!apiUrl || !selectedDevice) return;
@@ -452,20 +686,39 @@ export function useDeviceInstall({
452
686
  buildStatus,
453
687
  appleSigningStatus,
454
688
  appleTeams,
689
+ appleDevices,
690
+ appleAppIDs,
691
+ applePortalSummary,
455
692
  selectedAppleTeamID,
693
+ selectedAppleDeviceIDs,
694
+ connectedAppleDeviceRegistered,
695
+ appleBundleID,
456
696
  buildLogPanelOpen,
457
697
  busyAction,
458
698
  error,
459
- canBuild: !!apiUrl && !busyAction,
460
- canRequestUSBAccess: !busyAction && (buildStatus === 'succeeded' || stepStatuses.build === 'complete'),
699
+ canBuild: !!apiUrl && !busyAction && !!signingAssets,
700
+ canPrepareAppleSigningAssets:
701
+ !!apiUrl &&
702
+ !busyAction &&
703
+ !!appleIDLoginRef.current &&
704
+ !!selectedDevice &&
705
+ !!appleBundleID.trim() &&
706
+ !!selectedDeveloperTeamID() &&
707
+ selectedAppleDeviceIDs.length > 0 &&
708
+ !!signingFiles.certificatePassword,
709
+ canRequestUSBAccess: !busyAction && appleSigningStatus === 'authenticated' && !!appleBundleID.trim(),
461
710
  canPairBrowser: !!apiUrl && !busyAction && !!selectedDevice,
462
711
  canInstall: !!apiUrl && !busyAction && !!selectedDevice && !!pairRecord,
463
712
  setSigningFiles,
713
+ setAppleBundleID,
714
+ setSelectedAppleDeviceIDs,
464
715
  setBuildLogPanelOpen,
465
716
  startAppleIDLogin,
466
717
  submitAppleTwoFactorCode,
467
- setSelectedAppleTeamID,
468
- clearAppleIDSession,
718
+ setSelectedAppleTeamID: selectAppleTeam,
719
+ clearAppleIDLogin,
720
+ registerConnectedAppleDevice,
721
+ prepareAppleSigningAssets,
469
722
  startDeviceBuild,
470
723
  requestUSBAccess,
471
724
  pairBrowser,
@@ -484,29 +737,321 @@ async function refreshAppleTeams(
484
737
  token: string | undefined,
485
738
  setAppleTeams: (teams: AppleDeveloperPortalTeam[]) => void,
486
739
  setSelectedAppleTeamID: (teamID: string | undefined) => void,
740
+ accountSession?: AppleDeveloperPortalResponse,
487
741
  ) {
488
- const response = await proxyProvisioningRequest<{
489
- teams?: AppleDeveloperPortalTeam[];
490
- availableProviders?: AppleDeveloperPortalTeam[];
491
- provider?: AppleDeveloperPortalTeam;
492
- }>(apiUrl, appleSessionId, listTeamsRequest(), token);
493
- const teams = [
742
+ const response = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(apiUrl, appleSessionId, listTeamsRequest(), token);
743
+ assertApplePortalResponseOK(response.body, 'Apple Developer team list');
744
+ const teams = uniqueAppleTeams([
494
745
  ...(response.body?.teams ?? []),
495
- ...(response.body?.availableProviders ?? []),
496
- ...(response.body?.provider ? [response.body.provider] : []),
497
- ];
746
+ ]);
747
+ void accountSession;
498
748
  setAppleTeams(teams);
499
- const firstTeamID = teamIDFromPortalTeam(teams[0]);
500
- if (firstTeamID) {
501
- setSelectedAppleTeamID(firstTeamID);
749
+ const firstDeveloperTeam = teams.find((team) => developerPortalTeamID(team));
750
+ const firstSelectionID = appleTeamSelectionID(firstDeveloperTeam ?? teams[0]);
751
+ if (firstSelectionID) {
752
+ setSelectedAppleTeamID(firstSelectionID);
753
+ }
754
+ if (teams.length === 0) {
755
+ throw new Error('Apple Developer account did not return any teams or providers.');
502
756
  }
757
+ return teams.map(developerPortalTeamID).find((teamID) => !!teamID);
503
758
  }
504
759
 
505
- function teamIDFromPortalTeam(team?: AppleDeveloperPortalTeam) {
760
+ function uniqueAppleTeams(teams: AppleDeveloperPortalTeam[]) {
761
+ const seen = new Set<string>();
762
+ const result: AppleDeveloperPortalTeam[] = [];
763
+ for (const team of teams) {
764
+ const id = appleTeamSelectionID(team);
765
+ const key = id ?? team.name ?? JSON.stringify(team);
766
+ if (seen.has(key)) continue;
767
+ seen.add(key);
768
+ result.push(team);
769
+ }
770
+ return result;
771
+ }
772
+
773
+ async function refreshAppleAppIDs(
774
+ apiUrl: string,
775
+ appleSessionId: string,
776
+ token: string | undefined,
777
+ teamID: string | undefined,
778
+ setAppleAppIDs: (appIDs: AppleDeveloperPortalAppID[]) => void,
779
+ setAppleBundleID: (bundleID: string) => void,
780
+ ) {
781
+ if (!teamID) return;
782
+ const response = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
783
+ apiUrl,
784
+ appleSessionId,
785
+ findBundleIDRequest({ bundleID: '', teamID }),
786
+ token,
787
+ );
788
+ assertApplePortalResponseOK(response.body, 'Apple bundle ID list');
789
+ const appIDs = response.body?.appIds ?? [];
790
+ setAppleAppIDs(appIDs);
791
+ const firstBundleID = bundleIDFromAppleAppID(appIDs[0]);
792
+ if (firstBundleID) {
793
+ setAppleBundleID(firstBundleID);
794
+ }
795
+ }
796
+
797
+ async function refreshApplePortalSummary(
798
+ apiUrl: string,
799
+ appleSessionId: string,
800
+ token: string | undefined,
801
+ teamID: string | undefined,
802
+ setApplePortalSummary: (summary: ApplePortalSummary | undefined) => void,
803
+ log: (message: string, detail?: string) => void,
804
+ ) {
805
+ const [certificates, profiles] = await Promise.all([
806
+ proxyProvisioningRequest<AppleDeveloperPortalResponse>(
807
+ apiUrl,
808
+ appleSessionId,
809
+ findDevelopmentCertificatesRequest(teamID ?? ''),
810
+ token,
811
+ ),
812
+ proxyProvisioningRequest<AppleDeveloperPortalResponse>(
813
+ apiUrl,
814
+ appleSessionId,
815
+ findDevelopmentProfilesRequest({ bundleID: '', teamID: teamID ?? '' }),
816
+ token,
817
+ ),
818
+ ]);
819
+ assertApplePortalResponseOK(certificates.body, 'Apple Developer certificate list');
820
+ assertApplePortalResponseOK(profiles.body, 'Apple Developer profile list');
821
+ const summary = {
822
+ certificateCount: certificates.body?.certRequests?.length ?? 0,
823
+ profileCount: profiles.body?.provisioningProfiles?.length ?? 0,
824
+ };
825
+ setApplePortalSummary(summary);
826
+ log(
827
+ 'Apple Developer resources fetched',
828
+ `${summary.certificateCount} certificates, ${summary.profileCount} provisioning profiles`,
829
+ );
830
+ }
831
+
832
+ async function refreshAppleDevices({
833
+ apiUrl,
834
+ token,
835
+ appleSessionId,
836
+ teamID,
837
+ connectedUDID,
838
+ setAppleDevices,
839
+ setSelectedAppleDeviceIDs,
840
+ log,
841
+ }: {
842
+ apiUrl: string;
843
+ token?: string;
844
+ appleSessionId: string;
845
+ teamID: string;
846
+ connectedUDID?: string;
847
+ setAppleDevices: (devices: AppleDeveloperPortalDevice[]) => void;
848
+ setSelectedAppleDeviceIDs: (deviceIDs: string[]) => void;
849
+ log: (message: string, detail?: string) => void;
850
+ }) {
851
+ const response = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
852
+ apiUrl,
853
+ appleSessionId,
854
+ findDeviceRequest({ deviceUDID: connectedUDID ?? '', teamID }),
855
+ token,
856
+ );
857
+ assertApplePortalResponseOK(response.body, 'Apple device list');
858
+ const devices = response.body?.devices ?? [];
859
+ setAppleDevices(devices);
860
+ const connected = devices.find((device) => normalizeAppleUDID(device.deviceNumber) === normalizeAppleUDID(connectedUDID));
861
+ if (connected?.deviceId) {
862
+ setSelectedAppleDeviceIDs([connected.deviceId]);
863
+ log('Connected iPhone found in Apple Developer devices', connected.name ?? connected.deviceNumber);
864
+ } else {
865
+ setSelectedAppleDeviceIDs([]);
866
+ log('Connected iPhone is not registered with Apple Developer', connectedUDID);
867
+ }
868
+ }
869
+
870
+ async function prepareAppleSigningAssetsForDevice({
871
+ apiUrl,
872
+ token,
873
+ appleSessionId,
874
+ teamID,
875
+ bundleID,
876
+ deviceUDID,
877
+ certificatePassword,
878
+ deviceIDs,
879
+ }: {
880
+ apiUrl: string;
881
+ token?: string;
882
+ appleSessionId: string;
883
+ teamID: string;
884
+ bundleID: string;
885
+ deviceUDID: string;
886
+ deviceIDs: string[];
887
+ certificatePassword: string;
888
+ }) {
889
+ const normalizedUDID = deviceUDID.replace(/-/g, '').replace(/[^a-fA-F0-9]/g, '');
890
+ const keyMaterial = await generateAppleSigningKeyAndCSR({
891
+ commonName: `Limrun ${bundleID}`,
892
+ });
893
+
894
+ const appIDID = await findOrCreateAppleBundleID({
895
+ apiUrl,
896
+ token,
897
+ appleSessionId,
898
+ teamID,
899
+ bundleID,
900
+ });
901
+
902
+ const certificateResponse = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
903
+ apiUrl,
904
+ appleSessionId,
905
+ submitDevelopmentCSRRequest({ csrPEM: keyMaterial.csrPEM, teamID }),
906
+ token,
907
+ );
908
+ assertApplePortalResponseOK(certificateResponse.body, 'Apple Development certificate creation');
909
+ const certificateID =
910
+ stringField(certificateResponse.body?.certRequest, 'certificateId') ??
911
+ stringField(certificateResponse.body?.certRequest, 'certRequestId') ??
912
+ stringField(certificateResponse.body, 'certificateId') ??
913
+ stringField(certificateResponse.body, 'certRequestId');
914
+ if (!certificateID) {
915
+ throw new Error('Apple certificate creation did not return a certificate ID.');
916
+ }
917
+
918
+ const downloadedCertificate = await proxyProvisioningRequest(
919
+ apiUrl,
920
+ appleSessionId,
921
+ downloadCertificateRequest(certificateID, teamID),
922
+ token,
923
+ );
924
+ if (downloadedCertificate.status < 200 || downloadedCertificate.status >= 300 || !downloadedCertificate.rawBodyBase64) {
925
+ throw new Error(`Apple certificate download failed: HTTP ${downloadedCertificate.status}`);
926
+ }
927
+ const certificateP12Base64 = exportAppleCertificateP12({
928
+ privateKeyPKCS8Base64: keyMaterial.privateKeyPKCS8Base64,
929
+ certificateBase64: downloadedCertificate.rawBodyBase64,
930
+ password: certificatePassword,
931
+ friendlyName: `Apple Development ${bundleID}`,
932
+ });
933
+
934
+ const profileName = `Limrun ${bundleID}`;
935
+ const profileResponse = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
936
+ apiUrl,
937
+ appleSessionId,
938
+ createDevelopmentProfileRequest({
939
+ bundleID,
940
+ teamID,
941
+ appIDID,
942
+ certificateID,
943
+ deviceIDs,
944
+ name: profileName,
945
+ }),
946
+ token,
947
+ );
948
+ assertApplePortalResponseOK(profileResponse.body, 'Apple provisioning profile creation');
949
+ const profileID =
950
+ stringField(profileResponse.body?.provisioningProfile, 'provisioningProfileId') ??
951
+ stringField(profileResponse.body, 'provisioningProfileId');
952
+ if (!profileID) {
953
+ throw new Error('Apple provisioning profile creation did not return a profile ID.');
954
+ }
955
+
956
+ const downloadedProfile = await proxyProvisioningRequest(
957
+ apiUrl,
958
+ appleSessionId,
959
+ downloadProfileRequest(profileID, teamID),
960
+ token,
961
+ );
962
+ if (downloadedProfile.status < 200 || downloadedProfile.status >= 300 || !downloadedProfile.rawBodyBase64) {
963
+ throw new Error(`Apple provisioning profile download failed: HTTP ${downloadedProfile.status}`);
964
+ }
965
+ const provisioningProfileBase64 = downloadedProfile.rawBodyBase64;
966
+ const profile = parseProvisioningProfileBase64(provisioningProfileBase64);
967
+
968
+ return putAppleGeneratedSigningAssets({
969
+ bundleID,
970
+ deviceUDID: normalizedUDID,
971
+ teamID,
972
+ certificateID,
973
+ certificateP12Base64,
974
+ certificatePassword,
975
+ provisioningProfileBase64,
976
+ profile,
977
+ });
978
+ }
979
+
980
+ async function findOrCreateAppleBundleID({
981
+ apiUrl,
982
+ token,
983
+ appleSessionId,
984
+ teamID,
985
+ bundleID,
986
+ }: {
987
+ apiUrl: string;
988
+ token?: string;
989
+ appleSessionId: string;
990
+ teamID: string;
991
+ bundleID: string;
992
+ }) {
993
+ const existing = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
994
+ apiUrl,
995
+ appleSessionId,
996
+ findBundleIDRequest({ bundleID, teamID }),
997
+ token,
998
+ );
999
+ assertApplePortalResponseOK(existing.body, 'Apple bundle ID lookup');
1000
+ const found = existing.body?.appIds?.find((app) => stringField(app, 'identifier') === bundleID || stringField(app, 'bundleId') === bundleID);
1001
+ const foundID = stringField(found, 'appIdId') ?? stringField(found, 'appId');
1002
+ if (foundID) return foundID;
1003
+
1004
+ const created = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
1005
+ apiUrl,
1006
+ appleSessionId,
1007
+ createBundleIDRequest({ bundleID, teamID, name: bundleID }),
1008
+ token,
1009
+ );
1010
+ assertApplePortalResponseOK(created.body, 'Apple bundle ID creation');
1011
+ const createdID =
1012
+ stringField(created.body?.appId, 'appIdId') ??
1013
+ stringField(created.body?.appId, 'appId') ??
1014
+ stringField(created.body, 'appIdId') ??
1015
+ stringField(created.body, 'appId');
1016
+ if (!createdID) {
1017
+ throw new Error('Apple bundle ID creation did not return an App ID.');
1018
+ }
1019
+ return createdID;
1020
+ }
1021
+
1022
+ function assertApplePortalResponseOK(response: AppleDeveloperPortalResponse | undefined, label: string) {
1023
+ if (!response) {
1024
+ throw new Error(`${label} returned an empty response.`);
1025
+ }
1026
+ if (response.resultCode !== undefined && response.resultCode !== 0) {
1027
+ throw new Error(`${label} failed: ${response.userString ?? response.resultString ?? response.resultCode}`);
1028
+ }
1029
+ }
1030
+
1031
+ function stringField(record: Record<string, unknown> | undefined, key: string) {
1032
+ const value = record?.[key];
1033
+ if (typeof value === 'string') return value;
1034
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
1035
+ return undefined;
1036
+ }
1037
+
1038
+ function normalizeAppleUDID(udid?: string) {
1039
+ return (udid ?? '').replace(/-/g, '').replace(/[^a-fA-F0-9]/g, '').toUpperCase();
1040
+ }
1041
+
1042
+ function appleTeamSelectionID(team?: AppleDeveloperPortalTeam) {
506
1043
  const value = team?.teamId ?? team?.providerId ?? team?.publicProviderId;
507
1044
  return value === undefined || value === '' ? undefined : String(value);
508
1045
  }
509
1046
 
1047
+ function developerPortalTeamID(team?: AppleDeveloperPortalTeam) {
1048
+ return team?.teamId && team.teamId !== '' ? team.teamId : undefined;
1049
+ }
1050
+
1051
+ function bundleIDFromAppleAppID(appID?: AppleDeveloperPortalAppID) {
1052
+ return appID?.identifier || appID?.bundleId || undefined;
1053
+ }
1054
+
510
1055
  async function fileToBase64(file: File) {
511
1056
  const buffer = await file.arrayBuffer();
512
1057
  let binary = '';