@limrun/ui 0.9.0-rc.14 → 0.9.0-rc.15

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/components/remote-control.d.ts +125 -1
  2. package/dist/core/device-install/apple/provisioning.d.ts +55 -4
  3. package/dist/core/device-install/operations/limbuild-client.d.ts +15 -1
  4. package/dist/core/device-install/storage/browser-storage.d.ts +9 -5
  5. package/dist/core/device-install/types.d.ts +4 -1
  6. package/dist/device-install/index.cjs +1 -1
  7. package/dist/device-install/index.js +70 -64
  8. package/dist/device-install/react.cjs +1 -1
  9. package/dist/device-install/react.js +1 -1
  10. package/dist/device-install-dialog-DY35un0b.js +9 -0
  11. package/dist/device-install-dialog-chNLeiiL.mjs +2000 -0
  12. package/dist/device-install-dialog.css +1 -1
  13. package/dist/hooks/use-device-install.d.ts +5 -1
  14. package/dist/index.cjs +1 -1
  15. package/dist/index.js +1286 -1116
  16. package/dist/use-device-install-BIrl0v-k.js +31 -0
  17. package/dist/{use-device-install-sDVvby1V.mjs → use-device-install-LGfEdqyM.mjs} +4388 -4271
  18. package/package.json +4 -2
  19. package/src/components/device-install/device-install-dialog.css +29 -0
  20. package/src/components/device-install/device-install-dialog.tsx +91 -30
  21. package/src/components/remote-control.tsx +535 -5
  22. package/src/core/device-install/apple/provisioning.test.ts +84 -0
  23. package/src/core/device-install/apple/provisioning.ts +91 -7
  24. package/src/core/device-install/operations/limbuild-client.ts +32 -2
  25. package/src/core/device-install/storage/browser-storage.ts +29 -14
  26. package/src/core/device-install/types.ts +5 -1
  27. package/src/hooks/use-device-install.ts +135 -59
  28. package/dist/device-install-dialog-CjH25hnN.js +0 -2
  29. package/dist/device-install-dialog-W5Xv9kWL.mjs +0 -443
  30. package/dist/use-device-install-Y1u6vIBB.js +0 -31
@@ -0,0 +1,84 @@
1
+ // @vitest-environment node
2
+
3
+ import { describe, expect, test } from 'vitest';
4
+ import {
5
+ createAdHocProfileRequest,
6
+ createDevelopmentProfileRequest,
7
+ downloadDistributionCertificateRequest,
8
+ findAdHocProfilesRequest,
9
+ findDevelopmentCertificatesRequest,
10
+ findDistributionCertificatesRequest,
11
+ submitDistributionCSRRequest,
12
+ } from './provisioning';
13
+
14
+ describe('Apple provisioning request helpers', () => {
15
+ test('keeps development certificate requests on Apple Development types', () => {
16
+ expect(findDevelopmentCertificatesRequest('TEAM').payload).toMatchObject({
17
+ teamId: 'TEAM',
18
+ types: '83Q87W3TGH,5QPB9NHCEI',
19
+ });
20
+ });
21
+
22
+ test('uses distribution certificate requests for Ad Hoc signing', () => {
23
+ expect(findDistributionCertificatesRequest('TEAM').payload).toMatchObject({
24
+ teamId: 'TEAM',
25
+ types: 'WXV89964HE,R58UK2EWSO',
26
+ });
27
+ expect(submitDistributionCSRRequest({ csrPEM: 'csr', teamID: 'TEAM' }).payload).toMatchObject({
28
+ teamId: 'TEAM',
29
+ type: 'WXV89964HE',
30
+ csrContent: 'csr',
31
+ });
32
+ expect(downloadDistributionCertificateRequest('CERT', 'TEAM').payload).toMatchObject({
33
+ teamId: 'TEAM',
34
+ certificateId: 'CERT',
35
+ type: 'WXV89964HE',
36
+ });
37
+ });
38
+
39
+ test('builds separate development and Ad Hoc profile payloads', () => {
40
+ expect(
41
+ createDevelopmentProfileRequest({
42
+ bundleID: 'com.example.app',
43
+ teamID: 'TEAM',
44
+ appIDID: 'APP',
45
+ certificateID: 'CERT',
46
+ deviceIDs: ['DEVICE'],
47
+ }).payload,
48
+ ).toMatchObject({
49
+ teamId: 'TEAM',
50
+ provisioningProfileName: 'Limrun com.example.app',
51
+ certificateIds: ['CERT'],
52
+ appIdId: 'APP',
53
+ deviceIds: ['DEVICE'],
54
+ distributionType: 'limited',
55
+ subPlatform: 'ios',
56
+ });
57
+
58
+ expect(
59
+ createAdHocProfileRequest({
60
+ bundleID: 'com.example.app',
61
+ teamID: 'TEAM',
62
+ appIDID: 'APP',
63
+ certificateID: 'CERT',
64
+ deviceIDs: ['DEVICE'],
65
+ }).payload,
66
+ ).toMatchObject({
67
+ teamId: 'TEAM',
68
+ provisioningProfileName: 'Limrun Ad Hoc com.example.app',
69
+ certificateIds: ['CERT'],
70
+ appIdId: 'APP',
71
+ deviceIds: ['DEVICE'],
72
+ distributionType: 'adhoc',
73
+ subPlatform: 'ios',
74
+ });
75
+ });
76
+
77
+ test('filters Ad Hoc profile list requests by Ad Hoc distribution type', () => {
78
+ expect(findAdHocProfilesRequest({ bundleID: 'com.example.app', teamID: 'TEAM' }).payload).toMatchObject({
79
+ teamId: 'TEAM',
80
+ search: 'com.example.app',
81
+ distributionType: 'adhoc',
82
+ });
83
+ });
84
+ });
@@ -1,4 +1,4 @@
1
- import type { ProvisioningProfileInfo, StoredSigningAssets } from '../types';
1
+ import type { DeviceInstallSigningMode, ProvisioningProfileInfo, StoredSigningAssets } from '../types';
2
2
  import {
3
3
  getSigningAssets,
4
4
  normalizeBundleID,
@@ -64,15 +64,17 @@ export type AppleSigningAssetCacheInput = {
64
64
  bundleID: string;
65
65
  deviceUDID?: string;
66
66
  teamID?: string;
67
+ signingMode?: DeviceInstallSigningMode;
67
68
  };
68
69
 
69
70
  export type PutAppleGeneratedSigningAssetsInput = {
70
71
  bundleID: string;
71
72
  deviceUDID?: string;
72
73
  teamID?: string;
74
+ signingMode?: DeviceInstallSigningMode;
73
75
  certificateID?: string;
74
76
  certificateP12Base64: string;
75
- certificatePassword: string;
77
+ certificatePassword?: string;
76
78
  provisioningProfileBase64: string;
77
79
  profile: ProvisioningProfileInfo;
78
80
  };
@@ -105,6 +107,13 @@ export function findDevelopmentCertificatesRequest(teamID = '') {
105
107
  });
106
108
  }
107
109
 
110
+ export function findDistributionCertificatesRequest(teamID = '') {
111
+ return pagedRequest('/account/ios/certificate/listCertRequests.action', teamID, {
112
+ sort: 'name=asc',
113
+ types: 'WXV89964HE,R58UK2EWSO',
114
+ });
115
+ }
116
+
108
117
  export function findDevelopmentProfilesRequest({
109
118
  bundleID,
110
119
  teamID = '',
@@ -115,6 +124,17 @@ export function findDevelopmentProfilesRequest({
115
124
  });
116
125
  }
117
126
 
127
+ export function findAdHocProfilesRequest({
128
+ bundleID,
129
+ teamID = '',
130
+ }: Pick<AppleProvisioningContext, 'bundleID' | 'teamID'>) {
131
+ return pagedRequest('/account/ios/profile/listProvisioningProfiles.action', teamID, {
132
+ search: bundleID,
133
+ sort: 'name=asc',
134
+ distributionType: 'adhoc',
135
+ });
136
+ }
137
+
118
138
  export function registerDeviceRequest({
119
139
  deviceUDID,
120
140
  teamID = '',
@@ -168,6 +188,24 @@ export function submitDevelopmentCSRRequest({
168
188
  } satisfies AppleProvisioningRequest;
169
189
  }
170
190
 
191
+ export function submitDistributionCSRRequest({
192
+ csrPEM,
193
+ teamID = '',
194
+ }: {
195
+ csrPEM: string;
196
+ teamID?: string;
197
+ }) {
198
+ return {
199
+ method: 'POST',
200
+ path: '/account/ios/certificate/submitCertificateRequest.action',
201
+ payload: {
202
+ teamId: teamID,
203
+ type: 'WXV89964HE',
204
+ csrContent: csrPEM,
205
+ },
206
+ } satisfies AppleProvisioningRequest;
207
+ }
208
+
171
209
  export function downloadCertificateRequest(certificateID: string, teamID = '') {
172
210
  return {
173
211
  method: 'GET',
@@ -180,6 +218,18 @@ export function downloadCertificateRequest(certificateID: string, teamID = '') {
180
218
  } satisfies AppleProvisioningRequest;
181
219
  }
182
220
 
221
+ export function downloadDistributionCertificateRequest(certificateID: string, teamID = '') {
222
+ return {
223
+ method: 'GET',
224
+ path: '/account/ios/certificate/downloadCertificateContent.action',
225
+ payload: {
226
+ teamId: teamID,
227
+ certificateId: certificateID,
228
+ type: 'WXV89964HE',
229
+ },
230
+ } satisfies AppleProvisioningRequest;
231
+ }
232
+
183
233
  export function createDevelopmentProfileRequest({
184
234
  bundleID,
185
235
  teamID = '',
@@ -208,6 +258,34 @@ export function createDevelopmentProfileRequest({
208
258
  } satisfies AppleProvisioningRequest;
209
259
  }
210
260
 
261
+ export function createAdHocProfileRequest({
262
+ bundleID,
263
+ teamID = '',
264
+ appIDID,
265
+ certificateID,
266
+ deviceIDs,
267
+ name,
268
+ }: Pick<AppleProvisioningContext, 'bundleID' | 'teamID'> & {
269
+ appIDID: string;
270
+ certificateID: string;
271
+ deviceIDs: string[];
272
+ name?: string;
273
+ }) {
274
+ return {
275
+ method: 'POST',
276
+ path: '/account/ios/profile/createProvisioningProfile.action',
277
+ payload: {
278
+ teamId: teamID,
279
+ provisioningProfileName: name ?? `Limrun Ad Hoc ${bundleID}`,
280
+ certificateIds: [certificateID],
281
+ appIdId: appIDID,
282
+ deviceIds: deviceIDs,
283
+ distributionType: 'adhoc',
284
+ subPlatform: 'ios',
285
+ },
286
+ } satisfies AppleProvisioningRequest;
287
+ }
288
+
211
289
  export function downloadProfileRequest(profileID: string, teamID = '') {
212
290
  return {
213
291
  method: 'GET',
@@ -223,9 +301,10 @@ export async function getReusableAppleSigningAssets({
223
301
  bundleID,
224
302
  deviceUDID,
225
303
  teamID,
304
+ signingMode,
226
305
  }: AppleSigningAssetCacheInput) {
227
- const stored = await getSigningAssets({ bundleID, deviceUDID });
228
- if (!stored || !storedSigningAssetsReusable(stored, { bundleID, deviceUDID, teamID })) {
306
+ const stored = await getSigningAssets({ bundleID, deviceUDID, signingMode });
307
+ if (!stored || !storedSigningAssetsReusable(stored, { bundleID, deviceUDID, teamID, signingMode })) {
229
308
  return undefined;
230
309
  }
231
310
  return stored;
@@ -234,15 +313,20 @@ export async function getReusableAppleSigningAssets({
234
313
  export async function putAppleGeneratedSigningAssets(input: PutAppleGeneratedSigningAssetsInput) {
235
314
  return putSigningAssets({
236
315
  ...input,
237
- certificateFileName: input.certificateID ? `${input.certificateID}.p12` : 'apple-development.p12',
238
- profileFileName: input.profile.uuid ? `${input.profile.uuid}.mobileprovision` : 'apple-development.mobileprovision',
316
+ certificateFileName: input.certificateID ? `${input.certificateID}.p12` : `apple-${input.signingMode ?? 'development'}.p12`,
317
+ certificatePassword: input.certificatePassword || undefined,
318
+ signingMode: input.signingMode,
319
+ profileFileName: input.profile.uuid ? `${input.profile.uuid}.mobileprovision` : `${input.signingMode ?? 'development'}.mobileprovision`,
239
320
  });
240
321
  }
241
322
 
242
323
  export function storedSigningAssetsReusable(
243
324
  stored: StoredSigningAssets,
244
- { bundleID, deviceUDID, teamID }: AppleSigningAssetCacheInput,
325
+ { bundleID, deviceUDID, teamID, signingMode = 'development' }: AppleSigningAssetCacheInput,
245
326
  ) {
327
+ if ((stored.signingMode ?? 'development') !== signingMode) {
328
+ return false;
329
+ }
246
330
  if (!profileMatchesBundleID(stored.profile, bundleID)) {
247
331
  return false;
248
332
  }
@@ -12,7 +12,7 @@ export type StartSignedDeviceBuildOptions = {
12
12
  limbuildApiUrl: string;
13
13
  token?: string;
14
14
  certificateP12Base64: string;
15
- certificatePassword: string;
15
+ certificatePassword?: string;
16
16
  provisioningProfileBase64: string;
17
17
  };
18
18
 
@@ -25,6 +25,21 @@ export type BuildLogEventsOptions = {
25
25
  onError?: (error: Error) => void;
26
26
  };
27
27
 
28
+ export type IOSOTAInstall = {
29
+ installUrl: string;
30
+ landingUrl: string;
31
+ manifestUrl: string;
32
+ ipaUrl: string;
33
+ bundleId: string;
34
+ displayName: string;
35
+ };
36
+
37
+ export type GetIOSOTAInstallOptions = {
38
+ limbuildApiUrl: string;
39
+ execId: string;
40
+ token?: string;
41
+ };
42
+
28
43
  export async function fetchLimbuildInfo(limbuildApiUrl: string, token?: string) {
29
44
  const url = new URL(`${limbuildApiUrl}/info`);
30
45
  if (token) {
@@ -62,7 +77,7 @@ export async function startSignedDeviceBuild({
62
77
  xcodebuild: { sdk: 'iphoneos' },
63
78
  signing: {
64
79
  certificateP12Base64,
65
- certificatePassword,
80
+ ...(certificatePassword ? { certificatePassword } : {}),
66
81
  provisioningProfileBase64,
67
82
  },
68
83
  }),
@@ -74,6 +89,21 @@ export async function startSignedDeviceBuild({
74
89
  return (await response.json()) as { execId?: string };
75
90
  }
76
91
 
92
+ export async function getIOSOTAInstall({ limbuildApiUrl, execId, token }: GetIOSOTAInstallOptions) {
93
+ const url = new URL(`${limbuildApiUrl}/exec/${encodeURIComponent(execId)}/ios/ota`);
94
+ if (token) {
95
+ url.searchParams.set('token', token);
96
+ }
97
+ const response = await fetch(url.toString(), {
98
+ headers: token ? { Authorization: `Bearer ${token}` } : undefined,
99
+ });
100
+ if (!response.ok) {
101
+ const body = await response.text();
102
+ throw new Error(`OTA install metadata failed: HTTP ${response.status} ${body}`);
103
+ }
104
+ return (await response.json()) as IOSOTAInstall;
105
+ }
106
+
77
107
  export function watchBuildLogEvents({
78
108
  limbuildApiUrl,
79
109
  execId,
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ DeviceInstallSigningMode,
2
3
  ProvisioningProfileInfo,
3
4
  PutSigningAssetsInput,
4
5
  StoredPairRecord,
@@ -49,35 +50,42 @@ export async function putPairRecord(record: PairRecordPayload, metadata: { produ
49
50
  export async function getSigningAssets({
50
51
  deviceUDID,
51
52
  bundleID,
53
+ signingMode = 'development',
52
54
  }: {
53
55
  deviceUDID?: string;
54
56
  bundleID?: string;
57
+ signingMode?: DeviceInstallSigningMode;
55
58
  }) {
56
59
  const normalizedBundleID = normalizeBundleID(bundleID);
57
60
  if (!normalizedBundleID) return undefined;
58
61
  const normalizedUDID = normalizeUDID(deviceUDID);
59
- const bundleScoped = await getSigningAssetsByID(signingAssetID('bundle', normalizedBundleID));
62
+ const bundleScoped = await getSigningAssetsByID(signingAssetID('bundle', normalizedBundleID, signingMode));
60
63
  if (bundleScoped) return bundleScoped;
61
64
  if (normalizedUDID) {
62
- const exact = await getSigningAssetsByID(signingAssetID(normalizedUDID, normalizedBundleID));
65
+ const exact = await getSigningAssetsByID(signingAssetID(normalizedUDID, normalizedBundleID, signingMode));
63
66
  if (exact) return exact;
64
67
  }
65
- const candidates = await findSigningAssetsForBundle(normalizedBundleID);
68
+ const candidates = await findSigningAssetsForBundle(normalizedBundleID, signingMode);
66
69
  return candidates[0];
67
70
  }
68
71
 
69
- export async function getLatestSigningAssets() {
72
+ export async function getLatestSigningAssets(signingMode: DeviceInstallSigningMode = 'development') {
70
73
  const all = await getAllSigningAssets();
71
- return all.sort(
72
- (left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime(),
73
- )[0];
74
+ return all
75
+ .filter((asset) => (asset.signingMode ?? 'development') === signingMode)
76
+ .sort(
77
+ (left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime(),
78
+ )[0];
74
79
  }
75
80
 
76
- export async function getLatestSigningAssetsWithCertificate(teamID?: string) {
81
+ export async function getLatestSigningAssetsWithCertificate(teamID?: string, signingMode: DeviceInstallSigningMode = 'development') {
77
82
  const all = await getAllSigningAssets();
78
83
  return all
79
84
  .filter((asset) => {
80
- if (!asset.certificateID || !asset.certificateP12Base64 || !asset.certificatePassword) {
85
+ if (!asset.certificateID || !asset.certificateP12Base64) {
86
+ return false;
87
+ }
88
+ if ((asset.signingMode ?? 'development') !== signingMode) {
81
89
  return false;
82
90
  }
83
91
  return !teamID || !asset.teamID || asset.teamID === teamID;
@@ -91,11 +99,13 @@ export async function putSigningAssets(input: PutSigningAssetsInput) {
91
99
  throw new Error('Cannot store signing assets without a bundle ID.');
92
100
  }
93
101
  const normalizedUDID = normalizeUDID(input.deviceUDID);
94
- const id = signingAssetID('bundle', normalizedBundleID);
102
+ const signingMode = input.signingMode ?? 'development';
103
+ const id = signingAssetID('bundle', normalizedBundleID, signingMode);
95
104
  const stored: StoredSigningAssets = {
96
105
  ...input,
97
106
  id,
98
107
  deviceUDID: normalizedUDID || undefined,
108
+ signingMode,
99
109
  bundleID: normalizedBundleID,
100
110
  updatedAt: new Date().toISOString(),
101
111
  };
@@ -106,11 +116,11 @@ export async function putSigningAssets(input: PutSigningAssetsInput) {
106
116
  return stored;
107
117
  }
108
118
 
109
- export async function findSigningAssetsForBundle(bundleID?: string) {
119
+ export async function findSigningAssetsForBundle(bundleID?: string, signingMode: DeviceInstallSigningMode = 'development') {
110
120
  const normalized = normalizeBundleID(bundleID);
111
121
  if (!normalized) return [];
112
122
  const all = await getAllSigningAssets();
113
- return all.filter((asset) => asset.bundleID === normalized);
123
+ return all.filter((asset) => asset.bundleID === normalized && (asset.signingMode ?? 'development') === signingMode);
114
124
  }
115
125
 
116
126
  export function profileContainsDevice(profile: ProvisioningProfileInfo, deviceUDID?: string) {
@@ -174,6 +184,7 @@ export function parseProvisioningProfileBytes(bytes: Uint8Array) {
174
184
  applicationIdentifier,
175
185
  bundleID,
176
186
  provisionedDevices: stringArrayValue(value.ProvisionedDevices),
187
+ getTaskAllow: booleanValue(entitlements['get-task-allow']),
177
188
  expirationDate: stringValue(value.ExpirationDate),
178
189
  } satisfies ProvisioningProfileInfo;
179
190
  }
@@ -193,8 +204,8 @@ async function getAllSigningAssets() {
193
204
  );
194
205
  }
195
206
 
196
- function signingAssetID(deviceUDID: string, bundleID: string) {
197
- return `${deviceUDID}:${bundleID}`;
207
+ function signingAssetID(deviceUDID: string, bundleID: string, signingMode: DeviceInstallSigningMode) {
208
+ return `${signingMode}:${deviceUDID}:${bundleID}`;
198
209
  }
199
210
 
200
211
  function bundleIDFromApplicationIdentifier(applicationIdentifier?: string) {
@@ -237,6 +248,10 @@ function stringArrayValue(value: unknown) {
237
248
  return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
238
249
  }
239
250
 
251
+ function booleanValue(value: unknown) {
252
+ return typeof value === 'boolean' ? value : undefined;
253
+ }
254
+
240
255
  function isRecord(value: unknown): value is Record<string, unknown> {
241
256
  return typeof value === 'object' && value !== null && !Array.isArray(value);
242
257
  }
@@ -1,5 +1,7 @@
1
1
  export type DeviceInstallLog = (message: string, detail?: string) => void;
2
2
 
3
+ export type DeviceInstallSigningMode = 'development' | 'adhoc';
4
+
3
5
  export type DeviceInstallStep = 'signing' | 'connect' | 'build' | 'install';
4
6
 
5
7
  export type DeviceInstallStepStatus = 'idle' | 'active' | 'complete' | 'error';
@@ -44,6 +46,7 @@ export type ProvisioningProfileInfo = {
44
46
  applicationIdentifier?: string;
45
47
  bundleID?: string;
46
48
  provisionedDevices: string[];
49
+ getTaskAllow?: boolean;
47
50
  expirationDate?: string;
48
51
  };
49
52
 
@@ -51,11 +54,12 @@ export type StoredSigningAssets = {
51
54
  id: string;
52
55
  deviceUDID?: string;
53
56
  teamID?: string;
57
+ signingMode?: DeviceInstallSigningMode;
54
58
  bundleID: string;
55
59
  certificateID?: string;
56
60
  certificateP12Base64: string;
57
61
  certificateFileName?: string;
58
- certificatePassword: string;
62
+ certificatePassword?: string;
59
63
  provisioningProfileBase64: string;
60
64
  profileFileName?: string;
61
65
  profile: ProvisioningProfileInfo;