@better-update/cli 0.46.0 → 0.47.0

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/index.mjs CHANGED
@@ -35,7 +35,7 @@ var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
35
35
 
36
36
  //#endregion
37
37
  //#region package.json
38
- var version = "0.46.0";
38
+ var version = "0.47.0";
39
39
 
40
40
  //#endregion
41
41
  //#region src/lib/interactive-mode.ts
@@ -3253,7 +3253,7 @@ var UpdateRollbackError = class extends Data.TaggedError("UpdateRollbackError")
3253
3253
  var UpdatePromoteError = class extends Data.TaggedError("UpdatePromoteError") {};
3254
3254
  var CredentialValidationError = class extends Data.TaggedError("CredentialValidationError") {};
3255
3255
  var IdentityError = class extends Data.TaggedError("IdentityError") {};
3256
- var AppleAuthError$1 = class extends Data.TaggedError("AppleAuthError") {};
3256
+ var AppleAuthError = class extends Data.TaggedError("AppleAuthError") {};
3257
3257
  var InvalidArgumentError = class extends Data.TaggedError("InvalidArgumentError") {};
3258
3258
  var InteractiveProhibitedError = class extends Data.TaggedError("InteractiveProhibitedError") {};
3259
3259
  var CredentialsJsonError = class extends Data.TaggedError("CredentialsJsonError") {};
@@ -3485,7 +3485,7 @@ const readEnv = (name) => Effect.gen(function* () {
3485
3485
  });
3486
3486
  const parseProviderId = (raw) => {
3487
3487
  const id = Number(raw);
3488
- return Number.isInteger(id) ? Effect.succeed(id) : Effect.fail(new AppleAuthError$1({ message: `${APPLE_PROVIDER_ID_ENV} must be a numeric provider ID, got "${raw}".` }));
3488
+ return Number.isInteger(id) ? Effect.succeed(id) : Effect.fail(new AppleAuthError({ message: `${APPLE_PROVIDER_ID_ENV} must be a numeric provider ID, got "${raw}".` }));
3489
3489
  };
3490
3490
  const readEnvProviderId = Effect.gen(function* () {
3491
3491
  const raw = yield* readEnv(APPLE_PROVIDER_ID_ENV);
@@ -3494,7 +3494,7 @@ const readEnvProviderId = Effect.gen(function* () {
3494
3494
  });
3495
3495
  const switchSessionProvider = (appleUtils, providerId) => Effect.tryPromise({
3496
3496
  try: async () => appleUtils.Session.setSessionProviderIdAsync(providerId),
3497
- catch: (error) => new AppleAuthError$1({ message: `Failed to switch App Store Connect provider (${providerId}): ${String(error)}` })
3497
+ catch: (error) => new AppleAuthError({ message: `Failed to switch App Store Connect provider (${providerId}): ${String(error)}` })
3498
3498
  }).pipe(Effect.asVoid);
3499
3499
  /**
3500
3500
  * Resolve App Store Connect provider for the current session.
@@ -3597,7 +3597,7 @@ const AppleSessionStoreLive = Layer.effect(AppleSessionStore, Effect.gen(functio
3597
3597
  yield* fs.chmod(sessionDir, 448);
3598
3598
  yield* fs.writeFileString(sessionFile, `${JSON.stringify(session, null, 2)}\n`);
3599
3599
  yield* fs.chmod(sessionFile, 384);
3600
- }).pipe(Effect.mapError((cause) => new AppleAuthError$1({ message: `Failed to save Apple session: ${formatCause(cause)}` }))),
3600
+ }).pipe(Effect.mapError((cause) => new AppleAuthError({ message: `Failed to save Apple session: ${formatCause(cause)}` }))),
3601
3601
  clearSession: fs.remove(sessionFile).pipe(Effect.catchAll(() => Effect.void)),
3602
3602
  loadLastUsername: Effect.gen(function* () {
3603
3603
  const content = yield* fs.readFileString(usernameFile).pipe(Effect.orElseSucceed(() => null));
@@ -3611,7 +3611,7 @@ const AppleSessionStoreLive = Layer.effect(AppleSessionStore, Effect.gen(functio
3611
3611
  yield* fs.chmod(sessionDir, 448);
3612
3612
  yield* fs.writeFileString(usernameFile, `${JSON.stringify({ username }, null, 2)}\n`);
3613
3613
  yield* fs.chmod(usernameFile, 384);
3614
- }).pipe(Effect.mapError((cause) => new AppleAuthError$1({ message: `Failed to save Apple username: ${formatCause(cause)}` })))
3614
+ }).pipe(Effect.mapError((cause) => new AppleAuthError({ message: `Failed to save Apple username: ${formatCause(cause)}` })))
3615
3615
  };
3616
3616
  }));
3617
3617
 
@@ -3648,12 +3648,12 @@ const resolvePortalTeamId = (appleUtils, provider) => Effect.gen(function* () {
3648
3648
  if (TEN_CHAR_TEAM_ID.test(provider.publicProviderId)) return provider.publicProviderId;
3649
3649
  return (yield* Effect.tryPromise({
3650
3650
  try: async () => appleUtils.Teams.getTeamsAsync(),
3651
- catch: (cause) => new AppleAuthError$1({ message: `Failed to list Apple Developer teams: ${formatCause(cause)}` })
3651
+ catch: (cause) => new AppleAuthError({ message: `Failed to list Apple Developer teams: ${formatCause(cause)}` })
3652
3652
  })).find((team) => team.name === provider.name)?.teamId ?? provider.publicProviderId;
3653
3653
  });
3654
3654
  const restoreFromCookies = (appleUtils, cookies) => Effect.tryPromise({
3655
3655
  try: async () => appleUtils.Auth.loginWithCookiesAsync({ cookies }),
3656
- catch: (cause) => new AppleAuthError$1({ message: `Failed to restore Apple session: ${formatCause(cause)}` })
3656
+ catch: (cause) => new AppleAuthError({ message: `Failed to restore Apple session: ${formatCause(cause)}` })
3657
3657
  });
3658
3658
  /**
3659
3659
  * After a cookie restore or fresh credentials login, re-resolve the team via
@@ -3666,7 +3666,7 @@ const resolveSessionTeam = (appleUtils, state) => Effect.gen(function* () {
3666
3666
  const resolution = yield* resolveProvider(appleUtils, availableProviders, state.context.providerId ?? state.session.provider.providerId);
3667
3667
  const switched = resolution.switched && resolution.providerId !== void 0;
3668
3668
  const provider = switched ? availableProviders.find((entry) => entry.providerId === resolution.providerId) : state.session.provider;
3669
- if (provider === void 0) return yield* new AppleAuthError$1({ message: `Selected provider ${String(resolution.providerId)} not in available providers list.` });
3669
+ if (provider === void 0) return yield* new AppleAuthError({ message: `Selected provider ${String(resolution.providerId)} not in available providers list.` });
3670
3670
  const teamId = (!switched && state.context.teamId !== void 0 && TEN_CHAR_TEAM_ID.test(state.context.teamId) ? state.context.teamId : void 0) ?? (yield* resolvePortalTeamId(appleUtils, provider));
3671
3671
  return {
3672
3672
  username: state.username,
@@ -3677,7 +3677,7 @@ const resolveSessionTeam = (appleUtils, state) => Effect.gen(function* () {
3677
3677
  });
3678
3678
  const loginWithCredentials = (appleUtils, credentials) => Effect.tryPromise({
3679
3679
  try: async () => appleUtils.Auth.loginWithUserCredentialsAsync(credentials, { autoResolveProvider: true }),
3680
- catch: (cause) => new AppleAuthError$1({ message: `Apple login failed: ${formatCause(cause)}` })
3680
+ catch: (cause) => new AppleAuthError({ message: `Apple login failed: ${formatCause(cause)}` })
3681
3681
  });
3682
3682
  const readJarCookies = (appleUtils) => appleUtils.CookieFileCache.getCookiesJSON();
3683
3683
  const promptCredentials = (defaultUsername) => Effect.gen(function* () {
@@ -3700,7 +3700,7 @@ const interactiveLogin = (appleUtils, options, cachedUsername) => Effect.gen(fun
3700
3700
  username,
3701
3701
  password
3702
3702
  });
3703
- if (state === null) return yield* new AppleAuthError$1({ message: "Apple login returned no session (unexpected)." });
3703
+ if (state === null) return yield* new AppleAuthError({ message: "Apple login returned no session (unexpected)." });
3704
3704
  const session = yield* resolveSessionTeam(appleUtils, state);
3705
3705
  yield* store.saveSession({
3706
3706
  cookies: readJarCookies(appleUtils),
@@ -3726,7 +3726,7 @@ const makeAppleAuthLive = (appleUtils = defaultAppleUtils) => Layer.effect(Apple
3726
3726
  }),
3727
3727
  logout: store.clearSession.pipe(Effect.flatMap(() => Effect.tryPromise({
3728
3728
  try: async () => appleUtils.Auth.logoutAsync(),
3729
- catch: (cause) => new AppleAuthError$1({ message: formatCause(cause) })
3729
+ catch: (cause) => new AppleAuthError({ message: formatCause(cause) })
3730
3730
  }).pipe(Effect.catchAll(() => Effect.void)))),
3731
3731
  whoami: Effect.gen(function* () {
3732
3732
  const stored = yield* store.loadSession;
@@ -5059,7 +5059,7 @@ const writeEasJsonPatch = (projectRoot, patch) => Effect.gen(function* () {
5059
5059
  * other submit profile and key. Used after auto-resolving/creating an ASC API
5060
5060
  * key during `submit` so the next run reuses it instead of creating another.
5061
5061
  */
5062
- const setSubmitProfileAscApiKeyId = (projectRoot, profileName, ascApiKeyId) => Effect.gen(function* () {
5062
+ const setSubmitProfileIosField = (projectRoot, profileName, key, value) => Effect.gen(function* () {
5063
5063
  const existing = (yield* readEasJsonRaw(projectRoot)) ?? {};
5064
5064
  const submit = isRecord$1(existing["submit"]) ? existing["submit"] : {};
5065
5065
  const profile = isRecord$1(submit[profileName]) ? submit[profileName] : {};
@@ -5070,11 +5070,18 @@ const setSubmitProfileAscApiKeyId = (projectRoot, profileName, ascApiKeyId) => E
5070
5070
  ...profile,
5071
5071
  ios: {
5072
5072
  ...ios,
5073
- ascApiKeyId
5073
+ [key]: value
5074
5074
  }
5075
5075
  }
5076
5076
  } });
5077
5077
  });
5078
+ const setSubmitProfileAscApiKeyId = (projectRoot, profileName, ascApiKeyId) => setSubmitProfileIosField(projectRoot, profileName, "ascApiKeyId", ascApiKeyId);
5079
+ /**
5080
+ * Set `submit.<profileName>.ios.ascAppId` in `eas.json`, preserving every other
5081
+ * submit profile and key. Used after auto-resolving/creating the App Store
5082
+ * Connect app during `submit` so the next run reuses it instead of re-looking-up.
5083
+ */
5084
+ const setSubmitProfileAscAppId = (projectRoot, profileName, ascAppId) => setSubmitProfileIosField(projectRoot, profileName, "ascAppId", ascAppId);
5078
5085
  /**
5079
5086
  * Default `build` profiles scaffolded by `init` / `build configure`. Mirrors the
5080
5087
  * EAS three-tier convention: `development` (dev-client, internal), `preview`
@@ -21024,7 +21031,7 @@ const TokenResponseSchema = Schema.Struct({
21024
21031
  expires_in: Schema.optional(Schema.Number)
21025
21032
  });
21026
21033
  const stripPemHeaders = (pem) => pem.replace(/-----BEGIN [A-Z ]+-----/u, "").replace(/-----END [A-Z ]+-----/u, "").replaceAll(/\s+/gu, "");
21027
- const asArrayBuffer$1 = (bytes) => {
21034
+ const asArrayBuffer = (bytes) => {
21028
21035
  const buffer = new ArrayBuffer(bytes.byteLength);
21029
21036
  new Uint8Array(buffer).set(bytes);
21030
21037
  return buffer;
@@ -21032,7 +21039,7 @@ const asArrayBuffer$1 = (bytes) => {
21032
21039
  const importPrivateKey = (pem) => Effect.tryPromise({
21033
21040
  try: async () => {
21034
21041
  const pkcs8 = fromBase64(stripPemHeaders(pem));
21035
- return crypto.subtle.importKey("pkcs8", asArrayBuffer$1(pkcs8), {
21042
+ return crypto.subtle.importKey("pkcs8", asArrayBuffer(pkcs8), {
21036
21043
  name: "RSASSA-PKCS1-v1_5",
21037
21044
  hash: "SHA-256"
21038
21045
  }, false, ["sign"]);
@@ -21271,6 +21278,45 @@ const commitEdit = (params) => callJsonRaw({
21271
21278
  label: "edits.commit"
21272
21279
  });
21273
21280
 
21281
+ //#endregion
21282
+ //#region src/lib/apple-asc-connect.ts
21283
+ /**
21284
+ * Shared bridge to the `@expo/apple-utils` App Store Connect entity layer for
21285
+ * the **headless** (JWT) path. apple-utils routes by `RequestContext`: a context
21286
+ * carrying a signed `Token` hits the public ASC REST API
21287
+ * (`api.appstoreconnect.apple.com/v1`) with no cookie session — the same surface
21288
+ * the CLI's vault `.p8` keys authenticate against. Interactive flows pass a
21289
+ * cookie context from `AppleAuth.buildRequestContext` instead; both drive the
21290
+ * same entity managers.
21291
+ */
21292
+ var AppleConnectError = class extends Data.TaggedError("AppleConnectError") {};
21293
+ const messageOf = (cause) => cause instanceof Error ? cause.message : String(cause);
21294
+ /**
21295
+ * Apple returns the same "current certificate already exists / pending request"
21296
+ * wording whether the call came from a JWT ASC request or the Apple ID session,
21297
+ * so cert-limit detection lives here, in the shared connect layer.
21298
+ */
21299
+ const CERT_LIMIT_PATTERN = /already have a current.*certificate|pending certificate request/iu;
21300
+ const isCertificateLimitMessage = (message) => CERT_LIMIT_PATTERN.test(message);
21301
+ /**
21302
+ * Build a headless ASC `RequestContext` from a vault `.p8` key. The `Token`
21303
+ * signs ES256 JWTs on demand (apple-utils refreshes them); no `providerId`/
21304
+ * `teamId` is needed because the JWT's issuer selects the provider.
21305
+ */
21306
+ const buildTokenRequestContext = (credentials) => ({ token: new AppleUtils.Token({
21307
+ key: credentials.p8Pem,
21308
+ keyId: credentials.keyId,
21309
+ issuerId: credentials.issuerId
21310
+ }) });
21311
+ /** Run an apple-utils promise, tagging any rejection as an {@link AppleConnectError}. */
21312
+ const wrapConnect = (step, run) => Effect.tryPromise({
21313
+ try: run,
21314
+ catch: (cause) => new AppleConnectError({
21315
+ step,
21316
+ message: messageOf(cause)
21317
+ })
21318
+ });
21319
+
21274
21320
  //#endregion
21275
21321
  //#region src/lib/asc-credentials.ts
21276
21322
  /**
@@ -21304,367 +21350,28 @@ const fetchAscCredentials = (api, ascApiKeyId) => Effect.gen(function* () {
21304
21350
  });
21305
21351
 
21306
21352
  //#endregion
21307
- //#region src/lib/apple-pem.ts
21308
- const PEM_HEADER = "-----BEGIN PRIVATE KEY-----";
21309
- const PEM_FOOTER = "-----END PRIVATE KEY-----";
21310
- const pemToPkcs8Der = (pem) => {
21311
- const normalized = pem.replaceAll("\r\n", "\n").trim();
21312
- const start = normalized.indexOf(PEM_HEADER);
21313
- const end = normalized.indexOf(PEM_FOOTER);
21314
- if (start === -1 || end === -1 || end <= start) return null;
21315
- const body = normalized.slice(start + 27, end).replaceAll(/\s+/gu, "").trim();
21316
- if (body.length === 0) return null;
21317
- try {
21318
- return fromBase64(body);
21319
- } catch {
21320
- return null;
21321
- }
21322
- };
21323
-
21324
- //#endregion
21325
- //#region src/lib/apple-asc-jwt.ts
21326
- var AppleAuthError = class extends Data.TaggedError("AppleAuthError") {};
21327
- const MAX_JWT_LIFETIME_SECONDS = 1200;
21328
- const asArrayBuffer = (bytes) => {
21329
- const buffer = new ArrayBuffer(bytes.byteLength);
21330
- new Uint8Array(buffer).set(bytes);
21331
- return buffer;
21332
- };
21333
- const signAscJwt = (credentials) => Effect.gen(function* () {
21334
- const der = pemToPkcs8Der(credentials.p8Pem);
21335
- if (der === null) return yield* new AppleAuthError({ cause: /* @__PURE__ */ new Error("Invalid .p8 PEM") });
21336
- const header = {
21337
- alg: "ES256",
21338
- kid: credentials.keyId,
21339
- typ: "JWT"
21340
- };
21341
- const now = Math.floor(Date.now() / 1e3);
21342
- const payload = {
21343
- iss: credentials.issuerId,
21344
- iat: now,
21345
- exp: now + MAX_JWT_LIFETIME_SECONDS,
21346
- aud: "appstoreconnect-v1"
21347
- };
21348
- const signingInput = `${toBase64Url(new TextEncoder().encode(JSON.stringify(header)))}.${toBase64Url(new TextEncoder().encode(JSON.stringify(payload)))}`;
21349
- const key = yield* Effect.tryPromise({
21350
- try: async () => crypto.subtle.importKey("pkcs8", asArrayBuffer(der), {
21351
- name: "ECDSA",
21352
- namedCurve: "P-256"
21353
- }, false, ["sign"]),
21354
- catch: (cause) => new AppleAuthError({ cause })
21355
- });
21356
- const signature = yield* Effect.tryPromise({
21357
- try: async () => crypto.subtle.sign({
21358
- name: "ECDSA",
21359
- hash: "SHA-256"
21360
- }, key, new TextEncoder().encode(signingInput)),
21361
- catch: (cause) => new AppleAuthError({ cause })
21362
- });
21363
- return `${signingInput}.${toBase64Url(new Uint8Array(signature))}`;
21364
- });
21365
-
21366
- //#endregion
21367
- //#region src/lib/apple-asc-client.ts
21353
+ //#region src/application/ios-testflight-config.ts
21368
21354
  /**
21369
- * App Store Connect REST client authenticated with an ASC **API key** — a JWT
21370
- * signed from a `.p8` private key (see `apple-asc-jwt.ts`). Credentials are
21371
- * resolved non-interactively from the server (`fetchAscCredentials`), so this
21372
- * powers headless flows: build-credential resolution, provisioning-profile
21373
- * generation, and device sync.
21355
+ * Post-upload TestFlight configuration for iOS submissions. After `altool`
21356
+ * uploads the `.ipa`, App Store Connect spends several minutes *processing* the
21357
+ * binary before it can be configured. This module waits for that processing to
21358
+ * finish, then sets the build's "What to Test" text and assigns it to internal
21359
+ * TestFlight groups the same follow-up `eas submit` performs server-side.
21374
21360
  *
21375
- * Intentionally NOT built on `@expo/apple-utils`: that library authenticates
21376
- * via an interactive Apple-ID **cookie session** (username/password + 2FA, see
21377
- * `services/apple-auth.ts`) and exposes a cookie-based `RequestContext`. That is
21378
- * a different auth model that would force an interactive login here. The two
21379
- * coexist by design — apple-utils backs `apple login`; this client backs
21380
- * non-interactive ASC API-key access.
21381
- */
21382
- var AscApiError = class extends Data.TaggedError("AscApiError") {};
21383
- var AscNetworkError = class extends Data.TaggedError("AscNetworkError") {};
21384
- const API_BASE = "https://api.appstoreconnect.apple.com";
21385
- const extractErrors = (body) => {
21386
- if (!isRecord$1(body) || !Array.isArray(body["errors"])) return [];
21387
- return body["errors"].filter((value) => isRecord$1(value));
21388
- };
21389
- const parseApiError = (response, body, raw) => {
21390
- const [first] = extractErrors(body);
21391
- return new AscApiError({
21392
- status: response.status,
21393
- message: first?.detail ?? first?.title ?? response.statusText,
21394
- code: first?.code,
21395
- raw
21396
- });
21397
- };
21398
- const fetchRaw = (jwt, path, init) => Effect.gen(function* () {
21399
- const response = yield* Effect.tryPromise({
21400
- try: async () => fetch(`${API_BASE}${path}`, compact({
21401
- method: init?.method ?? "GET",
21402
- body: init?.body,
21403
- headers: {
21404
- authorization: `Bearer ${jwt}`,
21405
- "content-type": "application/json",
21406
- accept: "application/json"
21407
- }
21408
- })),
21409
- catch: (cause) => new AscNetworkError({ cause })
21410
- });
21411
- const text = yield* Effect.tryPromise({
21412
- try: async () => response.text(),
21413
- catch: (cause) => new AscNetworkError({ cause })
21414
- });
21415
- const body = yield* Effect.try({
21416
- try: () => text.length === 0 ? {} : JSON.parse(text),
21417
- catch: (cause) => new AscNetworkError({ cause })
21418
- });
21419
- if (!response.ok) return yield* parseApiError(response, body, text);
21420
- return body;
21421
- });
21422
- const toAscCertificate = (value) => {
21423
- if (!isRecord$1(value)) return null;
21424
- const { id, attributes } = value;
21425
- if (typeof id !== "string" || !isRecord$1(attributes)) return null;
21426
- const { serialNumber, certificateType, expirationDate, certificateContent, displayName } = attributes;
21427
- if (typeof serialNumber !== "string" || typeof certificateType !== "string" || typeof expirationDate !== "string") return null;
21428
- return {
21429
- id,
21430
- serialNumber,
21431
- certificateType,
21432
- expirationDate,
21433
- certificateContent: typeof certificateContent === "string" ? certificateContent : null,
21434
- displayName: typeof displayName === "string" ? displayName : null
21435
- };
21436
- };
21437
- const toAscBundleId = (value) => {
21438
- if (!isRecord$1(value)) return null;
21439
- const { id, attributes } = value;
21440
- if (typeof id !== "string" || !isRecord$1(attributes)) return null;
21441
- const { identifier, name } = attributes;
21442
- if (typeof identifier !== "string" || typeof name !== "string") return null;
21443
- return {
21444
- id,
21445
- identifier,
21446
- name
21447
- };
21448
- };
21449
- const PROFILE_TYPES = [
21450
- "IOS_APP_ADHOC",
21451
- "IOS_APP_DEVELOPMENT",
21452
- "IOS_APP_STORE",
21453
- "IOS_APP_INHOUSE"
21454
- ];
21455
- const asProfileType = (value) => {
21456
- const match = PROFILE_TYPES.find((entry) => entry === value);
21457
- return match === void 0 ? null : match;
21458
- };
21459
- const toAscProfile = (value) => {
21460
- if (!isRecord$1(value)) return null;
21461
- const { id, attributes } = value;
21462
- if (typeof id !== "string" || !isRecord$1(attributes)) return null;
21463
- const { name, uuid, expirationDate, profileContent } = attributes;
21464
- const profileType = asProfileType(attributes["profileType"]);
21465
- if (typeof name !== "string" || typeof uuid !== "string" || typeof expirationDate !== "string" || typeof profileContent !== "string" || profileType === null) return null;
21466
- return {
21467
- id,
21468
- name,
21469
- uuid,
21470
- expirationDate,
21471
- profileContent,
21472
- profileType
21473
- };
21474
- };
21475
- const toAscDevice = (value) => {
21476
- if (!isRecord$1(value)) return null;
21477
- const { id, attributes } = value;
21478
- if (typeof id !== "string" || !isRecord$1(attributes)) return null;
21479
- const { udid, name, deviceClass } = attributes;
21480
- if (typeof udid !== "string" || typeof name !== "string") return null;
21481
- return {
21482
- id,
21483
- udid,
21484
- name,
21485
- deviceClass: typeof deviceClass === "string" ? deviceClass : null
21486
- };
21487
- };
21488
- const extractList = (body, map) => {
21489
- if (!isRecord$1(body) || !Array.isArray(body["data"])) return [];
21490
- return body["data"].map(map).filter((value) => value !== null);
21491
- };
21492
- const extractSingle = (body, map) => {
21493
- if (!isRecord$1(body)) return null;
21494
- return map(body["data"]);
21495
- };
21496
- /**
21497
- * App Store Connect paginates list responses (default 200/page) and returns the
21498
- * absolute URL of the next page under `links.next`. Strip the base so it can be
21499
- * fed back into `fetchRaw`; return null when there is no further page.
21361
+ * Auth reuses the ASC **API key** already decrypted for the upload via a headless
21362
+ * `@expo/apple-utils` JWT context (no second credential prompt, no cookie login).
21363
+ * Failures surface as {@link TestFlightConfigError} so the caller can mark the
21364
+ * submission ERRORED with a precise reason.
21500
21365
  */
21501
- const nextPagePath = (body) => {
21502
- if (!isRecord$1(body)) return null;
21503
- const { links } = body;
21504
- if (!isRecord$1(links) || typeof links["next"] !== "string") return null;
21505
- const { next } = links;
21506
- return next.startsWith(API_BASE) ? next.slice(37) : next;
21507
- };
21508
- const malformed = (resource) => new AscApiError({
21509
- status: 500,
21510
- message: `Malformed ${resource} response`,
21511
- code: void 0,
21512
- raw: ""
21513
- });
21514
- const withJwt = (credentials, fn) => Effect.gen(function* () {
21515
- return yield* fn(yield* signAscJwt(credentials));
21516
- });
21517
- const listCertificates = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
21518
- return extractList(yield* fetchRaw(jwt, `/v1/certificates${params?.certificateType ? `?filter[certificateType]=${params.certificateType}&limit=200` : "?limit=200"}`), toAscCertificate);
21519
- }));
21520
- const createCertificate = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
21521
- const csrContent = params.csrPem.replaceAll("-----BEGIN CERTIFICATE REQUEST-----", "").replaceAll("-----END CERTIFICATE REQUEST-----", "").replaceAll(/\s+/gu, "");
21522
- const resource = extractSingle(yield* fetchRaw(jwt, "/v1/certificates", {
21523
- method: "POST",
21524
- body: JSON.stringify({ data: {
21525
- type: "certificates",
21526
- attributes: {
21527
- csrContent,
21528
- certificateType: params.certificateType
21529
- }
21530
- } })
21531
- }), toAscCertificate);
21532
- if (resource === null) return yield* malformed("certificate");
21533
- return resource;
21534
- }));
21535
- const deleteCertificate = (credentials, id) => withJwt(credentials, (jwt) => Effect.asVoid(fetchRaw(jwt, `/v1/certificates/${encodeURIComponent(id)}`, { method: "DELETE" })));
21536
- const listBundleIds = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
21537
- return extractList(yield* fetchRaw(jwt, "/v1/bundleIds?limit=200"), toAscBundleId);
21538
- }));
21539
- const createBundleId = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
21540
- const resource = extractSingle(yield* fetchRaw(jwt, "/v1/bundleIds", {
21541
- method: "POST",
21542
- body: JSON.stringify({ data: {
21543
- type: "bundleIds",
21544
- attributes: {
21545
- identifier: params.identifier,
21546
- name: params.name,
21547
- platform: "IOS"
21548
- }
21549
- } })
21550
- }), toAscBundleId);
21551
- if (resource === null) return yield* malformed("bundleId");
21552
- return resource;
21553
- }));
21554
- const listDevices = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
21555
- const devices = [];
21556
- let path = "/v1/devices?limit=200";
21557
- while (path !== null) {
21558
- const body = yield* fetchRaw(jwt, path);
21559
- devices.push(...extractList(body, toAscDevice));
21560
- path = nextPagePath(body);
21561
- }
21562
- return devices;
21563
- }));
21564
- const createDevice = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
21565
- const resource = extractSingle(yield* fetchRaw(jwt, "/v1/devices", {
21566
- method: "POST",
21567
- body: JSON.stringify({ data: {
21568
- type: "devices",
21569
- attributes: {
21570
- name: params.name,
21571
- udid: params.udid,
21572
- platform: "IOS"
21573
- }
21574
- } })
21575
- }), toAscDevice);
21576
- if (resource === null) return yield* malformed("device");
21577
- return resource;
21578
- }));
21579
- const createProvisioningProfile = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
21580
- const relationships = {
21581
- bundleId: { data: {
21582
- type: "bundleIds",
21583
- id: params.bundleIdAscId
21584
- } },
21585
- certificates: { data: params.certificateAscIds.map((id) => ({
21586
- type: "certificates",
21587
- id
21588
- })) },
21589
- ...params.deviceAscIds.length > 0 ? { devices: { data: params.deviceAscIds.map((id) => ({
21590
- type: "devices",
21591
- id
21592
- })) } } : {}
21593
- };
21594
- const resource = extractSingle(yield* fetchRaw(jwt, "/v1/profiles", {
21595
- method: "POST",
21596
- body: JSON.stringify({ data: {
21597
- type: "profiles",
21598
- attributes: {
21599
- name: params.profileName,
21600
- profileType: params.profileType
21601
- },
21602
- relationships
21603
- } })
21604
- }), toAscProfile);
21605
- if (resource === null) return yield* malformed("profile");
21606
- return resource;
21607
- }));
21608
- const isCertificateLimitError = (error) => {
21609
- if (error._tag !== "AscApiError") return false;
21610
- return /already have a current.*certificate|pending certificate request/iu.test(error.message);
21611
- };
21612
-
21613
- //#endregion
21614
- //#region src/lib/apple-asc-testflight.ts
21615
- /**
21616
- * App Store Connect TestFlight operations layered on the ASC API-key client
21617
- * ({@link ./apple-asc-client}). Used by the iOS submit flow to configure a build
21618
- * *after* `altool` uploads it: set the "What to Test" text and assign the build
21619
- * to internal beta groups — matching `eas submit`'s post-upload behaviour.
21620
- */
21621
- const toAscApp = (value) => {
21622
- if (!isRecord$1(value)) return null;
21623
- const { id, attributes } = value;
21624
- if (typeof id !== "string") return null;
21625
- const attrs = isRecord$1(attributes) ? attributes : {};
21626
- return {
21627
- id,
21628
- bundleId: typeof attrs["bundleId"] === "string" ? attrs["bundleId"] : null,
21629
- name: typeof attrs["name"] === "string" ? attrs["name"] : null
21630
- };
21631
- };
21632
- const toAscBuild = (value) => {
21633
- if (!isRecord$1(value)) return null;
21634
- const { id, attributes } = value;
21635
- if (typeof id !== "string") return null;
21636
- const attrs = isRecord$1(attributes) ? attributes : {};
21637
- return {
21638
- id,
21639
- version: typeof attrs["version"] === "string" ? attrs["version"] : null,
21640
- uploadedDate: typeof attrs["uploadedDate"] === "string" ? attrs["uploadedDate"] : null,
21641
- processingState: typeof attrs["processingState"] === "string" ? attrs["processingState"] : null
21642
- };
21643
- };
21644
- const toAscBetaGroup = (value) => {
21645
- if (!isRecord$1(value)) return null;
21646
- const { id, attributes } = value;
21647
- if (typeof id !== "string" || !isRecord$1(attributes)) return null;
21648
- const { name, isInternalGroup } = attributes;
21649
- if (typeof name !== "string") return null;
21650
- return {
21651
- id,
21652
- name,
21653
- isInternal: isInternalGroup === true
21654
- };
21655
- };
21656
- const toAscBetaBuildLocalization = (value) => {
21657
- if (!isRecord$1(value)) return null;
21658
- const { id, attributes } = value;
21659
- if (typeof id !== "string" || !isRecord$1(attributes)) return null;
21660
- const { locale, whatsNew } = attributes;
21661
- if (typeof locale !== "string") return null;
21662
- return {
21663
- id,
21664
- locale,
21665
- whatsNew: typeof whatsNew === "string" ? whatsNew : null
21666
- };
21667
- };
21366
+ var TestFlightConfigError = class extends Data.TaggedError("TestFlightConfigError") {};
21367
+ const DEFAULT_POLL_TIMEOUT_MS = 15 * 6e4;
21368
+ const DEFAULT_POLL_INTERVAL_MS = 3e4;
21369
+ const DEFAULT_LOCALE$1 = "en-US";
21370
+ /** Run an apple-utils call, mapping any failure to a coded {@link TestFlightConfigError}. */
21371
+ const call = (code, step, run) => wrapConnect(step, run).pipe(Effect.mapError((error) => new TestFlightConfigError({
21372
+ code,
21373
+ message: error.message
21374
+ })));
21668
21375
  /** Classify a raw `processingState`. Unknown/absent states stay `processing`
21669
21376
  * so the poller keeps waiting rather than failing early. */
21670
21377
  const classifyProcessingState = (state) => {
@@ -21673,10 +21380,10 @@ const classifyProcessingState = (state) => {
21673
21380
  return "processing";
21674
21381
  };
21675
21382
  /**
21676
- * Identify the build produced by *our* upload. `listRecentBuilds` returns builds
21677
- * newest-first; the freshly-uploaded build is the newest one whose id differs
21678
- * from the baseline captured before upload. Comparing ids (not timestamps) avoids
21679
- * both clock-skew misses and accidentally matching a pre-existing build.
21383
+ * Identify the build produced by *our* upload. `Build.getAsync` (sorted newest
21384
+ * first) returns builds newest-first; the freshly-uploaded build is the newest
21385
+ * one whose id differs from the baseline captured before upload. Comparing ids
21386
+ * (not timestamps) avoids both clock-skew misses and matching a pre-existing build.
21680
21387
  */
21681
21388
  const pickNewBuild = (builds, baselineLatestBuildId) => {
21682
21389
  const [newest] = builds;
@@ -21697,92 +21404,15 @@ const matchBetaGroupsByName = (groups, names) => {
21697
21404
  missing
21698
21405
  };
21699
21406
  };
21700
- /** Resolve the ASC app record for a bundle identifier, or null when none exists. */
21701
- const getAppByBundleId = (credentials, bundleId) => withJwt(credentials, (jwt) => Effect.gen(function* () {
21702
- const [first] = extractList(yield* fetchRaw(jwt, `/v1/apps?filter[bundleId]=${encodeURIComponent(bundleId)}&limit=1`), toAscApp);
21703
- return first === void 0 ? null : first;
21704
- }));
21705
- /** Builds for an app, newest upload first. */
21706
- const listRecentBuilds = (credentials, appId, limit = 20) => withJwt(credentials, (jwt) => Effect.gen(function* () {
21707
- return extractList(yield* fetchRaw(jwt, `/v1/builds?filter[app]=${encodeURIComponent(appId)}&sort=-uploadedDate&limit=${String(limit)}`), toAscBuild);
21708
- }));
21709
- const listBetaGroups = (credentials, appId) => withJwt(credentials, (jwt) => Effect.gen(function* () {
21710
- const groups = [];
21711
- let path = `/v1/betaGroups?filter[app]=${encodeURIComponent(appId)}&limit=200`;
21712
- while (path !== null) {
21713
- const body = yield* fetchRaw(jwt, path);
21714
- groups.push(...extractList(body, toAscBetaGroup));
21715
- path = nextPagePath(body);
21716
- }
21717
- return groups;
21718
- }));
21719
- const listBuildBetaLocalizations = (credentials, buildId) => withJwt(credentials, (jwt) => Effect.gen(function* () {
21720
- return extractList(yield* fetchRaw(jwt, `/v1/builds/${encodeURIComponent(buildId)}/betaBuildLocalizations?limit=200`), toAscBetaBuildLocalization);
21721
- }));
21722
- const createBetaBuildLocalization = (credentials, params) => withJwt(credentials, (jwt) => Effect.asVoid(fetchRaw(jwt, "/v1/betaBuildLocalizations", {
21723
- method: "POST",
21724
- body: JSON.stringify({ data: {
21725
- type: "betaBuildLocalizations",
21726
- attributes: {
21727
- locale: params.locale,
21728
- whatsNew: params.whatsNew
21729
- },
21730
- relationships: { build: { data: {
21731
- type: "builds",
21732
- id: params.buildId
21733
- } } }
21734
- } })
21735
- })));
21736
- const updateBetaBuildLocalization = (credentials, params) => withJwt(credentials, (jwt) => Effect.asVoid(fetchRaw(jwt, `/v1/betaBuildLocalizations/${encodeURIComponent(params.id)}`, {
21737
- method: "PATCH",
21738
- body: JSON.stringify({ data: {
21739
- type: "betaBuildLocalizations",
21740
- id: params.id,
21741
- attributes: { whatsNew: params.whatsNew }
21742
- } })
21743
- })));
21744
- const addBuildToBetaGroups = (credentials, buildId, groupIds) => withJwt(credentials, (jwt) => Effect.asVoid(fetchRaw(jwt, `/v1/builds/${encodeURIComponent(buildId)}/relationships/betaGroups`, {
21745
- method: "POST",
21746
- body: JSON.stringify({ data: groupIds.map((id) => ({
21747
- type: "betaGroups",
21748
- id
21749
- })) })
21750
- })));
21751
-
21752
- //#endregion
21753
- //#region src/application/ios-testflight-config.ts
21754
- /**
21755
- * Post-upload TestFlight configuration for iOS submissions. After `altool`
21756
- * uploads the `.ipa`, App Store Connect spends several minutes *processing* the
21757
- * binary before it can be configured. This module waits for that processing to
21758
- * finish, then sets the build's "What to Test" text and assigns it to internal
21759
- * TestFlight groups — the same follow-up `eas submit` performs server-side.
21760
- *
21761
- * Auth reuses the ASC **API key** already decrypted for the upload (no second
21762
- * credential prompt). Failures surface as {@link TestFlightConfigError} so the
21763
- * caller can mark the submission ERRORED with a precise reason.
21764
- */
21765
- var TestFlightConfigError = class extends Data.TaggedError("TestFlightConfigError") {};
21766
- const DEFAULT_POLL_TIMEOUT_MS = 15 * 6e4;
21767
- const DEFAULT_POLL_INTERVAL_MS = 3e4;
21768
- const DEFAULT_LOCALE = "en-US";
21769
- const ascErrorMessage$1 = (error) => {
21770
- if (error._tag === "AscApiError") return `App Store Connect API error ${String(error.status)}: ${error.message}`;
21771
- if (error._tag === "AscNetworkError") return `App Store Connect network error: ${String(error.cause)}`;
21772
- return `App Store Connect auth error: ${String(error.cause)}`;
21773
- };
21774
- const wrapAsc = (code) => (error) => new TestFlightConfigError({
21775
- code,
21776
- message: ascErrorMessage$1(error)
21777
- });
21778
21407
  /**
21779
21408
  * Resolve the ASC app id (preferring the explicit `ascAppId`) and snapshot the
21780
21409
  * latest existing build. Run this *before* `altool` so the freshly-uploaded
21781
21410
  * build can be distinguished from prior ones.
21782
21411
  */
21783
21412
  const captureTestFlightContext = (params) => Effect.gen(function* () {
21413
+ const ctx = buildTokenRequestContext(params.credentials);
21784
21414
  const appId = params.ascAppId ?? (yield* Effect.gen(function* () {
21785
- const app = yield* getAppByBundleId(params.credentials, params.bundleIdentifier).pipe(Effect.mapError(wrapAsc("TESTFLIGHT_APP_LOOKUP_FAILED")));
21415
+ const app = yield* call("TESTFLIGHT_APP_LOOKUP_FAILED", "apple-find-app", async () => AppleUtils.App.findAsync(ctx, { bundleId: params.bundleIdentifier }));
21786
21416
  if (app === null) return yield* new TestFlightConfigError({
21787
21417
  code: "TESTFLIGHT_APP_NOT_FOUND",
21788
21418
  message: `No App Store Connect app found for bundle id ${params.bundleIdentifier}. Set ascAppId in the eas.json submit profile.`
@@ -21791,7 +21421,11 @@ const captureTestFlightContext = (params) => Effect.gen(function* () {
21791
21421
  }));
21792
21422
  return {
21793
21423
  appId,
21794
- baselineLatestBuildId: toDbNull((yield* listRecentBuilds(params.credentials, appId, 1).pipe(Effect.mapError(wrapAsc("TESTFLIGHT_LIST_BUILDS_FAILED"))))[0]?.id)
21424
+ baselineLatestBuildId: toDbNull((yield* call("TESTFLIGHT_LIST_BUILDS_FAILED", "apple-list-builds", async () => AppleUtils.Build.getAsync(ctx, { query: {
21425
+ filter: { app: appId },
21426
+ sort: "-uploadedDate",
21427
+ limit: 1
21428
+ } })))[0]?.id)
21795
21429
  };
21796
21430
  });
21797
21431
  const pollForProcessedBuild = (params) => Effect.gen(function* () {
@@ -21803,12 +21437,16 @@ const pollForProcessedBuild = (params) => Effect.gen(function* () {
21803
21437
  while: (state) => state.build === null,
21804
21438
  body: (state) => Effect.gen(function* () {
21805
21439
  if (state.attempt > 0) yield* Effect.sleep(Duration.millis(params.pollIntervalMs));
21806
- const candidate = pickNewBuild(yield* listRecentBuilds(params.credentials, params.context.appId, 20).pipe(Effect.mapError(wrapAsc("TESTFLIGHT_LIST_BUILDS_FAILED"))), params.context.baselineLatestBuildId);
21440
+ const candidate = pickNewBuild(yield* call("TESTFLIGHT_LIST_BUILDS_FAILED", "apple-list-builds", async () => AppleUtils.Build.getAsync(params.ctx, { query: {
21441
+ filter: { app: params.context.appId },
21442
+ sort: "-uploadedDate",
21443
+ limit: 20
21444
+ } })), params.context.baselineLatestBuildId);
21807
21445
  if (candidate !== null) {
21808
- const processing = classifyProcessingState(candidate.processingState);
21446
+ const processing = classifyProcessingState(candidate.attributes.processingState);
21809
21447
  if (processing === "failed") return yield* new TestFlightConfigError({
21810
21448
  code: "TESTFLIGHT_BUILD_PROCESSING_FAILED",
21811
- message: `App Store Connect rejected build ${candidate.version ?? candidate.id} during processing (state ${candidate.processingState ?? "unknown"}).`
21449
+ message: `App Store Connect rejected build ${candidate.attributes.version} during processing (state ${candidate.attributes.processingState}).`
21812
21450
  });
21813
21451
  if (processing === "valid") return {
21814
21452
  build: candidate,
@@ -21833,59 +21471,65 @@ const pollForProcessedBuild = (params) => Effect.gen(function* () {
21833
21471
  return final.build;
21834
21472
  });
21835
21473
  const applyWhatToTest = (params) => Effect.gen(function* () {
21836
- const existing = (yield* listBuildBetaLocalizations(params.credentials, params.buildId).pipe(Effect.mapError(wrapAsc("TESTFLIGHT_LIST_LOCALIZATIONS_FAILED")))).find((loc) => loc.locale === params.locale);
21837
- yield* (existing === void 0 ? createBetaBuildLocalization(params.credentials, {
21838
- buildId: params.buildId,
21839
- locale: params.locale,
21840
- whatsNew: params.whatToTest
21841
- }) : updateBetaBuildLocalization(params.credentials, {
21842
- id: existing.id,
21843
- whatsNew: params.whatToTest
21844
- })).pipe(Effect.mapError(wrapAsc("TESTFLIGHT_SET_WHAT_TO_TEST_FAILED")));
21474
+ const existing = (yield* call("TESTFLIGHT_LIST_LOCALIZATIONS_FAILED", "apple-list-localizations", async () => params.build.getBetaBuildLocalizationsAsync())).find((loc) => loc.attributes.locale === params.locale);
21475
+ yield* call("TESTFLIGHT_SET_WHAT_TO_TEST_FAILED", "apple-set-what-to-test", async () => {
21476
+ if (existing === void 0) {
21477
+ await (await AppleUtils.BetaBuildLocalization.createAsync(params.ctx, {
21478
+ id: params.build.id,
21479
+ locale: params.locale
21480
+ })).updateAsync({ whatsNew: params.whatToTest });
21481
+ return;
21482
+ }
21483
+ await existing.updateAsync({ whatsNew: params.whatToTest });
21484
+ });
21845
21485
  });
21846
21486
  const applyGroups = (params) => Effect.gen(function* () {
21847
- const allGroups = yield* listBetaGroups(params.credentials, params.appId).pipe(Effect.mapError(wrapAsc("TESTFLIGHT_LIST_GROUPS_FAILED")));
21848
- const { matched, missing } = matchBetaGroupsByName(allGroups, params.groups);
21487
+ const named = (yield* call("TESTFLIGHT_LIST_GROUPS_FAILED", "apple-list-groups", async () => AppleUtils.BetaGroup.getAsync(params.ctx, { query: { filter: { app: params.appId } } }))).map((group) => ({
21488
+ id: group.id,
21489
+ name: group.attributes.name
21490
+ }));
21491
+ const { matched, missing } = matchBetaGroupsByName(named, params.groups);
21849
21492
  if (missing.length > 0) {
21850
- const available = allGroups.map((group) => group.name).join(", ") || "(none)";
21493
+ const available = named.map((group) => group.name).join(", ") || "(none)";
21851
21494
  return yield* new TestFlightConfigError({
21852
21495
  code: "TESTFLIGHT_GROUP_NOT_FOUND",
21853
21496
  message: `TestFlight group(s) not found: ${missing.join(", ")}. Available groups: ${available}.`
21854
21497
  });
21855
21498
  }
21856
- yield* addBuildToBetaGroups(params.credentials, params.buildId, matched.map((group) => group.id)).pipe(Effect.mapError(wrapAsc("TESTFLIGHT_ADD_TO_GROUPS_FAILED")));
21499
+ yield* call("TESTFLIGHT_ADD_TO_GROUPS_FAILED", "apple-add-to-groups", async () => params.build.addBetaGroupsAsync({ betaGroups: matched.map((group) => group.id) }));
21857
21500
  });
21858
21501
  /** Whether a profile has any TestFlight config that warrants the processing wait. */
21859
21502
  const needsTestFlightConfig = (params) => params.whatToTest !== void 0 || params.groups.length > 0;
21860
21503
  const applyTestFlightConfig = (inputs) => Effect.gen(function* () {
21504
+ const ctx = buildTokenRequestContext(inputs.credentials);
21861
21505
  yield* printHuman("Configuring TestFlight (waiting for build processing)...");
21862
21506
  const build = yield* pollForProcessedBuild({
21863
- credentials: inputs.credentials,
21507
+ ctx,
21864
21508
  context: inputs.context,
21865
21509
  pollTimeoutMs: inputs.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS,
21866
21510
  pollIntervalMs: inputs.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS
21867
21511
  });
21868
21512
  if (inputs.whatToTest !== void 0) {
21869
21513
  yield* applyWhatToTest({
21870
- credentials: inputs.credentials,
21871
- buildId: build.id,
21872
- locale: inputs.language ?? DEFAULT_LOCALE,
21514
+ ctx,
21515
+ build,
21516
+ locale: inputs.language ?? DEFAULT_LOCALE$1,
21873
21517
  whatToTest: inputs.whatToTest
21874
21518
  });
21875
- yield* printHuman(`Set "What to Test" on build ${build.version ?? build.id}.`);
21519
+ yield* printHuman(`Set "What to Test" on build ${build.attributes.version}.`);
21876
21520
  }
21877
21521
  if (inputs.groups.length > 0) {
21878
21522
  yield* applyGroups({
21879
- credentials: inputs.credentials,
21523
+ ctx,
21880
21524
  appId: inputs.context.appId,
21881
- buildId: build.id,
21525
+ build,
21882
21526
  groups: inputs.groups
21883
21527
  });
21884
21528
  yield* printHuman(`Assigned build to TestFlight group(s): ${inputs.groups.join(", ")}.`);
21885
21529
  }
21886
21530
  return {
21887
21531
  buildId: build.id,
21888
- buildVersion: build.version
21532
+ buildVersion: build.attributes.version
21889
21533
  };
21890
21534
  });
21891
21535
 
@@ -22034,10 +21678,21 @@ const resolveIosUploadAuth = (params) => {
22034
21678
  };
22035
21679
  return null;
22036
21680
  };
22037
- const resolveAscCredentials = (api, ascApiKeyId) => fetchAscCredentials(api, ascApiKeyId).pipe(Effect.mapError(() => new CliSubmitError({
22038
- code: "SUBMISSION_ASC_KEY_FETCH_FAILED",
22039
- message: `Failed to fetch or decrypt ASC API key ${ascApiKeyId}`
22040
- })));
21681
+ /**
21682
+ * Decrypt the ASC `.p8` once for a submit: needed for an asc-api-key upload, and
21683
+ * for post-upload TestFlight config regardless of upload auth. Returns null when
21684
+ * none is required or available; a decrypt failure logs a note and degrades to
21685
+ * null so the caller can queue-and-instruct rather than crash.
21686
+ */
21687
+ const resolveAscUploadCredentials = (params) => Effect.gen(function* () {
21688
+ const credsKeyId = params.auth.kind === "asc-api-key" ? params.auth.ascApiKeyId : params.ascApiKeyId;
21689
+ if (!(params.auth.kind === "asc-api-key" || params.wantsConfig) || credsKeyId === void 0) return null;
21690
+ return yield* fetchAscCredentials(params.api, credsKeyId).pipe(Effect.map((creds) => ({
21691
+ keyId: creds.keyId,
21692
+ issuerId: creds.issuerId,
21693
+ p8Pem: creds.p8Pem
21694
+ })), Effect.catchAll((error) => printHuman(`Could not prepare ASC API key ${credsKeyId} (${messageOf(error)}).`).pipe(Effect.as(null))));
21695
+ });
22041
21696
  /** `altool` reads the API key from `--apiKeyDir`; write the decrypted `.p8` there. */
22042
21697
  const writeP8ForAltool = (credentials) => Effect.gen(function* () {
22043
21698
  const target = path.join(tmpdir(), `better-update-submit-AuthKey_${credentials.keyId}.p8`);
@@ -22083,8 +21738,7 @@ const runIosSubmit = (inputs) => Effect.gen(function* () {
22083
21738
  whatToTest: inputs.config.whatToTest,
22084
21739
  groups: inputs.config.groups
22085
21740
  });
22086
- const credsKeyId = inputs.auth.kind === "asc-api-key" ? inputs.auth.ascApiKeyId : inputs.ascApiKeyId;
22087
- const ascCredentials = (inputs.auth.kind === "asc-api-key" || wantsConfig) && credsKeyId !== void 0 ? yield* resolveAscCredentials(inputs.api, credsKeyId) : null;
21741
+ const { ascCredentials } = inputs;
22088
21742
  const ipaPath = yield* resolveLocalArchivePath(inputs.archive, ".ipa");
22089
21743
  let tfContext = null;
22090
21744
  if (wantsConfig && ascCredentials !== null) tfContext = yield* captureTestFlightContext({
@@ -22325,24 +21979,38 @@ const runAutoSubmit = (input) => Effect.gen(function* () {
22325
21979
  });
22326
21980
  if (auth === null) yield* printHuman("Skipping iOS upload: configure ascApiKeyId or set EXPO_APPLE_APP_SPECIFIC_PASSWORD (+ appleId).");
22327
21981
  else {
22328
- yield* printHuman(auth.kind === "app-specific-password" ? "Running xcrun altool upload (Apple ID app-specific password)..." : "Running xcrun altool upload (ASC API key)...");
22329
- yield* runIosSubmit({
21982
+ const groups = easProfile.ios?.groups ?? [];
21983
+ const wantsConfig = needsTestFlightConfig({
21984
+ whatToTest: input.whatToTest,
21985
+ groups
21986
+ });
21987
+ const ascCredentials = yield* resolveAscUploadCredentials({
22330
21988
  api: input.api,
22331
- submissionId: submission.id,
22332
- archive: {
22333
- source: "build",
22334
- value: archiveUrl
22335
- },
22336
21989
  auth,
22337
21990
  ascApiKeyId: easProfile.ios?.ascApiKeyId,
22338
- config: {
22339
- bundleIdentifier: iosConfig.bundleIdentifier,
22340
- ascAppId: easProfile.ios?.ascAppId,
22341
- language: easProfile.ios?.language,
22342
- whatToTest: input.whatToTest,
22343
- groups: easProfile.ios?.groups ?? []
22344
- }
21991
+ wantsConfig
22345
21992
  });
21993
+ if (auth.kind === "asc-api-key" && ascCredentials === null) yield* printHuman("Skipping iOS upload: the ASC API key could not be prepared for upload.");
21994
+ else {
21995
+ yield* printHuman(auth.kind === "app-specific-password" ? "Running xcrun altool upload (Apple ID app-specific password)..." : "Running xcrun altool upload (ASC API key)...");
21996
+ yield* runIosSubmit({
21997
+ api: input.api,
21998
+ submissionId: submission.id,
21999
+ archive: {
22000
+ source: "build",
22001
+ value: archiveUrl
22002
+ },
22003
+ auth,
22004
+ ascCredentials,
22005
+ config: {
22006
+ bundleIdentifier: iosConfig.bundleIdentifier,
22007
+ ascAppId: easProfile.ios?.ascAppId,
22008
+ language: easProfile.ios?.language,
22009
+ whatToTest: input.whatToTest,
22010
+ groups
22011
+ }
22012
+ });
22013
+ }
22346
22014
  }
22347
22015
  }
22348
22016
  if (input.platform === "android" && androidConfig !== void 0 && easProfile.android !== void 0) {
@@ -22472,60 +22140,6 @@ const findAndroidArtifact = ({ projectRoot, format, flavor, buildType, minMtimeM
22472
22140
  return pickedFallback.path;
22473
22141
  });
22474
22142
 
22475
- //#endregion
22476
- //#region src/lib/android-keystore.ts
22477
- const DEFAULT_KEYSTORE_VALIDITY_DAYS = 1e4;
22478
- const renderDistinguishedName = (params) => `CN=${params.commonName}, O=${params.organization}`;
22479
- const FINGERPRINT_PATTERNS = {
22480
- md5: /MD5:\s*(?<value>[0-9A-F:]+)/iu,
22481
- sha1: /SHA-?1:\s*(?<value>[0-9A-F:]+)/iu,
22482
- sha256: /SHA-?256:\s*(?<value>[0-9A-F:]+)/iu
22483
- };
22484
- /**
22485
- * Parse certificate fingerprints out of `keytool -list -v` output. The fingerprint
22486
- * labels (`MD5:`, `SHA1:`, `SHA256:`) are stable across keytool locales — only the
22487
- * surrounding prose is translated — so label-anchored regexes are robust. MD5 is
22488
- * absent on modern JDKs (dropped from `-v` output); that field stays `undefined`.
22489
- * keytool already emits the canonical uppercase, colon-separated form the dashboard
22490
- * displays verbatim, so no normalization is needed.
22491
- */
22492
- const parseKeystoreFingerprints = (output) => ({
22493
- md5: output.match(FINGERPRINT_PATTERNS.md5)?.groups?.["value"],
22494
- sha1: output.match(FINGERPRINT_PATTERNS.sha1)?.groups?.["value"],
22495
- sha256: output.match(FINGERPRINT_PATTERNS.sha256)?.groups?.["value"]
22496
- });
22497
- /**
22498
- * Run `keytool -list -v` against an on-disk keystore and extract its certificate
22499
- * fingerprints. Only the store password is required to read a certificate. Used at
22500
- * upload/generate time to populate the public, server-visible fingerprint metadata
22501
- * the dashboard renders.
22502
- */
22503
- const extractKeystoreFingerprints = (params) => Command.string(Command.make("keytool", "-list", "-v", "-keystore", params.keystorePath, "-alias", params.keyAlias, "-storepass", params.storePassword).pipe(Command.env({ LC_ALL: "C" }))).pipe(Effect.mapError((cause) => new BuildFailedError({
22504
- step: "extract keystore fingerprints",
22505
- exitCode: 1,
22506
- message: `keytool -list failed to run (is the JDK installed?): ${String(cause)}`
22507
- })), Effect.flatMap((output) => {
22508
- const fingerprints = parseKeystoreFingerprints(output);
22509
- if (fingerprints.sha1 === void 0 && fingerprints.sha256 === void 0) return Effect.fail(new BuildFailedError({
22510
- step: "extract keystore fingerprints",
22511
- exitCode: 1,
22512
- message: "keytool produced no SHA-1/SHA-256 fingerprints — verify the key alias and keystore password"
22513
- }));
22514
- return Effect.succeed(fingerprints);
22515
- }));
22516
- const generateAndroidKeystore = (input) => Command.exitCode(Command.make("keytool", "-genkeypair", "-v", "-storetype", "JKS", "-keystore", input.outputPath, "-alias", input.keyAlias, "-keyalg", "RSA", "-keysize", "2048", "-validity", String(input.validityDays ?? DEFAULT_KEYSTORE_VALIDITY_DAYS), "-storepass", input.storePassword, "-keypass", input.keyPassword, "-dname", renderDistinguishedName({
22517
- commonName: input.commonName,
22518
- organization: input.organization
22519
- }), "-noprompt").pipe(Command.stdout("inherit"), Command.stderr("inherit"))).pipe(Effect.mapError((cause) => new BuildFailedError({
22520
- step: "generate android keystore",
22521
- exitCode: 1,
22522
- message: `generate android keystore failed to spawn: ${String(cause)}`
22523
- })), Effect.flatMap((code) => code === 0 ? Effect.void : Effect.fail(new BuildFailedError({
22524
- step: "generate android keystore",
22525
- exitCode: code,
22526
- message: `generate android keystore exited with code ${code}`
22527
- }))));
22528
-
22529
22143
  //#endregion
22530
22144
  //#region src/lib/apple-cert-to-p12.ts
22531
22145
  var CertParseError = class extends Data.TaggedError("CertParseError") {};
@@ -22547,11 +22161,6 @@ const extractTeamId$1 = (cert) => {
22547
22161
  if (cn === null) return null;
22548
22162
  return matchTeamFromCommonName(cn);
22549
22163
  };
22550
- const parseCert = (certDerBytes) => {
22551
- const asn1 = forge.asn1.fromDer(certDerBytes);
22552
- return forge.pki.certificateFromAsn1(asn1);
22553
- };
22554
- const generatePassword = () => forge.util.encode64(forge.random.getBytesSync(16));
22555
22164
  /**
22556
22165
  * Normalize an Apple certificate serial number for comparison.
22557
22166
  *
@@ -22595,84 +22204,69 @@ const extractMetadataFromP12 = (params) => Effect.gen(function* () {
22595
22204
  if (first?.cert === void 0) return yield* new CertParseError({ message: "PKCS#12 bundle does not contain a certificate" });
22596
22205
  return yield* extractCertMetadata(first.cert);
22597
22206
  });
22598
- const buildDistributionCertP12 = (params) => Effect.gen(function* () {
22599
- const result = yield* Effect.try({
22600
- try: () => {
22601
- const cert = parseCert(forge.util.decode64(params.certificateContentBase64));
22602
- const password = generatePassword();
22603
- const p12Asn1 = forge.pkcs12.toPkcs12Asn1(params.privateKey, [cert], password, {
22604
- friendlyName: "key",
22605
- algorithm: "3des"
22606
- });
22607
- return {
22608
- cert,
22609
- p12Base64: forge.util.encode64(forge.asn1.toDer(p12Asn1).getBytes()),
22610
- password
22611
- };
22612
- },
22613
- catch: (error) => new CertParseError({ message: `Failed to assemble .p12: ${error instanceof Error ? error.message : String(error)}` })
22614
- });
22615
- const metadata = yield* extractCertMetadata(result.cert);
22616
- return {
22617
- p12Base64: result.p12Base64,
22618
- password: result.password,
22619
- metadata
22620
- };
22621
- });
22622
22207
 
22623
22208
  //#endregion
22624
- //#region src/lib/apple-csr.ts
22625
- const generateRsaKeyPair = async () => new Promise((resolve) => {
22626
- forge.pki.rsa.generateKeyPair({
22627
- bits: 2048,
22628
- workers: 2
22629
- }, (_err, keyPair) => {
22630
- resolve(keyPair);
22631
- });
22632
- });
22633
- const generateCertificateSigningRequest = async () => {
22634
- const keyPair = await generateRsaKeyPair();
22635
- const csr = forge.pki.createCertificationRequest();
22636
- csr.publicKey = keyPair.publicKey;
22637
- csr.setSubject([{
22638
- name: "commonName",
22639
- shortName: "CN",
22640
- value: "PEM"
22641
- }]);
22642
- csr.sign(keyPair.privateKey, forge.md.sha1.create());
22643
- return {
22644
- csrPem: forge.pki.certificationRequestToPem(csr),
22645
- privateKeyPem: forge.pki.privateKeyToPem(keyPair.privateKey),
22646
- privateKey: keyPair.privateKey
22647
- };
22209
+ //#region src/lib/android-keystore.ts
22210
+ const DEFAULT_KEYSTORE_VALIDITY_DAYS = 1e4;
22211
+ const renderDistinguishedName = (params) => `CN=${params.commonName}, O=${params.organization}`;
22212
+ const FINGERPRINT_PATTERNS = {
22213
+ md5: /MD5:\s*(?<value>[0-9A-F:]+)/iu,
22214
+ sha1: /SHA-?1:\s*(?<value>[0-9A-F:]+)/iu,
22215
+ sha256: /SHA-?256:\s*(?<value>[0-9A-F:]+)/iu
22648
22216
  };
22217
+ /**
22218
+ * Parse certificate fingerprints out of `keytool -list -v` output. The fingerprint
22219
+ * labels (`MD5:`, `SHA1:`, `SHA256:`) are stable across keytool locales — only the
22220
+ * surrounding prose is translated — so label-anchored regexes are robust. MD5 is
22221
+ * absent on modern JDKs (dropped from `-v` output); that field stays `undefined`.
22222
+ * keytool already emits the canonical uppercase, colon-separated form the dashboard
22223
+ * displays verbatim, so no normalization is needed.
22224
+ */
22225
+ const parseKeystoreFingerprints = (output) => ({
22226
+ md5: output.match(FINGERPRINT_PATTERNS.md5)?.groups?.["value"],
22227
+ sha1: output.match(FINGERPRINT_PATTERNS.sha1)?.groups?.["value"],
22228
+ sha256: output.match(FINGERPRINT_PATTERNS.sha256)?.groups?.["value"]
22229
+ });
22230
+ /**
22231
+ * Run `keytool -list -v` against an on-disk keystore and extract its certificate
22232
+ * fingerprints. Only the store password is required to read a certificate. Used at
22233
+ * upload/generate time to populate the public, server-visible fingerprint metadata
22234
+ * the dashboard renders.
22235
+ */
22236
+ const extractKeystoreFingerprints = (params) => Command.string(Command.make("keytool", "-list", "-v", "-keystore", params.keystorePath, "-alias", params.keyAlias, "-storepass", params.storePassword).pipe(Command.env({ LC_ALL: "C" }))).pipe(Effect.mapError((cause) => new BuildFailedError({
22237
+ step: "extract keystore fingerprints",
22238
+ exitCode: 1,
22239
+ message: `keytool -list failed to run (is the JDK installed?): ${String(cause)}`
22240
+ })), Effect.flatMap((output) => {
22241
+ const fingerprints = parseKeystoreFingerprints(output);
22242
+ if (fingerprints.sha1 === void 0 && fingerprints.sha256 === void 0) return Effect.fail(new BuildFailedError({
22243
+ step: "extract keystore fingerprints",
22244
+ exitCode: 1,
22245
+ message: "keytool produced no SHA-1/SHA-256 fingerprints — verify the key alias and keystore password"
22246
+ }));
22247
+ return Effect.succeed(fingerprints);
22248
+ }));
22249
+ const generateAndroidKeystore = (input) => Command.exitCode(Command.make("keytool", "-genkeypair", "-v", "-storetype", "JKS", "-keystore", input.outputPath, "-alias", input.keyAlias, "-keyalg", "RSA", "-keysize", "2048", "-validity", String(input.validityDays ?? DEFAULT_KEYSTORE_VALIDITY_DAYS), "-storepass", input.storePassword, "-keypass", input.keyPassword, "-dname", renderDistinguishedName({
22250
+ commonName: input.commonName,
22251
+ organization: input.organization
22252
+ }), "-noprompt").pipe(Command.stdout("inherit"), Command.stderr("inherit"))).pipe(Effect.mapError((cause) => new BuildFailedError({
22253
+ step: "generate android keystore",
22254
+ exitCode: 1,
22255
+ message: `generate android keystore failed to spawn: ${String(cause)}`
22256
+ })), Effect.flatMap((code) => code === 0 ? Effect.void : Effect.fail(new BuildFailedError({
22257
+ step: "generate android keystore",
22258
+ exitCode: code,
22259
+ message: `generate android keystore exited with code ${code}`
22260
+ }))));
22649
22261
 
22650
22262
  //#endregion
22651
22263
  //#region src/lib/credentials-generator.ts
22652
- const DISTRIBUTION_TO_PROFILE_TYPE$1 = {
22653
- APP_STORE: "IOS_APP_STORE",
22654
- AD_HOC: "IOS_APP_ADHOC",
22655
- DEVELOPMENT: "IOS_APP_DEVELOPMENT",
22656
- ENTERPRISE: "IOS_APP_INHOUSE"
22657
- };
22264
+ /** Stable hash of an Apple device-id roster, used to detect profile drift. */
22658
22265
  const computeDeviceRosterHashHex = (ascDeviceIds) => {
22659
22266
  const sorted = [...ascDeviceIds].toSorted();
22660
22267
  return createHash("sha256").update(sorted.join(","), "utf8").digest("hex");
22661
22268
  };
22662
22269
  var CertificateLimitError = class extends Data.TaggedError("CertificateLimitError") {};
22663
- var GenerateFailedError = class extends Data.TaggedError("GenerateFailedError") {};
22664
- const messageForAscCause = (cause) => {
22665
- if (cause._tag === "AscApiError") return cause.message;
22666
- if (cause._tag === "AppleAuthError") return "Apple JWT signing failed";
22667
- return "Network error talking to Apple";
22668
- };
22669
- const wrapAscError = (step) => (cause) => {
22670
- if (cause._tag === "AscApiError" && isCertificateLimitError(cause)) return new CertificateLimitError({ message: cause.message });
22671
- return new GenerateFailedError({
22672
- step,
22673
- message: messageForAscCause(cause)
22674
- });
22675
- };
22676
22270
  const generateAndUploadKeystore = (api, input) => Effect.scoped(Effect.gen(function* () {
22677
22271
  const fs = yield* FileSystem.FileSystem;
22678
22272
  const tempDir = yield* acquireBuildTempDir;
@@ -22720,94 +22314,129 @@ const generateAndUploadKeystore = (api, input) => Effect.scoped(Effect.gen(funct
22720
22314
  keyAlias: created.keyAlias
22721
22315
  };
22722
22316
  }));
22317
+
22318
+ //#endregion
22319
+ //#region src/lib/credentials-generator-apple.ts
22320
+ /**
22321
+ * iOS signing-credential generation on `@expo/apple-utils`, parameterized by the
22322
+ * App Store Connect `RequestContext`: a headless JWT `Token` context (built from a
22323
+ * vault `.p8` via {@link buildTokenRequestContext}) or an interactive Apple ID
22324
+ * cookie session (`AppleAuth.buildRequestContext`). Both drive the same entity
22325
+ * managers — apple-utils routes to the public ASC API or the developer portal by
22326
+ * which context is supplied. This is the single home for cert/bundle-id/device/
22327
+ * profile generation; the JWT REST client it replaced is gone.
22328
+ */
22329
+ const DISTRIBUTION_TO_PROFILE_TYPE = {
22330
+ APP_STORE: AppleUtils.ProfileType.IOS_APP_STORE,
22331
+ AD_HOC: AppleUtils.ProfileType.IOS_APP_ADHOC,
22332
+ DEVELOPMENT: AppleUtils.ProfileType.IOS_APP_DEVELOPMENT,
22333
+ ENTERPRISE: AppleUtils.ProfileType.IOS_APP_INHOUSE
22334
+ };
22335
+ const DISTRIBUTION_TO_CERTIFICATE_TYPE = {
22336
+ APP_STORE: AppleUtils.CertificateType.IOS_DISTRIBUTION,
22337
+ AD_HOC: AppleUtils.CertificateType.IOS_DISTRIBUTION,
22338
+ ENTERPRISE: AppleUtils.CertificateType.IOS_DISTRIBUTION,
22339
+ DEVELOPMENT: AppleUtils.CertificateType.IOS_DEVELOPMENT
22340
+ };
22341
+ var AppleIdGenerateFailedError = class extends Data.TaggedError("AppleIdGenerateFailedError") {};
22342
+ const wrap = (step, run) => Effect.tryPromise({
22343
+ try: run,
22344
+ catch: (cause) => new AppleIdGenerateFailedError({
22345
+ step,
22346
+ message: messageOf(cause)
22347
+ })
22348
+ });
22349
+ /**
22350
+ * Build a headless ASC `RequestContext` by decrypting a stored ASC `.p8` key.
22351
+ * Used by the non-interactive (build/manager) callers that hold an `ascApiKeyId`
22352
+ * rather than an Apple ID cookie session.
22353
+ */
22354
+ const ascKeyRequestContext = (api, ascApiKeyId) => fetchAscCredentials(api, ascApiKeyId).pipe(Effect.map(buildTokenRequestContext));
22355
+ const wrapCertificateCreate = (run) => Effect.tryPromise({
22356
+ try: run,
22357
+ catch: (cause) => {
22358
+ const message = messageOf(cause);
22359
+ if (isCertificateLimitMessage(message)) return new CertificateLimitError({ message });
22360
+ return new AppleIdGenerateFailedError({
22361
+ step: "apple-create-certificate",
22362
+ message
22363
+ });
22364
+ }
22365
+ });
22366
+ const certificateTypeOf = (certificateType) => certificateType === "IOS_DEVELOPMENT" ? AppleUtils.CertificateType.IOS_DEVELOPMENT : AppleUtils.CertificateType.IOS_DISTRIBUTION;
22723
22367
  const generateAndUploadDistributionCertificate = (api, input) => Effect.gen(function* () {
22724
- const creds = yield* fetchAscCredentials(api, input.ascApiKeyId);
22725
- const ascCreds = {
22726
- keyId: creds.keyId,
22727
- issuerId: creds.issuerId,
22728
- p8Pem: creds.p8Pem
22729
- };
22730
- const csrResult = yield* Effect.tryPromise({
22731
- try: generateCertificateSigningRequest,
22732
- catch: (cause) => new GenerateFailedError({
22733
- step: "csr",
22734
- message: `CSR generation failed: ${cause instanceof Error ? cause.message : String(cause)}`
22735
- })
22736
- });
22737
- const certificateType = input.certificateType ?? "IOS_DISTRIBUTION";
22738
- const apple = yield* createCertificate(ascCreds, {
22739
- csrPem: csrResult.csrPem,
22740
- certificateType
22741
- }).pipe(Effect.mapError(wrapAscError("apple-create-certificate")));
22742
- if (apple.certificateContent === null) return yield* new GenerateFailedError({
22743
- step: "apple-create-certificate",
22744
- message: "Apple response missing certificateContent"
22745
- });
22746
- const bundle = yield* buildDistributionCertP12({
22747
- certificateContentBase64: apple.certificateContent,
22748
- privateKey: csrResult.privateKey
22749
- }).pipe(Effect.mapError((cause) => new GenerateFailedError({
22750
- step: "p12-build",
22368
+ const ctx = input.context;
22369
+ const result = yield* wrapCertificateCreate(async () => AppleUtils.createCertificateAndP12Async(ctx, { certificateType: certificateTypeOf(input.certificateType) }));
22370
+ const metadata = yield* extractMetadataFromP12({
22371
+ p12Base64: result.certificateP12,
22372
+ password: result.password
22373
+ }).pipe(Effect.mapError((cause) => new AppleIdGenerateFailedError({
22374
+ step: "parse-p12",
22751
22375
  message: cause.message
22752
22376
  })));
22753
22377
  const session = yield* openVaultSessionInteractive(api);
22754
- const metadata = {
22755
- serialNumber: bundle.metadata.serialNumber,
22756
- appleTeamIdentifier: bundle.metadata.appleTeamId,
22757
- validFrom: bundle.metadata.validFrom,
22758
- validUntil: bundle.metadata.validUntil
22378
+ const envelopeMetadata = {
22379
+ serialNumber: metadata.serialNumber,
22380
+ appleTeamIdentifier: metadata.appleTeamId,
22381
+ validFrom: metadata.validFrom,
22382
+ validUntil: metadata.validUntil
22759
22383
  };
22760
22384
  const envelope = yield* sealForUpload({
22761
22385
  session,
22762
22386
  credentialType: "distribution-certificate",
22763
- metadata,
22387
+ metadata: envelopeMetadata,
22764
22388
  secret: {
22765
- p12Base64: bundle.p12Base64,
22766
- p12Password: bundle.password
22389
+ p12Base64: result.certificateP12,
22390
+ p12Password: result.password
22767
22391
  }
22768
- });
22392
+ }).pipe(Effect.mapError((cause) => new AppleIdGenerateFailedError({
22393
+ step: "encrypt-p12",
22394
+ message: cause.message
22395
+ })));
22769
22396
  const created = yield* api.appleDistributionCertificates.upload({ payload: {
22770
22397
  ...toUploadEnvelope(envelope),
22771
- ...metadata,
22398
+ ...envelopeMetadata,
22772
22399
  ...compact({
22773
- appleTeamName: toOptional(bundle.metadata.appleTeamName),
22774
- developerIdIdentifier: toOptional(bundle.metadata.developerIdIdentifier)
22400
+ appleTeamName: toOptional(metadata.appleTeamName),
22401
+ developerIdIdentifier: toOptional(metadata.developerIdIdentifier)
22775
22402
  })
22776
22403
  } });
22777
22404
  return {
22778
22405
  id: created.id,
22779
- serialNumber: bundle.metadata.serialNumber,
22406
+ serialNumber: metadata.serialNumber,
22780
22407
  appleTeamId: created.appleTeamId,
22781
- appleTeamIdentifier: bundle.metadata.appleTeamId,
22782
- developerPortalIdentifier: apple.id
22408
+ appleTeamIdentifier: metadata.appleTeamId,
22409
+ developerPortalIdentifier: result.certificate.id
22783
22410
  };
22784
22411
  });
22785
- const revokeAppleCertificate = (api, input) => Effect.gen(function* () {
22786
- const creds = yield* fetchAscCredentials(api, input.ascApiKeyId);
22787
- yield* deleteCertificate({
22788
- keyId: creds.keyId,
22789
- issuerId: creds.issuerId,
22790
- p8Pem: creds.p8Pem
22791
- }, input.developerPortalIdentifier).pipe(Effect.mapError(wrapAscError("apple-revoke-certificate")));
22412
+ const listDistributionCerts = (ctx, certificateType = "IOS_DISTRIBUTION") => Effect.gen(function* () {
22413
+ return (yield* wrap("apple-list-certificates", async () => AppleUtils.Certificate.getAsync(ctx, { query: { filter: { certificateType: certificateTypeOf(certificateType) } } }))).map((entry) => ({
22414
+ developerPortalIdentifier: entry.id,
22415
+ serialNumber: entry.attributes.serialNumber,
22416
+ displayName: entry.attributes.displayName,
22417
+ certificateType: entry.attributes.certificateType,
22418
+ expirationDate: entry.attributes.expirationDate
22419
+ }));
22792
22420
  });
22421
+ const revokeDistributionCert = (ctx, developerPortalIdentifier) => wrap("apple-revoke-certificate", async () => AppleUtils.Certificate.deleteAsync(ctx, { id: developerPortalIdentifier }));
22422
+ /**
22423
+ * Revoke the distribution certificate behind a stored row: match it on Apple by
22424
+ * serial (across distribution + development), delete it there, and optionally
22425
+ * delete the local row. Builds a headless Token context from the ASC key.
22426
+ */
22793
22427
  const revokeLocalDistributionCertificate = (api, input) => Effect.gen(function* () {
22794
22428
  const local = (yield* api.appleDistributionCertificates.list()).items.find((entry) => entry.id === input.distributionCertificateId);
22795
- if (local === void 0) return yield* new GenerateFailedError({
22429
+ if (local === void 0) return yield* new AppleIdGenerateFailedError({
22796
22430
  step: "load-distribution-certificate",
22797
22431
  message: `Distribution certificate ${input.distributionCertificateId} not found on this account`
22798
22432
  });
22799
- const creds = yield* fetchAscCredentials(api, input.ascApiKeyId);
22800
- const ascCreds = {
22801
- keyId: creds.keyId,
22802
- issuerId: creds.issuerId,
22803
- p8Pem: creds.p8Pem
22804
- };
22433
+ const ctx = buildTokenRequestContext(yield* fetchAscCredentials(api, input.ascApiKeyId));
22805
22434
  const targetSerial = normalizeAppleSerial(local.serialNumber);
22806
- const matching = yield* Effect.all([listCertificates(ascCreds, { certificateType: "IOS_DISTRIBUTION" }), listCertificates(ascCreds, { certificateType: "IOS_DEVELOPMENT" })], { concurrency: 2 }).pipe(Effect.mapError(wrapAscError("apple-list-certificates")));
22807
- const ascMatch = [...matching[0], ...matching[1]].find((entry) => normalizeAppleSerial(entry.serialNumber) === targetSerial);
22435
+ const matching = yield* Effect.all([wrap("apple-list-certificates", async () => AppleUtils.Certificate.getAsync(ctx, { query: { filter: { certificateType: AppleUtils.CertificateType.IOS_DISTRIBUTION } } })), wrap("apple-list-certificates", async () => AppleUtils.Certificate.getAsync(ctx, { query: { filter: { certificateType: AppleUtils.CertificateType.IOS_DEVELOPMENT } } }))], { concurrency: 2 });
22436
+ const ascMatch = [...matching[0], ...matching[1]].find((entry) => normalizeAppleSerial(entry.attributes.serialNumber) === targetSerial);
22808
22437
  let revokedOnApple = false;
22809
22438
  if (ascMatch !== void 0) {
22810
- yield* deleteCertificate(ascCreds, ascMatch.id).pipe(Effect.mapError(wrapAscError("apple-revoke-certificate")));
22439
+ yield* wrap("apple-revoke-certificate", async () => AppleUtils.Certificate.deleteAsync(ctx, { id: ascMatch.id }));
22811
22440
  revokedOnApple = true;
22812
22441
  }
22813
22442
  let deletedLocally = false;
@@ -22822,67 +22451,59 @@ const revokeLocalDistributionCertificate = (api, input) => Effect.gen(function*
22822
22451
  deletedLocally
22823
22452
  };
22824
22453
  });
22825
- const listAppleCertificates = (api, input) => Effect.gen(function* () {
22826
- const creds = yield* fetchAscCredentials(api, input.ascApiKeyId);
22827
- return yield* listCertificates({
22828
- keyId: creds.keyId,
22829
- issuerId: creds.issuerId,
22830
- p8Pem: creds.p8Pem
22831
- }, compact({ certificateType: input.certificateType })).pipe(Effect.mapError(wrapAscError("apple-list-certificates")));
22454
+ const findOrCreateBundleId = (ctx, bundleIdentifier) => Effect.gen(function* () {
22455
+ const existing = yield* wrap("apple-find-bundle-id", async () => AppleUtils.BundleId.findAsync(ctx, { identifier: bundleIdentifier }));
22456
+ if (existing !== null) return existing.id;
22457
+ return (yield* wrap("apple-create-bundle-id", async () => AppleUtils.BundleId.createAsync(ctx, {
22458
+ identifier: bundleIdentifier,
22459
+ name: bundleIdentifier,
22460
+ platform: AppleUtils.BundleIdPlatform.IOS
22461
+ }))).id;
22832
22462
  });
22833
- const resolveCertAscId = (creds, serialNumber, certificateType) => Effect.gen(function* () {
22834
- const certs = yield* listCertificates(creds, { certificateType }).pipe(Effect.mapError(wrapAscError("apple-list-certificates")));
22463
+ const findAscCertificateId = (ctx, serialNumber, certificateType) => Effect.gen(function* () {
22464
+ const certs = yield* wrap("apple-list-certificates", async () => AppleUtils.Certificate.getAsync(ctx, { query: { filter: { certificateType } } }));
22835
22465
  const target = normalizeAppleSerial(serialNumber);
22836
- const match = certs.find((entry) => normalizeAppleSerial(entry.serialNumber) === target);
22837
- if (match === void 0) return yield* new GenerateFailedError({
22466
+ const match = certs.find((entry) => normalizeAppleSerial(entry.attributes.serialNumber) === target);
22467
+ if (match === void 0) return yield* new AppleIdGenerateFailedError({
22838
22468
  step: "match-apple-certificate",
22839
22469
  message: `Distribution certificate ${serialNumber} not present on Apple Developer Portal; upload or re-generate it`
22840
22470
  });
22841
22471
  return match.id;
22842
22472
  });
22843
- const ensureBundleId = (creds, bundleIdentifier) => Effect.gen(function* () {
22844
- const existing = (yield* listBundleIds(creds).pipe(Effect.mapError(wrapAscError("apple-list-bundle-ids")))).find((entry) => entry.identifier === bundleIdentifier);
22845
- if (existing !== void 0) return existing.id;
22846
- return (yield* createBundleId(creds, {
22847
- identifier: bundleIdentifier,
22848
- name: bundleIdentifier
22849
- }).pipe(Effect.mapError(wrapAscError("apple-create-bundle-id")))).id;
22850
- });
22851
- const collectDeviceAscIds = (creds, appleTeamId, deviceIds) => Effect.gen(function* () {
22852
- const devices = yield* listDevices(creds).pipe(Effect.mapError(wrapAscError("apple-list-devices")));
22853
- return {
22854
- ids: deviceIds === void 0 ? devices.map((device) => device.id) : devices.filter((device) => new Set(deviceIds).has(device.id)).map((device) => device.id),
22855
- appleTeamId
22856
- };
22473
+ const collectIosDeviceIds = (ctx, deviceIds) => Effect.gen(function* () {
22474
+ const devices = yield* wrap("apple-list-devices", async () => AppleUtils.Device.getAllIOSProfileDevicesAsync(ctx));
22475
+ if (deviceIds === void 0) return devices.map((device) => device.id);
22476
+ const allowed = new Set(deviceIds);
22477
+ return devices.filter((device) => allowed.has(device.id)).map((device) => device.id);
22857
22478
  });
22858
22479
  const generateAndUploadProvisioningProfile = (api, input) => Effect.gen(function* () {
22859
- const creds = yield* fetchAscCredentials(api, input.ascApiKeyId);
22860
- const ascCreds = {
22861
- keyId: creds.keyId,
22862
- issuerId: creds.issuerId,
22863
- p8Pem: creds.p8Pem
22864
- };
22865
- const cert = yield* api.appleDistributionCertificates.list().pipe(Effect.map(({ items }) => items.find((item) => item.id === input.distributionCertificateId)), Effect.flatMap((match) => match === void 0 ? Effect.fail(new GenerateFailedError({
22480
+ const ctx = input.context;
22481
+ const cert = yield* api.appleDistributionCertificates.list().pipe(Effect.map(({ items }) => items.find((item) => item.id === input.distributionCertificateId)), Effect.flatMap((match) => match === void 0 ? Effect.fail(new AppleIdGenerateFailedError({
22866
22482
  step: "load-distribution-certificate",
22867
22483
  message: `Distribution certificate ${input.distributionCertificateId} not found`
22868
22484
  })) : Effect.succeed(match)));
22869
- const certificateType = input.distributionType === "DEVELOPMENT" ? "IOS_DEVELOPMENT" : "IOS_DISTRIBUTION";
22870
- const [certAscId, bundleIdAscId] = yield* Effect.all([resolveCertAscId(ascCreds, cert.serialNumber.toUpperCase(), certificateType), ensureBundleId(ascCreds, input.bundleIdentifier)], { concurrency: 2 });
22485
+ const certificateType = DISTRIBUTION_TO_CERTIFICATE_TYPE[input.distributionType];
22486
+ const [certAscId, bundleIdAscId] = yield* Effect.all([findAscCertificateId(ctx, cert.serialNumber, certificateType), findOrCreateBundleId(ctx, input.bundleIdentifier)], { concurrency: 2 });
22871
22487
  const useDevices = input.distributionType === "AD_HOC" || input.distributionType === "DEVELOPMENT";
22872
- const { ids: deviceAscIds } = useDevices ? yield* collectDeviceAscIds(ascCreds, cert.appleTeamId, input.deviceIds) : { ids: [] };
22873
- if (useDevices && deviceAscIds.length === 0) return yield* new GenerateFailedError({
22488
+ const deviceIds = useDevices ? yield* collectIosDeviceIds(ctx, input.deviceIds) : [];
22489
+ if (useDevices && deviceIds.length === 0) return yield* new AppleIdGenerateFailedError({
22874
22490
  step: "collect-devices",
22875
22491
  message: "No registered devices to attach to the provisioning profile"
22876
22492
  });
22877
- const profileBytes = fromBase64((yield* createProvisioningProfile(ascCreds, {
22878
- profileName: `${input.bundleIdentifier} ${input.distributionType} ${Date.now()}`,
22879
- profileType: DISTRIBUTION_TO_PROFILE_TYPE$1[input.distributionType],
22880
- bundleIdAscId,
22881
- certificateAscIds: [certAscId],
22882
- deviceAscIds
22883
- }).pipe(Effect.mapError(wrapAscError("apple-create-profile")))).profileContent);
22884
- const rosterHash = useDevices ? computeDeviceRosterHashHex(deviceAscIds) : void 0;
22885
- const profileBase64 = toBase64(profileBytes);
22493
+ const profileName = `${input.bundleIdentifier} ${input.distributionType} ${Date.now()}`;
22494
+ const { profileContent } = (yield* wrap("apple-create-profile", async () => AppleUtils.Profile.createAsync(ctx, {
22495
+ bundleId: bundleIdAscId,
22496
+ certificates: [certAscId],
22497
+ devices: deviceIds,
22498
+ name: profileName,
22499
+ profileType: DISTRIBUTION_TO_PROFILE_TYPE[input.distributionType]
22500
+ }))).attributes;
22501
+ if (profileContent === null) return yield* new AppleIdGenerateFailedError({
22502
+ step: "extract-profile-content",
22503
+ message: "Apple returned a profile with no content (likely expired/invalid)"
22504
+ });
22505
+ const profileBase64 = toBase64(fromBase64(profileContent));
22506
+ const rosterHash = useDevices ? computeDeviceRosterHashHex(deviceIds) : void 0;
22886
22507
  const created = yield* api.appleProvisioningProfiles.upload({ payload: {
22887
22508
  profileBase64,
22888
22509
  appleDistributionCertificateId: input.distributionCertificateId,
@@ -22896,7 +22517,7 @@ const generateAndUploadProvisioningProfile = (api, input) => Effect.gen(function
22896
22517
  profileName: created.profileName,
22897
22518
  validUntil: created.validUntil,
22898
22519
  developerPortalIdentifier: created.developerPortalIdentifier,
22899
- /** Raw .mobileprovision bytes (base64) — callers can install directly without re-downloading. */
22520
+ /** Raw .mobileprovision bytes (base64) — callers can install without re-downloading. */
22900
22521
  profileBase64
22901
22522
  };
22902
22523
  });
@@ -22915,7 +22536,7 @@ const generateAndUploadProvisioningProfile = (api, input) => Effect.gen(function
22915
22536
  */
22916
22537
  const autoProvisionExtensionProfile = (api, input) => Effect.gen(function* () {
22917
22538
  const generated = yield* generateAndUploadProvisioningProfile(api, {
22918
- ascApiKeyId: input.ascApiKeyId,
22539
+ context: yield* ascKeyRequestContext(api, input.ascApiKeyId),
22919
22540
  distributionCertificateId: input.distributionCertificateId,
22920
22541
  bundleIdentifier: input.bundleIdentifier,
22921
22542
  distributionType: input.distributionType
@@ -23707,166 +23328,6 @@ const distributionCertChoice = (cert, teamLabel = cert.appleTeamId) => ({
23707
23328
  label: `${cert.serialNumber.slice(0, 12)}… (team ${teamLabel}, exp ${isoDate(cert.validUntil)})`
23708
23329
  });
23709
23330
 
23710
- //#endregion
23711
- //#region src/lib/credentials-generator-apple-id.ts
23712
- const DISTRIBUTION_TO_PROFILE_TYPE = {
23713
- APP_STORE: AppleUtils.ProfileType.IOS_APP_STORE,
23714
- AD_HOC: AppleUtils.ProfileType.IOS_APP_ADHOC,
23715
- DEVELOPMENT: AppleUtils.ProfileType.IOS_APP_DEVELOPMENT,
23716
- ENTERPRISE: AppleUtils.ProfileType.IOS_APP_INHOUSE
23717
- };
23718
- const DISTRIBUTION_TO_CERTIFICATE_TYPE = {
23719
- APP_STORE: AppleUtils.CertificateType.IOS_DISTRIBUTION,
23720
- AD_HOC: AppleUtils.CertificateType.IOS_DISTRIBUTION,
23721
- ENTERPRISE: AppleUtils.CertificateType.IOS_DISTRIBUTION,
23722
- DEVELOPMENT: AppleUtils.CertificateType.IOS_DEVELOPMENT
23723
- };
23724
- var AppleIdGenerateFailedError = class extends Data.TaggedError("AppleIdGenerateFailedError") {};
23725
- const CERT_LIMIT_PATTERN = /already have a current.*certificate|pending certificate request/iu;
23726
- const messageOf = (cause) => cause instanceof Error ? cause.message : String(cause);
23727
- const wrap = (step, run) => Effect.tryPromise({
23728
- try: run,
23729
- catch: (cause) => new AppleIdGenerateFailedError({
23730
- step,
23731
- message: messageOf(cause)
23732
- })
23733
- });
23734
- const wrapCertificateCreate = (run) => Effect.tryPromise({
23735
- try: run,
23736
- catch: (cause) => {
23737
- const message = messageOf(cause);
23738
- if (CERT_LIMIT_PATTERN.test(message)) return new CertificateLimitError({ message });
23739
- return new AppleIdGenerateFailedError({
23740
- step: "apple-create-certificate",
23741
- message
23742
- });
23743
- }
23744
- });
23745
- const generateAndUploadDistributionCertificateViaAppleId = (api, input) => Effect.gen(function* () {
23746
- const ctx = input.context;
23747
- const certificateType = input.certificateType === "IOS_DEVELOPMENT" ? AppleUtils.CertificateType.IOS_DEVELOPMENT : AppleUtils.CertificateType.IOS_DISTRIBUTION;
23748
- const result = yield* wrapCertificateCreate(async () => AppleUtils.createCertificateAndP12Async(ctx, { certificateType }));
23749
- const metadata = yield* extractMetadataFromP12({
23750
- p12Base64: result.certificateP12,
23751
- password: result.password
23752
- }).pipe(Effect.mapError((cause) => new AppleIdGenerateFailedError({
23753
- step: "parse-p12",
23754
- message: cause.message
23755
- })));
23756
- const session = yield* openVaultSessionInteractive(api);
23757
- const envelopeMetadata = {
23758
- serialNumber: metadata.serialNumber,
23759
- appleTeamIdentifier: metadata.appleTeamId,
23760
- validFrom: metadata.validFrom,
23761
- validUntil: metadata.validUntil
23762
- };
23763
- const envelope = yield* sealForUpload({
23764
- session,
23765
- credentialType: "distribution-certificate",
23766
- metadata: envelopeMetadata,
23767
- secret: {
23768
- p12Base64: result.certificateP12,
23769
- p12Password: result.password
23770
- }
23771
- }).pipe(Effect.mapError((cause) => new AppleIdGenerateFailedError({
23772
- step: "encrypt-p12",
23773
- message: cause.message
23774
- })));
23775
- const created = yield* api.appleDistributionCertificates.upload({ payload: {
23776
- ...toUploadEnvelope(envelope),
23777
- ...envelopeMetadata,
23778
- ...compact({
23779
- appleTeamName: toOptional(metadata.appleTeamName),
23780
- developerIdIdentifier: toOptional(metadata.developerIdIdentifier)
23781
- })
23782
- } });
23783
- return {
23784
- id: created.id,
23785
- serialNumber: metadata.serialNumber,
23786
- appleTeamId: created.appleTeamId,
23787
- appleTeamIdentifier: metadata.appleTeamId,
23788
- developerPortalIdentifier: result.certificate.id
23789
- };
23790
- });
23791
- const listDistributionCertsViaAppleId = (ctx, certificateType = "IOS_DISTRIBUTION") => Effect.gen(function* () {
23792
- const filter = certificateType === "IOS_DEVELOPMENT" ? AppleUtils.CertificateType.IOS_DEVELOPMENT : AppleUtils.CertificateType.IOS_DISTRIBUTION;
23793
- return (yield* wrap("apple-list-certificates", async () => AppleUtils.Certificate.getAsync(ctx, { query: { filter: { certificateType: filter } } }))).map((entry) => ({
23794
- developerPortalIdentifier: entry.id,
23795
- serialNumber: entry.attributes.serialNumber,
23796
- displayName: entry.attributes.displayName,
23797
- expirationDate: entry.attributes.expirationDate
23798
- }));
23799
- });
23800
- const revokeDistributionCertViaAppleId = (ctx, developerPortalIdentifier) => wrap("apple-revoke-certificate", async () => AppleUtils.Certificate.deleteAsync(ctx, { id: developerPortalIdentifier }));
23801
- const findOrCreateBundleId = (ctx, bundleIdentifier) => Effect.gen(function* () {
23802
- const existing = yield* wrap("apple-find-bundle-id", async () => AppleUtils.BundleId.findAsync(ctx, { identifier: bundleIdentifier }));
23803
- if (existing !== null) return existing.id;
23804
- return (yield* wrap("apple-create-bundle-id", async () => AppleUtils.BundleId.createAsync(ctx, {
23805
- identifier: bundleIdentifier,
23806
- name: bundleIdentifier,
23807
- platform: AppleUtils.BundleIdPlatform.IOS
23808
- }))).id;
23809
- });
23810
- const findAscCertificateId = (ctx, serialNumber, certificateType) => Effect.gen(function* () {
23811
- const certs = yield* wrap("apple-list-certificates", async () => AppleUtils.Certificate.getAsync(ctx, { query: { filter: { certificateType } } }));
23812
- const target = normalizeAppleSerial(serialNumber);
23813
- const match = certs.find((entry) => normalizeAppleSerial(entry.attributes.serialNumber) === target);
23814
- if (match === void 0) return yield* new AppleIdGenerateFailedError({
23815
- step: "match-apple-certificate",
23816
- message: `Distribution certificate ${serialNumber} not present on Apple Developer Portal; upload or re-generate it`
23817
- });
23818
- return match.id;
23819
- });
23820
- const collectIosDeviceIds = (ctx, deviceIds) => Effect.gen(function* () {
23821
- const devices = yield* wrap("apple-list-devices", async () => AppleUtils.Device.getAllIOSProfileDevicesAsync(ctx));
23822
- if (deviceIds === void 0) return devices.map((device) => device.id);
23823
- const allowed = new Set(deviceIds);
23824
- return devices.filter((device) => allowed.has(device.id)).map((device) => device.id);
23825
- });
23826
- const generateAndUploadProvisioningProfileViaAppleId = (api, input) => Effect.gen(function* () {
23827
- const ctx = input.context;
23828
- const cert = yield* api.appleDistributionCertificates.list().pipe(Effect.map(({ items }) => items.find((item) => item.id === input.distributionCertificateId)), Effect.flatMap((match) => match === void 0 ? Effect.fail(new AppleIdGenerateFailedError({
23829
- step: "load-distribution-certificate",
23830
- message: `Distribution certificate ${input.distributionCertificateId} not found`
23831
- })) : Effect.succeed(match)));
23832
- const certificateType = DISTRIBUTION_TO_CERTIFICATE_TYPE[input.distributionType];
23833
- const [certAscId, bundleIdAscId] = yield* Effect.all([findAscCertificateId(ctx, cert.serialNumber, certificateType), findOrCreateBundleId(ctx, input.bundleIdentifier)], { concurrency: 2 });
23834
- const useDevices = input.distributionType === "AD_HOC" || input.distributionType === "DEVELOPMENT";
23835
- const deviceIds = useDevices ? yield* collectIosDeviceIds(ctx, input.deviceIds) : [];
23836
- if (useDevices && deviceIds.length === 0) return yield* new AppleIdGenerateFailedError({
23837
- step: "collect-devices",
23838
- message: "No registered devices to attach to the provisioning profile"
23839
- });
23840
- const profileName = `${input.bundleIdentifier} ${input.distributionType} ${Date.now()}`;
23841
- const { profileContent } = (yield* wrap("apple-create-profile", async () => AppleUtils.Profile.createAsync(ctx, {
23842
- bundleId: bundleIdAscId,
23843
- certificates: [certAscId],
23844
- devices: deviceIds,
23845
- name: profileName,
23846
- profileType: DISTRIBUTION_TO_PROFILE_TYPE[input.distributionType]
23847
- }))).attributes;
23848
- if (profileContent === null) return yield* new AppleIdGenerateFailedError({
23849
- step: "extract-profile-content",
23850
- message: "Apple returned a profile with no content (likely expired/invalid)"
23851
- });
23852
- const profileBytes = fromBase64(profileContent);
23853
- const rosterHash = useDevices ? computeDeviceRosterHashHex(deviceIds) : void 0;
23854
- const created = yield* api.appleProvisioningProfiles.upload({ payload: {
23855
- profileBase64: toBase64(profileBytes),
23856
- appleDistributionCertificateId: input.distributionCertificateId,
23857
- isManaged: true,
23858
- ...compact({ deviceRosterHash: rosterHash })
23859
- } });
23860
- return {
23861
- id: created.id,
23862
- bundleIdentifier: created.bundleIdentifier,
23863
- distributionType: created.distributionType,
23864
- profileName: created.profileName,
23865
- validUntil: created.validUntil,
23866
- developerPortalIdentifier: created.developerPortalIdentifier
23867
- };
23868
- });
23869
-
23870
23331
  //#endregion
23871
23332
  //#region src/lib/credentials-generator-apns.ts
23872
23333
  const APNS_SERVICE_ID = "U27F4V844T";
@@ -24011,7 +23472,7 @@ const chooseIosSetupPath = (api) => Effect.gen(function* () {
24011
23472
  const interactiveAppleIdCertLimitRecover = (ctx) => Effect.gen(function* () {
24012
23473
  yield* Console.log("");
24013
23474
  yield* Console.log("Apple reports the certificate limit was hit (max 3 distribution certs per team).");
24014
- const certs = yield* listDistributionCertsViaAppleId(ctx, "IOS_DISTRIBUTION");
23475
+ const certs = yield* listDistributionCerts(ctx, "IOS_DISTRIBUTION");
24015
23476
  if (certs.length === 0) return yield* new AppleIdGenerateFailedError({
24016
23477
  step: "limit-recover",
24017
23478
  message: "Apple says the certificate limit is hit but no existing certificates were returned."
@@ -24020,7 +23481,7 @@ const interactiveAppleIdCertLimitRecover = (ctx) => Effect.gen(function* () {
24020
23481
  value: entry.developerPortalIdentifier,
24021
23482
  label: `${entry.serialNumber.slice(0, 12)}… (${entry.displayName}, exp ${entry.expirationDate.slice(0, 10)})`
24022
23483
  })), { required: true });
24023
- yield* Effect.forEach(toRevoke, (id) => revokeDistributionCertViaAppleId(ctx, id), { concurrency: "inherit" });
23484
+ yield* Effect.forEach(toRevoke, (id) => revokeDistributionCert(ctx, id), { concurrency: "inherit" });
24024
23485
  yield* Console.log(`Revoked ${toRevoke.length} certificate(s); retrying generation...`);
24025
23486
  });
24026
23487
  const defaultApnsKeyName = () => `better-update APNs (${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)})`;
@@ -24056,7 +23517,7 @@ const createApnsKeyViaAppleId = (api, name) => Effect.gen(function* () {
24056
23517
  });
24057
23518
  const generateDistributionCertViaAppleIdInteractive = (api, ctx) => Effect.gen(function* () {
24058
23519
  yield* Console.log("Generating distribution certificate via Apple ID...");
24059
- const generate = generateAndUploadDistributionCertificateViaAppleId(api, { context: ctx });
23520
+ const generate = generateAndUploadDistributionCertificate(api, { context: ctx });
24060
23521
  return yield* generate.pipe(Effect.catchTag("CertificateLimitError", () => interactiveAppleIdCertLimitRecover(ctx).pipe(Effect.flatMap(() => generate))));
24061
23522
  });
24062
23523
  const GENERATE_NEW = "__generate__";
@@ -24100,7 +23561,7 @@ const setupIosViaAppleId = (api, input) => Effect.gen(function* () {
24100
23561
  const cert = yield* chooseDistributionCertViaAppleId(api, ctx, session.teamId);
24101
23562
  const distributionType = IOS_DISTRIBUTION_TO_TYPE[input.distribution];
24102
23563
  yield* Console.log("Generating provisioning profile via Apple ID...");
24103
- const profile = yield* generateAndUploadProvisioningProfileViaAppleId(api, {
23564
+ const profile = yield* generateAndUploadProvisioningProfile(api, {
24104
23565
  context: ctx,
24105
23566
  distributionCertificateId: cert.id,
24106
23567
  bundleIdentifier: input.bundleIdentifier,
@@ -24119,7 +23580,7 @@ const regenerateProvisioningProfileViaAppleId = (api, input) => Effect.gen(funct
24119
23580
  const auth = yield* AppleAuth;
24120
23581
  const session = yield* auth.ensureLoggedIn();
24121
23582
  yield* Console.log("Regenerating provisioning profile via Apple ID...");
24122
- const created = yield* generateAndUploadProvisioningProfileViaAppleId(api, {
23583
+ const created = yield* generateAndUploadProvisioningProfile(api, {
24123
23584
  context: auth.buildRequestContext(session),
24124
23585
  distributionCertificateId: input.distributionCertificateId,
24125
23586
  bundleIdentifier: input.bundleIdentifier,
@@ -24137,22 +23598,17 @@ const regenerateProvisioningProfileViaAppleId = (api, input) => Effect.gen(funct
24137
23598
  const interactiveCertLimitRecover = (api, ascApiKeyId) => Effect.gen(function* () {
24138
23599
  yield* Console.log("");
24139
23600
  yield* Console.log("Apple reports the certificate limit was hit (max 3 distribution certs per team).");
24140
- const certs = yield* listAppleCertificates(api, {
24141
- ascApiKeyId,
24142
- certificateType: "IOS_DISTRIBUTION"
24143
- });
23601
+ const context = yield* ascKeyRequestContext(api, ascApiKeyId);
23602
+ const certs = yield* listDistributionCerts(context, "IOS_DISTRIBUTION");
24144
23603
  if (certs.length === 0) return yield* new MissingCredentialsError({
24145
23604
  message: "Apple says the certificate limit is hit but no existing certificates were returned.",
24146
23605
  hint: "Try again later or check the Apple Developer portal."
24147
23606
  });
24148
23607
  const toRevoke = yield* promptMultiSelect("Select one or more certificates to revoke before retrying", certs.map((entry) => ({
24149
- value: entry.id,
24150
- label: `${entry.serialNumber.slice(0, 12)}… (${entry.displayName ?? entry.certificateType}, exp ${entry.expirationDate.slice(0, 10)})`
23608
+ value: entry.developerPortalIdentifier,
23609
+ label: `${entry.serialNumber.slice(0, 12)}… (${entry.displayName || entry.certificateType}, exp ${entry.expirationDate.slice(0, 10)})`
24151
23610
  })), { required: true });
24152
- yield* Effect.forEach(toRevoke, (id) => revokeAppleCertificate(api, {
24153
- ascApiKeyId,
24154
- developerPortalIdentifier: id
24155
- }), { concurrency: "inherit" });
23611
+ yield* Effect.forEach(toRevoke, (id) => revokeDistributionCert(context, id), { concurrency: "inherit" });
24156
23612
  yield* Console.log(`Revoked ${toRevoke.length} certificate(s); retrying generation...`);
24157
23613
  });
24158
23614
  const generateDistributionCertInteractive = (api) => Effect.gen(function* () {
@@ -24165,8 +23621,8 @@ const generateDistributionCertInteractive = (api) => Effect.gen(function* () {
24165
23621
  value: key.id,
24166
23622
  label: `${key.name} (${key.keyId})`
24167
23623
  })));
24168
- yield* Console.log("Generating CSR and requesting certificate from Apple...");
24169
- const generate = generateAndUploadDistributionCertificate(api, { ascApiKeyId: ascKeyId });
23624
+ yield* Console.log("Requesting a distribution certificate from Apple...");
23625
+ const generate = generateAndUploadDistributionCertificate(api, { context: yield* ascKeyRequestContext(api, ascKeyId) });
24170
23626
  return yield* generate.pipe(Effect.catchTag("CertificateLimitError", () => interactiveCertLimitRecover(api, ascKeyId).pipe(Effect.flatMap(() => generate))));
24171
23627
  });
24172
23628
  const chooseIosCertificateId = (api) => Effect.gen(function* () {
@@ -24219,7 +23675,7 @@ const pickIosAscKey = (api, appleTeamId) => Effect.gen(function* () {
24219
23675
  const generateProvisioningProfileForBundle = (api, input, ctx) => Effect.gen(function* () {
24220
23676
  yield* Console.log("Generating provisioning profile via App Store Connect API...");
24221
23677
  return (yield* generateAndUploadProvisioningProfile(api, {
24222
- ascApiKeyId: ctx.ascKeyId,
23678
+ context: yield* ascKeyRequestContext(api, ctx.ascKeyId),
24223
23679
  distributionCertificateId: ctx.certId,
24224
23680
  bundleIdentifier: input.bundleIdentifier,
24225
23681
  distributionType: ctx.distributionType
@@ -24400,7 +23856,7 @@ const regenerateProvisioningProfile = (api, input) => Effect.gen(function* () {
24400
23856
  });
24401
23857
  yield* Console.log("Regenerating provisioning profile via App Store Connect API...");
24402
23858
  const created = yield* generateAndUploadProvisioningProfile(api, {
24403
- ascApiKeyId: config.ascApiKeyId,
23859
+ context: yield* ascKeyRequestContext(api, config.ascApiKeyId),
24404
23860
  distributionCertificateId: config.appleDistributionCertificateId,
24405
23861
  bundleIdentifier: input.bundleIdentifier,
24406
23862
  distributionType
@@ -28397,8 +27853,10 @@ const androidMenu = (ctx) => Effect.gen(function* () {
28397
27853
  //#endregion
28398
27854
  //#region src/lib/credentials-generator-asc-key.ts
28399
27855
  const toUserRole = (role) => role === "APP_MANAGER" ? AppleUtils.UserRole.APP_MANAGER : AppleUtils.UserRole.ADMIN;
27856
+ const ASC_API_KEY_NICKNAME_MAX_LENGTH = 30;
27857
+ const clampAscApiKeyNickname = (nickname) => nickname.slice(0, ASC_API_KEY_NICKNAME_MAX_LENGTH);
28400
27858
  /** Default nickname shown in App Store Connect → Users and Access → Integrations. */
28401
- const defaultAscApiKeyNickname = () => `[better-update] ${(/* @__PURE__ */ new Date()).toISOString()}`;
27859
+ const defaultAscApiKeyNickname = () => `[better-update] ${Date.now().toString(36)}`;
28402
27860
  const ASC_KEY_NOT_READY_PATTERN = /no resource of type|resource does not exist/iu;
28403
27861
  const ASC_KEY_DOWNLOAD_RETRY = Schedule.exponential("1 second", 2).pipe(Schedule.intersect(Schedule.recurs(6)));
28404
27862
  const downloadAscKeyWithRetry = (key) => Effect.tryPromise({
@@ -28432,7 +27890,7 @@ const writeRescueP8 = (keyId, p8Pem) => Effect.gen(function* () {
28432
27890
  const generateAndUploadAscApiKeyViaAppleId = (api, input) => Effect.gen(function* () {
28433
27891
  const ctx = input.context;
28434
27892
  const key = yield* wrap("apple-create-asc-key", async () => AppleUtils.ApiKey.createAsync(ctx, {
28435
- nickname: input.nickname,
27893
+ nickname: clampAscApiKeyNickname(input.nickname),
28436
27894
  allAppsVisible: true,
28437
27895
  roles: [toUserRole(input.role)],
28438
27896
  keyType: AppleUtils.ApiKeyType.PUBLIC_API
@@ -28460,7 +27918,8 @@ const generateAndUploadAscApiKeyViaAppleId = (api, input) => Effect.gen(function
28460
27918
  return {
28461
27919
  id: (yield* api.ascApiKeys.upload({ payload: {
28462
27920
  ...toUploadEnvelope(envelope),
28463
- ...metadata
27921
+ ...metadata,
27922
+ roles: [input.role]
28464
27923
  } })).id,
28465
27924
  issuerId
28466
27925
  };
@@ -28761,8 +28220,9 @@ const generateNewIosDistributionCert = (ctx) => Effect.gen(function* () {
28761
28220
  value: key.id,
28762
28221
  label: `${key.name} (${key.keyId})`
28763
28222
  })));
28764
- yield* Console.log("Generating CSR and requesting certificate from Apple...");
28765
- const created = yield* generateAndUploadDistributionCertificate(ctx.api, { ascApiKeyId: ascKeyId });
28223
+ yield* Console.log("Requesting a distribution certificate from Apple...");
28224
+ const context = yield* ascKeyRequestContext(ctx.api, ascKeyId);
28225
+ const created = yield* generateAndUploadDistributionCertificate(ctx.api, { context });
28766
28226
  yield* Console.log("Distribution certificate generated.");
28767
28227
  yield* printKeyValue([
28768
28228
  ["ID", created.id],
@@ -30545,7 +30005,7 @@ const ascKeyCommand = defineCommand({
30545
30005
  },
30546
30006
  nickname: {
30547
30007
  type: "string",
30548
- description: "Nickname shown in App Store Connect (defaults to a timestamped name)"
30008
+ description: "Nickname shown in App Store Connect (defaults to a timestamped name; Apple caps it at 30 chars, longer values are truncated)"
30549
30009
  }
30550
30010
  },
30551
30011
  run: async ({ args }) => runEffect(Effect.gen(function* () {
@@ -30796,7 +30256,7 @@ const pushKeyCommand$1 = defineCommand({
30796
30256
  const GENERATE_EXIT_EXTRAS = {
30797
30257
  CredentialValidationError: 2,
30798
30258
  BuildFailedError: 6,
30799
- GenerateFailedError: 6,
30259
+ AppleIdGenerateFailedError: 6,
30800
30260
  CertificateLimitError: 6
30801
30261
  };
30802
30262
  const ensureNonEmpty = (value, label) => value === void 0 || value.trim().length === 0 ? Effect.fail(new CredentialValidationError({ message: `Missing --${label}` })) : Effect.succeed(value);
@@ -30906,12 +30366,13 @@ const distributionCertificateCommand$1 = defineCommand({
30906
30366
  run: async ({ args }) => runEffect(Effect.gen(function* () {
30907
30367
  const api = yield* apiClient;
30908
30368
  const certificateType = args.type === "development" ? "IOS_DEVELOPMENT" : "IOS_DISTRIBUTION";
30909
- yield* printHuman("Generating CSR and requesting certificate from Apple...");
30369
+ yield* printHuman("Requesting a distribution certificate from Apple...");
30370
+ const context = yield* ascKeyRequestContext(api, args["asc-key-id"]);
30910
30371
  const attempt = generateAndUploadDistributionCertificate(api, {
30911
- ascApiKeyId: args["asc-key-id"],
30372
+ context,
30912
30373
  certificateType
30913
30374
  });
30914
- const created = yield* attempt.pipe(Effect.catchTag("CertificateLimitError", () => handleCertLimitInteractive(api, args["asc-key-id"], certificateType).pipe(Effect.flatMap(() => attempt))));
30375
+ const created = yield* attempt.pipe(Effect.catchTag("CertificateLimitError", () => handleCertLimitInteractive(context, certificateType).pipe(Effect.flatMap(() => attempt))));
30915
30376
  yield* printHuman("Distribution certificate generated and stored.");
30916
30377
  yield* printHumanKeyValue([
30917
30378
  ["ID", created.id],
@@ -30925,22 +30386,16 @@ const distributionCertificateCommand$1 = defineCommand({
30925
30386
  json: "value"
30926
30387
  })
30927
30388
  });
30928
- const handleCertLimitInteractive = (api, ascApiKeyId, certificateType) => Effect.gen(function* () {
30389
+ const handleCertLimitInteractive = (context, certificateType) => Effect.gen(function* () {
30929
30390
  yield* printHuman("");
30930
30391
  yield* printHuman("Apple reports the certificate limit was hit (max 3 distribution certs).");
30931
- const certs = yield* listAppleCertificates(api, {
30932
- ascApiKeyId,
30933
- certificateType
30934
- });
30392
+ const certs = yield* listDistributionCerts(context, certificateType);
30935
30393
  if (certs.length === 0) return yield* new CertificateLimitError({ message: "Apple says the certificate limit is hit but no existing certificates were returned — try again later." });
30936
30394
  const toRevoke = yield* promptMultiSelect("Select one or more certificates to revoke before retrying", certs.map((entry) => ({
30937
- value: entry.id,
30938
- label: `${entry.serialNumber.slice(0, 12)}… (${entry.displayName ?? entry.certificateType}, exp ${entry.expirationDate.slice(0, 10)})`
30395
+ value: entry.developerPortalIdentifier,
30396
+ label: `${entry.serialNumber.slice(0, 12)}… (${entry.displayName || entry.certificateType}, exp ${entry.expirationDate.slice(0, 10)})`
30939
30397
  })), { required: true });
30940
- yield* Effect.forEach(toRevoke, (id) => revokeAppleCertificate(api, {
30941
- ascApiKeyId,
30942
- developerPortalIdentifier: id
30943
- }), { concurrency: "inherit" });
30398
+ yield* Effect.forEach(toRevoke, (id) => revokeDistributionCert(context, id), { concurrency: "inherit" });
30944
30399
  yield* printHuman(`Revoked ${toRevoke.length} certificate(s); retrying generation...`);
30945
30400
  });
30946
30401
  const provisioningProfileCommand = defineCommand({
@@ -30984,7 +30439,7 @@ const provisioningProfileCommand = defineCommand({
30984
30439
  const api = yield* apiClient;
30985
30440
  const deviceIds = parseDeviceIds(args["device-ids"]);
30986
30441
  const created = yield* generateAndUploadProvisioningProfile(api, {
30987
- ascApiKeyId: args["asc-key-id"],
30442
+ context: yield* ascKeyRequestContext(api, args["asc-key-id"]),
30988
30443
  distributionCertificateId: args["cert-id"],
30989
30444
  bundleIdentifier: args.bundle,
30990
30445
  distributionType: args.distribution,
@@ -31547,7 +31002,6 @@ const resolveType = (raw, available) => Effect.gen(function* () {
31547
31002
  //#region src/commands/credentials/revoke.ts
31548
31003
  const REVOKE_EXIT_EXTRAS = {
31549
31004
  CredentialValidationError: 2,
31550
- GenerateFailedError: 6,
31551
31005
  AppleIdGenerateFailedError: 6,
31552
31006
  AppleAuthError: 4,
31553
31007
  InteractiveProhibitedError: 4
@@ -33051,7 +32505,12 @@ const APPLE_DEVICE_CLASS = {
33051
32505
  MAC: "MAC"
33052
32506
  };
33053
32507
  const toDeviceClass = (raw) => raw === null ? "UNKNOWN" : APPLE_DEVICE_CLASS[raw] ?? "UNKNOWN";
33054
- const ascErrorMessage = (error) => error._tag === "AscApiError" ? error.message : `Apple request failed: ${String(error.cause)}`;
32508
+ const toAppleDevice = (device) => ({
32509
+ id: device.id,
32510
+ udid: device.attributes.udid,
32511
+ name: device.attributes.name,
32512
+ deviceClass: device.attributes.deviceClass
32513
+ });
33055
32514
  const LIST_LIMIT = 100;
33056
32515
  /**
33057
32516
  * Resolve which ASC key authenticates the sync and which internal team it
@@ -33129,13 +32588,8 @@ const syncDeviceCommand = defineCommand({
33129
32588
  run: async ({ args }) => runEffect(Effect.gen(function* () {
33130
32589
  const api = yield* apiClient;
33131
32590
  const target = yield* resolveTarget(api, args);
33132
- const creds = yield* fetchAscCredentials(api, target.ascApiKeyId);
33133
- const ascCreds = {
33134
- keyId: creds.keyId,
33135
- issuerId: creds.issuerId,
33136
- p8Pem: creds.p8Pem
33137
- };
33138
- const appleDevices = yield* listDevices(ascCreds);
32591
+ const ctx = buildTokenRequestContext(yield* fetchAscCredentials(api, target.ascApiKeyId));
32592
+ const appleDevices = (yield* wrapConnect("apple-list-devices", async () => AppleUtils.Device.getAsync(ctx))).map(toAppleDevice);
33139
32593
  const local = yield* listAllLocalDevices(api, target.appleTeamId);
33140
32594
  const localUdids = new Set(local.map((device) => device.identifier.toLowerCase()));
33141
32595
  const pushed = [];
@@ -33144,14 +32598,15 @@ const syncDeviceCommand = defineCommand({
33144
32598
  const appleUdids = new Set(appleDevices.map((device) => device.udid.toLowerCase()));
33145
32599
  const toPush = local.filter((device) => !appleUdids.has(device.identifier.toLowerCase()));
33146
32600
  for (const device of toPush) {
33147
- const result = yield* Effect.either(createDevice(ascCreds, {
32601
+ const result = yield* Effect.either(wrapConnect("apple-create-device", async () => AppleUtils.Device.createAsync(ctx, {
33148
32602
  name: device.name,
33149
- udid: device.identifier
33150
- }));
33151
- if (Either.isRight(result)) pushed.push(result.right);
32603
+ udid: device.identifier,
32604
+ platform: AppleUtils.BundleIdPlatform.IOS
32605
+ })));
32606
+ if (Either.isRight(result)) pushed.push(toAppleDevice(result.right));
33152
32607
  else pushFailures.push({
33153
32608
  identifier: device.identifier,
33154
- message: ascErrorMessage(result.left)
32609
+ message: result.left.message
33155
32610
  });
33156
32611
  }
33157
32612
  }
@@ -35553,6 +35008,66 @@ const statusCommand = defineCommand({
35553
35008
  }), { json: "value" })
35554
35009
  });
35555
35010
 
35011
+ //#endregion
35012
+ //#region src/application/submit-asc-app.ts
35013
+ /**
35014
+ * Resolve (and, with consent, create) the App Store Connect app record a `submit`
35015
+ * needs for TestFlight config. EAS's `ensureAppExists` equivalent: look the app up
35016
+ * headlessly via the vault `.p8` (no Apple login when it already exists), and only
35017
+ * when it's missing fall back to an interactive `App.createAsync` from the Apple ID
35018
+ * cookie session. The resolved `ascAppId` is persisted to `eas.json` for reuse.
35019
+ *
35020
+ * Returns the app id, or `null` when none could be resolved — non-interactive runs,
35021
+ * a declined prompt, or any failure (login/create/network) degrade to `null` so the
35022
+ * caller queues the submission with guidance rather than crashing.
35023
+ */
35024
+ const DEFAULT_LOCALE = "en-US";
35025
+ /** Apple's documented `App.createAsync` rejections → an actionable hint. */
35026
+ const APP_CREATE_HINTS = {
35027
+ APP_CREATE_INSUFFICIENT_ROLE: "your Apple ID needs the \"App Manager\" or \"Admin\" role for this provider to create apps",
35028
+ APP_CREATE_BUNDLE_ID_NOT_REGISTERED: "register the bundle id in your Apple Developer account first (a build or `credentials` run does this)",
35029
+ APP_CREATE_NAME_UNAVAILABLE: "that app name is already taken on the App Store — choose another",
35030
+ APP_CREATE_NAME_INVALID: "the app name contains invalid characters"
35031
+ };
35032
+ /** Best-effort: write the resolved id back to eas.json so the next run reuses it. */
35033
+ const persist$1 = (input, ascAppId) => setSubmitProfileAscAppId(input.projectRoot, input.profileName, ascAppId).pipe(Effect.flatMap((path) => printHuman(`Saved ascAppId to ${path} (submit profile "${input.profileName}") for reuse.`)), Effect.catchAll((error) => printHuman(`Note: could not write ascAppId to eas.json (${error.message}). Add it manually to reuse it.`)));
35034
+ const createApp = (cookieCtx, name, input) => wrapConnect("apple-create-app", async () => AppleUtils.App.createAsync(cookieCtx, compact({
35035
+ name,
35036
+ bundleId: input.bundleIdentifier,
35037
+ sku: input.sku ?? input.bundleIdentifier,
35038
+ primaryLocale: input.primaryLocale ?? DEFAULT_LOCALE,
35039
+ companyName: input.companyName,
35040
+ platforms: [AppleUtils.Platform.IOS]
35041
+ }))).pipe(Effect.mapError((error) => {
35042
+ const hint = Object.entries(APP_CREATE_HINTS).find(([code]) => error.message.includes(code));
35043
+ return hint === void 0 ? error : new AppleConnectError({
35044
+ step: error.step,
35045
+ message: `${error.message} — ${hint[1]}.`
35046
+ });
35047
+ }));
35048
+ const ensureAscAppForSubmit = (input) => Effect.gen(function* () {
35049
+ const ctx = buildTokenRequestContext(input.credentials);
35050
+ const existing = yield* wrapConnect("apple-find-app", async () => AppleUtils.App.findAsync(ctx, { bundleId: input.bundleIdentifier }));
35051
+ if (existing !== null) {
35052
+ yield* persist$1(input, existing.id);
35053
+ return existing.id;
35054
+ }
35055
+ if (!(yield* InteractiveMode).allow) {
35056
+ yield* printHuman(`No App Store Connect app exists for bundle id ${input.bundleIdentifier}. Set ascAppId in the eas.json submit profile, or re-run interactively to create it.`);
35057
+ return null;
35058
+ }
35059
+ if (!(yield* promptConfirm(`No App Store Connect app exists for bundle id ${input.bundleIdentifier}. Create it now from your Apple ID?`, { initialValue: true }))) return null;
35060
+ const name = input.appName ?? (yield* promptText("App name (as shown on the App Store)", { placeholder: input.bundleIdentifier }));
35061
+ const auth = yield* AppleAuth;
35062
+ const session = yield* auth.ensureLoggedIn();
35063
+ const cookieCtx = auth.buildRequestContext(session);
35064
+ yield* printHuman("Creating the App Store Connect app via your Apple ID...");
35065
+ const app = yield* createApp(cookieCtx, name, input);
35066
+ yield* printHuman(`Created App Store Connect app "${name}" (${app.id}).`);
35067
+ yield* persist$1(input, app.id);
35068
+ return app.id;
35069
+ }).pipe(Effect.catchAll((error) => printHuman(`Could not resolve or create the App Store Connect app (${messageOf(error)}). The submission was queued — set ascAppId in eas.json and re-run.`).pipe(Effect.as(null))));
35070
+
35556
35071
  //#endregion
35557
35072
  //#region src/application/submit-asc-key.ts
35558
35073
  const ROLE_CHOICES = [{
@@ -35689,6 +35204,79 @@ const buildAndroidCreatePayload = (androidProfile) => {
35689
35204
  rollout: androidProfile.rollout
35690
35205
  });
35691
35206
  };
35207
+ /**
35208
+ * Run the iOS upload branch: resolve upload auth (stored key, app-specific
35209
+ * password, or an interactively-created ASC key), decrypt the `.p8` once, resolve
35210
+ * (or create) the ASC app for TestFlight config, then upload via `altool`.
35211
+ * Returns `false` when the submission was only queued (no client upload ran).
35212
+ */
35213
+ const submitIosBranch = (params) => Effect.gen(function* () {
35214
+ const { api, iosProfile, iosConfig } = params;
35215
+ const auth = resolveIosUploadAuth({
35216
+ appleId: iosProfile?.appleId,
35217
+ ascApiKeyId: iosProfile?.ascApiKeyId,
35218
+ hasAppSpecificPassword: hasAppleAppSpecificPassword()
35219
+ }) ?? (yield* Effect.gen(function* () {
35220
+ const resolvedKeyId = yield* ensureAscApiKeyForSubmit({
35221
+ api,
35222
+ projectRoot: params.projectRoot,
35223
+ profileName: params.profile
35224
+ });
35225
+ return resolvedKeyId === null ? null : {
35226
+ kind: "asc-api-key",
35227
+ ascApiKeyId: resolvedKeyId
35228
+ };
35229
+ }));
35230
+ if (auth === null) {
35231
+ yield* printHuman("iOS submission queued. Add ascApiKeyId to the eas.json submit profile, or set appleId + the EXPO_APPLE_APP_SPECIFIC_PASSWORD env var, to enable client-side altool upload.");
35232
+ return false;
35233
+ }
35234
+ const groups = iosProfile?.groups ?? [];
35235
+ const wantsConfig = needsTestFlightConfig({
35236
+ whatToTest: params.whatToTest,
35237
+ groups
35238
+ });
35239
+ const ascCredentials = yield* resolveAscUploadCredentials({
35240
+ api,
35241
+ auth,
35242
+ ascApiKeyId: iosProfile?.ascApiKeyId,
35243
+ wantsConfig
35244
+ });
35245
+ if (auth.kind === "asc-api-key" && ascCredentials === null) {
35246
+ yield* printHuman("iOS submission queued — the ASC API key could not be prepared for upload.");
35247
+ return false;
35248
+ }
35249
+ let resolvedAscAppId = iosProfile?.ascAppId;
35250
+ if (wantsConfig && resolvedAscAppId === void 0 && ascCredentials !== null) resolvedAscAppId = toOptional(yield* ensureAscAppForSubmit({
35251
+ credentials: ascCredentials,
35252
+ projectRoot: params.projectRoot,
35253
+ profileName: params.profile,
35254
+ bundleIdentifier: iosConfig.bundleIdentifier,
35255
+ appName: iosProfile?.appName,
35256
+ sku: iosProfile?.sku,
35257
+ companyName: iosProfile?.companyName,
35258
+ primaryLocale: iosProfile?.language
35259
+ }));
35260
+ yield* printHuman(auth.kind === "app-specific-password" ? "Running xcrun altool upload (Apple ID app-specific password)..." : "Running xcrun altool upload (ASC API key)...");
35261
+ yield* runIosSubmit({
35262
+ api,
35263
+ submissionId: params.submissionId,
35264
+ archive: {
35265
+ source: params.archive.archiveSource,
35266
+ value: params.archive.archiveUrl
35267
+ },
35268
+ auth,
35269
+ ascCredentials,
35270
+ config: {
35271
+ bundleIdentifier: iosConfig.bundleIdentifier,
35272
+ ascAppId: resolvedAscAppId,
35273
+ language: iosProfile?.language,
35274
+ whatToTest: params.whatToTest,
35275
+ groups
35276
+ }
35277
+ });
35278
+ return true;
35279
+ });
35692
35280
  const runFlow = (api, projectId, args) => Effect.gen(function* () {
35693
35281
  const iosConfig = buildIosCreatePayload(args.easProfile.ios, args.whatToTest);
35694
35282
  const androidConfig = buildAndroidCreatePayload(args.easProfile.android);
@@ -35706,44 +35294,16 @@ const runFlow = (api, projectId, args) => Effect.gen(function* () {
35706
35294
  });
35707
35295
  yield* printHuman(`Submission created: ${submission.id} (${submission.status})`);
35708
35296
  if (args.platform === "ios" && iosConfig !== void 0) {
35709
- const iosProfile = args.easProfile.ios;
35710
- const auth = resolveIosUploadAuth({
35711
- appleId: iosProfile?.appleId,
35712
- ascApiKeyId: iosProfile?.ascApiKeyId,
35713
- hasAppSpecificPassword: hasAppleAppSpecificPassword()
35714
- }) ?? (yield* Effect.gen(function* () {
35715
- const resolvedKeyId = yield* ensureAscApiKeyForSubmit({
35716
- api,
35717
- projectRoot: args.projectRoot,
35718
- profileName: args.profile
35719
- });
35720
- return resolvedKeyId === null ? null : {
35721
- kind: "asc-api-key",
35722
- ascApiKeyId: resolvedKeyId
35723
- };
35724
- }));
35725
- if (auth === null) {
35726
- yield* printHuman("iOS submission queued. Add ascApiKeyId to the eas.json submit profile, or set appleId + the EXPO_APPLE_APP_SPECIFIC_PASSWORD env var, to enable client-side altool upload.");
35727
- return submission;
35728
- }
35729
- yield* printHuman(auth.kind === "app-specific-password" ? "Running xcrun altool upload (Apple ID app-specific password)..." : "Running xcrun altool upload (ASC API key)...");
35730
- yield* runIosSubmit({
35297
+ if (!(yield* submitIosBranch({
35731
35298
  api,
35732
35299
  submissionId: submission.id,
35733
- archive: {
35734
- source: args.archive.archiveSource,
35735
- value: args.archive.archiveUrl
35736
- },
35737
- auth,
35738
- ascApiKeyId: auth.kind === "asc-api-key" ? auth.ascApiKeyId : iosProfile?.ascApiKeyId,
35739
- config: {
35740
- bundleIdentifier: iosConfig.bundleIdentifier,
35741
- ascAppId: iosProfile?.ascAppId,
35742
- language: iosProfile?.language,
35743
- whatToTest: args.whatToTest,
35744
- groups: iosProfile?.groups ?? []
35745
- }
35746
- });
35300
+ projectRoot: args.projectRoot,
35301
+ profile: args.profile,
35302
+ archive: args.archive,
35303
+ whatToTest: args.whatToTest,
35304
+ iosProfile: args.easProfile.ios,
35305
+ iosConfig
35306
+ }))) return submission;
35747
35307
  }
35748
35308
  if (args.platform === "android" && args.easProfile.android !== void 0) {
35749
35309
  yield* printHuman("Uploading bundle to Google Play locally...");