@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limrun/ui",
3
- "version": "0.9.0-rc.1",
3
+ "version": "0.9.0-rc.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -171,23 +171,49 @@ export function DeviceInstallDialog({
171
171
  </div>
172
172
  )}
173
173
  {deviceInstall.appleTeams.length > 0 && (
174
+ <>
175
+ <label className="lr-device-install__field">
176
+ <span>Apple Developer team</span>
177
+ <select
178
+ value={deviceInstall.selectedAppleTeamID ?? ''}
179
+ onChange={(event) =>
180
+ deviceInstall.setSelectedAppleTeamID(event.currentTarget.value || undefined)
181
+ }
182
+ >
183
+ {deviceInstall.appleTeams.map((team, index) => {
184
+ const teamID =
185
+ team.teamId ??
186
+ (team.providerId === undefined ? undefined : String(team.providerId)) ??
187
+ team.publicProviderId ??
188
+ '';
189
+ return (
190
+ <option key={`${teamID}-${index}`} value={teamID}>
191
+ {team.name ?? 'Apple Developer Team'} {teamID ? `(${teamID})` : ''}
192
+ </option>
193
+ );
194
+ })}
195
+ </select>
196
+ </label>
197
+ {deviceInstall.applePortalSummary && (
198
+ <p className="lr-device-install__hint">
199
+ Found {deviceInstall.applePortalSummary.certificateCount} certificates and{' '}
200
+ {deviceInstall.applePortalSummary.profileCount} provisioning profiles.
201
+ </p>
202
+ )}
203
+ </>
204
+ )}
205
+ {deviceInstall.appleAppIDs.length > 0 && (
174
206
  <label className="lr-device-install__field">
175
- <span>Apple Developer team</span>
207
+ <span>Bundle ID</span>
176
208
  <select
177
- value={deviceInstall.selectedAppleTeamID ?? ''}
178
- onChange={(event) =>
179
- deviceInstall.setSelectedAppleTeamID(event.currentTarget.value || undefined)
180
- }
209
+ value={deviceInstall.appleBundleID}
210
+ onChange={(event) => deviceInstall.setAppleBundleID(event.currentTarget.value)}
181
211
  >
182
- {deviceInstall.appleTeams.map((team, index) => {
183
- const teamID =
184
- team.teamId ??
185
- (team.providerId === undefined ? undefined : String(team.providerId)) ??
186
- team.publicProviderId ??
187
- '';
212
+ {deviceInstall.appleAppIDs.map((appID, index) => {
213
+ const bundleID = appID.identifier ?? appID.bundleId ?? '';
188
214
  return (
189
- <option key={`${teamID}-${index}`} value={teamID}>
190
- {team.name ?? 'Apple Developer Team'} {teamID ? `(${teamID})` : ''}
215
+ <option key={`${bundleID}-${index}`} value={bundleID}>
216
+ {appID.name ?? bundleID} {bundleID ? `(${bundleID})` : ''}
191
217
  </option>
192
218
  );
193
219
  })}
@@ -269,6 +295,49 @@ export function DeviceInstallDialog({
269
295
  }`.trim()}
270
296
  </div>
271
297
  )}
298
+ {deviceInstall.appleDevices.length > 0 && (
299
+ <label className="lr-device-install__field">
300
+ <span>Apple Developer devices</span>
301
+ <select
302
+ multiple
303
+ value={deviceInstall.selectedAppleDeviceIDs}
304
+ onChange={(event) =>
305
+ deviceInstall.setSelectedAppleDeviceIDs(
306
+ Array.from(event.currentTarget.selectedOptions).map((option) => option.value),
307
+ )
308
+ }
309
+ >
310
+ {deviceInstall.appleDevices.map((appleDevice) => (
311
+ <option key={appleDevice.deviceId ?? appleDevice.deviceNumber} value={appleDevice.deviceId ?? ''}>
312
+ {appleDevice.name ?? appleDevice.model ?? 'Apple device'} {appleDevice.deviceNumber ?? ''}
313
+ </option>
314
+ ))}
315
+ </select>
316
+ </label>
317
+ )}
318
+ {deviceInstall.device && !deviceInstall.connectedAppleDeviceRegistered && (
319
+ <button
320
+ type="button"
321
+ className="lr-device-install__secondary"
322
+ disabled={disabled || !!deviceInstall.busyAction}
323
+ onClick={() => void deviceInstall.registerConnectedAppleDevice()}
324
+ >
325
+ Register connected iPhone
326
+ </button>
327
+ )}
328
+ <button
329
+ type="button"
330
+ className="lr-device-install__secondary"
331
+ disabled={disabled || !deviceInstall.canPrepareAppleSigningAssets}
332
+ onClick={() => void deviceInstall.prepareAppleSigningAssets()}
333
+ >
334
+ {deviceInstall.appleSigningStatus === 'preparing-assets'
335
+ ? 'Preparing signing assets...'
336
+ : 'Generate certificate and profile'}
337
+ </button>
338
+ {deviceInstall.hasSigningAssets && (
339
+ <p>Signing assets are stored in this browser for the selected bundle and device.</p>
340
+ )}
272
341
  </div>
273
342
  )}
274
343
 
@@ -2,10 +2,13 @@ import { AppleGsaSrpClient } from './gsa-srp';
2
2
  import {
3
3
  createAppleRelaySession,
4
4
  deleteAppleRelaySession,
5
- finalizeAppleRelaySession,
5
+ fetchAppleAccountSession,
6
+ proxyPhoneTwoFactorCode,
6
7
  proxySrpComplete,
7
8
  proxySrpInit,
8
9
  proxyTwoFactorCode,
10
+ triggerPhoneTwoFactor,
11
+ triggerTrustedDeviceTwoFactor,
9
12
  type AppleRelayResponse,
10
13
  } from './relay';
11
14
 
@@ -19,12 +22,17 @@ export type AppleIDLoginInput = {
19
22
  export type AppleIDLoginResult = {
20
23
  appleSessionId: string;
21
24
  completeResponse: AppleRelayResponse;
25
+ twoFactorChallengeResponse?: AppleRelayResponse;
22
26
  requiresTwoFactor: boolean;
23
27
  finishTwoFactor: (code: string) => Promise<AppleRelayResponse>;
24
28
  finalize: () => Promise<AppleRelayResponse>;
25
29
  close: () => Promise<void>;
26
30
  };
27
31
 
32
+ type TwoFactorMethod =
33
+ | { type: 'trustedDevice' }
34
+ | { type: 'phone'; phoneNumberId: number; mode: string };
35
+
28
36
  export async function startBrowserOwnedAppleIDLogin({
29
37
  limbuildApiUrl,
30
38
  accountName,
@@ -35,6 +43,9 @@ export async function startBrowserOwnedAppleIDLogin({
35
43
  try {
36
44
  const srp = new AppleGsaSrpClient(accountName);
37
45
  const initResponse = await proxySrpInit(limbuildApiUrl, appleSessionId, await srp.init(), token);
46
+ if (initResponse.status < 200 || initResponse.status >= 300) {
47
+ throw new Error(`Apple SRP init failed: HTTP ${initResponse.status} ${initResponse.rawBody ?? ''}`.trim());
48
+ }
38
49
  if (!initResponse.body) {
39
50
  throw new Error('Apple SRP init response did not include a body.');
40
51
  }
@@ -49,12 +60,64 @@ export async function startBrowserOwnedAppleIDLogin({
49
60
  },
50
61
  token,
51
62
  );
63
+ const requiresTwoFactor = completeResponse.status === 409;
64
+ let twoFactorChallengeResponse: AppleRelayResponse | undefined;
65
+ let twoFactorMethod: TwoFactorMethod = { type: 'trustedDevice' };
66
+ if (requiresTwoFactor) {
67
+ twoFactorChallengeResponse = await triggerTrustedDeviceTwoFactor(limbuildApiUrl, appleSessionId, token);
68
+ const phone = trustedPhoneNumberFromChallenge(twoFactorChallengeResponse.body);
69
+ if (phone) {
70
+ twoFactorMethod = {
71
+ type: 'phone',
72
+ phoneNumberId: phone.id,
73
+ mode: phone.pushMode ?? 'sms',
74
+ };
75
+ }
76
+ if (twoFactorChallengeResponse.status === 412) {
77
+ if (!phone) {
78
+ throw new Error('Apple requested phone verification but did not include a trusted phone number.');
79
+ }
80
+ twoFactorChallengeResponse = await triggerPhoneTwoFactor(
81
+ limbuildApiUrl,
82
+ appleSessionId,
83
+ phone.id,
84
+ phone.pushMode ?? 'sms',
85
+ token,
86
+ );
87
+ }
88
+ if (twoFactorChallengeResponse.status < 200 || twoFactorChallengeResponse.status >= 300) {
89
+ throw new Error(
90
+ `Apple two-factor challenge failed: HTTP ${twoFactorChallengeResponse.status} ${
91
+ twoFactorChallengeResponse.rawBody ?? ''
92
+ }`.trim(),
93
+ );
94
+ }
95
+ } else if (completeResponse.status < 200 || completeResponse.status >= 300) {
96
+ throw new Error(`Apple SRP complete failed: HTTP ${completeResponse.status} ${completeResponse.rawBody ?? ''}`.trim());
97
+ }
52
98
  return {
53
99
  appleSessionId,
54
100
  completeResponse,
55
- requiresTwoFactor: completeResponse.status === 409,
56
- finishTwoFactor: (code) => proxyTwoFactorCode(limbuildApiUrl, appleSessionId, code, token),
57
- finalize: () => finalizeAppleRelaySession(limbuildApiUrl, appleSessionId, token),
101
+ twoFactorChallengeResponse,
102
+ requiresTwoFactor,
103
+ finishTwoFactor: async (code) => {
104
+ const response =
105
+ twoFactorMethod.type === 'phone'
106
+ ? await proxyPhoneTwoFactorCode(
107
+ limbuildApiUrl,
108
+ appleSessionId,
109
+ twoFactorMethod.phoneNumberId,
110
+ code,
111
+ twoFactorMethod.mode,
112
+ token,
113
+ )
114
+ : await proxyTwoFactorCode(limbuildApiUrl, appleSessionId, code, token);
115
+ if (response.status < 200 || response.status >= 300) {
116
+ throw new Error(`Apple two-factor code failed: HTTP ${response.status} ${response.rawBody ?? ''}`.trim());
117
+ }
118
+ return response;
119
+ },
120
+ finalize: async () => fetchAppleAccountSession(limbuildApiUrl, appleSessionId, token),
58
121
  close: () => deleteAppleRelaySession(limbuildApiUrl, appleSessionId, token),
59
122
  };
60
123
  } catch (error) {
@@ -62,3 +125,28 @@ export async function startBrowserOwnedAppleIDLogin({
62
125
  throw error;
63
126
  }
64
127
  }
128
+
129
+ function trustedPhoneNumberFromChallenge(body: unknown) {
130
+ if (!isRecord(body)) return undefined;
131
+ const verification = isRecord(body.phoneNumberVerification) ? body.phoneNumberVerification : undefined;
132
+ const trustedPhoneNumber =
133
+ recordValue(verification?.trustedPhoneNumber) ??
134
+ recordValue(body.trustedPhoneNumber) ??
135
+ recordValue(body.phoneNumber);
136
+ if (!trustedPhoneNumber) return undefined;
137
+ const id = trustedPhoneNumber.id;
138
+ if (typeof id !== 'number') return undefined;
139
+ const pushMode =
140
+ typeof trustedPhoneNumber.pushMode === 'string' ? trustedPhoneNumber.pushMode
141
+ : typeof body.mode === 'string' ? body.mode
142
+ : undefined;
143
+ return { id, pushMode };
144
+ }
145
+
146
+ function recordValue(value: unknown) {
147
+ return isRecord(value) ? value : undefined;
148
+ }
149
+
150
+ function isRecord(value: unknown): value is Record<string, unknown> {
151
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
152
+ }
@@ -18,13 +18,39 @@ export type AppleDeveloperPortalTeam = {
18
18
  subType?: string;
19
19
  };
20
20
 
21
+ export type AppleDeveloperPortalDevice = {
22
+ deviceId?: string;
23
+ name?: string;
24
+ deviceNumber?: string;
25
+ deviceClass?: string;
26
+ model?: string;
27
+ status?: string;
28
+ };
29
+
30
+ export type AppleDeveloperPortalAppID = {
31
+ appId?: string;
32
+ appIdId?: string;
33
+ identifier?: string;
34
+ bundleId?: string;
35
+ name?: string;
36
+ prefix?: string;
37
+ platform?: string;
38
+ };
39
+
21
40
  export type AppleDeveloperPortalResponse = {
41
+ resultCode?: number;
42
+ resultString?: string;
43
+ userString?: string;
22
44
  teams?: AppleDeveloperPortalTeam[];
23
45
  provider?: AppleDeveloperPortalTeam;
24
46
  availableProviders?: AppleDeveloperPortalTeam[];
25
- appIds?: Array<Record<string, unknown>>;
26
- devices?: Array<Record<string, unknown>>;
47
+ appIds?: AppleDeveloperPortalAppID[];
48
+ devices?: AppleDeveloperPortalDevice[];
27
49
  certRequests?: Array<Record<string, unknown>>;
50
+ certRequest?: Record<string, unknown>;
51
+ appId?: Record<string, unknown>;
52
+ device?: Record<string, unknown>;
53
+ provisioningProfile?: Record<string, unknown>;
28
54
  provisioningProfiles?: Array<Record<string, unknown>>;
29
55
  };
30
56
 
@@ -60,37 +86,49 @@ export function listTeamsRequest(): AppleProvisioningRequest {
60
86
  }
61
87
 
62
88
  export function findBundleIDRequest({ bundleID, teamID = '' }: Pick<AppleProvisioningContext, 'bundleID' | 'teamID'>) {
63
- return pagedRequest('/account/ios/identifiers/listAppIds.action', teamID, { search: bundleID });
89
+ void bundleID;
90
+ return pagedRequest('/account/ios/identifiers/listAppIds.action', teamID, { sort: 'name=asc' });
64
91
  }
65
92
 
66
93
  export function findDeviceRequest({ deviceUDID, teamID = '' }: Pick<AppleProvisioningContext, 'deviceUDID' | 'teamID'>) {
67
- return pagedRequest('/account/ios/device/listDevices.action', teamID, { search: normalizeUDID(deviceUDID) });
94
+ void deviceUDID;
95
+ return pagedRequest('/account/ios/device/listDevices.action', teamID, {
96
+ sort: 'name=asc',
97
+ includeRemovedDevices: false,
98
+ });
68
99
  }
69
100
 
70
101
  export function findDevelopmentCertificatesRequest(teamID = '') {
71
- return pagedRequest('/account/ios/certificate/listCertRequests.action', teamID);
102
+ return pagedRequest('/account/ios/certificate/listCertRequests.action', teamID, {
103
+ sort: 'name=asc',
104
+ types: '83Q87W3TGH,5QPB9NHCEI',
105
+ });
72
106
  }
73
107
 
74
108
  export function findDevelopmentProfilesRequest({
75
109
  bundleID,
76
110
  teamID = '',
77
111
  }: Pick<AppleProvisioningContext, 'bundleID' | 'teamID'>) {
78
- return pagedRequest('/account/ios/profile/listProvisioningProfiles.action', teamID, { search: bundleID });
112
+ return pagedRequest('/account/ios/profile/listProvisioningProfiles.action', teamID, {
113
+ search: bundleID,
114
+ sort: 'name=asc',
115
+ });
79
116
  }
80
117
 
81
118
  export function registerDeviceRequest({
82
119
  deviceUDID,
83
120
  teamID = '',
84
121
  name = 'Limrun iPhone',
85
- }: AppleProvisioningContext & { name?: string }) {
122
+ }: Pick<AppleProvisioningContext, 'deviceUDID' | 'teamID'> & { name?: string }) {
86
123
  return {
87
124
  method: 'POST',
88
- path: '/account/ios/device/addDevice.action',
125
+ path: '/account/ios/device/addDevices.action',
89
126
  payload: {
90
127
  teamId: teamID,
91
- name,
92
- deviceNumber: normalizeUDID(deviceUDID),
93
- deviceClass: 'iphone',
128
+ deviceNames: name,
129
+ deviceNumbers: normalizeUDID(deviceUDID),
130
+ deviceClasses: 'iphone',
131
+ register: 'single',
94
132
  },
95
133
  } satisfies AppleProvisioningRequest;
96
134
  }
@@ -121,9 +159,10 @@ export function submitDevelopmentCSRRequest({
121
159
  }) {
122
160
  return {
123
161
  method: 'POST',
124
- path: '/account/ios/certificate/submitDevelopmentCSR.action',
162
+ path: '/account/ios/certificate/submitCertificateRequest.action',
125
163
  payload: {
126
164
  teamId: teamID,
165
+ type: '83Q87W3TGH',
127
166
  csrContent: csrPEM,
128
167
  },
129
168
  } satisfies AppleProvisioningRequest;
@@ -131,11 +170,12 @@ export function submitDevelopmentCSRRequest({
131
170
 
132
171
  export function downloadCertificateRequest(certificateID: string, teamID = '') {
133
172
  return {
134
- method: 'POST',
173
+ method: 'GET',
135
174
  path: '/account/ios/certificate/downloadCertificateContent.action',
136
175
  payload: {
137
176
  teamId: teamID,
138
177
  certificateId: certificateID,
178
+ type: '83Q87W3TGH',
139
179
  },
140
180
  } satisfies AppleProvisioningRequest;
141
181
  }
@@ -158,11 +198,11 @@ export function createDevelopmentProfileRequest({
158
198
  path: '/account/ios/profile/createProvisioningProfile.action',
159
199
  payload: {
160
200
  teamId: teamID,
161
- appIdId: appIDID,
201
+ provisioningProfileName: name ?? `Limrun ${bundleID}`,
162
202
  certificateIds: [certificateID],
203
+ appIdId: appIDID,
163
204
  deviceIds: deviceIDs,
164
- distributionMethod: 'development',
165
- name: name ?? `Limrun ${bundleID}`,
205
+ distributionType: 'limited',
166
206
  subPlatform: 'ios',
167
207
  },
168
208
  } satisfies AppleProvisioningRequest;
@@ -170,8 +210,8 @@ export function createDevelopmentProfileRequest({
170
210
 
171
211
  export function downloadProfileRequest(profileID: string, teamID = '') {
172
212
  return {
173
- method: 'POST',
174
- path: '/account/ios/profile/downloadProfileContent.action',
213
+ method: 'GET',
214
+ path: '/account/ios/profile/downloadProfileContent',
175
215
  payload: {
176
216
  teamId: teamID,
177
217
  provisioningProfileId: profileID,
@@ -242,14 +282,17 @@ export function teamIDCandidates(body: unknown): string[] {
242
282
  }
243
283
 
244
284
  function pagedRequest(path: string, teamID: string, payload: Record<string, unknown> = {}) {
285
+ const basePayload: Record<string, unknown> = {
286
+ pageNumber: 1,
287
+ pageSize: 200,
288
+ ...payload,
289
+ };
290
+ if (teamID) {
291
+ basePayload.teamId = teamID;
292
+ }
245
293
  return {
246
294
  method: 'POST',
247
295
  path,
248
- payload: {
249
- pageNumber: 1,
250
- pageSize: 200,
251
- teamId: teamID,
252
- ...payload,
253
- },
296
+ payload: basePayload,
254
297
  } satisfies AppleProvisioningRequest;
255
298
  }