@better-update/cli 0.45.0 → 0.46.1

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.45.0";
38
+ var version = "0.46.1";
39
39
 
40
40
  //#endregion
41
41
  //#region src/lib/interactive-mode.ts
@@ -5055,6 +5055,27 @@ const writeEasJsonPatch = (projectRoot, patch) => Effect.gen(function* () {
5055
5055
  return filePath;
5056
5056
  });
5057
5057
  /**
5058
+ * Set `submit.<profileName>.ios.ascApiKeyId` in `eas.json`, preserving every
5059
+ * other submit profile and key. Used after auto-resolving/creating an ASC API
5060
+ * key during `submit` so the next run reuses it instead of creating another.
5061
+ */
5062
+ const setSubmitProfileAscApiKeyId = (projectRoot, profileName, ascApiKeyId) => Effect.gen(function* () {
5063
+ const existing = (yield* readEasJsonRaw(projectRoot)) ?? {};
5064
+ const submit = isRecord$1(existing["submit"]) ? existing["submit"] : {};
5065
+ const profile = isRecord$1(submit[profileName]) ? submit[profileName] : {};
5066
+ const ios = isRecord$1(profile["ios"]) ? profile["ios"] : {};
5067
+ return yield* writeEasJsonPatch(projectRoot, { submit: {
5068
+ ...submit,
5069
+ [profileName]: {
5070
+ ...profile,
5071
+ ios: {
5072
+ ...ios,
5073
+ ascApiKeyId
5074
+ }
5075
+ }
5076
+ } });
5077
+ });
5078
+ /**
5058
5079
  * Default `build` profiles scaffolded by `init` / `build configure`. Mirrors the
5059
5080
  * EAS three-tier convention: `development` (dev-client, internal), `preview`
5060
5081
  * (internal QA) and `production` (store). Keep in sync with the build-profile
@@ -28376,8 +28397,10 @@ const androidMenu = (ctx) => Effect.gen(function* () {
28376
28397
  //#endregion
28377
28398
  //#region src/lib/credentials-generator-asc-key.ts
28378
28399
  const toUserRole = (role) => role === "APP_MANAGER" ? AppleUtils.UserRole.APP_MANAGER : AppleUtils.UserRole.ADMIN;
28400
+ const ASC_API_KEY_NICKNAME_MAX_LENGTH = 30;
28401
+ const clampAscApiKeyNickname = (nickname) => nickname.slice(0, ASC_API_KEY_NICKNAME_MAX_LENGTH);
28379
28402
  /** Default nickname shown in App Store Connect → Users and Access → Integrations. */
28380
- const defaultAscApiKeyNickname = () => `[better-update] ${(/* @__PURE__ */ new Date()).toISOString()}`;
28403
+ const defaultAscApiKeyNickname = () => `[better-update] ${Date.now().toString(36)}`;
28381
28404
  const ASC_KEY_NOT_READY_PATTERN = /no resource of type|resource does not exist/iu;
28382
28405
  const ASC_KEY_DOWNLOAD_RETRY = Schedule.exponential("1 second", 2).pipe(Schedule.intersect(Schedule.recurs(6)));
28383
28406
  const downloadAscKeyWithRetry = (key) => Effect.tryPromise({
@@ -28411,7 +28434,7 @@ const writeRescueP8 = (keyId, p8Pem) => Effect.gen(function* () {
28411
28434
  const generateAndUploadAscApiKeyViaAppleId = (api, input) => Effect.gen(function* () {
28412
28435
  const ctx = input.context;
28413
28436
  const key = yield* wrap("apple-create-asc-key", async () => AppleUtils.ApiKey.createAsync(ctx, {
28414
- nickname: input.nickname,
28437
+ nickname: clampAscApiKeyNickname(input.nickname),
28415
28438
  allAppsVisible: true,
28416
28439
  roles: [toUserRole(input.role)],
28417
28440
  keyType: AppleUtils.ApiKeyType.PUBLIC_API
@@ -28459,6 +28482,20 @@ const generateAndUploadAscApiKeyViaAppleId = (api, input) => Effect.gen(function
28459
28482
  role: input.role
28460
28483
  };
28461
28484
  });
28485
+ /**
28486
+ * List the team's active App Store Connect API keys as seen on Apple (via the
28487
+ * cookie session). Used before auto-creating a key to avoid making a redundant
28488
+ * one — note a key's `.p8` is downloadable only once, so a key listed here is
28489
+ * usable for publishing only if its `.p8` was captured at creation (i.e. it is
28490
+ * already in the vault). Surfacing them lets the caller warn + respect Apple's
28491
+ * per-team key cap rather than blindly creating another.
28492
+ */
28493
+ const listAscApiKeysViaAppleId = (ctx) => Effect.gen(function* () {
28494
+ return (yield* wrap("apple-list-asc-keys", async () => AppleUtils.ApiKey.getAsync(ctx))).filter((key) => key.attributes.isActive).map((key) => ({
28495
+ keyId: key.id,
28496
+ nickname: key.attributes.nickname
28497
+ }));
28498
+ });
28462
28499
 
28463
28500
  //#endregion
28464
28501
  //#region src/application/credentials-manager-ios-asc.ts
@@ -30510,7 +30547,7 @@ const ascKeyCommand = defineCommand({
30510
30547
  },
30511
30548
  nickname: {
30512
30549
  type: "string",
30513
- description: "Nickname shown in App Store Connect (defaults to a timestamped name)"
30550
+ description: "Nickname shown in App Store Connect (defaults to a timestamped name; Apple caps it at 30 chars, longer values are truncated)"
30514
30551
  }
30515
30552
  },
30516
30553
  run: async ({ args }) => runEffect(Effect.gen(function* () {
@@ -35518,6 +35555,78 @@ const statusCommand = defineCommand({
35518
35555
  }), { json: "value" })
35519
35556
  });
35520
35557
 
35558
+ //#endregion
35559
+ //#region src/application/submit-asc-key.ts
35560
+ const ROLE_CHOICES = [{
35561
+ value: "ADMIN",
35562
+ label: "ADMIN (default)"
35563
+ }, {
35564
+ value: "APP_MANAGER",
35565
+ label: "APP_MANAGER (least privilege for app management)"
35566
+ }];
35567
+ const CREATE_CHOICE = "__create__";
35568
+ /** Best-effort: write the resolved id back to eas.json so the next run reuses it. */
35569
+ const persist = (input, keyId) => setSubmitProfileAscApiKeyId(input.projectRoot, input.profileName, keyId).pipe(Effect.flatMap((path) => printHuman(`Saved ascApiKeyId to ${path} (submit profile "${input.profileName}") for reuse.`)), Effect.catchAll((error) => printHuman(`Note: could not write ascApiKeyId to eas.json (${error.message}). Add it manually to reuse this key.`)));
35570
+ /** Log in, warn about any existing team keys, then (with consent) create + persist. */
35571
+ const createAndPersist = (input) => Effect.gen(function* () {
35572
+ const auth = yield* AppleAuth;
35573
+ const session = yield* auth.ensureLoggedIn();
35574
+ const ctx = auth.buildRequestContext(session);
35575
+ const teamKeys = yield* listAscApiKeysViaAppleId(ctx).pipe(Effect.orElseSucceed(() => []));
35576
+ const hasTeamKeys = teamKeys.length > 0;
35577
+ if (hasTeamKeys) {
35578
+ yield* printHuman(`Your Apple team already has ${String(teamKeys.length)} App Store Connect API key(s): ${teamKeys.map((key) => key.nickname).join(", ")}.`);
35579
+ yield* printHuman("A key's .p8 is downloadable only once at creation, so an existing key is reusable only if you still have its .p8 (import it with `credentials upload-asc-key`). Apple also caps the number of keys per team.");
35580
+ }
35581
+ if (!(yield* promptConfirm(hasTeamKeys ? "Create a new ASC API key anyway?" : "No App Store Connect API key found. Create one now from your Apple ID?", { initialValue: !hasTeamKeys }))) return null;
35582
+ const role = yield* promptSelect("Select a role for the generated API key", ROLE_CHOICES);
35583
+ yield* printHuman("Creating an App Store Connect API key via your Apple ID...");
35584
+ const created = yield* generateAndUploadAscApiKeyViaAppleId(input.api, {
35585
+ context: ctx,
35586
+ appleTeamIdentifier: session.teamId,
35587
+ nickname: defaultAscApiKeyNickname(),
35588
+ role
35589
+ });
35590
+ yield* printHuman(`Created and stored ASC API key ${created.keyId}.`);
35591
+ yield* persist(input, created.id);
35592
+ return created.id;
35593
+ });
35594
+ /**
35595
+ * Resolve an ASC API key id to upload a `submit` build with when none is set in
35596
+ * the submit profile. Reuses a stored vault key when possible (the only keys we
35597
+ * hold a usable `.p8` for), else offers to create one from the Apple ID session.
35598
+ * Returns the resolved id, or `null` when none could be resolved — non-interactive
35599
+ * runs, a declined prompt, or any failure (login/create/network) degrade to `null`
35600
+ * so the caller falls back to queuing the submission with guidance rather than
35601
+ * crashing. Persists the resolved id to `eas.json` for reuse.
35602
+ */
35603
+ const ensureAscApiKeyForSubmit = (input) => Effect.gen(function* () {
35604
+ if (!(yield* InteractiveMode).allow) return null;
35605
+ const stored = yield* input.api.ascApiKeys.list();
35606
+ if (stored.items.length === 1) {
35607
+ const [only] = stored.items;
35608
+ if (only !== void 0) {
35609
+ yield* printHuman(`Using your stored ASC API key "${only.name}" (${only.keyId}).`);
35610
+ yield* persist(input, only.id);
35611
+ return only.id;
35612
+ }
35613
+ }
35614
+ if (stored.items.length > 1) {
35615
+ const picked = yield* promptSelect("No ASC API key in this submit profile. Pick one to use, or create a new one:", [...stored.items.map((key) => ({
35616
+ value: key.id,
35617
+ label: `${key.name} (${key.keyId})`
35618
+ })), {
35619
+ value: CREATE_CHOICE,
35620
+ label: "Create a new ASC API key from my Apple ID"
35621
+ }]);
35622
+ if (picked !== CREATE_CHOICE) {
35623
+ yield* persist(input, picked);
35624
+ return picked;
35625
+ }
35626
+ }
35627
+ return yield* createAndPersist(input);
35628
+ }).pipe(Effect.catchAll((error) => printHuman(`Could not set up an App Store Connect API key (${messageOf(error)}). The submission was queued — create one with \`credentials generate asc-key\` and re-run.`).pipe(Effect.as(null))));
35629
+
35521
35630
  //#endregion
35522
35631
  //#region src/commands/submit/index.ts
35523
35632
  const PLATFORMS = ["ios", "android"];
@@ -35604,7 +35713,17 @@ const runFlow = (api, projectId, args) => Effect.gen(function* () {
35604
35713
  appleId: iosProfile?.appleId,
35605
35714
  ascApiKeyId: iosProfile?.ascApiKeyId,
35606
35715
  hasAppSpecificPassword: hasAppleAppSpecificPassword()
35607
- });
35716
+ }) ?? (yield* Effect.gen(function* () {
35717
+ const resolvedKeyId = yield* ensureAscApiKeyForSubmit({
35718
+ api,
35719
+ projectRoot: args.projectRoot,
35720
+ profileName: args.profile
35721
+ });
35722
+ return resolvedKeyId === null ? null : {
35723
+ kind: "asc-api-key",
35724
+ ascApiKeyId: resolvedKeyId
35725
+ };
35726
+ }));
35608
35727
  if (auth === null) {
35609
35728
  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.");
35610
35729
  return submission;
@@ -35618,7 +35737,7 @@ const runFlow = (api, projectId, args) => Effect.gen(function* () {
35618
35737
  value: args.archive.archiveUrl
35619
35738
  },
35620
35739
  auth,
35621
- ascApiKeyId: iosProfile?.ascApiKeyId,
35740
+ ascApiKeyId: auth.kind === "asc-api-key" ? auth.ascApiKeyId : iosProfile?.ascApiKeyId,
35622
35741
  config: {
35623
35742
  bundleIdentifier: iosConfig.bundleIdentifier,
35624
35743
  ascAppId: iosProfile?.ascAppId,
@@ -35702,7 +35821,8 @@ const submitCommand = defineCommand({
35702
35821
  }
35703
35822
  const projectId = yield* readProjectId;
35704
35823
  const api = yield* apiClient;
35705
- const easProfile = yield* readSubmitProfile(yield* (yield* CliRuntime).cwd, args.profile);
35824
+ const projectRoot = yield* (yield* CliRuntime).cwd;
35825
+ const easProfile = yield* readSubmitProfile(projectRoot, args.profile);
35706
35826
  const archive = yield* resolveArchive(api, projectId, platform, {
35707
35827
  id: args.id,
35708
35828
  path: args.path,
@@ -35716,6 +35836,7 @@ const submitCommand = defineCommand({
35716
35836
  yield* runFlow(api, projectId, {
35717
35837
  platform,
35718
35838
  profile: args.profile,
35839
+ projectRoot,
35719
35840
  easProfile,
35720
35841
  archive,
35721
35842
  wait: args.wait,