@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.
Files changed (31) hide show
  1. package/dist/app-store-relay/index.cjs +3 -3
  2. package/dist/app-store-relay/index.d.ts +108 -4
  3. package/dist/app-store-relay/index.js +353 -160
  4. package/dist/app-store-relay/react.cjs +1 -1
  5. package/dist/app-store-relay/react.js +1 -1
  6. package/dist/apple-credentials/index.cjs +1 -1
  7. package/dist/apple-credentials/index.d.ts +59 -8
  8. package/dist/apple-credentials/index.js +197 -133
  9. package/dist/client-C9z_sgOf.js +1 -0
  10. package/dist/{client-DSuNtWDv.mjs → client-CFDB4mX5.mjs} +37 -30
  11. package/dist/core/device-install/apple/provisioning.d.ts +25 -0
  12. package/dist/core/device-install/apple/relay.d.ts +13 -0
  13. package/dist/core/device-install/storage/secret-store.d.ts +24 -1
  14. package/dist/device-build/index.cjs +1 -1
  15. package/dist/device-build/index.js +1 -1
  16. package/dist/device-install/index.cjs +1 -1
  17. package/dist/device-install/index.js +175 -172
  18. package/dist/{provisioning-B_DefBEp.mjs → provisioning-CDPvxd1h.mjs} +63 -32
  19. package/dist/provisioning-CbC4h--7.js +1 -0
  20. package/package.json +1 -1
  21. package/src/app-store-relay/index.ts +358 -23
  22. package/src/apple-credentials/index.test.ts +22 -0
  23. package/src/apple-credentials/index.ts +161 -14
  24. package/src/core/device-install/apple/client.ts +13 -6
  25. package/src/core/device-install/apple/provisioning.test.ts +30 -0
  26. package/src/core/device-install/apple/provisioning.ts +39 -0
  27. package/src/core/device-install/apple/relay.ts +20 -0
  28. package/src/core/device-install/operations/operations.ts +6 -1
  29. package/src/core/device-install/storage/secret-store.ts +36 -2
  30. package/dist/client-D1drDXxE.js +0 -1
  31. package/dist/provisioning-BlQMccX_.js +0 -1
@@ -1,18 +1,18 @@
1
1
  import { e as c, a as u, n as d, b as f, c as l, i as r } from "./browser-storage-BnO-Ma-e.mjs";
2
- function g() {
2
+ function P() {
3
3
  return {
4
4
  method: "POST",
5
5
  path: "/account/listTeams.action",
6
6
  payload: {}
7
7
  };
8
8
  }
9
- function v({
9
+ function g({
10
10
  bundleID: e,
11
11
  teamID: i = ""
12
12
  }) {
13
13
  return n("/account/ios/identifiers/listAppIds.action", i, { sort: "name=asc" });
14
14
  }
15
- function P({
15
+ function v({
16
16
  deviceUDID: e,
17
17
  teamID: i = ""
18
18
  }) {
@@ -21,13 +21,13 @@ function P({
21
21
  includeRemovedDevices: !1
22
22
  });
23
23
  }
24
- function h(e = "") {
24
+ function I(e = "") {
25
25
  return n("/account/ios/certificate/listCertRequests.action", e, {
26
26
  sort: "name=asc",
27
27
  types: "83Q87W3TGH,5QPB9NHCEI"
28
28
  });
29
29
  }
30
- function I(e = "") {
30
+ function h(e = "") {
31
31
  return n("/account/ios/certificate/listCertRequests.action", e, {
32
32
  sort: "name=asc",
33
33
  types: "WXV89964HE,R58UK2EWSO"
@@ -52,7 +52,17 @@ function y({
52
52
  distributionType: "adhoc"
53
53
  });
54
54
  }
55
- function D({
55
+ function q({
56
+ bundleID: e,
57
+ teamID: i = ""
58
+ }) {
59
+ return n("/account/ios/profile/listProvisioningProfiles.action", i, {
60
+ search: e,
61
+ sort: "name=asc",
62
+ distributionType: "store"
63
+ });
64
+ }
65
+ function b({
56
66
  deviceUDID: e,
57
67
  teamID: i = "",
58
68
  name: t = "Limrun iPhone"
@@ -69,7 +79,7 @@ function D({
69
79
  }
70
80
  };
71
81
  }
72
- function q({
82
+ function T({
73
83
  bundleID: e,
74
84
  teamID: i = "",
75
85
  name: t
@@ -85,7 +95,7 @@ function q({
85
95
  }
86
96
  };
87
97
  }
88
- function b({ csrPEM: e, teamID: i = "" }) {
98
+ function D({ csrPEM: e, teamID: i = "" }) {
89
99
  return {
90
100
  method: "POST",
91
101
  path: "/account/ios/certificate/submitCertificateRequest.action",
@@ -107,7 +117,7 @@ function C({ csrPEM: e, teamID: i = "" }) {
107
117
  }
108
118
  };
109
119
  }
110
- function T(e, i = "") {
120
+ function S(e, i = "") {
111
121
  return {
112
122
  method: "GET",
113
123
  path: "/account/ios/certificate/downloadCertificateContent.action",
@@ -118,7 +128,7 @@ function T(e, i = "") {
118
128
  }
119
129
  };
120
130
  }
121
- function S(e, i = "") {
131
+ function A(e, i = "") {
122
132
  return {
123
133
  method: "GET",
124
134
  path: "/account/ios/certificate/downloadCertificateContent.action",
@@ -129,7 +139,7 @@ function S(e, i = "") {
129
139
  }
130
140
  };
131
141
  }
132
- function A({
142
+ function w({
133
143
  bundleID: e,
134
144
  teamID: i = "",
135
145
  appIDID: t,
@@ -151,7 +161,7 @@ function A({
151
161
  }
152
162
  };
153
163
  }
154
- function w({
164
+ function H({
155
165
  bundleID: e,
156
166
  teamID: i = "",
157
167
  appIDID: t,
@@ -173,7 +183,26 @@ function w({
173
183
  }
174
184
  };
175
185
  }
176
- function H(e, i = "") {
186
+ function O({
187
+ teamID: e = "",
188
+ appIDID: i,
189
+ certificateID: t,
190
+ name: o
191
+ }) {
192
+ return {
193
+ method: "POST",
194
+ path: "/account/ios/profile/createProvisioningProfile.action",
195
+ payload: {
196
+ teamId: e,
197
+ provisioningProfileName: o,
198
+ certificateIds: [t],
199
+ appIdId: i,
200
+ distributionType: "store",
201
+ subPlatform: "ios"
202
+ }
203
+ };
204
+ }
205
+ function N(e, i = "") {
177
206
  return {
178
207
  method: "GET",
179
208
  path: "/account/ios/profile/downloadProfileContent",
@@ -183,7 +212,7 @@ function H(e, i = "") {
183
212
  }
184
213
  };
185
214
  }
186
- async function O({
215
+ async function E({
187
216
  bundleID: e,
188
217
  deviceUDID: i,
189
218
  teamID: t,
@@ -193,7 +222,7 @@ async function O({
193
222
  if (!(!a || !p(a, { bundleID: e, deviceUDID: i, teamID: t, signingMode: o })))
194
223
  return a;
195
224
  }
196
- async function E(e) {
225
+ async function G(e) {
197
226
  return u({
198
227
  ...e,
199
228
  certificateFileName: e.certificateID ? `${e.certificateID}.p12` : `apple-${e.signingMode ?? "development"}.p12`,
@@ -218,22 +247,24 @@ function n(e, i, t = {}) {
218
247
  };
219
248
  }
220
249
  export {
221
- h as a,
222
- b,
223
- T as c,
224
- S as d,
225
- v as e,
226
- I as f,
227
- O as g,
228
- q as h,
229
- P as i,
250
+ I as a,
251
+ D as b,
252
+ S as c,
253
+ A as d,
254
+ g as e,
255
+ h as f,
256
+ E as g,
257
+ T as h,
258
+ v as i,
230
259
  y as j,
231
- R as k,
232
- g as l,
233
- w as m,
234
- A as n,
235
- H as o,
236
- E as p,
237
- D as r,
238
- C as s
260
+ q as k,
261
+ P as l,
262
+ R as m,
263
+ N as n,
264
+ O as o,
265
+ G as p,
266
+ H as q,
267
+ b as r,
268
+ C as s,
269
+ w as t
239
270
  };
@@ -0,0 +1 @@
1
+ "use strict";const s=require("./browser-storage-DLxIqZk5.js");function c(){return{method:"POST",path:"/account/listTeams.action",payload:{}}}function u({bundleID:e,teamID:t=""}){return r("/account/ios/identifiers/listAppIds.action",t,{sort:"name=asc"})}function d({deviceUDID:e,teamID:t=""}){return r("/account/ios/device/listDevices.action",t,{sort:"name=asc",includeRemovedDevices:!1})}function f(e=""){return r("/account/ios/certificate/listCertRequests.action",e,{sort:"name=asc",types:"83Q87W3TGH,5QPB9NHCEI"})}function l(e=""){return r("/account/ios/certificate/listCertRequests.action",e,{sort:"name=asc",types:"WXV89964HE,R58UK2EWSO"})}function p({bundleID:e,teamID:t=""}){return r("/account/ios/profile/listProvisioningProfiles.action",t,{search:e,sort:"name=asc"})}function m({bundleID:e,teamID:t=""}){return r("/account/ios/profile/listProvisioningProfiles.action",t,{search:e,sort:"name=asc",distributionType:"adhoc"})}function R({bundleID:e,teamID:t=""}){return r("/account/ios/profile/listProvisioningProfiles.action",t,{search:e,sort:"name=asc",distributionType:"store"})}function P({deviceUDID:e,teamID:t="",name:i="Limrun iPhone"}){return{method:"POST",path:"/account/ios/device/addDevices.action",payload:{teamId:t,deviceNames:i,deviceNumbers:s.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 I(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 h({bundleID:e,teamID:t="",appIDID:i,certificateID:o,deviceIDs:n,name:a}){return{method:"POST",path:"/account/ios/profile/createProvisioningProfile.action",payload:{teamId:t,provisioningProfileName:a??`Limrun ${e}`,certificateIds:[o],appIdId:i,deviceIds:n,distributionType:"limited",subPlatform:"ios"}}}function b({bundleID:e,teamID:t="",appIDID:i,certificateID:o,deviceIDs:n,name:a}){return{method:"POST",path:"/account/ios/profile/createProvisioningProfile.action",payload:{teamId:t,provisioningProfileName:a??`Limrun Ad Hoc ${e}`,certificateIds:[o],appIdId:i,deviceIds:n,distributionType:"adhoc",subPlatform:"ios"}}}function y({teamID:e="",appIDID:t,certificateID:i,name:o}){return{method:"POST",path:"/account/ios/profile/createProvisioningProfile.action",payload:{teamId:e,provisioningProfileName:o,certificateIds:[i],appIdId:t,distributionType:"store",subPlatform:"ios"}}}function S(e,t=""){return{method:"GET",path:"/account/ios/profile/downloadProfileContent",payload:{teamId:t,provisioningProfileId:e}}}async function C({bundleID:e,deviceUDID:t,teamID:i,signingMode:o}){const n=await s.getSigningAssets({bundleID:e,deviceUDID:t,signingMode:o});if(!(!n||!A(n,{bundleID:e,deviceUDID:t,teamID:i,signingMode:o})))return n}async function T(e){return s.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 A(e,{bundleID:t,deviceUDID:i,teamID:o,signingMode:n="development"}){return(e.signingMode??"development")!==n||!s.profileMatchesBundleID(e.profile,t)||o&&e.teamID&&e.teamID!==o||i&&!s.profileContainsDevice(e.profile,i)||e.profile.expirationDate&&new Date(e.profile.expirationDate).getTime()<=Date.now()?!1:s.normalizeBundleID(e.bundleID)===s.normalizeBundleID(t)}function r(e,t,i={}){const o={pageNumber:1,pageSize:200,...i};return t&&(o.teamId=t),{method:"POST",path:e,payload:o}}exports.createAdHocProfileRequest=b;exports.createAppStoreProfileRequest=y;exports.createBundleIDRequest=g;exports.createDevelopmentProfileRequest=h;exports.downloadCertificateRequest=I;exports.downloadDistributionCertificateRequest=D;exports.downloadProfileRequest=S;exports.findAdHocProfilesRequest=m;exports.findAppStoreProfilesRequest=R;exports.findBundleIDRequest=u;exports.findDevelopmentCertificatesRequest=f;exports.findDevelopmentProfilesRequest=p;exports.findDeviceRequest=d;exports.findDistributionCertificatesRequest=l;exports.getReusableAppleSigningAssets=C;exports.listTeamsRequest=c;exports.putAppleGeneratedSigningAssets=T;exports.registerDeviceRequest=P;exports.submitDevelopmentCSRRequest=q;exports.submitDistributionCSRRequest=v;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limrun/ui",
3
- "version": "0.14.0-rc.4",
3
+ "version": "0.14.0-rc.5",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "tag": "next"
@@ -1,17 +1,20 @@
1
1
  import {
2
2
  createAdHocProfileRequest,
3
+ createAppStoreProfileRequest,
3
4
  createBundleIDRequest,
4
5
  createDevelopmentProfileRequest,
5
6
  downloadCertificateRequest,
6
7
  downloadDistributionCertificateRequest,
7
8
  downloadProfileRequest,
8
9
  findAdHocProfilesRequest,
10
+ findAppStoreProfilesRequest,
9
11
  findBundleIDRequest,
10
12
  findDevelopmentCertificatesRequest,
11
13
  findDevelopmentProfilesRequest,
12
14
  findDeviceRequest,
13
15
  findDistributionCertificatesRequest,
14
16
  listTeamsRequest,
17
+ proxyAppStoreConnectRequest,
15
18
  proxyProvisioningRequest,
16
19
  registerDeviceRequest,
17
20
  submitDevelopmentCSRRequest,
@@ -35,11 +38,13 @@ export type {
35
38
  AppleDeveloperPortalTeam,
36
39
  AppleProvisioningRequest,
37
40
  AppleRelayResponse,
41
+ AppStoreConnectRequest,
38
42
  } from '../core/device-install/apple';
39
43
  export {
40
44
  AppleRelayWebSocketClient,
41
45
  fetchAppleAccountSession,
42
46
  openAppleRelayWebSocket,
47
+ proxyAppStoreConnectRequest,
43
48
  proxyPhoneTwoFactorCode,
44
49
  proxySrpComplete,
45
50
  proxySrpInit,
@@ -58,7 +63,7 @@ export type AppleTeamScopedOptions = AppleRelayClientOptions & {
58
63
 
59
64
  export type AppleCertificateKind = 'development' | 'distribution';
60
65
 
61
- export type AppleProfileKind = 'development' | 'adhoc';
66
+ export type AppleProfileKind = 'development' | 'adhoc' | 'appstore';
62
67
 
63
68
  export type RequestAppleProvisioningOptions<T = unknown> = AppleRelayClientOptions & {
64
69
  request: AppleProvisioningRequest;
@@ -134,7 +139,9 @@ export type CreateAppleProfileOptions = AppleTeamScopedOptions & {
134
139
  bundleId: string;
135
140
  appIdId: string;
136
141
  certificateIds: string[];
137
- deviceIds: string[];
142
+ /** Devices the profile binds. Ignored for App Store profiles, which bind none. */
143
+ deviceIds?: string[];
144
+ /** Profile name. Required for App Store profiles; there is no default. */
138
145
  name?: string;
139
146
  };
140
147
 
@@ -408,8 +415,8 @@ export async function listAppleProfiles({
408
415
  return requestAppleProvisioning<Array<Record<string, unknown>>>({
409
416
  ...options,
410
417
  request:
411
- profileKind === 'adhoc' ?
412
- findAdHocProfilesRequest({ bundleID: bundleId, teamID: teamId })
418
+ profileKind === 'adhoc' ? findAdHocProfilesRequest({ bundleID: bundleId, teamID: teamId })
419
+ : profileKind === 'appstore' ? findAppStoreProfilesRequest({ bundleID: bundleId, teamID: teamId })
413
420
  : findDevelopmentProfilesRequest({ bundleID: bundleId, teamID: teamId }),
414
421
  label: 'Apple provisioning profile list',
415
422
  mapBody: (body) => body?.provisioningProfiles ?? [],
@@ -422,7 +429,7 @@ export async function createAppleProfile({
422
429
  bundleId,
423
430
  appIdId,
424
431
  certificateIds,
425
- deviceIds,
432
+ deviceIds = [],
426
433
  name,
427
434
  ...options
428
435
  }: CreateAppleProfileOptions) {
@@ -430,26 +437,39 @@ export async function createAppleProfile({
430
437
  if (!certificateId) {
431
438
  throw new Error('At least one certificate ID is required to create an Apple provisioning profile.');
432
439
  }
440
+ let request: AppleProvisioningRequest;
441
+ if (profileKind === 'appstore') {
442
+ if (!name) {
443
+ throw new Error('An explicit name is required to create an App Store provisioning profile.');
444
+ }
445
+ request = createAppStoreProfileRequest({
446
+ teamID: teamId,
447
+ appIDID: appIdId,
448
+ certificateID: certificateId,
449
+ name,
450
+ });
451
+ } else if (profileKind === 'adhoc') {
452
+ request = createAdHocProfileRequest({
453
+ bundleID: bundleId,
454
+ teamID: teamId,
455
+ appIDID: appIdId,
456
+ certificateID: certificateId,
457
+ deviceIDs: deviceIds,
458
+ name,
459
+ });
460
+ } else {
461
+ request = createDevelopmentProfileRequest({
462
+ bundleID: bundleId,
463
+ teamID: teamId,
464
+ appIDID: appIdId,
465
+ certificateID: certificateId,
466
+ deviceIDs: deviceIds,
467
+ name,
468
+ });
469
+ }
433
470
  return requestAppleProvisioning<Record<string, unknown> | undefined>({
434
471
  ...options,
435
- request:
436
- profileKind === 'adhoc' ?
437
- createAdHocProfileRequest({
438
- bundleID: bundleId,
439
- teamID: teamId,
440
- appIDID: appIdId,
441
- certificateID: certificateId,
442
- deviceIDs: deviceIds,
443
- name,
444
- })
445
- : createDevelopmentProfileRequest({
446
- bundleID: bundleId,
447
- teamID: teamId,
448
- appIDID: appIdId,
449
- certificateID: certificateId,
450
- deviceIDs: deviceIds,
451
- name,
452
- }),
472
+ request,
453
473
  label: 'Apple provisioning profile creation',
454
474
  mapBody: (body) => body?.provisioningProfile,
455
475
  });
@@ -482,6 +502,321 @@ export async function deleteAppleProfile({ teamId = '', profileId, ...options }:
482
502
  });
483
503
  }
484
504
 
505
+ /**
506
+ * A JSON:API resource returned by the session-authenticated App Store
507
+ * Connect API. Kept loose on purpose: the endpoint is not a public contract
508
+ * and Apple adds attributes freely.
509
+ */
510
+ export type AppStoreConnectResource = {
511
+ type?: string;
512
+ id?: string;
513
+ attributes?: Record<string, unknown>;
514
+ relationships?: Record<string, unknown>;
515
+ };
516
+
517
+ type AppStoreConnectEnvelope = {
518
+ data?: AppStoreConnectResource | AppStoreConnectResource[];
519
+ included?: AppStoreConnectResource[];
520
+ errors?: Array<{ status?: string; code?: string; title?: string; detail?: string }>;
521
+ };
522
+
523
+ export type SwitchAppStoreConnectProviderOptions = AppleRelayClientOptions & {
524
+ /** The numeric provider ID of the team, from the team list's providerId. */
525
+ providerId: string | number;
526
+ };
527
+
528
+ /**
529
+ * Points the App Store Connect session at the given team (provider). Must
530
+ * run before any other App Store Connect call when the account belongs to
531
+ * multiple teams, because the session carries an active provider.
532
+ */
533
+ export async function switchAppStoreConnectProvider({
534
+ relay,
535
+ providerId,
536
+ }: SwitchAppStoreConnectProviderOptions) {
537
+ const numericProviderId = Number(providerId);
538
+ const response = await proxyAppStoreConnectRequest<Record<string, unknown>>(relay, {
539
+ method: 'POST',
540
+ path: '/olympus/v1/session',
541
+ payload: {
542
+ provider: { providerId: Number.isNaN(numericProviderId) ? providerId : numericProviderId },
543
+ },
544
+ });
545
+ if (response.status < 200 || response.status >= 300) {
546
+ throw new Error(
547
+ `App Store Connect provider switch failed: HTTP ${response.status} ${response.rawBody ?? ''}`.trim(),
548
+ );
549
+ }
550
+ return response;
551
+ }
552
+
553
+ /**
554
+ * Roles Apple accepts on App Store Connect API keys. APP_MANAGER is enough
555
+ * to upload builds and manage TestFlight, which is why it is the default
556
+ * for keys minted by ensureAppStoreConnectApiKeySecret.
557
+ */
558
+ export type AppStoreConnectApiKeyRole =
559
+ | 'ADMIN'
560
+ | 'APP_MANAGER'
561
+ | 'CUSTOMER_SUPPORT'
562
+ | 'DEVELOPER'
563
+ | 'FINANCE'
564
+ | 'MARKETING'
565
+ | 'SALES';
566
+
567
+ export type ListAppStoreConnectApiKeysOptions = AppleRelayClientOptions;
568
+
569
+ /** Lists the team's App Store Connect API keys. Requires an Admin session. */
570
+ export async function listAppStoreConnectApiKeys({ relay }: ListAppStoreConnectApiKeysOptions) {
571
+ const response = await proxyAppStoreConnectRequest<AppStoreConnectEnvelope>(relay, {
572
+ path: '/iris/v1/apiKeys',
573
+ });
574
+ assertAppStoreConnectResponseOK(response, 'App Store Connect API key list');
575
+ return resourceArray(response.body?.data);
576
+ }
577
+
578
+ export type CreateAppStoreConnectApiKeyOptions = AppleRelayClientOptions & {
579
+ /** Display name of the key. Required; implementors brand keys themselves. */
580
+ nickname: string;
581
+ roles?: AppStoreConnectApiKeyRole[];
582
+ allAppsVisible?: boolean;
583
+ };
584
+
585
+ /**
586
+ * Creates a team App Store Connect API key through the logged-in session,
587
+ * the same way the App Store Connect website does. The session user must
588
+ * be a team Admin. The private key is NOT part of the response; download
589
+ * it once with downloadAppStoreConnectApiKeyPrivateKey.
590
+ */
591
+ export async function createAppStoreConnectApiKey({
592
+ relay,
593
+ nickname,
594
+ roles = ['APP_MANAGER'],
595
+ allAppsVisible = true,
596
+ }: CreateAppStoreConnectApiKeyOptions) {
597
+ if (!nickname) {
598
+ throw new Error('A nickname is required to create an App Store Connect API key.');
599
+ }
600
+ const response = await proxyAppStoreConnectRequest<AppStoreConnectEnvelope>(relay, {
601
+ method: 'POST',
602
+ path: '/iris/v1/apiKeys',
603
+ payload: {
604
+ data: {
605
+ type: 'apiKeys',
606
+ attributes: {
607
+ nickname,
608
+ allAppsVisible,
609
+ keyType: 'PUBLIC_API',
610
+ roles,
611
+ },
612
+ },
613
+ },
614
+ });
615
+ assertAppStoreConnectResponseOK(response, 'App Store Connect API key creation');
616
+ const key = singleResource(response.body?.data);
617
+ if (!key?.id) {
618
+ throw new Error('App Store Connect API key creation did not return a key ID.');
619
+ }
620
+ return key;
621
+ }
622
+
623
+ export type DownloadAppStoreConnectApiKeyOptions = AppleRelayClientOptions & {
624
+ keyId: string;
625
+ };
626
+
627
+ export type DownloadedAppStoreConnectApiKey = {
628
+ /** PEM contents of the .p8 private key. */
629
+ privateKeyPem: string;
630
+ /** Issuer ID for JWTs signed with this key, when Apple returned it. */
631
+ issuerId?: string;
632
+ };
633
+
634
+ /**
635
+ * Downloads the private half of an App Store Connect API key. Apple serves
636
+ * it exactly once per key; afterwards the sparse fieldset comes back empty
637
+ * and the key must be revoked and re-minted.
638
+ */
639
+ export async function downloadAppStoreConnectApiKeyPrivateKey({
640
+ relay,
641
+ keyId,
642
+ }: DownloadAppStoreConnectApiKeyOptions): Promise<DownloadedAppStoreConnectApiKey> {
643
+ const response = await proxyAppStoreConnectRequest<AppStoreConnectEnvelope>(relay, {
644
+ path: `/iris/v1/apiKeys/${encodeURIComponent(keyId)}`,
645
+ query: { 'fields[apiKeys]': 'privateKey', include: 'provider' },
646
+ });
647
+ assertAppStoreConnectResponseOK(response, 'App Store Connect API key download');
648
+ const key = singleResource(response.body?.data);
649
+ const privateKeyPem = stringField(key?.attributes, 'privateKey');
650
+ if (!privateKeyPem) {
651
+ throw new Error(
652
+ `App Store Connect API key ${keyId} returned no private key. Apple serves it only once; ` +
653
+ 'revoke the key and create a new one.',
654
+ );
655
+ }
656
+ const provider = (response.body?.included ?? []).find((item) => item.type === 'providers');
657
+ const issuerId =
658
+ stringField(provider?.attributes, 'publicProviderId') ??
659
+ stringField(key?.attributes, 'issuerId') ??
660
+ provider?.id;
661
+ return { privateKeyPem, issuerId };
662
+ }
663
+
664
+ export type FindAppStoreConnectAppOptions = AppleRelayClientOptions & {
665
+ bundleId: string;
666
+ };
667
+
668
+ /** Looks up the App Store Connect app record for a bundle ID, if any. */
669
+ export async function findAppStoreConnectApp({ relay, bundleId }: FindAppStoreConnectAppOptions) {
670
+ const response = await proxyAppStoreConnectRequest<AppStoreConnectEnvelope>(relay, {
671
+ path: '/iris/v1/apps',
672
+ query: { 'filter[bundleId]': bundleId },
673
+ });
674
+ assertAppStoreConnectResponseOK(response, 'App Store Connect app lookup');
675
+ return resourceArray(response.body?.data).find(
676
+ (app) => stringField(app.attributes, 'bundleId') === bundleId,
677
+ );
678
+ }
679
+
680
+ export type CreateAppStoreConnectAppOptions = AppleRelayClientOptions & {
681
+ bundleId: string;
682
+ /** App name shown on the App Store. Required; there is no default. */
683
+ name: string;
684
+ /** Unique internal identifier. Defaults to the bundle ID. */
685
+ sku?: string;
686
+ /** Primary App Store locale. Defaults to en-US. */
687
+ primaryLocale?: string;
688
+ /** Version string of the first App Store version. Defaults to 1.0. */
689
+ versionString?: string;
690
+ };
691
+
692
+ /**
693
+ * Creates the App Store Connect app record for a bundle ID. Only the
694
+ * session-authenticated API can create app records (the key-authenticated
695
+ * public API cannot), which is why this goes through the relay. The body
696
+ * mirrors what the App Store Connect website and fastlane's produce send.
697
+ */
698
+ export async function createAppStoreConnectApp({
699
+ relay,
700
+ bundleId,
701
+ name,
702
+ sku,
703
+ primaryLocale = 'en-US',
704
+ versionString = '1.0',
705
+ }: CreateAppStoreConnectAppOptions) {
706
+ if (!name) {
707
+ throw new Error('An app name is required to create an App Store Connect app record.');
708
+ }
709
+ const platform = 'IOS';
710
+ const response = await proxyAppStoreConnectRequest<AppStoreConnectEnvelope>(relay, {
711
+ method: 'POST',
712
+ path: '/iris/v1/apps',
713
+ payload: {
714
+ data: {
715
+ type: 'apps',
716
+ attributes: {
717
+ sku: sku || bundleId,
718
+ primaryLocale,
719
+ bundleId,
720
+ },
721
+ relationships: {
722
+ appStoreVersions: {
723
+ data: [{ type: 'appStoreVersions', id: `\${store-version-${platform}}` }],
724
+ },
725
+ appInfos: {
726
+ data: [{ type: 'appInfos', id: '${new-appInfo-id}' }],
727
+ },
728
+ },
729
+ },
730
+ included: [
731
+ {
732
+ type: 'appInfos',
733
+ id: '${new-appInfo-id}',
734
+ relationships: {
735
+ appInfoLocalizations: {
736
+ data: [{ type: 'appInfoLocalizations', id: '${new-appInfoLocalization-id}' }],
737
+ },
738
+ },
739
+ },
740
+ {
741
+ type: 'appInfoLocalizations',
742
+ id: '${new-appInfoLocalization-id}',
743
+ attributes: { locale: primaryLocale, name },
744
+ },
745
+ {
746
+ type: 'appStoreVersions',
747
+ id: `\${store-version-${platform}}`,
748
+ attributes: { platform, versionString },
749
+ relationships: {
750
+ appStoreVersionLocalizations: {
751
+ data: [
752
+ { type: 'appStoreVersionLocalizations', id: `\${new-${platform}VersionLocalization-id}` },
753
+ ],
754
+ },
755
+ },
756
+ },
757
+ {
758
+ type: 'appStoreVersionLocalizations',
759
+ id: `\${new-${platform}VersionLocalization-id}`,
760
+ attributes: { locale: primaryLocale },
761
+ },
762
+ ],
763
+ },
764
+ });
765
+ assertAppStoreConnectResponseOK(response, 'App Store Connect app creation');
766
+ const app = singleResource(response.body?.data);
767
+ if (!app?.id) {
768
+ throw new Error('App Store Connect app creation did not return an app ID.');
769
+ }
770
+ return app;
771
+ }
772
+
773
+ export type EnsureAppStoreConnectAppOptions = CreateAppStoreConnectAppOptions;
774
+
775
+ /**
776
+ * Returns the App Store Connect app record for the bundle ID, creating it
777
+ * when it does not exist yet. A first-time IPA upload fails without an app
778
+ * record, so run this before the first publish of a new bundle ID.
779
+ */
780
+ export async function ensureAppStoreConnectApp(options: EnsureAppStoreConnectAppOptions) {
781
+ const existing = await findAppStoreConnectApp({ relay: options.relay, bundleId: options.bundleId });
782
+ if (existing) {
783
+ return { app: existing, created: false };
784
+ }
785
+ return { app: await createAppStoreConnectApp(options), created: true };
786
+ }
787
+
788
+ export function assertAppStoreConnectResponseOK(
789
+ response: AppleRelayResponse<AppStoreConnectEnvelope>,
790
+ label: string,
791
+ ) {
792
+ const errors = response.body?.errors;
793
+ if (errors && errors.length > 0) {
794
+ const detail = errors
795
+ .map((error) => error.detail ?? error.title ?? error.code ?? error.status)
796
+ .filter(Boolean)
797
+ .join('; ');
798
+ throw new Error(`${label} failed: ${detail || `HTTP ${response.status}`}`);
799
+ }
800
+ if (response.status < 200 || response.status >= 300) {
801
+ throw new Error(`${label} failed: HTTP ${response.status} ${response.rawBody ?? ''}`.trim());
802
+ }
803
+ }
804
+
805
+ function resourceArray(data: AppStoreConnectResource | AppStoreConnectResource[] | undefined) {
806
+ if (!data) return [];
807
+ return Array.isArray(data) ? data : [data];
808
+ }
809
+
810
+ function singleResource(data: AppStoreConnectResource | AppStoreConnectResource[] | undefined) {
811
+ if (!data) return undefined;
812
+ return Array.isArray(data) ? data[0] : data;
813
+ }
814
+
815
+ function stringField(record: Record<string, unknown> | undefined, key: string) {
816
+ const value = record?.[key];
817
+ return typeof value === 'string' && value !== '' ? value : undefined;
818
+ }
819
+
485
820
  export function assertAppleDeveloperPortalResponseOK(
486
821
  response: AppleDeveloperPortalResponse | undefined,
487
822
  label: string,
@@ -134,6 +134,28 @@ describe('ensureAppleCertificateSecret', () => {
134
134
  expect(stored?.data.certificateP12Base64).toBeTruthy();
135
135
  });
136
136
 
137
+ test('mints distribution certificates under their own secret name', async () => {
138
+ const state: PortalState = {
139
+ mintedCertificateIds: [],
140
+ certificateBase64: selfSignedCertificateBase64(),
141
+ };
142
+ const org = memorySecretStore();
143
+
144
+ const result = await ensureAppleCertificateSecret({
145
+ relay: fakePortalRelay(state),
146
+ teamId,
147
+ secretStore: org.store,
148
+ certificateKind: 'distribution',
149
+ });
150
+ expect(result.created).toBe(true);
151
+ const stored = await org.store.get(
152
+ APPLE_CERTIFICATE_SECRET_TYPE,
153
+ appleCertificateSecretName(teamId, 'DISTRIBUTION'),
154
+ );
155
+ expect(stored?.data.certificateType).toBe('DISTRIBUTION');
156
+ expect(stored?.data.certificateP12Base64).toBeTruthy();
157
+ });
158
+
137
159
  test('reuses the stored certificate when it is still on the team', async () => {
138
160
  const state: PortalState = {
139
161
  mintedCertificateIds: ['CERT1'],