@better-update/cli 0.44.0 → 0.45.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.44.0";
38
+ var version = "0.45.0";
39
39
 
40
40
  //#endregion
41
41
  //#region src/lib/interactive-mode.ts
@@ -23861,7 +23861,7 @@ const wrapKeyCreate = (run) => Effect.tryPromise({
23861
23861
  });
23862
23862
  }
23863
23863
  });
23864
- const writeRescueP8 = (keyId, p8Pem) => Effect.gen(function* () {
23864
+ const writeRescueP8$1 = (keyId, p8Pem) => Effect.gen(function* () {
23865
23865
  const fs = yield* FileSystem.FileSystem;
23866
23866
  const filePath = `AuthKey_${keyId}.p8`;
23867
23867
  yield* fs.writeFileString(filePath, p8Pem, { mode: 384 });
@@ -23892,7 +23892,7 @@ const generateAndUploadApnsKeyViaAppleId = (api, input) => Effect.gen(function*
23892
23892
  ...compact({ appleTeamName: toOptional(input.appleTeamName) })
23893
23893
  } });
23894
23894
  }).pipe(Effect.catchAll((cause) => Effect.gen(function* () {
23895
- const rescuePath = yield* writeRescueP8(key.id, p8Pem).pipe(Effect.catchAll(() => Effect.succeed(null)));
23895
+ const rescuePath = yield* writeRescueP8$1(key.id, p8Pem).pipe(Effect.catchAll(() => Effect.succeed(null)));
23896
23896
  const where = rescuePath === null ? "could not be saved locally and is now unrecoverable" : `was saved to ${rescuePath} — re-import with \`credentials generate push-key --p8 ${rescuePath} --key-id ${key.id} --apple-team-id ${input.appleTeamIdentifier}\``;
23897
23897
  return yield* new AppleIdGenerateFailedError({
23898
23898
  step: "store-apns-key",
@@ -28373,6 +28373,93 @@ const androidMenu = (ctx) => Effect.gen(function* () {
28373
28373
  yield* androidMenu(ctx);
28374
28374
  });
28375
28375
 
28376
+ //#endregion
28377
+ //#region src/lib/credentials-generator-asc-key.ts
28378
+ const toUserRole = (role) => role === "APP_MANAGER" ? AppleUtils.UserRole.APP_MANAGER : AppleUtils.UserRole.ADMIN;
28379
+ /** Default nickname shown in App Store Connect → Users and Access → Integrations. */
28380
+ const defaultAscApiKeyNickname = () => `[better-update] ${(/* @__PURE__ */ new Date()).toISOString()}`;
28381
+ const ASC_KEY_NOT_READY_PATTERN = /no resource of type|resource does not exist/iu;
28382
+ const ASC_KEY_DOWNLOAD_RETRY = Schedule.exponential("1 second", 2).pipe(Schedule.intersect(Schedule.recurs(6)));
28383
+ const downloadAscKeyWithRetry = (key) => Effect.tryPromise({
28384
+ try: async () => key.downloadAsync(),
28385
+ catch: (cause) => new AppleIdGenerateFailedError({
28386
+ step: "apple-download-asc-key",
28387
+ message: messageOf(cause)
28388
+ })
28389
+ }).pipe(Effect.flatMap((pem) => pem === null || pem.length === 0 ? Effect.fail(new AppleIdGenerateFailedError({
28390
+ step: "apple-download-asc-key",
28391
+ message: "App Store Connect returned no private key for the new API key — it may already have been downloaded. A key can only be downloaded once; create a new one or upload the .p8 manually with `credentials upload-asc-key`."
28392
+ })) : Effect.succeed(pem)), Effect.retry({
28393
+ while: (error) => ASC_KEY_NOT_READY_PATTERN.test(error.message),
28394
+ schedule: ASC_KEY_DOWNLOAD_RETRY
28395
+ }), Effect.catchIf((error) => ASC_KEY_NOT_READY_PATTERN.test(error.message), () => Effect.fail(new AppleIdGenerateFailedError({
28396
+ step: "apple-download-asc-key",
28397
+ message: "App Store Connect is still provisioning the new API key (this can take up to a minute). The key was created — re-run shortly, or download the .p8 from App Store Connect → Users and Access → Integrations and import it with `credentials upload-asc-key`."
28398
+ }))));
28399
+ const writeRescueP8 = (keyId, p8Pem) => Effect.gen(function* () {
28400
+ const fs = yield* FileSystem.FileSystem;
28401
+ const filePath = `AuthKey_${keyId}.p8`;
28402
+ yield* fs.writeFileString(filePath, p8Pem, { mode: 384 });
28403
+ return filePath;
28404
+ });
28405
+ /**
28406
+ * Create an App Store Connect API key from the Apple ID cookie session, download
28407
+ * its one-shot `.p8`, resolve the issuer id, and store the sealed envelope in the
28408
+ * vault — the zero-knowledge equivalent of downloading a key from App Store Connect
28409
+ * and running `credentials upload-asc-key`, but without the manual round-trip.
28410
+ */
28411
+ const generateAndUploadAscApiKeyViaAppleId = (api, input) => Effect.gen(function* () {
28412
+ const ctx = input.context;
28413
+ const key = yield* wrap("apple-create-asc-key", async () => AppleUtils.ApiKey.createAsync(ctx, {
28414
+ nickname: input.nickname,
28415
+ allAppsVisible: true,
28416
+ roles: [toUserRole(input.role)],
28417
+ keyType: AppleUtils.ApiKeyType.PUBLIC_API
28418
+ }));
28419
+ const p8Pem = yield* downloadAscKeyWithRetry(key);
28420
+ const displayName = input.name ?? key.id;
28421
+ const stored = yield* Effect.gen(function* () {
28422
+ const issuerId = (yield* wrap("apple-fetch-asc-key", async () => AppleUtils.ApiKey.infoAsync(ctx, { id: key.id }))).attributes.provider?.id;
28423
+ if (issuerId === void 0 || issuerId.length === 0) return yield* new AppleIdGenerateFailedError({
28424
+ step: "resolve-issuer-id",
28425
+ message: "App Store Connect did not return an issuer ID for the new key. Find it under Users and Access → Integrations and import the .p8 with `credentials upload-asc-key`."
28426
+ });
28427
+ const metadata = compact({
28428
+ name: displayName,
28429
+ keyId: key.id,
28430
+ issuerId,
28431
+ appleTeamIdentifier: input.appleTeamIdentifier
28432
+ });
28433
+ const envelope = yield* sealForUpload({
28434
+ session: yield* openVaultSessionInteractive(api),
28435
+ credentialType: "asc-api-key",
28436
+ metadata,
28437
+ secret: { p8Pem }
28438
+ });
28439
+ return {
28440
+ id: (yield* api.ascApiKeys.upload({ payload: {
28441
+ ...toUploadEnvelope(envelope),
28442
+ ...metadata
28443
+ } })).id,
28444
+ issuerId
28445
+ };
28446
+ }).pipe(Effect.catchAll((cause) => Effect.gen(function* () {
28447
+ const rescuePath = yield* writeRescueP8(key.id, p8Pem).pipe(Effect.catchAll(() => Effect.succeed(null)));
28448
+ const where = rescuePath === null ? "could not be saved locally and is now unrecoverable" : `was saved to ${rescuePath} — re-import with \`credentials upload-asc-key --p8 ${rescuePath} --key-id ${key.id}\` (find the issuer ID under App Store Connect → Users and Access → Integrations)`;
28449
+ return yield* new AppleIdGenerateFailedError({
28450
+ step: "store-asc-key",
28451
+ message: `Created App Store Connect API key ${key.id} on Apple but failed to store it (${messageOf(cause)}). The downloaded .p8 ${where}.`
28452
+ });
28453
+ })));
28454
+ return {
28455
+ id: stored.id,
28456
+ keyId: key.id,
28457
+ issuerId: stored.issuerId,
28458
+ name: displayName,
28459
+ role: input.role
28460
+ };
28461
+ });
28462
+
28376
28463
  //#endregion
28377
28464
  //#region src/application/credentials-manager-ios-asc.ts
28378
28465
  const uploadIosAscKey = (ctx) => Effect.gen(function* () {
@@ -28392,6 +28479,33 @@ const uploadIosAscKey = (ctx) => Effect.gen(function* () {
28392
28479
  yield* Console.log("ASC API key uploaded.");
28393
28480
  yield* printKeyValue([["ID", created.id], ["Key ID", keyId]]);
28394
28481
  });
28482
+ const generateAscKeyViaAppleId = (ctx) => Effect.gen(function* () {
28483
+ const auth = yield* AppleAuth;
28484
+ const session = yield* auth.ensureLoggedIn();
28485
+ const role = yield* promptSelect("Select a role for the generated API key", [{
28486
+ value: "ADMIN",
28487
+ label: "ADMIN (default)"
28488
+ }, {
28489
+ value: "APP_MANAGER",
28490
+ label: "APP_MANAGER (least privilege for app management)"
28491
+ }]);
28492
+ yield* Console.log("Creating an App Store Connect API key via your Apple ID...");
28493
+ return yield* generateAndUploadAscApiKeyViaAppleId(ctx.api, {
28494
+ context: auth.buildRequestContext(session),
28495
+ appleTeamIdentifier: session.teamId,
28496
+ nickname: defaultAscApiKeyNickname(),
28497
+ role
28498
+ });
28499
+ });
28500
+ const createIosAscKeyViaAppleId = (ctx) => Effect.gen(function* () {
28501
+ const created = yield* generateAscKeyViaAppleId(ctx);
28502
+ yield* Console.log(`Created and stored ASC API key ${created.keyId}.`);
28503
+ yield* printKeyValue([
28504
+ ["ID", created.id],
28505
+ ["Key ID", created.keyId],
28506
+ ["Issuer ID", created.issuerId]
28507
+ ]);
28508
+ });
28395
28509
  const bindIosAscKey = (ctx) => Effect.gen(function* () {
28396
28510
  const keys = yield* ctx.api.ascApiKeys.list();
28397
28511
  if (keys.items.length === 0) return yield* new MissingCredentialsError({
@@ -28428,18 +28542,26 @@ const uploadNewAscKey = (ctx) => Effect.gen(function* () {
28428
28542
  });
28429
28543
  const setupProjectAscApiKey = (ctx) => Effect.gen(function* () {
28430
28544
  const keys = yield* ctx.api.ascApiKeys.list();
28431
- const choice = keys.items.length === 0 ? "upload" : yield* promptSelect("How would you like to set up the ASC key?", [{
28432
- value: "existing",
28433
- label: `Use an existing ASC API key (${String(keys.items.length)})`
28545
+ const baseChoices = [{
28546
+ value: "generate",
28547
+ label: "Create a new ASC API key from your Apple ID (no .p8 needed)"
28434
28548
  }, {
28435
28549
  value: "upload",
28436
- label: "Upload a new ASC API key"
28437
- }]);
28550
+ label: "Upload an existing .p8 key"
28551
+ }];
28552
+ const choice = keys.items.length === 0 ? yield* promptSelect("How would you like to set up the ASC key?", baseChoices) : yield* promptSelect("How would you like to set up the ASC key?", [{
28553
+ value: "existing",
28554
+ label: `Use an existing ASC API key (${String(keys.items.length)})`
28555
+ }, ...baseChoices]);
28438
28556
  const config = yield* promptForBundleConfig(ctx);
28439
- const ascKeyId = choice === "upload" ? yield* uploadNewAscKey(ctx) : yield* promptSelect("Select an ASC API key to bind", keys.items.map((key) => ({
28440
- value: key.id,
28441
- label: `${key.name} (${key.keyId})`
28442
- })));
28557
+ const ascKeyId = yield* Effect.gen(function* () {
28558
+ if (choice === "generate") return (yield* generateAscKeyViaAppleId(ctx)).id;
28559
+ if (choice === "upload") return yield* uploadNewAscKey(ctx);
28560
+ return yield* promptSelect("Select an ASC API key to bind", keys.items.map((key) => ({
28561
+ value: key.id,
28562
+ label: `${key.name} (${key.keyId})`
28563
+ })));
28564
+ });
28443
28565
  yield* ctx.api.iosBundleConfigurations.update({
28444
28566
  path: { id: config.id },
28445
28567
  payload: { ascApiKeyId: ascKeyId }
@@ -28453,6 +28575,10 @@ const iosAscKeysMenu = (ctx) => Effect.gen(function* () {
28453
28575
  value: "setup",
28454
28576
  label: "Set up your project to use an ASC API Key"
28455
28577
  },
28578
+ {
28579
+ value: "create",
28580
+ label: "Create a new ASC API key from your Apple ID (no .p8 needed)"
28581
+ },
28456
28582
  {
28457
28583
  value: "upload",
28458
28584
  label: "Add a new ASC API key"
@@ -28472,6 +28598,7 @@ const iosAscKeysMenu = (ctx) => Effect.gen(function* () {
28472
28598
  ]));
28473
28599
  if (choice === "__back__") return;
28474
28600
  if (choice === "setup") yield* safely("set up ASC key", setupProjectAscApiKey(ctx));
28601
+ else if (choice === "create") yield* safely("create ASC key", createIosAscKeyViaAppleId(ctx));
28475
28602
  else if (choice === "upload") yield* safely("upload ASC key", uploadIosAscKey(ctx));
28476
28603
  else if (choice === "bind") yield* safely("bind ASC key", bindIosAscKey(ctx));
28477
28604
  else if (choice === "delete") yield* safely("delete ASC key", pickAndDelete(ctx, "asc-api-key", "ASC API key"));
@@ -30353,6 +30480,66 @@ const envVaultCommand = defineCommand({
30353
30480
  default: "status"
30354
30481
  });
30355
30482
 
30483
+ //#endregion
30484
+ //#region src/commands/credentials/generate-asc-key.ts
30485
+ const ASC_KEY_EXIT_EXTRAS = {
30486
+ CredentialValidationError: 2,
30487
+ AppleIdGenerateFailedError: 6,
30488
+ AppleAuthError: 4,
30489
+ InteractiveProhibitedError: 4
30490
+ };
30491
+ const normalizeRole = (raw) => {
30492
+ if (raw === void 0) return Effect.succeed("ADMIN");
30493
+ const upper = raw.trim().toUpperCase();
30494
+ if (upper === "ADMIN" || upper === "APP_MANAGER") return Effect.succeed(upper);
30495
+ return Effect.fail(new CredentialValidationError({ message: `Unknown ASC API key role "${raw}". Use ADMIN or APP_MANAGER.` }));
30496
+ };
30497
+ const ascKeyCommand = defineCommand({
30498
+ meta: {
30499
+ name: "asc-key",
30500
+ description: "Create an App Store Connect API key (.p8) directly from your Apple ID login — no manual download. Stored encrypted in your vault, it can issue certificates, sync devices, and upload builds. Requires the Account Holder to have agreed to the API Terms once under Users and Access → Integrations."
30501
+ },
30502
+ args: {
30503
+ role: {
30504
+ type: "string",
30505
+ description: "ADMIN (default) or APP_MANAGER (least privilege)"
30506
+ },
30507
+ name: {
30508
+ type: "string",
30509
+ description: "Display name for the stored key (defaults to the key ID)"
30510
+ },
30511
+ nickname: {
30512
+ type: "string",
30513
+ description: "Nickname shown in App Store Connect (defaults to a timestamped name)"
30514
+ }
30515
+ },
30516
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
30517
+ const role = yield* normalizeRole(args.role);
30518
+ const api = yield* apiClient;
30519
+ const auth = yield* AppleAuth;
30520
+ const session = yield* auth.ensureLoggedIn();
30521
+ yield* printHuman("Creating an App Store Connect API key via your Apple ID...");
30522
+ const created = yield* generateAndUploadAscApiKeyViaAppleId(api, {
30523
+ context: auth.buildRequestContext(session),
30524
+ appleTeamIdentifier: session.teamId,
30525
+ nickname: args.nickname ?? defaultAscApiKeyNickname(),
30526
+ role,
30527
+ ...compact({ name: args.name })
30528
+ });
30529
+ yield* printHuman("App Store Connect API key created and stored.");
30530
+ yield* printHumanKeyValue([
30531
+ ["ID", created.id],
30532
+ ["Key ID", created.keyId],
30533
+ ["Issuer ID", created.issuerId],
30534
+ ["Role", created.role]
30535
+ ]);
30536
+ return created;
30537
+ }), {
30538
+ exits: ASC_KEY_EXIT_EXTRAS,
30539
+ json: "value"
30540
+ })
30541
+ });
30542
+
30356
30543
  //#endregion
30357
30544
  //#region src/lib/credentials-generator-merchant.ts
30358
30545
  /**
@@ -30856,6 +31043,7 @@ const generateCommand$1 = defineCommand({
30856
31043
  "provisioning-profile": provisioningProfileCommand,
30857
31044
  "push-key": pushKeyCommand$1,
30858
31045
  "merchant-id": merchantIdCommand,
31046
+ "asc-key": ascKeyCommand,
30859
31047
  "gsa-key": gsaKeyCommand
30860
31048
  }
30861
31049
  });