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

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