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

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 (30) hide show
  1. package/dist/core/device-install/apple/client.d.ts +1 -0
  2. package/dist/core/device-install/apple/provisioning.d.ts +42 -31
  3. package/dist/core/device-install/apple/relay.d.ts +5 -9
  4. package/dist/core/device-install/storage/browser-storage.d.ts +19 -0
  5. package/dist/core/device-install/types.d.ts +2 -2
  6. package/dist/device-install/index.cjs +1 -9
  7. package/dist/device-install/index.js +76 -210
  8. package/dist/device-install/react.cjs +1 -1
  9. package/dist/device-install/react.js +1 -1
  10. package/dist/device-install-dialog-86RDdoK9.js +2 -0
  11. package/dist/device-install-dialog-CnyDWf0q.mjs +462 -0
  12. package/dist/device-install-dialog.css +1 -1
  13. package/dist/hooks/use-device-install.d.ts +21 -3
  14. package/dist/index.cjs +1 -1
  15. package/dist/index.js +3 -3
  16. package/dist/use-device-install-CbGVvwPp.js +31 -0
  17. package/dist/use-device-install-j1Gekpl4.mjs +13623 -0
  18. package/package.json +1 -1
  19. package/src/components/device-install/device-install-dialog.css +82 -1
  20. package/src/components/device-install/device-install-dialog.tsx +337 -187
  21. package/src/core/device-install/apple/client.ts +92 -4
  22. package/src/core/device-install/apple/provisioning.ts +67 -24
  23. package/src/core/device-install/apple/relay.ts +121 -205
  24. package/src/core/device-install/storage/browser-storage.ts +26 -1
  25. package/src/core/device-install/types.ts +2 -2
  26. package/src/hooks/use-device-install.ts +748 -60
  27. package/dist/device-install-dialog-CTwVViYY.js +0 -2
  28. package/dist/device-install-dialog-zzKJu7SM.mjs +0 -328
  29. package/dist/use-device-install-CgrOKKyi.mjs +0 -13042
  30. package/dist/use-device-install-DDKRf6IL.js +0 -23
@@ -1,23 +1,41 @@
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,
11
23
  proxyProvisioningRequest,
24
+ putAppleGeneratedSigningAssets,
12
25
  putPairRecord,
13
26
  putSigningAssets,
27
+ registerDeviceRequest,
14
28
  requestUSBAccess as requestDeviceUSBAccess,
15
29
  startBrowserOwnedAppleIDLogin,
16
30
  startSignedDeviceBuild,
17
31
  startInstallRelay,
18
32
  startPairingRelay,
33
+ submitDevelopmentCSRRequest,
19
34
  watchBuildLogEvents,
20
35
  type AppleIDLoginResult,
36
+ type AppleDeveloperPortalDevice,
37
+ type AppleDeveloperPortalAppID,
38
+ type AppleDeveloperPortalResponse,
21
39
  type AppleDeveloperPortalTeam,
22
40
  type BuildLogLine,
23
41
  type DeviceInstallBuildStatus,
@@ -32,6 +50,13 @@ import type { RelayClient } from '../core/device-install/operations';
32
50
 
33
51
  type DeviceInstallStepStatuses = Record<DeviceInstallStep, DeviceInstallStepStatus>;
34
52
 
53
+ type ReusableAppleCertificate = Pick<
54
+ StoredSigningAssets,
55
+ 'certificateID' | 'certificateP12Base64' | 'certificatePassword' | 'teamID'
56
+ > & {
57
+ certificateID: string;
58
+ };
59
+
35
60
  export type UseDeviceInstallOptions = {
36
61
  apiUrl?: string;
37
62
  token?: string;
@@ -43,26 +68,40 @@ export type UseDeviceInstallResult = {
43
68
  device?: DeviceInstallDevice;
44
69
  hasPairRecord: boolean;
45
70
  hasSigningAssets: boolean;
71
+ hasSigningInputs: boolean;
46
72
  pairConfirmationRequired: boolean;
47
73
  logs: string[];
48
74
  buildLogs: BuildLogLine[];
49
75
  buildStatus: DeviceInstallBuildStatus;
50
76
  appleSigningStatus: DeviceInstallAppleSigningStatus;
51
77
  appleTeams: AppleDeveloperPortalTeam[];
78
+ appleDevices: AppleDeveloperPortalDevice[];
79
+ appleAppIDs: AppleDeveloperPortalAppID[];
80
+ applePortalSummary?: ApplePortalSummary;
52
81
  selectedAppleTeamID?: string;
82
+ selectedAppleDeviceIDs: string[];
83
+ connectedAppleDeviceRegistered: boolean;
84
+ connectedDeviceInProfile?: boolean;
85
+ hasReusableAppleCertificate: boolean;
86
+ appleBundleID: string;
53
87
  buildLogPanelOpen: boolean;
54
88
  busyAction?: DeviceInstallBusyAction;
55
89
  error?: string;
56
90
  canBuild: boolean;
91
+ canPrepareAppleSigningAssets: boolean;
57
92
  canRequestUSBAccess: boolean;
58
93
  canPairBrowser: boolean;
59
94
  canInstall: boolean;
60
95
  setSigningFiles: (files: DeviceInstallSigningFiles) => void;
96
+ setAppleBundleID: (bundleID: string) => void;
97
+ setSelectedAppleDeviceIDs: (deviceIDs: string[]) => void;
61
98
  setBuildLogPanelOpen: (open: boolean) => void;
62
99
  startAppleIDLogin: (input: DeviceInstallAppleIDLoginInput) => Promise<void>;
63
100
  submitAppleTwoFactorCode: (code: string) => Promise<void>;
64
101
  setSelectedAppleTeamID: (teamID: string | undefined) => void;
65
- clearAppleIDSession: () => Promise<void>;
102
+ clearAppleIDLogin: () => void;
103
+ registerConnectedAppleDevice: () => Promise<void>;
104
+ prepareAppleSigningAssets: () => Promise<void>;
66
105
  startDeviceBuild: () => Promise<void>;
67
106
  requestUSBAccess: () => Promise<void>;
68
107
  pairBrowser: () => Promise<void>;
@@ -76,6 +115,7 @@ export type DeviceInstallAppleSigningStatus =
76
115
  | 'two-factor-required'
77
116
  | 'authenticated'
78
117
  | 'preparing-assets'
118
+ | 'assets-ready'
79
119
  | 'using-cached-profile'
80
120
  | 'error';
81
121
 
@@ -96,10 +136,15 @@ export type DeviceInstallAppleIDLoginInput = {
96
136
  password: string;
97
137
  };
98
138
 
139
+ export type ApplePortalSummary = {
140
+ certificateCount: number;
141
+ profileCount: number;
142
+ };
143
+
99
144
  const initialStepStatuses: DeviceInstallStepStatuses = {
145
+ signing: 'idle',
146
+ connect: 'idle',
100
147
  build: 'idle',
101
- usb: 'idle',
102
- pair: 'idle',
103
148
  install: 'idle',
104
149
  };
105
150
 
@@ -107,19 +152,25 @@ export function useDeviceInstall({
107
152
  apiUrl,
108
153
  token,
109
154
  }: UseDeviceInstallOptions): UseDeviceInstallResult {
110
- const [currentStep, setCurrentStep] = useState<DeviceInstallStep>('build');
155
+ const [currentStep, setCurrentStep] = useState<DeviceInstallStep>('signing');
111
156
  const [stepStatuses, setStepStatuses] = useState<DeviceInstallStepStatuses>(initialStepStatuses);
112
157
  const [selectedDevice, setSelectedDevice] = useState<DeviceRelayTarget | undefined>();
113
158
  const [pairRecord, setPairRecord] = useState<StoredPairRecord | undefined>();
114
159
  const [signingAssets, setSigningAssets] = useState<StoredSigningAssets | undefined>();
115
160
  const [logs, setLogs] = useState<string[]>([
116
- 'Ready. Start a signed device build, allow USB access, pair this browser, then install.',
161
+ 'Ready. Prepare signing assets, connect and pair the iPhone, build, then install.',
117
162
  ]);
118
163
  const [buildLogs, setBuildLogs] = useState<BuildLogLine[]>([]);
119
164
  const [buildStatus, setBuildStatus] = useState<DeviceInstallBuildStatus>('idle');
120
165
  const [appleSigningStatus, setAppleSigningStatus] = useState<DeviceInstallAppleSigningStatus>('idle');
121
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>();
122
171
  const [selectedAppleTeamID, setSelectedAppleTeamID] = useState<string | undefined>();
172
+ const [appleBundleID, setAppleBundleID] = useState('');
173
+ const [reusableAppleCertificate, setReusableAppleCertificate] = useState<ReusableAppleCertificate | undefined>();
123
174
  const [buildLogPanelOpen, setBuildLogPanelOpen] = useState(false);
124
175
  const [busyAction, setBusyAction] = useState<DeviceInstallBusyAction | undefined>();
125
176
  const [error, setError] = useState<string | undefined>();
@@ -128,7 +179,7 @@ export function useDeviceInstall({
128
179
  const relayRef = useRef<RelayClient | undefined>(undefined);
129
180
  const selectedDeviceRef = useRef<DeviceRelayTarget | undefined>(undefined);
130
181
  const stopBuildWatcherRef = useRef<(() => void) | undefined>(undefined);
131
- const appleIDSessionRef = useRef<AppleIDLoginResult | undefined>(undefined);
182
+ const appleIDLoginRef = useRef<AppleIDLoginResult | undefined>(undefined);
132
183
 
133
184
  const log = useCallback((message: string, detail?: string) => {
134
185
  const line = detail ? `${message}\n${detail}` : message;
@@ -140,9 +191,67 @@ export function useDeviceInstall({
140
191
  }, []);
141
192
 
142
193
  const setSigningFiles = useCallback((files: DeviceInstallSigningFiles) => {
143
- setSigningFilesState((current) => ({ ...current, ...files }));
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
+ });
144
204
  setSigningAssets(undefined);
145
- }, []);
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;
146
255
 
147
256
  useEffect(() => {
148
257
  selectedDeviceRef.current = selectedDevice;
@@ -157,16 +266,19 @@ export function useDeviceInstall({
157
266
  useEffect(() => {
158
267
  return () => {
159
268
  stopBuildWatcherRef.current?.();
160
- void appleIDSessionRef.current?.close();
269
+ void appleIDLoginRef.current?.close();
270
+ appleIDLoginRef.current = undefined;
161
271
  void cleanupDeviceAccess();
162
272
  };
163
273
  }, [cleanupDeviceAccess]);
164
274
 
165
275
  const resolveSigningAssetsForBuild = useCallback(async () => {
166
- const info = apiUrl ? await fetchStoredBuildInfo(apiUrl, token).catch(() => undefined) : undefined;
167
- if (info?.lastBuildConfig?.bundleId) {
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) {
168
280
  const cached = await getReusableAppleSigningAssets({
169
- bundleID: info.lastBuildConfig.bundleId,
281
+ bundleID,
170
282
  deviceUDID: selectedDevice?.hello.serialNumber,
171
283
  teamID: selectedAppleTeamID,
172
284
  });
@@ -213,21 +325,43 @@ export function useDeviceInstall({
213
325
  setSigningAssets(storedAssets);
214
326
  log('Signing assets stored locally', storageBundleId);
215
327
  return storedAssets;
216
- }, [apiUrl, log, selectedAppleTeamID, selectedDevice?.hello.serialNumber, signingFiles, token]);
328
+ }, [apiUrl, appleBundleID, log, selectedAppleTeamID, selectedDevice?.hello.serialNumber, signingFiles, token]);
217
329
 
218
330
  const startAppleIDLogin = useCallback(
219
331
  async ({ accountName, password }: DeviceInstallAppleIDLoginInput) => {
220
332
  if (!apiUrl) return;
221
- setBusyAction('build');
333
+ setBusyAction('signing');
222
334
  setError(undefined);
335
+ setCurrentStep('signing');
336
+ setStepStatus('signing', 'active');
223
337
  setAppleSigningStatus('authenticating');
224
338
  try {
225
- await appleIDSessionRef.current?.close().catch(() => undefined);
339
+ await appleIDLoginRef.current?.close().catch(() => undefined);
226
340
  const session = await startBrowserOwnedAppleIDLogin({ limbuildApiUrl: apiUrl, token, accountName, password });
227
- appleIDSessionRef.current = session;
341
+ appleIDLoginRef.current = session;
228
342
  if (!session.requiresTwoFactor) {
229
- await session.finalize();
230
- await refreshAppleTeams(apiUrl, session.appleSessionId, token, setAppleTeams, setSelectedAppleTeamID);
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);
231
365
  }
232
366
  setAppleSigningStatus(session.requiresTwoFactor ? 'two-factor-required' : 'authenticated');
233
367
  log(
@@ -243,22 +377,44 @@ export function useDeviceInstall({
243
377
  setBusyAction(undefined);
244
378
  }
245
379
  },
246
- [apiUrl, log, token],
380
+ [apiUrl, log, setStepStatus, token],
247
381
  );
248
382
 
249
383
  const submitAppleTwoFactorCode = useCallback(
250
384
  async (code: string) => {
251
- const session = appleIDSessionRef.current;
385
+ const session = appleIDLoginRef.current;
252
386
  if (!session) {
253
387
  throw new Error('Start Apple ID login before submitting a two-factor code.');
254
388
  }
255
- setBusyAction('build');
389
+ setBusyAction('signing');
256
390
  setError(undefined);
391
+ setCurrentStep('signing');
392
+ setStepStatus('signing', 'active');
257
393
  try {
258
394
  await session.finishTwoFactor(code);
259
- await session.finalize();
260
395
  if (apiUrl) {
261
- await refreshAppleTeams(apiUrl, session.appleSessionId, token, setAppleTeams, setSelectedAppleTeamID);
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);
262
418
  }
263
419
  setAppleSigningStatus('authenticated');
264
420
  log('Apple ID two-factor authentication accepted');
@@ -271,17 +427,181 @@ export function useDeviceInstall({
271
427
  setBusyAction(undefined);
272
428
  }
273
429
  },
274
- [apiUrl, log, token],
430
+ [apiUrl, log, setStepStatus, token],
275
431
  );
276
432
 
277
- const clearAppleIDSession = useCallback(async () => {
278
- await appleIDSessionRef.current?.close().catch(() => undefined);
279
- appleIDSessionRef.current = undefined;
433
+ const clearAppleIDLogin = useCallback(() => {
434
+ void appleIDLoginRef.current?.close();
435
+ appleIDLoginRef.current = undefined;
280
436
  setAppleTeams([]);
437
+ setAppleDevices([]);
438
+ setAppleAppIDs([]);
439
+ setSelectedAppleDeviceIDs([]);
440
+ setApplePortalSummary(undefined);
281
441
  setSelectedAppleTeamID(undefined);
442
+ setReusableAppleCertificate(undefined);
282
443
  setAppleSigningStatus('idle');
283
- log('Apple ID session cleared');
284
- }, [log]);
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
+ ]);
285
605
 
286
606
  const startDeviceBuild = useCallback(async () => {
287
607
  if (!apiUrl) return;
@@ -316,7 +636,7 @@ export function useDeviceInstall({
316
636
  setBuildStatus(status);
317
637
  if (status === 'succeeded') {
318
638
  setStepStatus('build', 'complete');
319
- setCurrentStep('usb');
639
+ setCurrentStep('install');
320
640
  } else if (status === 'failed' || status === 'cancelled') {
321
641
  setStepStatus('build', 'error');
322
642
  }
@@ -336,13 +656,13 @@ export function useDeviceInstall({
336
656
  } finally {
337
657
  setBusyAction(undefined);
338
658
  }
339
- }, [apiUrl, log, resolveSigningAssetsForBuild, setStepStatus, token]);
659
+ }, [apiUrl, log, pairRecord, resolveSigningAssetsForBuild, setStepStatus, token]);
340
660
 
341
661
  const requestUSBAccess = useCallback(async () => {
342
662
  setBusyAction('usb');
343
663
  setError(undefined);
344
- setCurrentStep('usb');
345
- setStepStatus('usb', 'active');
664
+ setCurrentStep('connect');
665
+ setStepStatus('connect', 'active');
346
666
  try {
347
667
  await cleanupDeviceAccess();
348
668
  const target = await requestDeviceUSBAccess({ log });
@@ -350,33 +670,48 @@ export function useDeviceInstall({
350
670
  setPairConfirmationRequired(false);
351
671
  const storedPairRecord = await getPairRecord(target.hello.serialNumber);
352
672
  setPairRecord(storedPairRecord);
353
- const storedSigningAssets = await getLatestSigningAssets();
673
+ const storedSigningAssets = manualSigningFilesReady ? undefined : await getLatestSigningAssets();
354
674
  if (storedSigningAssets) {
355
675
  if (!profileContainsDevice(storedSigningAssets.profile, target.hello.serialNumber)) {
356
676
  throw new Error('Stored provisioning profile does not include the selected iPhone.');
357
677
  }
358
678
  setSigningAssets(storedSigningAssets);
359
679
  }
360
- setStepStatus('usb', 'complete');
361
- setCurrentStep(storedPairRecord ? 'install' : 'pair');
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');
362
697
  log(storedPairRecord ? 'Pair record found' : 'No pair record found', target.hello.serialNumber);
363
698
  } catch (caught) {
364
699
  const message = errorMessage(caught);
365
700
  setError(message);
366
- setStepStatus('usb', 'error');
701
+ setStepStatus('connect', 'error');
367
702
  log('USB access failed', message);
368
703
  } finally {
369
704
  setBusyAction(undefined);
370
705
  }
371
- }, [cleanupDeviceAccess, log, setStepStatus]);
706
+ }, [apiUrl, cleanupDeviceAccess, log, manualSigningFilesReady, selectedDeveloperTeamID, setStepStatus, token]);
372
707
 
373
708
  const pairBrowser = useCallback(async () => {
374
709
  if (!apiUrl || !selectedDevice) return;
375
710
  setBusyAction('pair');
376
711
  setError(undefined);
377
712
  setPairConfirmationRequired(false);
378
- setCurrentStep('pair');
379
- setStepStatus('pair', 'active');
713
+ setCurrentStep('connect');
714
+ setStepStatus('connect', 'active');
380
715
  try {
381
716
  await cleanupDeviceAccess();
382
717
  const result = await startPairingRelay({
@@ -392,15 +727,15 @@ export function useDeviceInstall({
392
727
  await closeDeviceRelayTarget(selectedDevice, log);
393
728
  setPairRecord(stored);
394
729
  setPairConfirmationRequired(false);
395
- setStepStatus('pair', 'complete');
396
- setCurrentStep('install');
730
+ setStepStatus('connect', 'complete');
731
+ setCurrentStep('build');
397
732
  log('Device paired', 'The pair record was stored locally in this browser.');
398
733
  } catch (caught) {
399
734
  await closeDeviceRelayTarget(selectedDevice, log);
400
735
  const message = errorMessage(caught);
401
736
  setPairConfirmationRequired(true);
402
737
  setError('Unlock the iPhone, tap Trust, then confirm the pair record.');
403
- setStepStatus('pair', 'error');
738
+ setStepStatus('connect', 'error');
404
739
  log('Device pairing failed', message);
405
740
  } finally {
406
741
  setBusyAction(undefined);
@@ -446,26 +781,53 @@ export function useDeviceInstall({
446
781
  device: selectedDevice?.hello,
447
782
  hasPairRecord: !!pairRecord,
448
783
  hasSigningAssets: !!signingAssets,
784
+ hasSigningInputs: signingInputsReady,
449
785
  pairConfirmationRequired,
450
786
  logs,
451
787
  buildLogs,
452
788
  buildStatus,
453
789
  appleSigningStatus,
454
790
  appleTeams,
791
+ appleDevices,
792
+ appleAppIDs,
793
+ applePortalSummary,
455
794
  selectedAppleTeamID,
795
+ selectedAppleDeviceIDs,
796
+ connectedAppleDeviceRegistered,
797
+ connectedDeviceInProfile,
798
+ hasReusableAppleCertificate: !!reusableAppleCertificate,
799
+ appleBundleID,
456
800
  buildLogPanelOpen,
457
801
  busyAction,
458
802
  error,
459
- canBuild: !!apiUrl && !busyAction,
460
- canRequestUSBAccess: !busyAction && (buildStatus === 'succeeded' || stepStatuses.build === 'complete'),
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,
461
819
  canPairBrowser: !!apiUrl && !busyAction && !!selectedDevice,
462
820
  canInstall: !!apiUrl && !busyAction && !!selectedDevice && !!pairRecord,
463
821
  setSigningFiles,
822
+ setAppleBundleID,
823
+ setSelectedAppleDeviceIDs,
464
824
  setBuildLogPanelOpen,
465
825
  startAppleIDLogin,
466
826
  submitAppleTwoFactorCode,
467
- setSelectedAppleTeamID,
468
- clearAppleIDSession,
827
+ setSelectedAppleTeamID: selectAppleTeam,
828
+ clearAppleIDLogin,
829
+ registerConnectedAppleDevice,
830
+ prepareAppleSigningAssets,
469
831
  startDeviceBuild,
470
832
  requestUSBAccess,
471
833
  pairBrowser,
@@ -478,35 +840,361 @@ async function fetchStoredBuildInfo(apiUrl: string, token?: string) {
478
840
  return fetchLimbuildInfo(apiUrl, token);
479
841
  }
480
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
+
481
856
  async function refreshAppleTeams(
482
857
  apiUrl: string,
483
858
  appleSessionId: string,
484
859
  token: string | undefined,
485
860
  setAppleTeams: (teams: AppleDeveloperPortalTeam[]) => void,
486
861
  setSelectedAppleTeamID: (teamID: string | undefined) => void,
862
+ accountSession?: AppleDeveloperPortalResponse,
487
863
  ) {
488
- const response = await proxyProvisioningRequest<{
489
- teams?: AppleDeveloperPortalTeam[];
490
- availableProviders?: AppleDeveloperPortalTeam[];
491
- provider?: AppleDeveloperPortalTeam;
492
- }>(apiUrl, appleSessionId, listTeamsRequest(), token);
493
- const teams = [
864
+ const response = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(apiUrl, appleSessionId, listTeamsRequest(), token);
865
+ assertApplePortalResponseOK(response.body, 'Apple Developer team list');
866
+ const teams = uniqueAppleTeams([
494
867
  ...(response.body?.teams ?? []),
495
- ...(response.body?.availableProviders ?? []),
496
- ...(response.body?.provider ? [response.body.provider] : []),
497
- ];
868
+ ]);
869
+ void accountSession;
498
870
  setAppleTeams(teams);
499
- const firstTeamID = teamIDFromPortalTeam(teams[0]);
500
- if (firstTeamID) {
501
- setSelectedAppleTeamID(firstTeamID);
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);
502
1000
  }
503
1001
  }
504
1002
 
505
- function teamIDFromPortalTeam(team?: AppleDeveloperPortalTeam) {
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) {
506
1186
  const value = team?.teamId ?? team?.providerId ?? team?.publicProviderId;
507
1187
  return value === undefined || value === '' ? undefined : String(value);
508
1188
  }
509
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
+
510
1198
  async function fileToBase64(file: File) {
511
1199
  const buffer = await file.arrayBuffer();
512
1200
  let binary = '';