@limrun/ui 0.13.3-rc.1 → 0.13.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-store-relay/index.cjs +4 -4
- package/dist/app-store-relay/index.js +175 -119
- package/dist/app-store-relay/react.cjs +1 -1
- package/dist/app-store-relay/react.js +2 -2
- package/dist/{browser-storage-BJ__DGJ6.mjs → browser-storage-Cx9eW-bm.mjs} +48 -45
- package/dist/browser-storage-jJaIht4K.js +1 -0
- package/dist/{client-CnbCWvCs.js → client-ASLtHFLh.js} +1 -1
- package/dist/{client-Bk1N3XIF.mjs → client-CGdh8yoJ.mjs} +33 -27
- package/dist/components/remote-control.d.ts +144 -0
- package/dist/core/device-install/apple/provisioning.d.ts +4 -4
- package/dist/device-build/index.cjs +1 -1
- package/dist/device-build/index.js +5 -5
- package/dist/device-build/react.cjs +1 -1
- package/dist/device-build/react.js +15 -13
- 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 +1 -1
- package/dist/device-install/index.js +134 -121
- package/dist/device-install/react.cjs +1 -1
- package/dist/device-install/react.js +55 -50
- package/dist/{index-BXg7HdIs.mjs → index-DFdCvi2_.mjs} +3 -7
- package/dist/index.cjs +1 -1
- package/dist/index.js +1336 -1089
- package/dist/limbuild-client-CgEncrgx.js +1 -0
- package/dist/{limbuild-client-CFJhYsRx.mjs → limbuild-client-D7U7w342.mjs} +4 -2
- package/dist/{provisioning-CdseoMJQ.mjs → provisioning-DKbkBO-p.mjs} +11 -11
- package/dist/{provisioning-D2ZZQeyX.js → provisioning-Va0PH4Kk.js} +1 -1
- package/package.json +1 -1
- package/src/app-store-relay/index.test.ts +1 -5
- package/src/app-store-relay/index.ts +109 -42
- package/src/app-store-relay/react.ts +6 -2
- package/src/components/remote-control.tsx +726 -0
- 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.ts +16 -18
- package/src/core/device-install/apple/relay.ts +5 -1
- package/src/core/device-install/operations/limbuild-client.ts +8 -1
- 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 +20 -6
- package/src/core/device-install/types.ts +1 -7
- package/src/device-build/react.ts +4 -1
- 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 +7 -7
- package/src/device-install/react.ts +26 -17
- package/src/index.ts +1 -1
- package/dist/browser-storage-C1jQLXat.js +0 -1
- package/dist/limbuild-client-C_CMNLYV.js +0 -1
|
@@ -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',
|
|
@@ -87,12 +87,18 @@ export function listTeamsRequest(): AppleProvisioningRequest {
|
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
export function findBundleIDRequest({
|
|
90
|
+
export function findBundleIDRequest({
|
|
91
|
+
bundleID,
|
|
92
|
+
teamID = '',
|
|
93
|
+
}: Pick<AppleProvisioningContext, 'bundleID' | 'teamID'>) {
|
|
91
94
|
void bundleID;
|
|
92
95
|
return pagedRequest('/account/ios/identifiers/listAppIds.action', teamID, { sort: 'name=asc' });
|
|
93
96
|
}
|
|
94
97
|
|
|
95
|
-
export function findDeviceRequest({
|
|
98
|
+
export function findDeviceRequest({
|
|
99
|
+
deviceUDID,
|
|
100
|
+
teamID = '',
|
|
101
|
+
}: Pick<AppleProvisioningContext, 'deviceUDID' | 'teamID'>) {
|
|
96
102
|
void deviceUDID;
|
|
97
103
|
return pagedRequest('/account/ios/device/listDevices.action', teamID, {
|
|
98
104
|
sort: 'name=asc',
|
|
@@ -170,13 +176,7 @@ export function createBundleIDRequest({
|
|
|
170
176
|
} satisfies AppleProvisioningRequest;
|
|
171
177
|
}
|
|
172
178
|
|
|
173
|
-
export function submitDevelopmentCSRRequest({
|
|
174
|
-
csrPEM,
|
|
175
|
-
teamID = '',
|
|
176
|
-
}: {
|
|
177
|
-
csrPEM: string;
|
|
178
|
-
teamID?: string;
|
|
179
|
-
}) {
|
|
179
|
+
export function submitDevelopmentCSRRequest({ csrPEM, teamID = '' }: { csrPEM: string; teamID?: string }) {
|
|
180
180
|
return {
|
|
181
181
|
method: 'POST',
|
|
182
182
|
path: '/account/ios/certificate/submitCertificateRequest.action',
|
|
@@ -188,13 +188,7 @@ export function submitDevelopmentCSRRequest({
|
|
|
188
188
|
} satisfies AppleProvisioningRequest;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
export function submitDistributionCSRRequest({
|
|
192
|
-
csrPEM,
|
|
193
|
-
teamID = '',
|
|
194
|
-
}: {
|
|
195
|
-
csrPEM: string;
|
|
196
|
-
teamID?: string;
|
|
197
|
-
}) {
|
|
191
|
+
export function submitDistributionCSRRequest({ csrPEM, teamID = '' }: { csrPEM: string; teamID?: string }) {
|
|
198
192
|
return {
|
|
199
193
|
method: 'POST',
|
|
200
194
|
path: '/account/ios/certificate/submitCertificateRequest.action',
|
|
@@ -313,10 +307,14 @@ export async function getReusableAppleSigningAssets({
|
|
|
313
307
|
export async function putAppleGeneratedSigningAssets(input: PutAppleGeneratedSigningAssetsInput) {
|
|
314
308
|
return putSigningAssets({
|
|
315
309
|
...input,
|
|
316
|
-
certificateFileName:
|
|
310
|
+
certificateFileName:
|
|
311
|
+
input.certificateID ? `${input.certificateID}.p12` : `apple-${input.signingMode ?? 'development'}.p12`,
|
|
317
312
|
certificatePassword: input.certificatePassword || undefined,
|
|
318
313
|
signingMode: input.signingMode,
|
|
319
|
-
profileFileName:
|
|
314
|
+
profileFileName:
|
|
315
|
+
input.profile.uuid ?
|
|
316
|
+
`${input.profile.uuid}.mobileprovision`
|
|
317
|
+
: `${input.signingMode ?? 'development'}.mobileprovision`,
|
|
320
318
|
});
|
|
321
319
|
}
|
|
322
320
|
|
|
@@ -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),
|
|
@@ -123,11 +123,18 @@ export function watchBuildLogEvents({
|
|
|
123
123
|
events.addEventListener('stderr', (event) => onLine({ type: 'stderr', data: event.data }));
|
|
124
124
|
events.addEventListener('exitCode', (event) => {
|
|
125
125
|
const code = parseInt(event.data, 10);
|
|
126
|
-
onStatus(
|
|
126
|
+
onStatus(
|
|
127
|
+
code === 0 ? 'succeeded'
|
|
128
|
+
: code < 0 ? 'cancelled'
|
|
129
|
+
: 'failed',
|
|
130
|
+
);
|
|
127
131
|
events.close();
|
|
128
132
|
});
|
|
129
133
|
events.onerror = () => {
|
|
130
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');
|
|
131
138
|
onError?.(new Error('Build log stream closed before completion.'));
|
|
132
139
|
};
|
|
133
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;
|
|
@@ -39,7 +39,9 @@ export function findUsbmuxCandidates(device: USBDevice): UsbmuxCandidate[] {
|
|
|
39
39
|
return candidates;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function findConfigurationUsbmuxCandidates(
|
|
42
|
+
function findConfigurationUsbmuxCandidates(
|
|
43
|
+
configuration: USBDevice['configurations'][number],
|
|
44
|
+
): UsbmuxCandidate[] {
|
|
43
45
|
const candidates: UsbmuxCandidate[] = [];
|
|
44
46
|
for (const usbInterface of configuration.interfaces) {
|
|
45
47
|
for (const alternate of usbInterface.alternates) {
|
|
@@ -68,9 +70,12 @@ export async function claimUsbmux(device: USBDevice, candidate: UsbmuxCandidate)
|
|
|
68
70
|
await device.selectConfiguration(candidate.configurationValue);
|
|
69
71
|
}
|
|
70
72
|
await device.claimInterface(candidate.interfaceNumber);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
// Always select the alternate (even alt 0). On macOS Chrome the
|
|
74
|
+
// interface's endpoints are not treated as active until the alternate
|
|
75
|
+
// is explicitly selected, which otherwise surfaces as
|
|
76
|
+
// "endpoint is not part of a claimed and selected alternate interface"
|
|
77
|
+
// on the first transferIn/transferOut.
|
|
78
|
+
await device.selectAlternateInterface(candidate.interfaceNumber, candidate.alternateSetting);
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
export function getBulkEndpoints(candidate: UsbmuxCandidate) {
|
|
@@ -73,12 +73,13 @@ export async function getLatestSigningAssets(signingMode: DeviceInstallSigningMo
|
|
|
73
73
|
const all = await getAllSigningAssets();
|
|
74
74
|
return all
|
|
75
75
|
.filter((asset) => (asset.signingMode ?? 'development') === signingMode)
|
|
76
|
-
.sort(
|
|
77
|
-
(left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime(),
|
|
78
|
-
)[0];
|
|
76
|
+
.sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime())[0];
|
|
79
77
|
}
|
|
80
78
|
|
|
81
|
-
export async function getLatestSigningAssetsWithCertificate(
|
|
79
|
+
export async function getLatestSigningAssetsWithCertificate(
|
|
80
|
+
teamID?: string,
|
|
81
|
+
signingMode: DeviceInstallSigningMode = 'development',
|
|
82
|
+
) {
|
|
82
83
|
const all = await getAllSigningAssets();
|
|
83
84
|
return all
|
|
84
85
|
.filter((asset) => {
|
|
@@ -116,11 +117,16 @@ export async function putSigningAssets(input: PutSigningAssetsInput) {
|
|
|
116
117
|
return stored;
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
export async function findSigningAssetsForBundle(
|
|
120
|
+
export async function findSigningAssetsForBundle(
|
|
121
|
+
bundleID?: string,
|
|
122
|
+
signingMode: DeviceInstallSigningMode = 'development',
|
|
123
|
+
) {
|
|
120
124
|
const normalized = normalizeBundleID(bundleID);
|
|
121
125
|
if (!normalized) return [];
|
|
122
126
|
const all = await getAllSigningAssets();
|
|
123
|
-
return all.filter(
|
|
127
|
+
return all.filter(
|
|
128
|
+
(asset) => asset.bundleID === normalized && (asset.signingMode ?? 'development') === signingMode,
|
|
129
|
+
);
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
export function profileContainsDevice(profile: ProvisioningProfileInfo, deviceUDID?: string) {
|
|
@@ -160,6 +166,14 @@ export function parseProvisioningProfileBytes(bytes: Uint8Array) {
|
|
|
160
166
|
throw new Error('Provisioning profile plist not found.');
|
|
161
167
|
}
|
|
162
168
|
const xml = text.slice(start, end + '</plist>'.length);
|
|
169
|
+
// Defensively reject entity declarations before parsing: they are the vector
|
|
170
|
+
// for XML entity-expansion (billion-laughs) attacks and never appear in a
|
|
171
|
+
// real provisioning profile. The standard Apple `<!DOCTYPE plist PUBLIC ...>`
|
|
172
|
+
// (an external DTD reference with no internal entity subset) is benign —
|
|
173
|
+
// browsers' DOMParser does not fetch it — so it is intentionally allowed.
|
|
174
|
+
if (/<!ENTITY/i.test(xml)) {
|
|
175
|
+
throw new Error('Provisioning profile plist contains a disallowed ENTITY declaration.');
|
|
176
|
+
}
|
|
163
177
|
const doc = new DOMParser().parseFromString(xml, 'application/xml');
|
|
164
178
|
if (doc.querySelector('parsererror')) {
|
|
165
179
|
throw new Error('Provisioning profile plist could not be parsed.');
|
|
@@ -8,13 +8,7 @@ export type DeviceInstallStepStatus = 'idle' | 'active' | 'complete' | 'error';
|
|
|
8
8
|
|
|
9
9
|
export type DeviceInstallBusyAction = 'signing' | 'usb' | 'pair' | 'build' | 'install';
|
|
10
10
|
|
|
11
|
-
export type DeviceInstallBuildStatus =
|
|
12
|
-
| 'idle'
|
|
13
|
-
| 'queued'
|
|
14
|
-
| 'running'
|
|
15
|
-
| 'succeeded'
|
|
16
|
-
| 'failed'
|
|
17
|
-
| 'cancelled';
|
|
11
|
+
export type DeviceInstallBuildStatus = 'idle' | 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled';
|
|
18
12
|
|
|
19
13
|
export type BuildLogLine = {
|
|
20
14
|
type: 'command' | 'stdout' | 'stderr' | 'meta';
|
|
@@ -100,7 +100,10 @@ export function useDeviceBuild({
|
|
|
100
100
|
.catch((caught) => setError(errorMessage(caught)));
|
|
101
101
|
}
|
|
102
102
|
},
|
|
103
|
-
onError: (caught) =>
|
|
103
|
+
onError: (caught) => {
|
|
104
|
+
setStatus('failed');
|
|
105
|
+
setError(caught.message);
|
|
106
|
+
},
|
|
104
107
|
});
|
|
105
108
|
return result.execId;
|
|
106
109
|
} catch (caught) {
|