@limrun/ui 0.14.0-rc.4 → 0.14.0-rc.5
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 +3 -3
- package/dist/app-store-relay/index.d.ts +108 -4
- package/dist/app-store-relay/index.js +353 -160
- package/dist/app-store-relay/react.cjs +1 -1
- package/dist/app-store-relay/react.js +1 -1
- package/dist/apple-credentials/index.cjs +1 -1
- package/dist/apple-credentials/index.d.ts +59 -8
- package/dist/apple-credentials/index.js +197 -133
- package/dist/client-C9z_sgOf.js +1 -0
- package/dist/{client-DSuNtWDv.mjs → client-CFDB4mX5.mjs} +37 -30
- package/dist/core/device-install/apple/provisioning.d.ts +25 -0
- package/dist/core/device-install/apple/relay.d.ts +13 -0
- package/dist/core/device-install/storage/secret-store.d.ts +24 -1
- package/dist/device-build/index.cjs +1 -1
- package/dist/device-build/index.js +1 -1
- package/dist/device-install/index.cjs +1 -1
- package/dist/device-install/index.js +175 -172
- package/dist/{provisioning-B_DefBEp.mjs → provisioning-CDPvxd1h.mjs} +63 -32
- package/dist/provisioning-CbC4h--7.js +1 -0
- package/package.json +1 -1
- package/src/app-store-relay/index.ts +358 -23
- package/src/apple-credentials/index.test.ts +22 -0
- package/src/apple-credentials/index.ts +161 -14
- package/src/core/device-install/apple/client.ts +13 -6
- package/src/core/device-install/apple/provisioning.test.ts +30 -0
- package/src/core/device-install/apple/provisioning.ts +39 -0
- package/src/core/device-install/apple/relay.ts +20 -0
- package/src/core/device-install/operations/operations.ts +6 -1
- package/src/core/device-install/storage/secret-store.ts +36 -2
- package/dist/client-D1drDXxE.js +0 -1
- package/dist/provisioning-BlQMccX_.js +0 -1
|
@@ -7,12 +7,19 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import {
|
|
9
9
|
createAppleCertificate,
|
|
10
|
+
createAppStoreConnectApiKey,
|
|
10
11
|
downloadAppleCertificate,
|
|
11
12
|
downloadAppleProfile,
|
|
13
|
+
downloadAppStoreConnectApiKeyPrivateKey,
|
|
12
14
|
exportAppleCertificateP12,
|
|
13
15
|
generateAppleSigningKeyAndCSR,
|
|
14
16
|
listAppleCertificates,
|
|
15
17
|
listAppleProfiles,
|
|
18
|
+
listAppStoreConnectApiKeys,
|
|
19
|
+
switchAppStoreConnectProvider,
|
|
20
|
+
type AppleCertificateKind,
|
|
21
|
+
type AppleProfileKind,
|
|
22
|
+
type AppStoreConnectApiKeyRole,
|
|
16
23
|
} from '../app-store-relay';
|
|
17
24
|
import type { AppleRelayWebSocketClient } from '../core/device-install/apple';
|
|
18
25
|
import {
|
|
@@ -20,25 +27,31 @@ import {
|
|
|
20
27
|
parseProvisioningProfileBase64,
|
|
21
28
|
} from '../core/device-install/storage/browser-storage';
|
|
22
29
|
import {
|
|
30
|
+
APP_STORE_CONNECT_API_KEY_SECRET_TYPE,
|
|
23
31
|
APPLE_CERTIFICATE_SECRET_TYPE,
|
|
24
32
|
putAppleCertificateSecret,
|
|
25
33
|
putAppleProvisioningProfileSecret,
|
|
34
|
+
putAppStoreConnectApiKeySecret,
|
|
26
35
|
type AppleCertificateSecretData,
|
|
27
36
|
type AppleCertificateType,
|
|
37
|
+
type AppStoreConnectApiKeySecretData,
|
|
28
38
|
type SigningSecret,
|
|
29
39
|
type SigningSecretStore,
|
|
30
40
|
} from '../core/device-install/storage/secret-store';
|
|
31
41
|
|
|
32
42
|
export {
|
|
43
|
+
APP_STORE_CONNECT_API_KEY_SECRET_TYPE,
|
|
33
44
|
APPLE_CERTIFICATE_SECRET_TYPE,
|
|
34
45
|
APPLE_PROVISIONING_PROFILE_SECRET_TYPE,
|
|
35
46
|
createBrowserSecretStore,
|
|
36
47
|
createLimrunSecretStore,
|
|
37
48
|
putAppleCertificateSecret,
|
|
38
49
|
putAppleProvisioningProfileSecret,
|
|
50
|
+
putAppStoreConnectApiKeySecret,
|
|
39
51
|
type AppleCertificateSecretData,
|
|
40
52
|
type AppleCertificateType,
|
|
41
53
|
type AppleProvisioningProfileSecretData,
|
|
54
|
+
type AppStoreConnectApiKeySecretData,
|
|
42
55
|
type LimrunSecretStoreOptions,
|
|
43
56
|
type SigningSecret,
|
|
44
57
|
type SigningSecretData,
|
|
@@ -61,6 +74,11 @@ export type EnsureAppleCertificateInput = {
|
|
|
61
74
|
relay: AppleRelayWebSocketClient;
|
|
62
75
|
teamId: string;
|
|
63
76
|
secretStore: SigningSecretStore;
|
|
77
|
+
/**
|
|
78
|
+
* Which portal certificate kind to ensure. Ad-hoc and App Store signing
|
|
79
|
+
* both use the distribution certificate. Defaults to development.
|
|
80
|
+
*/
|
|
81
|
+
certificateKind?: AppleCertificateKind;
|
|
64
82
|
/** Common name used when minting a new certificate. */
|
|
65
83
|
commonName?: string;
|
|
66
84
|
log?: (message: string, detail?: string) => void;
|
|
@@ -74,10 +92,11 @@ export type EnsureAppleCertificateResult = {
|
|
|
74
92
|
};
|
|
75
93
|
|
|
76
94
|
/**
|
|
77
|
-
* Returns a usable
|
|
78
|
-
* stored one when its certificate is still on the team and
|
|
79
|
-
* one otherwise. Apple caps
|
|
80
|
-
*
|
|
95
|
+
* Returns a usable certificate secret of the requested kind for the team,
|
|
96
|
+
* reusing the stored one when its certificate is still on the team and
|
|
97
|
+
* minting a new one otherwise. Apple caps certificates per kind (2 for
|
|
98
|
+
* development, 3 for distribution) and never returns private keys, so
|
|
99
|
+
* reuse of the stored p12 is strongly preferred.
|
|
81
100
|
*
|
|
82
101
|
* Durability of the stored material is the secret store's concern:
|
|
83
102
|
* implementors who want retries or fallbacks build them into their
|
|
@@ -87,14 +106,16 @@ export async function ensureAppleCertificateSecret({
|
|
|
87
106
|
relay,
|
|
88
107
|
teamId,
|
|
89
108
|
secretStore,
|
|
109
|
+
certificateKind = 'development',
|
|
90
110
|
commonName,
|
|
91
111
|
log = () => {},
|
|
92
112
|
}: EnsureAppleCertificateInput): Promise<EnsureAppleCertificateResult> {
|
|
93
|
-
// The
|
|
94
|
-
//
|
|
95
|
-
const certificateType: AppleCertificateType =
|
|
113
|
+
// The portal kinds map onto Apple's App Store Connect certificate type
|
|
114
|
+
// enum, which is what consumers of the stored secret filter on.
|
|
115
|
+
const certificateType: AppleCertificateType =
|
|
116
|
+
certificateKind === 'distribution' ? 'DISTRIBUTION' : 'DEVELOPMENT';
|
|
96
117
|
const secretName = appleCertificateSecretName(teamId, certificateType);
|
|
97
|
-
const current = await listAppleCertificates({ relay, teamId, certificateKind
|
|
118
|
+
const current = await listAppleCertificates({ relay, teamId, certificateKind });
|
|
98
119
|
const findOnTeam = (certificateId: string | undefined) =>
|
|
99
120
|
certificateId === undefined ? undefined : (
|
|
100
121
|
current.find(
|
|
@@ -125,7 +146,7 @@ export async function ensureAppleCertificateSecret({
|
|
|
125
146
|
const certificate = await createAppleCertificate({
|
|
126
147
|
relay,
|
|
127
148
|
teamId,
|
|
128
|
-
certificateKind
|
|
149
|
+
certificateKind,
|
|
129
150
|
csrPEM: key.csrPEM,
|
|
130
151
|
});
|
|
131
152
|
const certificateId =
|
|
@@ -136,7 +157,7 @@ export async function ensureAppleCertificateSecret({
|
|
|
136
157
|
const downloaded = await downloadAppleCertificate({
|
|
137
158
|
relay,
|
|
138
159
|
teamId,
|
|
139
|
-
certificateKind
|
|
160
|
+
certificateKind,
|
|
140
161
|
certificateId,
|
|
141
162
|
});
|
|
142
163
|
if (!downloaded.rawBodyBase64) {
|
|
@@ -147,7 +168,7 @@ export async function ensureAppleCertificateSecret({
|
|
|
147
168
|
privateKeyPKCS8Base64: key.privateKeyPKCS8Base64,
|
|
148
169
|
certificateBase64: downloaded.rawBodyBase64,
|
|
149
170
|
password: '',
|
|
150
|
-
friendlyName: `Apple Development ${teamId}`,
|
|
171
|
+
friendlyName: `Apple ${certificateKind === 'distribution' ? 'Distribution' : 'Development'} ${teamId}`,
|
|
151
172
|
}),
|
|
152
173
|
certificateType,
|
|
153
174
|
teamID: teamId,
|
|
@@ -222,14 +243,140 @@ export async function saveAppleProfileSecret({
|
|
|
222
243
|
return secret;
|
|
223
244
|
}
|
|
224
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Conventional secret name of a team's App Store Connect API key: one
|
|
248
|
+
* shared key per team.
|
|
249
|
+
*/
|
|
250
|
+
export function appStoreConnectApiKeySecretName(teamId: string) {
|
|
251
|
+
return `${teamId}/APP_STORE_CONNECT_API_KEY`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export type EnsureAppStoreConnectApiKeyInput = {
|
|
255
|
+
relay: AppleRelayWebSocketClient;
|
|
256
|
+
teamId: string;
|
|
257
|
+
/**
|
|
258
|
+
* Numeric provider ID of the team from the Apple team list. When given,
|
|
259
|
+
* the App Store Connect session is switched to this provider first;
|
|
260
|
+
* required for accounts that belong to multiple teams.
|
|
261
|
+
*/
|
|
262
|
+
providerId?: string | number;
|
|
263
|
+
secretStore: SigningSecretStore;
|
|
264
|
+
/** Display name for a newly minted key. Required; there is no default. */
|
|
265
|
+
nickname: string;
|
|
266
|
+
/** Roles of a newly minted key. Defaults to APP_MANAGER. */
|
|
267
|
+
roles?: AppStoreConnectApiKeyRole[];
|
|
268
|
+
log?: (message: string, detail?: string) => void;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export type EnsureAppStoreConnectApiKeyResult = {
|
|
272
|
+
secret: SigningSecret;
|
|
273
|
+
keyId: string;
|
|
274
|
+
/** True when a new key was minted instead of reusing the stored one. */
|
|
275
|
+
created: boolean;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Returns a usable App Store Connect API key secret for the team, reusing
|
|
280
|
+
* the stored one when its key is still active and minting a new one
|
|
281
|
+
* otherwise. Apple serves a key's private half exactly once (at creation
|
|
282
|
+
* time through the sparse fieldset download), so reuse of the stored .p8
|
|
283
|
+
* is strongly preferred.
|
|
284
|
+
*
|
|
285
|
+
* The session user must be a team Admin to list or create keys.
|
|
286
|
+
*/
|
|
287
|
+
export async function ensureAppStoreConnectApiKeySecret({
|
|
288
|
+
relay,
|
|
289
|
+
teamId,
|
|
290
|
+
providerId,
|
|
291
|
+
secretStore,
|
|
292
|
+
nickname,
|
|
293
|
+
roles,
|
|
294
|
+
log = () => {},
|
|
295
|
+
}: EnsureAppStoreConnectApiKeyInput): Promise<EnsureAppStoreConnectApiKeyResult> {
|
|
296
|
+
if (!nickname) {
|
|
297
|
+
throw new Error('A nickname is required to ensure an App Store Connect API key.');
|
|
298
|
+
}
|
|
299
|
+
if (providerId !== undefined) {
|
|
300
|
+
await switchAppStoreConnectProvider({ relay, providerId });
|
|
301
|
+
}
|
|
302
|
+
const secretName = appStoreConnectApiKeySecretName(teamId);
|
|
303
|
+
// Listing also primes the session's CSRF context, which Apple requires
|
|
304
|
+
// on the creation POST below.
|
|
305
|
+
const current = await listAppStoreConnectApiKeys({ relay });
|
|
306
|
+
|
|
307
|
+
const stored = await secretStore.get(APP_STORE_CONNECT_API_KEY_SECRET_TYPE, secretName);
|
|
308
|
+
const storedKeyId = stringField(stored?.data, 'keyId');
|
|
309
|
+
if (stored && storedKeyId && stored.data.privateKeyP8Base64) {
|
|
310
|
+
const matched = current.find((item) => item.id === storedKeyId && !keyRevoked(item.attributes));
|
|
311
|
+
if (matched) {
|
|
312
|
+
log('Reusing stored App Store Connect API key', storedKeyId);
|
|
313
|
+
return { secret: stored, keyId: storedKeyId, created: false };
|
|
314
|
+
}
|
|
315
|
+
log('Stored App Store Connect API key is no longer active', 'Minting a new one.');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const created = await createAppStoreConnectApiKey({ relay, nickname, roles });
|
|
319
|
+
const keyId = created.id!;
|
|
320
|
+
const downloaded = await downloadAppStoreConnectApiKeyPrivateKey({ relay, keyId });
|
|
321
|
+
const data: AppStoreConnectApiKeySecretData = {
|
|
322
|
+
privateKeyP8Base64: base64FromText(downloaded.privateKeyPem),
|
|
323
|
+
keyId,
|
|
324
|
+
issuerId: downloaded.issuerId,
|
|
325
|
+
nickname,
|
|
326
|
+
teamID: teamId,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
let secret: SigningSecret;
|
|
330
|
+
try {
|
|
331
|
+
secret = await putAppStoreConnectApiKeySecret(secretStore, secretName, data);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
// Apple never serves the private key again; once this throw unwinds
|
|
334
|
+
// the downloaded copy is gone and the portal key is unusable.
|
|
335
|
+
throw new Error(
|
|
336
|
+
`App Store Connect API key ${keyId} was created but saving its private key to the secret ` +
|
|
337
|
+
`store failed: ${errorText(error)}. The key cannot be downloaded again; revoke key ` +
|
|
338
|
+
`${keyId} in App Store Connect before retrying, or the retry will mint another key.`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
log('App Store Connect API key stored', keyId);
|
|
342
|
+
return { secret, keyId, created: true };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function keyRevoked(attributes: Record<string, unknown> | undefined) {
|
|
346
|
+
if (!attributes) return false;
|
|
347
|
+
if (attributes.isActive === false) return true;
|
|
348
|
+
const revokingDate = attributes.revokingDate ?? attributes.revocationDate;
|
|
349
|
+
return typeof revokingDate === 'string' && revokingDate !== '';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function base64FromText(text: string) {
|
|
353
|
+
const bytes = new TextEncoder().encode(text);
|
|
354
|
+
let binary = '';
|
|
355
|
+
for (const byte of bytes) {
|
|
356
|
+
binary += String.fromCharCode(byte);
|
|
357
|
+
}
|
|
358
|
+
return btoa(binary);
|
|
359
|
+
}
|
|
360
|
+
|
|
225
361
|
export type ListTeamProfilesInput = {
|
|
226
362
|
relay: AppleRelayWebSocketClient;
|
|
227
363
|
teamId: string;
|
|
364
|
+
/**
|
|
365
|
+
* Which portal profile listing to use. The development listing is
|
|
366
|
+
* unfiltered on the portal side and returns profiles of every
|
|
367
|
+
* distribution method; adhoc narrows to ad-hoc profiles. Defaults to
|
|
368
|
+
* development.
|
|
369
|
+
*/
|
|
370
|
+
profileKind?: AppleProfileKind;
|
|
228
371
|
};
|
|
229
372
|
|
|
230
|
-
/** Lists the team's
|
|
231
|
-
export async function listTeamAppleProfiles({
|
|
232
|
-
|
|
373
|
+
/** Lists the team's provisioning profiles from the portal. */
|
|
374
|
+
export async function listTeamAppleProfiles({
|
|
375
|
+
relay,
|
|
376
|
+
teamId,
|
|
377
|
+
profileKind = 'development',
|
|
378
|
+
}: ListTeamProfilesInput) {
|
|
379
|
+
return listAppleProfiles({ relay, teamId, profileKind });
|
|
233
380
|
}
|
|
234
381
|
|
|
235
382
|
/** Read a string-ish value from a loosely typed portal record. */
|
|
@@ -63,12 +63,15 @@ export async function startBrowserOwnedAppleIDLogin({
|
|
|
63
63
|
if (requiresTwoFactor) {
|
|
64
64
|
twoFactorChallengeResponse = await triggerTrustedDeviceTwoFactor(relay);
|
|
65
65
|
const phone = trustedPhoneNumberFromChallenge(twoFactorChallengeResponse.body);
|
|
66
|
-
//
|
|
67
|
-
// falls back to phone verification
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
// trusted-device
|
|
71
|
-
|
|
66
|
+
// Route codes through the phone/SMS endpoint only when Apple actually
|
|
67
|
+
// falls back to phone verification: HTTP 412 on the trusted-device
|
|
68
|
+
// trigger, or a 2xx challenge whose body says noTrustedDevices (seen on
|
|
69
|
+
// managed Apple accounts). The trusted-device challenge body frequently
|
|
70
|
+
// includes trusted-phone metadata even for a normal device push;
|
|
71
|
+
// routing those codes to the phone endpoint breaks trusted-device
|
|
72
|
+
// login, while submitting an SMS-fallback code to the trusted-device
|
|
73
|
+
// endpoint returns 201 without completing authentication.
|
|
74
|
+
if (twoFactorChallengeResponse.status === 412 || challengeHasNoTrustedDevices(twoFactorChallengeResponse.body)) {
|
|
72
75
|
if (!phone) {
|
|
73
76
|
throw new Error('Apple requested phone verification but did not include a trusted phone number.');
|
|
74
77
|
}
|
|
@@ -117,6 +120,10 @@ export async function startBrowserOwnedAppleIDLogin({
|
|
|
117
120
|
}
|
|
118
121
|
}
|
|
119
122
|
|
|
123
|
+
function challengeHasNoTrustedDevices(body: unknown) {
|
|
124
|
+
return isRecord(body) && body.noTrustedDevices === true;
|
|
125
|
+
}
|
|
126
|
+
|
|
120
127
|
function trustedPhoneNumberFromChallenge(body: unknown) {
|
|
121
128
|
if (!isRecord(body)) return undefined;
|
|
122
129
|
const verification = isRecord(body.phoneNumberVerification) ? body.phoneNumberVerification : undefined;
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
import { describe, expect, test } from 'vitest';
|
|
4
4
|
import {
|
|
5
5
|
createAdHocProfileRequest,
|
|
6
|
+
createAppStoreProfileRequest,
|
|
6
7
|
createDevelopmentProfileRequest,
|
|
7
8
|
downloadDistributionCertificateRequest,
|
|
8
9
|
findAdHocProfilesRequest,
|
|
10
|
+
findAppStoreProfilesRequest,
|
|
9
11
|
findDevelopmentCertificatesRequest,
|
|
10
12
|
findDistributionCertificatesRequest,
|
|
11
13
|
submitDistributionCSRRequest,
|
|
@@ -81,4 +83,32 @@ describe('Apple provisioning request helpers', () => {
|
|
|
81
83
|
distributionType: 'adhoc',
|
|
82
84
|
});
|
|
83
85
|
});
|
|
86
|
+
|
|
87
|
+
test('builds App Store profile payloads without devices and with the caller-provided name', () => {
|
|
88
|
+
expect(
|
|
89
|
+
createAppStoreProfileRequest({
|
|
90
|
+
teamID: 'TEAM',
|
|
91
|
+
appIDID: 'APP',
|
|
92
|
+
certificateID: 'CERT',
|
|
93
|
+
name: 'Acme App Store com.example.app',
|
|
94
|
+
}).payload,
|
|
95
|
+
).toEqual({
|
|
96
|
+
teamId: 'TEAM',
|
|
97
|
+
provisioningProfileName: 'Acme App Store com.example.app',
|
|
98
|
+
certificateIds: ['CERT'],
|
|
99
|
+
appIdId: 'APP',
|
|
100
|
+
distributionType: 'store',
|
|
101
|
+
subPlatform: 'ios',
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('filters App Store profile list requests by store distribution type', () => {
|
|
106
|
+
expect(
|
|
107
|
+
findAppStoreProfilesRequest({ bundleID: 'com.example.app', teamID: 'TEAM' }).payload,
|
|
108
|
+
).toMatchObject({
|
|
109
|
+
teamId: 'TEAM',
|
|
110
|
+
search: 'com.example.app',
|
|
111
|
+
distributionType: 'store',
|
|
112
|
+
});
|
|
113
|
+
});
|
|
84
114
|
});
|
|
@@ -141,6 +141,17 @@ export function findAdHocProfilesRequest({
|
|
|
141
141
|
});
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
export function findAppStoreProfilesRequest({
|
|
145
|
+
bundleID,
|
|
146
|
+
teamID = '',
|
|
147
|
+
}: Pick<AppleProvisioningContext, 'bundleID' | 'teamID'>) {
|
|
148
|
+
return pagedRequest('/account/ios/profile/listProvisioningProfiles.action', teamID, {
|
|
149
|
+
search: bundleID,
|
|
150
|
+
sort: 'name=asc',
|
|
151
|
+
distributionType: 'store',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
144
155
|
export function registerDeviceRequest({
|
|
145
156
|
deviceUDID,
|
|
146
157
|
teamID = '',
|
|
@@ -280,6 +291,34 @@ export function createAdHocProfileRequest({
|
|
|
280
291
|
} satisfies AppleProvisioningRequest;
|
|
281
292
|
}
|
|
282
293
|
|
|
294
|
+
/**
|
|
295
|
+
* App Store distribution profiles bind no devices, and the name is required
|
|
296
|
+
* because implementors brand profiles themselves; there is no default.
|
|
297
|
+
*/
|
|
298
|
+
export function createAppStoreProfileRequest({
|
|
299
|
+
teamID = '',
|
|
300
|
+
appIDID,
|
|
301
|
+
certificateID,
|
|
302
|
+
name,
|
|
303
|
+
}: Pick<AppleProvisioningContext, 'teamID'> & {
|
|
304
|
+
appIDID: string;
|
|
305
|
+
certificateID: string;
|
|
306
|
+
name: string;
|
|
307
|
+
}) {
|
|
308
|
+
return {
|
|
309
|
+
method: 'POST',
|
|
310
|
+
path: '/account/ios/profile/createProvisioningProfile.action',
|
|
311
|
+
payload: {
|
|
312
|
+
teamId: teamID,
|
|
313
|
+
provisioningProfileName: name,
|
|
314
|
+
certificateIds: [certificateID],
|
|
315
|
+
appIdId: appIDID,
|
|
316
|
+
distributionType: 'store',
|
|
317
|
+
subPlatform: 'ios',
|
|
318
|
+
},
|
|
319
|
+
} satisfies AppleProvisioningRequest;
|
|
320
|
+
}
|
|
321
|
+
|
|
283
322
|
export function downloadProfileRequest(profileID: string, teamID = '') {
|
|
284
323
|
return {
|
|
285
324
|
method: 'GET',
|
|
@@ -15,6 +15,19 @@ export type AppleProvisioningRequest = {
|
|
|
15
15
|
payload?: unknown;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* A session-authenticated App Store Connect request proxied by the relay.
|
|
20
|
+
* Unlike provisioning requests, payloads pass through as JSON and GET
|
|
21
|
+
* parameters go into an explicit query map because the App Store Connect
|
|
22
|
+
* API uses bracketed JSON:API keys like fields[apiKeys].
|
|
23
|
+
*/
|
|
24
|
+
export type AppStoreConnectRequest = {
|
|
25
|
+
method?: 'GET' | 'POST';
|
|
26
|
+
path: string;
|
|
27
|
+
query?: Record<string, string>;
|
|
28
|
+
payload?: unknown;
|
|
29
|
+
};
|
|
30
|
+
|
|
18
31
|
type AppleRelayWebSocketMessage<T = unknown> = {
|
|
19
32
|
id: string;
|
|
20
33
|
ok: boolean;
|
|
@@ -154,6 +167,13 @@ export async function proxyProvisioningRequest<T = unknown>(
|
|
|
154
167
|
return relay.request<T>('provisioning', request);
|
|
155
168
|
}
|
|
156
169
|
|
|
170
|
+
export async function proxyAppStoreConnectRequest<T = unknown>(
|
|
171
|
+
relay: AppleRelayWebSocketClient,
|
|
172
|
+
request: AppStoreConnectRequest,
|
|
173
|
+
) {
|
|
174
|
+
return relay.request<T>('appstoreconnect', request);
|
|
175
|
+
}
|
|
176
|
+
|
|
157
177
|
function normalizeAppleProxyResponse<T>(response: AppleRelayResponse<T>) {
|
|
158
178
|
if (response.body !== undefined || !response.rawBody) {
|
|
159
179
|
return response;
|
|
@@ -180,7 +180,12 @@ async function claimBestUsbmuxCandidate(target: DeviceRelayTarget, log: DeviceIn
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
|
-
|
|
183
|
+
const detail = lastError ? ` (${errorMessage(lastError)})` : '';
|
|
184
|
+
throw new Error(
|
|
185
|
+
'Could not get exclusive USB access to the iPhone' +
|
|
186
|
+
`${detail}. Another app on this computer is likely connected to it. ` +
|
|
187
|
+
'Close other browser tabs that use this device, quit iPhone Mirroring, Xcode, eject iPhone in Finder windows showing the phone and try again.',
|
|
188
|
+
);
|
|
184
189
|
}
|
|
185
190
|
|
|
186
191
|
function pickUsbmuxCandidate(device: USBDevice) {
|
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
export const APPLE_CERTIFICATE_SECRET_TYPE = 'appleCertificate';
|
|
12
12
|
export const APPLE_PROVISIONING_PROFILE_SECRET_TYPE = 'appleProvisioningProfile';
|
|
13
|
+
export const APP_STORE_CONNECT_API_KEY_SECRET_TYPE = 'appStoreConnectApiKey';
|
|
13
14
|
|
|
14
15
|
export type SigningSecretType =
|
|
15
16
|
| typeof APPLE_CERTIFICATE_SECRET_TYPE
|
|
16
|
-
| typeof APPLE_PROVISIONING_PROFILE_SECRET_TYPE
|
|
17
|
+
| typeof APPLE_PROVISIONING_PROFILE_SECRET_TYPE
|
|
18
|
+
| typeof APP_STORE_CONNECT_API_KEY_SECRET_TYPE;
|
|
17
19
|
|
|
18
20
|
export type SigningSecretMetadata = {
|
|
19
21
|
type: string;
|
|
@@ -94,6 +96,24 @@ export type AppleProvisioningProfileSecretData = {
|
|
|
94
96
|
expirationDate?: string;
|
|
95
97
|
};
|
|
96
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Data payload of an appStoreConnectApiKey secret: the private half of an
|
|
101
|
+
* App Store Connect API key plus the identifiers needed to sign JWTs with
|
|
102
|
+
* it. Apple serves the private key exactly once, so the stored copy is the
|
|
103
|
+
* only one.
|
|
104
|
+
*/
|
|
105
|
+
export type AppStoreConnectApiKeySecretData = {
|
|
106
|
+
/** Base64 of the .p8 private key PEM. */
|
|
107
|
+
privateKeyP8Base64: string;
|
|
108
|
+
/** Apple's key ID, e.g. 2X9R4HXF34. */
|
|
109
|
+
keyId: string;
|
|
110
|
+
/** Issuer ID for team keys. Absent for individual keys. */
|
|
111
|
+
issuerId?: string;
|
|
112
|
+
nickname?: string;
|
|
113
|
+
teamID?: string;
|
|
114
|
+
expirationDate?: string;
|
|
115
|
+
};
|
|
116
|
+
|
|
97
117
|
function compactData(data: Record<string, string | undefined>): SigningSecretData {
|
|
98
118
|
const result: SigningSecretData = {};
|
|
99
119
|
for (const [key, value] of Object.entries(data)) {
|
|
@@ -124,6 +144,18 @@ export async function putAppleProvisioningProfileSecret(
|
|
|
124
144
|
return store.put(APPLE_PROVISIONING_PROFILE_SECRET_TYPE, name, compactData(data));
|
|
125
145
|
}
|
|
126
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Stores an App Store Connect API key, conventionally named
|
|
149
|
+
* `${teamID}/APP_STORE_CONNECT_API_KEY`: one shared key per team.
|
|
150
|
+
*/
|
|
151
|
+
export async function putAppStoreConnectApiKeySecret(
|
|
152
|
+
store: SigningSecretStore,
|
|
153
|
+
name: string,
|
|
154
|
+
data: AppStoreConnectApiKeySecretData,
|
|
155
|
+
) {
|
|
156
|
+
return store.put(APP_STORE_CONNECT_API_KEY_SECRET_TYPE, name, compactData(data));
|
|
157
|
+
}
|
|
158
|
+
|
|
127
159
|
export type LimrunSecretStoreOptions = {
|
|
128
160
|
/** Base URL of the Limrun backend API, e.g. https://api.limrun.com */
|
|
129
161
|
apiUrl: string;
|
|
@@ -200,7 +232,9 @@ export function createLimrunSecretStore(options: LimrunSecretStoreOptions): Sign
|
|
|
200
232
|
return result
|
|
201
233
|
.filter(
|
|
202
234
|
(s) =>
|
|
203
|
-
s.type === APPLE_CERTIFICATE_SECRET_TYPE ||
|
|
235
|
+
s.type === APPLE_CERTIFICATE_SECRET_TYPE ||
|
|
236
|
+
s.type === APPLE_PROVISIONING_PROFILE_SECRET_TYPE ||
|
|
237
|
+
s.type === APP_STORE_CONNECT_API_KEY_SECRET_TYPE,
|
|
204
238
|
)
|
|
205
239
|
.map((s) => ({ type: s.type, name: s.name, createdAt: s.createdAt }));
|
|
206
240
|
},
|
package/dist/client-D1drDXxE.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"use strict";const F=require("./gsa-srp-CCIfUz7Y.js");class g{constructor(t,r,o){this.apiUrl=t,this.token=r,this.organizationId=o}socket;nextId=1;pending=new Map;async connect(){if(this.socket&&this.socket.readyState===WebSocket.OPEN)return;const t=new WebSocket(E(this.apiUrl,this.token,this.organizationId));this.socket=t,t.onmessage=r=>this.handleMessage(r),t.onclose=()=>this.rejectPending(new Error("Apple relay WebSocket closed")),t.onerror=()=>this.rejectPending(new Error("Apple relay WebSocket failed")),await new Promise((r,o)=>{t.onopen=()=>r(),t.onerror=()=>o(new Error("Apple relay WebSocket connection failed"))})}request(t,r){const o=this.socket;if(!o||o.readyState!==WebSocket.OPEN)throw new Error("Apple relay WebSocket is not connected.");const a=String(this.nextId++),n=new Promise((p,i)=>{this.pending.set(a,{resolve:h=>p(q(h)),reject:i})});return o.send(JSON.stringify({id:a,type:t,...r===void 0?{}:{payload:r}})),n}close(){this.rejectPending(new Error("Apple relay WebSocket closed")),this.socket?.close(),this.socket=void 0}handleMessage(t){const r=JSON.parse(String(t.data)),o=this.pending.get(r.id);if(o){if(this.pending.delete(r.id),!r.ok){o.reject(new Error(r.error||"Apple relay request failed"));return}if(!r.response){o.reject(new Error("Apple relay response was empty."));return}o.resolve(r.response)}}rejectPending(t){for(const r of this.pending.values())r.reject(t);this.pending.clear()}}async function m(e,t,r){const o=new g(e,t,r);return await o.connect(),o}async function S(e,t){return e.request("srpInit",t)}async function A(e,t){return e.request("srpComplete",t)}async function P(e){return e.request("triggerTrustedDevice2FA")}async function b(e,t,r="sms"){return e.request("triggerPhone2FA",{phoneNumberId:t,mode:r})}async function k(e,t){return e.request("submitTrustedDevice2FA",{code:t})}async function T(e,t,r,o="sms"){return e.request("submitPhone2FA",{phoneNumberId:t,mode:o,code:r})}async function v(e){return e.request("finalize")}async function R(e,t){return e.request("provisioning",t)}function q(e){if(e.body!==void 0||!e.rawBody)return e;try{return{...e,body:JSON.parse(e.rawBody)}}catch{return e}}function E(e,t,r){const o=new URL(e);return o.protocol=o.protocol==="https:"?"wss:":"ws:",o.pathname=`${o.pathname.replace(/\/$/,"")}/ios/appstoreconnect/ws`,t&&o.searchParams.set("token",t),r&&o.searchParams.set("organization",r),o.toString()}async function N({registryApiUrl:e,accountName:t,password:r,token:o,organizationId:a}){const n=await m(e,o,a);try{const p=new F.AppleGsaSrpClient(t),i=await S(n,await p.init());if(i.status<200||i.status>=300)throw new Error(`Apple SRP init failed: HTTP ${i.status} ${i.rawBody??""}`.trim());if(!i.body)throw new Error("Apple SRP init response did not include a body.");const h=await p.complete(r,i.body),u=await A(n,{...h,rememberMe:!1,trustTokens:[]}),y=u.status===409;let s,d={type:"trustedDevice"};if(y){s=await P(n);const c=x(s.body);if(s.status===412){if(!c)throw new Error("Apple requested phone verification but did not include a trusted phone number.");d={type:"phone",phoneNumberId:c.id,mode:c.pushMode??"sms"},s=await b(n,c.id,c.pushMode??"sms")}if(s.status<200||s.status>=300)throw new Error(`Apple two-factor challenge failed: HTTP ${s.status} ${s.rawBody??""}`.trim())}else if(u.status<200||u.status>=300)throw new Error(`Apple SRP complete failed: HTTP ${u.status} ${u.rawBody??""}`.trim());return{relay:n,completeResponse:u,twoFactorChallengeResponse:s,requiresTwoFactor:y,finishTwoFactor:async c=>{const l=d.type==="phone"?await T(n,d.phoneNumberId,c,d.mode):await k(n,c);if(l.status<200||l.status>=300)throw new Error(`Apple two-factor code failed: HTTP ${l.status} ${l.rawBody??""}`.trim());return l},finalize:async()=>v(n),close:async()=>n.close()}}catch(p){throw n.close(),p}}function x(e){if(!f(e))return;const t=f(e.phoneNumberVerification)?e.phoneNumberVerification:void 0,r=w(t?.trustedPhoneNumber)??w(e.trustedPhoneNumber)??w(e.phoneNumber);if(!r)return;const o=r.id;if(typeof o!="number")return;const a=typeof r.pushMode=="string"?r.pushMode:typeof e.mode=="string"?e.mode:void 0;return{id:o,pushMode:a}}function w(e){return f(e)?e:void 0}function f(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}exports.AppleRelayWebSocketClient=g;exports.fetchAppleAccountSession=v;exports.openAppleRelayWebSocket=m;exports.proxyPhoneTwoFactorCode=T;exports.proxyProvisioningRequest=R;exports.proxySrpComplete=A;exports.proxySrpInit=S;exports.proxyTwoFactorCode=k;exports.startBrowserOwnedAppleIDLogin=N;exports.triggerPhoneTwoFactor=b;exports.triggerTrustedDeviceTwoFactor=P;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"use strict";const a=require("./browser-storage-DLxIqZk5.js");function c(){return{method:"POST",path:"/account/listTeams.action",payload:{}}}function u({bundleID:e,teamID:t=""}){return s("/account/ios/identifiers/listAppIds.action",t,{sort:"name=asc"})}function d({deviceUDID:e,teamID:t=""}){return s("/account/ios/device/listDevices.action",t,{sort:"name=asc",includeRemovedDevices:!1})}function f(e=""){return s("/account/ios/certificate/listCertRequests.action",e,{sort:"name=asc",types:"83Q87W3TGH,5QPB9NHCEI"})}function l(e=""){return s("/account/ios/certificate/listCertRequests.action",e,{sort:"name=asc",types:"WXV89964HE,R58UK2EWSO"})}function p({bundleID:e,teamID:t=""}){return s("/account/ios/profile/listProvisioningProfiles.action",t,{search:e,sort:"name=asc"})}function m({bundleID:e,teamID:t=""}){return s("/account/ios/profile/listProvisioningProfiles.action",t,{search:e,sort:"name=asc",distributionType:"adhoc"})}function R({deviceUDID:e,teamID:t="",name:i="Limrun iPhone"}){return{method:"POST",path:"/account/ios/device/addDevices.action",payload:{teamId:t,deviceNames:i,deviceNumbers:a.normalizeUDID(e),deviceClasses:"iphone",register:"single"}}}function g({bundleID:e,teamID:t="",name:i}){return{method:"POST",path:"/account/ios/identifiers/addAppId.action",payload:{teamId:t,name:i??e,identifier:e,type:"explicit"}}}function q({csrPEM:e,teamID:t=""}){return{method:"POST",path:"/account/ios/certificate/submitCertificateRequest.action",payload:{teamId:t,type:"83Q87W3TGH",csrContent:e}}}function v({csrPEM:e,teamID:t=""}){return{method:"POST",path:"/account/ios/certificate/submitCertificateRequest.action",payload:{teamId:t,type:"WXV89964HE",csrContent:e}}}function P(e,t=""){return{method:"GET",path:"/account/ios/certificate/downloadCertificateContent.action",payload:{teamId:t,certificateId:e,type:"83Q87W3TGH"}}}function D(e,t=""){return{method:"GET",path:"/account/ios/certificate/downloadCertificateContent.action",payload:{teamId:t,certificateId:e,type:"WXV89964HE"}}}function I({bundleID:e,teamID:t="",appIDID:i,certificateID:o,deviceIDs:n,name:r}){return{method:"POST",path:"/account/ios/profile/createProvisioningProfile.action",payload:{teamId:t,provisioningProfileName:r??`Limrun ${e}`,certificateIds:[o],appIdId:i,deviceIds:n,distributionType:"limited",subPlatform:"ios"}}}function h({bundleID:e,teamID:t="",appIDID:i,certificateID:o,deviceIDs:n,name:r}){return{method:"POST",path:"/account/ios/profile/createProvisioningProfile.action",payload:{teamId:t,provisioningProfileName:r??`Limrun Ad Hoc ${e}`,certificateIds:[o],appIdId:i,deviceIds:n,distributionType:"adhoc",subPlatform:"ios"}}}function C(e,t=""){return{method:"GET",path:"/account/ios/profile/downloadProfileContent",payload:{teamId:t,provisioningProfileId:e}}}async function b({bundleID:e,deviceUDID:t,teamID:i,signingMode:o}){const n=await a.getSigningAssets({bundleID:e,deviceUDID:t,signingMode:o});if(!(!n||!S(n,{bundleID:e,deviceUDID:t,teamID:i,signingMode:o})))return n}async function y(e){return a.putSigningAssets({...e,certificateFileName:e.certificateID?`${e.certificateID}.p12`:`apple-${e.signingMode??"development"}.p12`,certificatePassword:e.certificatePassword||void 0,signingMode:e.signingMode,profileFileName:e.profile.uuid?`${e.profile.uuid}.mobileprovision`:`${e.signingMode??"development"}.mobileprovision`})}function S(e,{bundleID:t,deviceUDID:i,teamID:o,signingMode:n="development"}){return(e.signingMode??"development")!==n||!a.profileMatchesBundleID(e.profile,t)||o&&e.teamID&&e.teamID!==o||i&&!a.profileContainsDevice(e.profile,i)||e.profile.expirationDate&&new Date(e.profile.expirationDate).getTime()<=Date.now()?!1:a.normalizeBundleID(e.bundleID)===a.normalizeBundleID(t)}function s(e,t,i={}){const o={pageNumber:1,pageSize:200,...i};return t&&(o.teamId=t),{method:"POST",path:e,payload:o}}exports.createAdHocProfileRequest=h;exports.createBundleIDRequest=g;exports.createDevelopmentProfileRequest=I;exports.downloadCertificateRequest=P;exports.downloadDistributionCertificateRequest=D;exports.downloadProfileRequest=C;exports.findAdHocProfilesRequest=m;exports.findBundleIDRequest=u;exports.findDevelopmentCertificatesRequest=f;exports.findDevelopmentProfilesRequest=p;exports.findDeviceRequest=d;exports.findDistributionCertificatesRequest=l;exports.getReusableAppleSigningAssets=b;exports.listTeamsRequest=c;exports.putAppleGeneratedSigningAssets=y;exports.registerDeviceRequest=R;exports.submitDevelopmentCSRRequest=q;exports.submitDistributionCSRRequest=v;
|