@limrun/ui 0.9.0-rc.9 → 0.13.3
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/README.md +25 -8
- package/dist/app-store-relay/index.cjs +9 -0
- package/dist/app-store-relay/index.d.ts +102 -0
- package/dist/app-store-relay/index.js +486 -0
- package/dist/app-store-relay/react.cjs +1 -0
- package/dist/app-store-relay/react.d.ts +15 -0
- package/dist/app-store-relay/react.js +65 -0
- package/dist/browser-storage-Cx9eW-bm.mjs +205 -0
- package/dist/browser-storage-jJaIht4K.js +1 -0
- package/dist/client-ASLtHFLh.js +1 -0
- package/dist/client-CGdh8yoJ.mjs +234 -0
- package/dist/components/device-install/index.d.ts +1 -2
- package/dist/core/device-install/apple/provisioning.d.ts +58 -7
- package/dist/core/device-install/operations/limbuild-client.d.ts +15 -1
- package/dist/core/device-install/storage/browser-storage.d.ts +9 -5
- package/dist/core/device-install/types.d.ts +4 -1
- package/dist/device-build/index.cjs +1 -0
- package/dist/device-build/index.d.ts +4 -0
- package/dist/device-build/index.js +84 -0
- package/dist/device-build/react.cjs +1 -0
- package/dist/device-build/react.d.ts +20 -0
- package/dist/device-build/react.js +68 -0
- package/dist/device-build/signing.d.ts +20 -0
- package/dist/device-install/demo/demo.d.ts +0 -0
- package/dist/device-install/index.cjs +1 -1
- package/dist/device-install/index.d.ts +18 -3
- package/dist/device-install/index.js +583 -76
- package/dist/device-install/react.cjs +1 -1
- package/dist/device-install/react.d.ts +23 -1
- package/dist/device-install/react.js +98 -2
- package/dist/hooks/index.d.ts +1 -1
- package/dist/index-CMeQfhYy.js +22 -0
- package/dist/index-DFdCvi2_.mjs +11543 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js +502 -495
- package/dist/limbuild-client-CgEncrgx.js +1 -0
- package/dist/limbuild-client-D7U7w342.mjs +81 -0
- package/dist/provisioning-DKbkBO-p.mjs +239 -0
- package/dist/provisioning-Va0PH4Kk.js +1 -0
- package/package.json +21 -1
- package/src/app-store-relay/index.test.ts +70 -0
- package/src/app-store-relay/index.ts +514 -0
- package/src/app-store-relay/react.ts +129 -0
- package/src/components/device-install/index.ts +1 -2
- package/src/core/device-install/apple/client.ts +29 -22
- package/src/core/device-install/apple/crypto.ts +9 -6
- package/src/core/device-install/apple/gsa-srp.ts +3 -7
- package/src/core/device-install/apple/provisioning.test.ts +84 -0
- package/src/core/device-install/apple/provisioning.ts +98 -16
- package/src/core/device-install/apple/relay.ts +5 -1
- package/src/core/device-install/operations/limbuild-client.ts +41 -4
- package/src/core/device-install/operations/operations.ts +3 -1
- package/src/core/device-install/operations/relay-client.ts +57 -27
- package/src/core/device-install/operations/usbmux.ts +3 -3
- package/src/core/device-install/operations/webusb.ts +9 -4
- package/src/core/device-install/storage/browser-storage.ts +43 -14
- package/src/core/device-install/types.ts +6 -8
- package/src/device-build/index.ts +42 -0
- package/src/device-build/react.ts +131 -0
- package/src/device-build/signing.ts +94 -0
- package/src/device-install/README.md +423 -0
- package/src/device-install/demo/demo.css +493 -0
- package/src/device-install/demo/demo.tsx +1069 -0
- package/src/device-install/demo/index.html +12 -0
- package/src/device-install/index.ts +49 -3
- package/src/device-install/react.ts +189 -1
- package/src/hooks/index.ts +1 -1
- package/src/index.ts +0 -3
- package/vite.config.ts +4 -0
- package/dist/components/device-install/device-install-dialog.d.ts +0 -5
- package/dist/device-install-dialog-CqxDEH85.mjs +0 -443
- package/dist/device-install-dialog-jvqQ-fuo.js +0 -2
- package/dist/device-install-dialog.css +0 -1
- package/dist/hooks/use-device-install.d.ts +0 -73
- package/dist/use-device-install-CAPli9MR.js +0 -31
- package/dist/use-device-install-H8dqqvbR.mjs +0 -13624
- package/src/components/device-install/device-install-dialog.css +0 -325
- package/src/components/device-install/device-install-dialog.tsx +0 -495
- package/src/hooks/use-device-install.ts +0 -1219
|
@@ -29,9 +29,7 @@ export type AppleIDLoginResult = {
|
|
|
29
29
|
close: () => Promise<void>;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
type TwoFactorMethod =
|
|
33
|
-
| { type: 'trustedDevice' }
|
|
34
|
-
| { type: 'phone'; phoneNumberId: number; mode: string };
|
|
32
|
+
type TwoFactorMethod = { type: 'trustedDevice' } | { type: 'phone'; phoneNumberId: number; mode: string };
|
|
35
33
|
|
|
36
34
|
export async function startBrowserOwnedAppleIDLogin({
|
|
37
35
|
limbuildApiUrl,
|
|
@@ -44,7 +42,9 @@ export async function startBrowserOwnedAppleIDLogin({
|
|
|
44
42
|
const srp = new AppleGsaSrpClient(accountName);
|
|
45
43
|
const initResponse = await proxySrpInit(limbuildApiUrl, appleSessionId, await srp.init(), token);
|
|
46
44
|
if (initResponse.status < 200 || initResponse.status >= 300) {
|
|
47
|
-
throw new Error(
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Apple SRP init failed: HTTP ${initResponse.status} ${initResponse.rawBody ?? ''}`.trim(),
|
|
47
|
+
);
|
|
48
48
|
}
|
|
49
49
|
if (!initResponse.body) {
|
|
50
50
|
throw new Error('Apple SRP init response did not include a body.');
|
|
@@ -66,17 +66,20 @@ export async function startBrowserOwnedAppleIDLogin({
|
|
|
66
66
|
if (requiresTwoFactor) {
|
|
67
67
|
twoFactorChallengeResponse = await triggerTrustedDeviceTwoFactor(limbuildApiUrl, appleSessionId, token);
|
|
68
68
|
const phone = trustedPhoneNumberFromChallenge(twoFactorChallengeResponse.body);
|
|
69
|
-
|
|
69
|
+
// Only route codes through the phone/SMS endpoint when Apple actually
|
|
70
|
+
// falls back to phone verification (HTTP 412). The trusted-device
|
|
71
|
+
// challenge body frequently includes trusted-phone metadata even for a
|
|
72
|
+
// normal device push; routing those codes to the phone endpoint breaks
|
|
73
|
+
// trusted-device login.
|
|
74
|
+
if (twoFactorChallengeResponse.status === 412) {
|
|
75
|
+
if (!phone) {
|
|
76
|
+
throw new Error('Apple requested phone verification but did not include a trusted phone number.');
|
|
77
|
+
}
|
|
70
78
|
twoFactorMethod = {
|
|
71
79
|
type: 'phone',
|
|
72
80
|
phoneNumberId: phone.id,
|
|
73
81
|
mode: phone.pushMode ?? 'sms',
|
|
74
82
|
};
|
|
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
83
|
twoFactorChallengeResponse = await triggerPhoneTwoFactor(
|
|
81
84
|
limbuildApiUrl,
|
|
82
85
|
appleSessionId,
|
|
@@ -93,7 +96,9 @@ export async function startBrowserOwnedAppleIDLogin({
|
|
|
93
96
|
);
|
|
94
97
|
}
|
|
95
98
|
} else if (completeResponse.status < 200 || completeResponse.status >= 300) {
|
|
96
|
-
throw new Error(
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Apple SRP complete failed: HTTP ${completeResponse.status} ${completeResponse.rawBody ?? ''}`.trim(),
|
|
101
|
+
);
|
|
97
102
|
}
|
|
98
103
|
return {
|
|
99
104
|
appleSessionId,
|
|
@@ -102,18 +107,20 @@ export async function startBrowserOwnedAppleIDLogin({
|
|
|
102
107
|
requiresTwoFactor,
|
|
103
108
|
finishTwoFactor: async (code) => {
|
|
104
109
|
const response =
|
|
105
|
-
twoFactorMethod.type === 'phone'
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
twoFactorMethod.type === 'phone' ?
|
|
111
|
+
await proxyPhoneTwoFactorCode(
|
|
112
|
+
limbuildApiUrl,
|
|
113
|
+
appleSessionId,
|
|
114
|
+
twoFactorMethod.phoneNumberId,
|
|
115
|
+
code,
|
|
116
|
+
twoFactorMethod.mode,
|
|
117
|
+
token,
|
|
118
|
+
)
|
|
119
|
+
: await proxyTwoFactorCode(limbuildApiUrl, appleSessionId, code, token);
|
|
115
120
|
if (response.status < 200 || response.status >= 300) {
|
|
116
|
-
throw new Error(
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Apple two-factor code failed: HTTP ${response.status} ${response.rawBody ?? ''}`.trim(),
|
|
123
|
+
);
|
|
117
124
|
}
|
|
118
125
|
return response;
|
|
119
126
|
},
|
|
@@ -41,7 +41,11 @@ export async function generateAppleSigningKeyAndCSR(input: AppleCSRInput): Promi
|
|
|
41
41
|
derContext(0, new Uint8Array()),
|
|
42
42
|
);
|
|
43
43
|
const signature = new Uint8Array(
|
|
44
|
-
await crypto.subtle.sign(
|
|
44
|
+
await crypto.subtle.sign(
|
|
45
|
+
'RSASSA-PKCS1-v1_5',
|
|
46
|
+
keyPair.privateKey,
|
|
47
|
+
toArrayBuffer(certificationRequestInfo),
|
|
48
|
+
),
|
|
45
49
|
);
|
|
46
50
|
const csrDER = derSequence(
|
|
47
51
|
certificationRequestInfo,
|
|
@@ -62,11 +66,10 @@ export function exportAppleCertificateP12(input: ExportP12Input) {
|
|
|
62
66
|
if (!input.certificateBase64 && !input.certificatePEM) {
|
|
63
67
|
throw new Error('certificateBase64 or certificatePEM is required.');
|
|
64
68
|
}
|
|
65
|
-
const privateKey = forge.pki.privateKeyFromPem(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
? forge.pki.certificateFromPem(input.certificatePEM)
|
|
69
|
+
const privateKey = forge.pki.privateKeyFromPem(pemFromBase64('PRIVATE KEY', input.privateKeyPKCS8Base64));
|
|
70
|
+
const certificate =
|
|
71
|
+
input.certificatePEM ?
|
|
72
|
+
forge.pki.certificateFromPem(input.certificatePEM)
|
|
70
73
|
: forge.pki.certificateFromAsn1(
|
|
71
74
|
forge.asn1.fromDer(forge.util.createBuffer(base64ToBinary(input.certificateBase64!))),
|
|
72
75
|
);
|
|
@@ -79,13 +79,9 @@ async function deriveApplePassword(
|
|
|
79
79
|
if (protocol === 's2k_fo') {
|
|
80
80
|
passHash = stringToBytes(util.toHex(passHash));
|
|
81
81
|
}
|
|
82
|
-
const imported = await crypto.subtle.importKey(
|
|
83
|
-
'
|
|
84
|
-
|
|
85
|
-
{ name: 'PBKDF2' },
|
|
86
|
-
false,
|
|
87
|
-
['deriveBits'],
|
|
88
|
-
);
|
|
82
|
+
const imported = await crypto.subtle.importKey('raw', toArrayBuffer(passHash), { name: 'PBKDF2' }, false, [
|
|
83
|
+
'deriveBits',
|
|
84
|
+
]);
|
|
89
85
|
const derived = await crypto.subtle.deriveBits(
|
|
90
86
|
{
|
|
91
87
|
name: 'PBKDF2',
|
|
@@ -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
|
|
77
|
+
certificatePassword?: string;
|
|
76
78
|
provisioningProfileBase64: string;
|
|
77
79
|
profile: ProvisioningProfileInfo;
|
|
78
80
|
};
|
|
@@ -85,12 +87,18 @@ export function listTeamsRequest(): AppleProvisioningRequest {
|
|
|
85
87
|
};
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
export function findBundleIDRequest({
|
|
90
|
+
export function findBundleIDRequest({
|
|
91
|
+
bundleID,
|
|
92
|
+
teamID = '',
|
|
93
|
+
}: Pick<AppleProvisioningContext, 'bundleID' | 'teamID'>) {
|
|
89
94
|
void bundleID;
|
|
90
95
|
return pagedRequest('/account/ios/identifiers/listAppIds.action', teamID, { sort: 'name=asc' });
|
|
91
96
|
}
|
|
92
97
|
|
|
93
|
-
export function findDeviceRequest({
|
|
98
|
+
export function findDeviceRequest({
|
|
99
|
+
deviceUDID,
|
|
100
|
+
teamID = '',
|
|
101
|
+
}: Pick<AppleProvisioningContext, 'deviceUDID' | 'teamID'>) {
|
|
94
102
|
void deviceUDID;
|
|
95
103
|
return pagedRequest('/account/ios/device/listDevices.action', teamID, {
|
|
96
104
|
sort: 'name=asc',
|
|
@@ -105,6 +113,13 @@ export function findDevelopmentCertificatesRequest(teamID = '') {
|
|
|
105
113
|
});
|
|
106
114
|
}
|
|
107
115
|
|
|
116
|
+
export function findDistributionCertificatesRequest(teamID = '') {
|
|
117
|
+
return pagedRequest('/account/ios/certificate/listCertRequests.action', teamID, {
|
|
118
|
+
sort: 'name=asc',
|
|
119
|
+
types: 'WXV89964HE,R58UK2EWSO',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
108
123
|
export function findDevelopmentProfilesRequest({
|
|
109
124
|
bundleID,
|
|
110
125
|
teamID = '',
|
|
@@ -115,6 +130,17 @@ export function findDevelopmentProfilesRequest({
|
|
|
115
130
|
});
|
|
116
131
|
}
|
|
117
132
|
|
|
133
|
+
export function findAdHocProfilesRequest({
|
|
134
|
+
bundleID,
|
|
135
|
+
teamID = '',
|
|
136
|
+
}: Pick<AppleProvisioningContext, 'bundleID' | 'teamID'>) {
|
|
137
|
+
return pagedRequest('/account/ios/profile/listProvisioningProfiles.action', teamID, {
|
|
138
|
+
search: bundleID,
|
|
139
|
+
sort: 'name=asc',
|
|
140
|
+
distributionType: 'adhoc',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
118
144
|
export function registerDeviceRequest({
|
|
119
145
|
deviceUDID,
|
|
120
146
|
teamID = '',
|
|
@@ -150,13 +176,7 @@ export function createBundleIDRequest({
|
|
|
150
176
|
} satisfies AppleProvisioningRequest;
|
|
151
177
|
}
|
|
152
178
|
|
|
153
|
-
export function submitDevelopmentCSRRequest({
|
|
154
|
-
csrPEM,
|
|
155
|
-
teamID = '',
|
|
156
|
-
}: {
|
|
157
|
-
csrPEM: string;
|
|
158
|
-
teamID?: string;
|
|
159
|
-
}) {
|
|
179
|
+
export function submitDevelopmentCSRRequest({ csrPEM, teamID = '' }: { csrPEM: string; teamID?: string }) {
|
|
160
180
|
return {
|
|
161
181
|
method: 'POST',
|
|
162
182
|
path: '/account/ios/certificate/submitCertificateRequest.action',
|
|
@@ -168,6 +188,18 @@ export function submitDevelopmentCSRRequest({
|
|
|
168
188
|
} satisfies AppleProvisioningRequest;
|
|
169
189
|
}
|
|
170
190
|
|
|
191
|
+
export function submitDistributionCSRRequest({ csrPEM, teamID = '' }: { csrPEM: string; teamID?: string }) {
|
|
192
|
+
return {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
path: '/account/ios/certificate/submitCertificateRequest.action',
|
|
195
|
+
payload: {
|
|
196
|
+
teamId: teamID,
|
|
197
|
+
type: 'WXV89964HE',
|
|
198
|
+
csrContent: csrPEM,
|
|
199
|
+
},
|
|
200
|
+
} satisfies AppleProvisioningRequest;
|
|
201
|
+
}
|
|
202
|
+
|
|
171
203
|
export function downloadCertificateRequest(certificateID: string, teamID = '') {
|
|
172
204
|
return {
|
|
173
205
|
method: 'GET',
|
|
@@ -180,6 +212,18 @@ export function downloadCertificateRequest(certificateID: string, teamID = '') {
|
|
|
180
212
|
} satisfies AppleProvisioningRequest;
|
|
181
213
|
}
|
|
182
214
|
|
|
215
|
+
export function downloadDistributionCertificateRequest(certificateID: string, teamID = '') {
|
|
216
|
+
return {
|
|
217
|
+
method: 'GET',
|
|
218
|
+
path: '/account/ios/certificate/downloadCertificateContent.action',
|
|
219
|
+
payload: {
|
|
220
|
+
teamId: teamID,
|
|
221
|
+
certificateId: certificateID,
|
|
222
|
+
type: 'WXV89964HE',
|
|
223
|
+
},
|
|
224
|
+
} satisfies AppleProvisioningRequest;
|
|
225
|
+
}
|
|
226
|
+
|
|
183
227
|
export function createDevelopmentProfileRequest({
|
|
184
228
|
bundleID,
|
|
185
229
|
teamID = '',
|
|
@@ -208,6 +252,34 @@ export function createDevelopmentProfileRequest({
|
|
|
208
252
|
} satisfies AppleProvisioningRequest;
|
|
209
253
|
}
|
|
210
254
|
|
|
255
|
+
export function createAdHocProfileRequest({
|
|
256
|
+
bundleID,
|
|
257
|
+
teamID = '',
|
|
258
|
+
appIDID,
|
|
259
|
+
certificateID,
|
|
260
|
+
deviceIDs,
|
|
261
|
+
name,
|
|
262
|
+
}: Pick<AppleProvisioningContext, 'bundleID' | 'teamID'> & {
|
|
263
|
+
appIDID: string;
|
|
264
|
+
certificateID: string;
|
|
265
|
+
deviceIDs: string[];
|
|
266
|
+
name?: string;
|
|
267
|
+
}) {
|
|
268
|
+
return {
|
|
269
|
+
method: 'POST',
|
|
270
|
+
path: '/account/ios/profile/createProvisioningProfile.action',
|
|
271
|
+
payload: {
|
|
272
|
+
teamId: teamID,
|
|
273
|
+
provisioningProfileName: name ?? `Limrun Ad Hoc ${bundleID}`,
|
|
274
|
+
certificateIds: [certificateID],
|
|
275
|
+
appIdId: appIDID,
|
|
276
|
+
deviceIds: deviceIDs,
|
|
277
|
+
distributionType: 'adhoc',
|
|
278
|
+
subPlatform: 'ios',
|
|
279
|
+
},
|
|
280
|
+
} satisfies AppleProvisioningRequest;
|
|
281
|
+
}
|
|
282
|
+
|
|
211
283
|
export function downloadProfileRequest(profileID: string, teamID = '') {
|
|
212
284
|
return {
|
|
213
285
|
method: 'GET',
|
|
@@ -223,9 +295,10 @@ export async function getReusableAppleSigningAssets({
|
|
|
223
295
|
bundleID,
|
|
224
296
|
deviceUDID,
|
|
225
297
|
teamID,
|
|
298
|
+
signingMode,
|
|
226
299
|
}: AppleSigningAssetCacheInput) {
|
|
227
|
-
const stored = await getSigningAssets({ bundleID, deviceUDID });
|
|
228
|
-
if (!stored || !storedSigningAssetsReusable(stored, { bundleID, deviceUDID, teamID })) {
|
|
300
|
+
const stored = await getSigningAssets({ bundleID, deviceUDID, signingMode });
|
|
301
|
+
if (!stored || !storedSigningAssetsReusable(stored, { bundleID, deviceUDID, teamID, signingMode })) {
|
|
229
302
|
return undefined;
|
|
230
303
|
}
|
|
231
304
|
return stored;
|
|
@@ -234,15 +307,24 @@ export async function getReusableAppleSigningAssets({
|
|
|
234
307
|
export async function putAppleGeneratedSigningAssets(input: PutAppleGeneratedSigningAssetsInput) {
|
|
235
308
|
return putSigningAssets({
|
|
236
309
|
...input,
|
|
237
|
-
certificateFileName:
|
|
238
|
-
|
|
310
|
+
certificateFileName:
|
|
311
|
+
input.certificateID ? `${input.certificateID}.p12` : `apple-${input.signingMode ?? 'development'}.p12`,
|
|
312
|
+
certificatePassword: input.certificatePassword || undefined,
|
|
313
|
+
signingMode: input.signingMode,
|
|
314
|
+
profileFileName:
|
|
315
|
+
input.profile.uuid ?
|
|
316
|
+
`${input.profile.uuid}.mobileprovision`
|
|
317
|
+
: `${input.signingMode ?? 'development'}.mobileprovision`,
|
|
239
318
|
});
|
|
240
319
|
}
|
|
241
320
|
|
|
242
321
|
export function storedSigningAssetsReusable(
|
|
243
322
|
stored: StoredSigningAssets,
|
|
244
|
-
{ bundleID, deviceUDID, teamID }: AppleSigningAssetCacheInput,
|
|
323
|
+
{ bundleID, deviceUDID, teamID, signingMode = 'development' }: AppleSigningAssetCacheInput,
|
|
245
324
|
) {
|
|
325
|
+
if ((stored.signingMode ?? 'development') !== signingMode) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
246
328
|
if (!profileMatchesBundleID(stored.profile, bundleID)) {
|
|
247
329
|
return false;
|
|
248
330
|
}
|
|
@@ -26,7 +26,11 @@ export async function createAppleRelaySession(limbuildApiUrl: string, token?: st
|
|
|
26
26
|
return (await response.json()) as { appleSessionId: string };
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export async function deleteAppleRelaySession(
|
|
29
|
+
export async function deleteAppleRelaySession(
|
|
30
|
+
limbuildApiUrl: string,
|
|
31
|
+
appleSessionId: string,
|
|
32
|
+
token?: string,
|
|
33
|
+
) {
|
|
30
34
|
const response = await fetch(limbuildURL(limbuildApiUrl, '/apple/auth/session/delete', token), {
|
|
31
35
|
method: 'POST',
|
|
32
36
|
headers: jsonHeaders(token),
|
|
@@ -12,7 +12,7 @@ export type StartSignedDeviceBuildOptions = {
|
|
|
12
12
|
limbuildApiUrl: string;
|
|
13
13
|
token?: string;
|
|
14
14
|
certificateP12Base64: string;
|
|
15
|
-
certificatePassword
|
|
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) {
|
|
@@ -59,10 +74,10 @@ export async function startSignedDeviceBuild({
|
|
|
59
74
|
},
|
|
60
75
|
body: JSON.stringify({
|
|
61
76
|
command: 'xcodebuild',
|
|
62
|
-
xcodebuild: { sdk: 'iphoneos'
|
|
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,
|
|
@@ -93,11 +123,18 @@ export function watchBuildLogEvents({
|
|
|
93
123
|
events.addEventListener('stderr', (event) => onLine({ type: 'stderr', data: event.data }));
|
|
94
124
|
events.addEventListener('exitCode', (event) => {
|
|
95
125
|
const code = parseInt(event.data, 10);
|
|
96
|
-
onStatus(
|
|
126
|
+
onStatus(
|
|
127
|
+
code === 0 ? 'succeeded'
|
|
128
|
+
: code < 0 ? 'cancelled'
|
|
129
|
+
: 'failed',
|
|
130
|
+
);
|
|
97
131
|
events.close();
|
|
98
132
|
});
|
|
99
133
|
events.onerror = () => {
|
|
100
134
|
events.close();
|
|
135
|
+
// The stream closed without a terminal exitCode event. Mark the build as
|
|
136
|
+
// failed so consumers don't stay stuck on "running" and can retry.
|
|
137
|
+
onStatus('failed');
|
|
101
138
|
onError?.(new Error('Build log stream closed before completion.'));
|
|
102
139
|
};
|
|
103
140
|
return () => events.close();
|
|
@@ -155,7 +155,9 @@ async function claimBestUsbmuxCandidate(target: DeviceRelayTarget, log: DeviceIn
|
|
|
155
155
|
lastError = error;
|
|
156
156
|
log(
|
|
157
157
|
'USB interface claim failed',
|
|
158
|
-
`configuration ${candidate.configurationValue}, interface ${
|
|
158
|
+
`configuration ${candidate.configurationValue}, interface ${
|
|
159
|
+
candidate.interfaceNumber
|
|
160
|
+
}: ${errorMessage(error)}`,
|
|
159
161
|
);
|
|
160
162
|
await resetUSBDevice(target);
|
|
161
163
|
await sleep(250);
|
|
@@ -40,10 +40,17 @@ export class RelayClient {
|
|
|
40
40
|
socket.onclose = () => {
|
|
41
41
|
this.closed = true;
|
|
42
42
|
this.log('Relay socket closed');
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
// The server often sends PairRecordReady and then closes immediately. In
|
|
44
|
+
// Chromium, onclose can be delivered before the queued onmessage handler
|
|
45
|
+
// has finished processing that final frame. Drain the frame queue before
|
|
46
|
+
// rejecting the waiter so a successful pair is not reported as
|
|
47
|
+
// "Relay socket closed".
|
|
48
|
+
void this.frameQueue.finally(() => {
|
|
49
|
+
if (this.pairRecordWaiter) {
|
|
50
|
+
this.pairRecordWaiter.reject(new Error('Relay socket closed'));
|
|
51
|
+
this.pairRecordWaiter = undefined;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
47
54
|
};
|
|
48
55
|
await new Promise<void>((resolve, reject) => {
|
|
49
56
|
socket.onopen = () => resolve();
|
|
@@ -92,9 +99,11 @@ export class RelayClient {
|
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
private enqueueFrame(data: Uint8Array) {
|
|
95
|
-
this.frameQueue = this.frameQueue
|
|
96
|
-
|
|
97
|
-
|
|
102
|
+
this.frameQueue = this.frameQueue
|
|
103
|
+
.then(() => this.handleFrame(decodeFrame(data)))
|
|
104
|
+
.catch((error) => {
|
|
105
|
+
this.log('Relay frame handling failed', error instanceof Error ? error.message : String(error));
|
|
106
|
+
});
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
private async handleFrame(frame: ReturnType<typeof decodeFrame>) {
|
|
@@ -163,7 +172,10 @@ export class RelayClient {
|
|
|
163
172
|
this.log(`Opened device stream ${streamId} to port ${port}`);
|
|
164
173
|
void this.pumpDeviceToServer(streamId, stream);
|
|
165
174
|
} catch (error) {
|
|
166
|
-
this.log(
|
|
175
|
+
this.log(
|
|
176
|
+
`Open device stream ${streamId} failed`,
|
|
177
|
+
error instanceof Error ? error.message : String(error),
|
|
178
|
+
);
|
|
167
179
|
await this.send({
|
|
168
180
|
type: RelayMessageType.OpenResult,
|
|
169
181
|
requestId,
|
|
@@ -230,26 +242,44 @@ function formatInstallProgress(message: string) {
|
|
|
230
242
|
if (!message.startsWith(prefix)) {
|
|
231
243
|
return message;
|
|
232
244
|
}
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
245
|
+
const plist = message.slice(prefix.length);
|
|
246
|
+
// The relay message is untrusted device input. Parsing it with DOMParser is
|
|
247
|
+
// both an XSS DOM sink and an XML entity-expansion (billion-laughs) vector,
|
|
248
|
+
// so extract only the fields we render via plain string scanning instead of
|
|
249
|
+
// building a DOM.
|
|
250
|
+
// Surface the real failure reason (e.g. ApplicationVerificationFailed) rather
|
|
251
|
+
// than collapsing it to an opaque numeric code downstream.
|
|
252
|
+
const error = readPlistValue(plist, 'Error');
|
|
253
|
+
if (error) {
|
|
254
|
+
const description = readPlistValue(plist, 'ErrorDescription');
|
|
255
|
+
return `Install error: ${error}${description ? ` — ${description}` : ''}`;
|
|
238
256
|
}
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
return `Install progress: ${percent}${status}`;
|
|
257
|
+
const status = readPlistValue(plist, 'Status') ?? 'Unknown';
|
|
258
|
+
const percent = readPlistValue(plist, 'PercentComplete');
|
|
259
|
+
return `Install progress: ${percent ? `${percent}% ` : ''}${status}`;
|
|
243
260
|
}
|
|
244
261
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return
|
|
262
|
+
// Returns the <string>/<integer>/<real> value immediately following
|
|
263
|
+
// <key>NAME</key> in an Apple plist fragment, without an XML/DOM parser.
|
|
264
|
+
function readPlistValue(plist: string, key: string): string | undefined {
|
|
265
|
+
const pattern = new RegExp(
|
|
266
|
+
`<key>\\s*${escapeRegExp(
|
|
267
|
+
key,
|
|
268
|
+
)}\\s*</key>\\s*<(?:string|integer|real)>([\\s\\S]*?)</(?:string|integer|real)>`,
|
|
269
|
+
);
|
|
270
|
+
const match = pattern.exec(plist);
|
|
271
|
+
return match ? decodeBasicXmlEntities(match[1].trim()) : undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function escapeRegExp(value: string) {
|
|
275
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function decodeBasicXmlEntities(value: string) {
|
|
279
|
+
return value
|
|
280
|
+
.replace(/</g, '<')
|
|
281
|
+
.replace(/>/g, '>')
|
|
282
|
+
.replace(/"/g, '"')
|
|
283
|
+
.replace(/'/g, "'")
|
|
284
|
+
.replace(/&/g, '&');
|
|
255
285
|
}
|
|
@@ -150,9 +150,9 @@ async function sendMux(session: UsbmuxSession, protocol: number, payload: Uint8A
|
|
|
150
150
|
session.writeChain = session.writeChain.then(async () => {
|
|
151
151
|
if (session.closed) return;
|
|
152
152
|
const packet =
|
|
153
|
-
session.muxVersion >= 2
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
session.muxVersion >= 2 ?
|
|
154
|
+
buildV2Packet(protocol, payload, session.txSeq++, session.rxSeq)
|
|
155
|
+
: buildV1Packet(protocol, payload);
|
|
156
156
|
await transferOutWithZlp(session.device, session.outEndpoint, packet);
|
|
157
157
|
});
|
|
158
158
|
return session.writeChain;
|