@better-update/cli 0.32.0 → 0.33.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
@@ -34,7 +34,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
34
34
 
35
35
  //#endregion
36
36
  //#region package.json
37
- var version = "0.32.0";
37
+ var version = "0.33.0";
38
38
 
39
39
  //#endregion
40
40
  //#region src/lib/interactive-mode.ts
@@ -3078,8 +3078,10 @@ const AppleSessionStoreLive = Layer.effect(AppleSessionStore, Effect.gen(functio
3078
3078
  const defaultAppleUtils = {
3079
3079
  Auth: AppleUtils.Auth,
3080
3080
  Session: AppleUtils.Session,
3081
+ Teams: AppleUtils.Teams,
3081
3082
  CookieFileCache: AppleUtils.CookieFileCache
3082
3083
  };
3084
+ const TEN_CHAR_TEAM_ID = /^[A-Z0-9]{10}$/u;
3083
3085
  var AppleAuth = class extends Context.Tag("cli/AppleAuth")() {};
3084
3086
  const sessionFromAuthState = (state) => ({
3085
3087
  username: state.username,
@@ -3093,11 +3095,19 @@ const sessionFromInfo = (username, info) => ({
3093
3095
  teamName: info.provider.name,
3094
3096
  providerId: info.provider.providerId
3095
3097
  });
3096
- const sessionFromProvider = (username, provider) => ({
3097
- username,
3098
- teamId: provider.publicProviderId,
3099
- teamName: provider.name,
3100
- providerId: provider.providerId
3098
+ /**
3099
+ * Resolve the 10-char Developer Portal Team ID for a selected App Store Connect
3100
+ * provider. Uses the provider's `publicProviderId` directly when it already is a
3101
+ * Team ID; otherwise (UUID providers) looks it up from the Developer Portal team
3102
+ * list by name — the only field both surfaces share. Falls back to the
3103
+ * `publicProviderId` if no match, preserving prior behavior.
3104
+ */
3105
+ const resolvePortalTeamId = (appleUtils, provider) => Effect.gen(function* () {
3106
+ if (TEN_CHAR_TEAM_ID.test(provider.publicProviderId)) return provider.publicProviderId;
3107
+ return (yield* Effect.tryPromise({
3108
+ try: async () => appleUtils.Teams.getTeamsAsync(),
3109
+ catch: (cause) => new AppleAuthError$1({ message: `Failed to list Apple Developer teams: ${formatCause(cause)}` })
3110
+ })).find((team) => team.name === provider.name)?.teamId ?? provider.publicProviderId;
3101
3111
  });
3102
3112
  const restoreFromCookies = (appleUtils, cookies) => Effect.tryPromise({
3103
3113
  try: async () => appleUtils.Auth.loginWithCookiesAsync({ cookies }),
@@ -3112,10 +3122,16 @@ const restoreFromCookies = (appleUtils, cookies) => Effect.tryPromise({
3112
3122
  const resolveSessionTeam = (appleUtils, state) => Effect.gen(function* () {
3113
3123
  const { availableProviders } = state.session;
3114
3124
  const resolution = yield* resolveProvider(appleUtils, availableProviders, state.context.providerId ?? state.session.provider.providerId);
3115
- if (!resolution.switched || resolution.providerId === void 0) return sessionFromAuthState(state);
3116
- const picked = availableProviders.find((provider) => provider.providerId === resolution.providerId);
3117
- if (picked === void 0) return yield* new AppleAuthError$1({ message: `Selected provider ${String(resolution.providerId)} not in available providers list.` });
3118
- return sessionFromProvider(state.username, picked);
3125
+ const switched = resolution.switched && resolution.providerId !== void 0;
3126
+ const provider = switched ? availableProviders.find((entry) => entry.providerId === resolution.providerId) : state.session.provider;
3127
+ if (provider === void 0) return yield* new AppleAuthError$1({ message: `Selected provider ${String(resolution.providerId)} not in available providers list.` });
3128
+ 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));
3129
+ return {
3130
+ username: state.username,
3131
+ teamId,
3132
+ teamName: provider.name,
3133
+ providerId: provider.providerId
3134
+ };
3119
3135
  });
3120
3136
  const loginWithCredentials = (appleUtils, credentials) => Effect.tryPromise({
3121
3137
  try: async () => appleUtils.Auth.loginWithUserCredentialsAsync(credentials, { autoResolveProvider: true }),
@@ -20755,6 +20771,7 @@ const DISTRIBUTION_TO_CERTIFICATE_TYPE = {
20755
20771
  DEVELOPMENT: AppleUtils.CertificateType.IOS_DEVELOPMENT
20756
20772
  };
20757
20773
  var AppleIdGenerateFailedError = class extends Data.TaggedError("AppleIdGenerateFailedError") {};
20774
+ var ApnsKeyLimitError = class extends Data.TaggedError("ApnsKeyLimitError") {};
20758
20775
  const CERT_LIMIT_PATTERN = /already have a current.*certificate|pending certificate request/iu;
20759
20776
  const messageOf = (cause) => cause instanceof Error ? cause.message : String(cause);
20760
20777
  const wrap = (step, run) => Effect.tryPromise({
@@ -20899,6 +20916,86 @@ const generateAndUploadProvisioningProfileViaAppleId = (api, input) => Effect.ge
20899
20916
  developerPortalIdentifier: created.developerPortalIdentifier
20900
20917
  };
20901
20918
  });
20919
+ const APNS_SERVICE_ID = "U27F4V844T";
20920
+ const APNS_KEY_LIMIT_PATTERN = /maximum allowed number of .*keys/iu;
20921
+ const wrapKeyCreate = (run) => Effect.tryPromise({
20922
+ try: run,
20923
+ catch: (cause) => {
20924
+ const message = messageOf(cause);
20925
+ return cause instanceof AppleUtils.Keys.MaxKeysCreatedError || APNS_KEY_LIMIT_PATTERN.test(message) ? new ApnsKeyLimitError({ message }) : new AppleIdGenerateFailedError({
20926
+ step: "apple-create-key",
20927
+ message
20928
+ });
20929
+ }
20930
+ });
20931
+ const writeRescueP8 = (keyId, p8Pem) => Effect.gen(function* () {
20932
+ const fs = yield* FileSystem.FileSystem;
20933
+ const filePath = `AuthKey_${keyId}.p8`;
20934
+ yield* fs.writeFileString(filePath, p8Pem, { mode: 384 });
20935
+ return filePath;
20936
+ });
20937
+ const generateAndUploadApnsKeyViaAppleId = (api, input) => Effect.gen(function* () {
20938
+ const ctx = input.context;
20939
+ const key = yield* wrapKeyCreate(async () => AppleUtils.Keys.createKeyAsync(ctx, {
20940
+ name: input.name,
20941
+ isApns: true
20942
+ }));
20943
+ const p8Pem = yield* wrap("apple-download-key", async () => AppleUtils.Keys.downloadKeyAsync(ctx, { id: key.id }));
20944
+ const metadata = {
20945
+ keyId: key.id,
20946
+ appleTeamIdentifier: input.appleTeamIdentifier
20947
+ };
20948
+ return {
20949
+ id: (yield* Effect.gen(function* () {
20950
+ const envelope = yield* sealForUpload({
20951
+ session: yield* openVaultSessionInteractive(api),
20952
+ credentialType: "push-key",
20953
+ metadata,
20954
+ secret: { p8Pem }
20955
+ });
20956
+ return yield* api.applePushKeys.upload({ payload: {
20957
+ ...toUploadEnvelope(envelope),
20958
+ ...metadata
20959
+ } });
20960
+ }).pipe(Effect.catchAll((cause) => Effect.gen(function* () {
20961
+ const rescuePath = yield* writeRescueP8(key.id, p8Pem).pipe(Effect.catchAll(() => Effect.succeed(null)));
20962
+ 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}\``;
20963
+ return yield* new AppleIdGenerateFailedError({
20964
+ step: "store-apns-key",
20965
+ message: `Created APNs key ${key.id} on Apple but failed to store it (${messageOf(cause)}). The downloaded .p8 ${where}.`
20966
+ });
20967
+ })))).id,
20968
+ keyId: key.id,
20969
+ appleTeamIdentifier: input.appleTeamIdentifier,
20970
+ name: key.name
20971
+ };
20972
+ });
20973
+ const listApnsKeysViaAppleId = (ctx) => Effect.gen(function* () {
20974
+ const keys = yield* wrap("apple-list-keys", async () => AppleUtils.Keys.getKeysAsync(ctx));
20975
+ return (yield* Effect.forEach(keys, (key) => wrap("apple-get-key-info", async () => AppleUtils.Keys.getKeyInfoAsync(ctx, { id: key.id })), { concurrency: 4 })).filter((info) => info.services.some((service) => service.id === APNS_SERVICE_ID)).map((info) => ({
20976
+ developerPortalKeyId: info.id,
20977
+ name: info.name,
20978
+ canRevoke: info.canRevoke
20979
+ }));
20980
+ });
20981
+ const revokeApnsKeyViaAppleId = (ctx, developerPortalKeyId) => wrap("apple-revoke-key", async () => AppleUtils.Keys.revokeKeyAsync(ctx, { id: developerPortalKeyId }));
20982
+ /**
20983
+ * Revoke an APNs key on Apple and (optionally) delete the stored copy. Only keys
20984
+ * still present on the portal are revoked — one already gone upstream is treated
20985
+ * as `revokedOnApple: false` and still deleted locally, so cleanup never wedges.
20986
+ * Shared by the `revoke push-key` command and the interactive wizard.
20987
+ */
20988
+ const revokeLocalApnsKey = (api, input) => Effect.gen(function* () {
20989
+ const present = (yield* listApnsKeysViaAppleId(input.context)).some((entry) => entry.developerPortalKeyId === input.keyId);
20990
+ if (present) yield* revokeApnsKeyViaAppleId(input.context, input.keyId);
20991
+ if (!input.keepLocal) yield* api.applePushKeys.delete({ path: { id: input.pushKeyId } });
20992
+ return {
20993
+ localId: input.pushKeyId,
20994
+ keyId: input.keyId,
20995
+ revokedOnApple: present,
20996
+ deletedLocally: !input.keepLocal
20997
+ };
20998
+ });
20902
20999
 
20903
21000
  //#endregion
20904
21001
  //#region src/lib/ios-bundle-config-upsert.ts
@@ -20971,6 +21068,36 @@ const interactiveAppleIdCertLimitRecover = (ctx) => Effect.gen(function* () {
20971
21068
  yield* Effect.forEach(toRevoke, (id) => revokeDistributionCertViaAppleId(ctx, id), { concurrency: "inherit" });
20972
21069
  yield* Console.log(`Revoked ${toRevoke.length} certificate(s); retrying generation...`);
20973
21070
  });
21071
+ const defaultApnsKeyName = () => `better-update APNs (${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)})`;
21072
+ const apnsKeyLimitRecover = (ctx) => Effect.gen(function* () {
21073
+ yield* Console.log("");
21074
+ yield* Console.log("Apple reports the APNs key limit was hit (max 2 keys per team).");
21075
+ const revocable = (yield* listApnsKeysViaAppleId(ctx)).filter((entry) => entry.canRevoke);
21076
+ if (revocable.length === 0) return yield* new CredentialValidationError({ message: "Apple says the APNs key limit is hit but no revocable keys were returned." });
21077
+ const toRevoke = yield* promptMultiSelect("Select one or more APNs keys to revoke before retrying", revocable.map((entry) => ({
21078
+ value: entry.developerPortalKeyId,
21079
+ label: `${entry.name} (${entry.developerPortalKeyId})`
21080
+ })), { required: true });
21081
+ yield* Effect.forEach(toRevoke, (id) => revokeApnsKeyViaAppleId(ctx, id), { concurrency: "inherit" });
21082
+ yield* Console.log(`Revoked ${toRevoke.length} key(s); retrying creation...`);
21083
+ });
21084
+ /**
21085
+ * Log in with Apple ID, create a fresh APNs `.p8` on the portal, download it, and
21086
+ * upload it end-to-end encrypted — recovering interactively from the key limit.
21087
+ * Returns the stored credential; callers render their own success output. Shared
21088
+ * by the `generate push-key` command and the interactive wizard.
21089
+ */
21090
+ const createApnsKeyViaAppleId = (api, name) => Effect.gen(function* () {
21091
+ const auth = yield* AppleAuth;
21092
+ const session = yield* auth.ensureLoggedIn();
21093
+ const ctx = auth.buildRequestContext(session);
21094
+ const generate = generateAndUploadApnsKeyViaAppleId(api, {
21095
+ context: ctx,
21096
+ appleTeamIdentifier: session.teamId,
21097
+ name
21098
+ });
21099
+ return yield* generate.pipe(Effect.catchTag("ApnsKeyLimitError", () => apnsKeyLimitRecover(ctx).pipe(Effect.flatMap(() => generate))));
21100
+ });
20974
21101
  const generateDistributionCertViaAppleIdInteractive = (api, ctx) => Effect.gen(function* () {
20975
21102
  yield* Console.log("Generating distribution certificate via Apple ID...");
20976
21103
  const generate = generateAndUploadDistributionCertificateViaAppleId(api, { context: ctx });
@@ -26992,6 +27119,39 @@ const revokeIosDistributionCert = (ctx) => Effect.gen(function* () {
26992
27119
  ["Deleted locally", result.deletedLocally ? "yes" : "no (kept)"]
26993
27120
  ]);
26994
27121
  });
27122
+ const revokeIosPushKey = (ctx) => Effect.gen(function* () {
27123
+ const { items } = yield* ctx.api.applePushKeys.list();
27124
+ if (items.length === 0) return yield* new MissingCredentialsError({
27125
+ message: "No APNs push keys in this account.",
27126
+ hint: "Run 'Add a new push key' to create one first."
27127
+ });
27128
+ const localId = yield* promptSelect("Select a push key to revoke", items.map((key) => ({
27129
+ value: key.id,
27130
+ label: `${key.keyId} (team ${key.appleTeamId})`
27131
+ })));
27132
+ const target = items.find((entry) => entry.id === localId);
27133
+ if (target === void 0) return yield* new MissingCredentialsError({
27134
+ message: `Selected push key ${localId} not found.`,
27135
+ hint: "Re-run and pick again."
27136
+ });
27137
+ const keepLocal = yield* promptConfirm("Keep the key in this account after revoking?", { initialValue: false });
27138
+ const auth = yield* AppleAuth;
27139
+ const session = yield* auth.ensureLoggedIn();
27140
+ yield* Console.log("Logging in to Apple and revoking the push key...");
27141
+ const result = yield* revokeLocalApnsKey(ctx.api, {
27142
+ context: auth.buildRequestContext(session),
27143
+ pushKeyId: target.id,
27144
+ keyId: target.keyId,
27145
+ keepLocal
27146
+ });
27147
+ yield* Console.log("Revoke complete.");
27148
+ yield* printKeyValue([
27149
+ ["Local ID", result.localId],
27150
+ ["Key ID", result.keyId],
27151
+ ["Revoked on Apple", result.revokedOnApple ? "yes" : "no (not present on portal)"],
27152
+ ["Deleted locally", result.deletedLocally ? "yes" : "no (kept)"]
27153
+ ]);
27154
+ });
26995
27155
 
26996
27156
  //#endregion
26997
27157
  //#region src/application/credentials-manager-ios.ts
@@ -27056,8 +27216,26 @@ const generateNewIosDistributionCert = (ctx) => Effect.gen(function* () {
27056
27216
  ["Apple team", created.appleTeamIdentifier]
27057
27217
  ]);
27058
27218
  });
27219
+ const promptPushKeyMethod = () => promptSelect("How do you want to provide the APNs auth key?", [{
27220
+ value: "apple-id",
27221
+ label: "Create a new key by logging in with your Apple ID (recommended)"
27222
+ }, {
27223
+ value: "upload",
27224
+ label: "Upload a .p8 you already downloaded from the Apple portal"
27225
+ }]);
27059
27226
  const addIosPushKey = (ctx) => Effect.gen(function* () {
27060
- yield* printHuman("Apple does not expose APNs key creation via API.");
27227
+ if ((yield* promptPushKeyMethod()) === "apple-id") {
27228
+ const created = yield* createApnsKeyViaAppleId(ctx.api, defaultApnsKeyName());
27229
+ yield* Console.log("APNs push key created and registered.");
27230
+ yield* printKeyValue([
27231
+ ["ID", created.id],
27232
+ ["Key ID", created.keyId],
27233
+ ["Apple team", created.appleTeamIdentifier],
27234
+ ["Name", created.name]
27235
+ ]);
27236
+ return;
27237
+ }
27238
+ yield* printHuman("Apple does not expose APNs key creation via the public ASC API.");
27061
27239
  yield* printHuman(`Create one here, download .p8, then return: ${APPLE_PUSH_KEY_PORTAL_URL$1}`);
27062
27240
  const keyId = (yield* promptText("APNs key ID (10 uppercase alphanumeric)")).trim().toUpperCase();
27063
27241
  if (!APPLE_TEN_CHARS.test(keyId)) return yield* new CredentialValidationError({ message: `Push key ID "${keyId}" must be 10 uppercase alphanumeric characters.` });
@@ -27119,7 +27297,12 @@ const setupProjectPushNotifications = (ctx) => Effect.gen(function* () {
27119
27297
  yield* Console.log(`Push notifications set up: key ${pushKeyId} bound to ${config.bundleIdentifier} (${config.distributionType}).`);
27120
27298
  });
27121
27299
  const createNewPushKeyForBundle = (ctx, fallbackTeamId) => Effect.gen(function* () {
27122
- yield* printHuman("Apple does not expose APNs key creation via API.");
27300
+ if ((yield* promptPushKeyMethod()) === "apple-id") {
27301
+ const created = yield* createApnsKeyViaAppleId(ctx.api, defaultApnsKeyName());
27302
+ yield* Console.log(`APNs push key ${created.keyId} created.`);
27303
+ return created.id;
27304
+ }
27305
+ yield* printHuman("Apple does not expose APNs key creation via the public ASC API.");
27123
27306
  yield* printHuman(`Create one here, download .p8, then return: ${APPLE_PUSH_KEY_PORTAL_URL$1}`);
27124
27307
  const rawKeyId = (yield* promptText("APNs key ID (10 uppercase alphanumeric)")).trim().toUpperCase();
27125
27308
  if (!APPLE_TEN_CHARS.test(rawKeyId)) return yield* new CredentialValidationError({ message: `Push key ID "${rawKeyId}" must be 10 uppercase alphanumeric characters.` });
@@ -27206,9 +27389,13 @@ const iosPushKeysMenu = (ctx) => Effect.gen(function* () {
27206
27389
  value: "bind",
27207
27390
  label: "Use an existing push key"
27208
27391
  },
27392
+ {
27393
+ value: "revoke",
27394
+ label: "Revoke a push key (Apple Developer Portal)"
27395
+ },
27209
27396
  {
27210
27397
  value: "remove",
27211
- label: "Remove a push key"
27398
+ label: "Remove a push key (local only)"
27212
27399
  },
27213
27400
  {
27214
27401
  value: BACK,
@@ -27219,6 +27406,7 @@ const iosPushKeysMenu = (ctx) => Effect.gen(function* () {
27219
27406
  if (choice === "setup") yield* safely("set up push notifications", setupProjectPushNotifications(ctx));
27220
27407
  else if (choice === "add") yield* safely("add push key", addIosPushKey(ctx));
27221
27408
  else if (choice === "bind") yield* safely("bind push key", bindIosPushKey(ctx));
27409
+ else if (choice === "revoke") yield* safely("revoke push key", revokeIosPushKey(ctx));
27222
27410
  else if (choice === "remove") yield* safely("remove push key", pickAndDelete(ctx, "push-key", "APNs push key"));
27223
27411
  yield* iosPushKeysMenu(ctx);
27224
27412
  });
@@ -28193,6 +28381,129 @@ const downloadCommand = defineCommand({
28193
28381
  }), { json: "value" })
28194
28382
  });
28195
28383
 
28384
+ //#endregion
28385
+ //#region src/commands/credentials/generate-push-key.ts
28386
+ const PUSH_KEY_EXIT_EXTRAS = {
28387
+ CredentialValidationError: 2,
28388
+ AppleIdGenerateFailedError: 6,
28389
+ ApnsKeyLimitError: 6,
28390
+ AppleAuthError: 4,
28391
+ InteractiveProhibitedError: 4
28392
+ };
28393
+ const APPLE_PUSH_KEY_PORTAL_URL = "https://developer.apple.com/account/resources/authkeys/list";
28394
+ const KEY_ID_PATTERN = /^[A-Z0-9]{10}$/u;
28395
+ const APPLE_TEAM_ID_PATTERN = /^[A-Z0-9]{10}$/u;
28396
+ const resolveAppleTeamFromAscKey = (api, ascApiKeyId) => Effect.gen(function* () {
28397
+ if (ascApiKeyId === void 0) return;
28398
+ const teamId = (yield* api.ascApiKeys.list()).items.find((entry) => entry.id === ascApiKeyId)?.appleTeamId;
28399
+ return typeof teamId === "string" ? teamId : void 0;
28400
+ });
28401
+ const validateKeyId = (value) => KEY_ID_PATTERN.test(value) ? Effect.succeed(value) : Effect.fail(new CredentialValidationError({ message: `Push key ID "${value}" must be 10 uppercase alphanumeric characters.` }));
28402
+ const validateAppleTeamId = (value) => APPLE_TEAM_ID_PATTERN.test(value) ? Effect.succeed(value) : Effect.fail(new CredentialValidationError({ message: `Apple Team ID "${value}" must be 10 uppercase alphanumeric characters.` }));
28403
+ const resolvePushKeyInput = (api, args) => Effect.gen(function* () {
28404
+ const derivedTeamId = yield* resolveAppleTeamFromAscKey(api, args["asc-key-id"]);
28405
+ const keyId = yield* validateKeyId((args["key-id"] ?? (yield* promptText("APNs key ID (10 uppercase alphanumeric)"))).trim().toUpperCase());
28406
+ const appleTeamIdentifier = yield* validateAppleTeamId((args["apple-team-id"] ?? derivedTeamId ?? (yield* promptText("Apple Team identifier (10 uppercase alphanumeric)"))).trim().toUpperCase());
28407
+ const p8Path = args.p8 ?? (yield* promptText("Path to the AuthKey_XXXXXXXXXX.p8 file you downloaded"));
28408
+ if (p8Path.trim().length === 0) return yield* new CredentialValidationError({ message: "Missing --p8 path" });
28409
+ return {
28410
+ keyId,
28411
+ appleTeamIdentifier,
28412
+ p8Path,
28413
+ name: args.name ?? keyId
28414
+ };
28415
+ });
28416
+ const resolvePushKeyMethod = (args) => Effect.gen(function* () {
28417
+ if (args.p8 !== void 0 && args.p8.trim().length > 0) return "upload";
28418
+ if (args.method === "upload" || args.method === "apple-id") return args.method;
28419
+ return yield* promptSelect("How do you want to provide the APNs auth key?", [{
28420
+ value: "apple-id",
28421
+ label: "Create a new key by logging in with your Apple ID (recommended)"
28422
+ }, {
28423
+ value: "upload",
28424
+ label: "Upload a .p8 you already downloaded from the Apple portal"
28425
+ }]);
28426
+ });
28427
+ const uploadPushKeyFromFile = (api, args) => Effect.gen(function* () {
28428
+ if (args["skip-portal-hint"] !== true) {
28429
+ yield* printHuman("Apple does not expose APNs key creation via the public ASC API.");
28430
+ yield* printHuman("Create the key here, download the .p8, then come back:");
28431
+ yield* printHuman(` ${APPLE_PUSH_KEY_PORTAL_URL}`);
28432
+ yield* printHuman("");
28433
+ }
28434
+ const resolved = yield* resolvePushKeyInput(api, args);
28435
+ yield* printHuman("Uploading APNs auth key...");
28436
+ const credential = yield* uploadCredential(api, {
28437
+ platform: "ios",
28438
+ type: "push-key",
28439
+ name: resolved.name,
28440
+ filePath: resolved.p8Path,
28441
+ keyId: resolved.keyId,
28442
+ appleTeamIdentifier: resolved.appleTeamIdentifier
28443
+ });
28444
+ yield* printHuman("APNs push key registered.");
28445
+ yield* printHumanKeyValue([
28446
+ ["ID", credential.id],
28447
+ ["Key ID", resolved.keyId],
28448
+ ["Apple team", resolved.appleTeamIdentifier]
28449
+ ]);
28450
+ return credential;
28451
+ });
28452
+ const pushKeyCommand$1 = defineCommand({
28453
+ meta: {
28454
+ name: "push-key",
28455
+ description: "Create an APNs auth key (.p8) by logging in with your Apple ID, or upload one you downloaded; the key is end-to-end encrypted before upload"
28456
+ },
28457
+ args: {
28458
+ method: {
28459
+ type: "enum",
28460
+ options: ["apple-id", "upload"],
28461
+ description: "How to obtain the key: 'apple-id' (create via login) or 'upload' (provide --p8)"
28462
+ },
28463
+ "key-id": {
28464
+ type: "string",
28465
+ description: "APNs key ID — upload only (10 uppercase alphanumeric)"
28466
+ },
28467
+ "apple-team-id": {
28468
+ type: "string",
28469
+ description: "Apple Team identifier — upload only"
28470
+ },
28471
+ p8: {
28472
+ type: "string",
28473
+ description: "Path to the AuthKey_XXXXXXXXXX.p8 file (forces upload)"
28474
+ },
28475
+ "asc-key-id": {
28476
+ type: "string",
28477
+ description: "ASC API key ID to derive --apple-team-id automatically (upload only)"
28478
+ },
28479
+ name: {
28480
+ type: "string",
28481
+ description: "Display name (Apple ID: key name; upload: defaults to key ID)"
28482
+ },
28483
+ "skip-portal-hint": {
28484
+ type: "boolean",
28485
+ description: "Skip the Apple Developer portal URL hint (upload only)"
28486
+ }
28487
+ },
28488
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
28489
+ const api = yield* apiClient;
28490
+ if ((yield* resolvePushKeyMethod(args)) === "upload") return yield* uploadPushKeyFromFile(api, args);
28491
+ yield* printHuman("Creating an APNs auth key via your Apple ID...");
28492
+ const created = yield* createApnsKeyViaAppleId(api, args.name ?? defaultApnsKeyName());
28493
+ yield* printHuman("APNs push key created and registered.");
28494
+ yield* printHumanKeyValue([
28495
+ ["ID", created.id],
28496
+ ["Key ID", created.keyId],
28497
+ ["Apple team", created.appleTeamIdentifier],
28498
+ ["Name", created.name]
28499
+ ]);
28500
+ return created;
28501
+ }), {
28502
+ exits: PUSH_KEY_EXIT_EXTRAS,
28503
+ json: "value"
28504
+ })
28505
+ });
28506
+
28196
28507
  //#endregion
28197
28508
  //#region src/commands/credentials/generate.ts
28198
28509
  const GENERATE_EXIT_EXTRAS = {
@@ -28401,90 +28712,6 @@ const parseDeviceIds = (raw) => {
28401
28712
  const ids = raw.split(",").map((id) => id.trim()).filter((id) => id.length > 0);
28402
28713
  return ids.length === 0 ? void 0 : ids;
28403
28714
  };
28404
- const APPLE_PUSH_KEY_PORTAL_URL = "https://developer.apple.com/account/resources/authkeys/list";
28405
- const KEY_ID_PATTERN = /^[A-Z0-9]{10}$/u;
28406
- const APPLE_TEAM_ID_PATTERN = /^[A-Z0-9]{10}$/u;
28407
- const resolveAppleTeamFromAscKey = (api, ascApiKeyId) => Effect.gen(function* () {
28408
- if (ascApiKeyId === void 0) return;
28409
- const teamId = (yield* api.ascApiKeys.list()).items.find((entry) => entry.id === ascApiKeyId)?.appleTeamId;
28410
- return typeof teamId === "string" ? teamId : void 0;
28411
- });
28412
- const validateKeyId = (value) => KEY_ID_PATTERN.test(value) ? Effect.succeed(value) : Effect.fail(new CredentialValidationError({ message: `Push key ID "${value}" must be 10 uppercase alphanumeric characters.` }));
28413
- const validateAppleTeamId = (value) => APPLE_TEAM_ID_PATTERN.test(value) ? Effect.succeed(value) : Effect.fail(new CredentialValidationError({ message: `Apple Team ID "${value}" must be 10 uppercase alphanumeric characters.` }));
28414
- const pushKeyCommand = defineCommand({
28415
- meta: {
28416
- name: "push-key",
28417
- description: "Register an APNs auth key (.p8) — guides you through creating one in the Apple Developer portal, then uploads it"
28418
- },
28419
- args: {
28420
- "key-id": {
28421
- type: "string",
28422
- description: "APNs key ID (10 uppercase alphanumeric)"
28423
- },
28424
- "apple-team-id": {
28425
- type: "string",
28426
- description: "Apple Team identifier"
28427
- },
28428
- p8: {
28429
- type: "string",
28430
- description: "Path to the AuthKey_XXXXXXXXXX.p8 file"
28431
- },
28432
- "asc-key-id": {
28433
- type: "string",
28434
- description: "ASC API key ID to derive --apple-team-id automatically"
28435
- },
28436
- name: {
28437
- type: "string",
28438
- description: "Display name (defaults to the key ID)"
28439
- },
28440
- "skip-portal-hint": {
28441
- type: "boolean",
28442
- description: "Skip the Apple Developer portal URL hint (already created the key)"
28443
- }
28444
- },
28445
- run: async ({ args }) => runEffect(Effect.gen(function* () {
28446
- const api = yield* apiClient;
28447
- if (args["skip-portal-hint"] !== true) {
28448
- yield* printHuman("Apple does not expose APNs key creation via the public ASC API.");
28449
- yield* printHuman("Create the key here, download the .p8, then come back:");
28450
- yield* printHuman(` ${APPLE_PUSH_KEY_PORTAL_URL}`);
28451
- yield* printHuman("");
28452
- }
28453
- const resolved = yield* resolvePushKeyInput(api, args);
28454
- yield* printHuman("Uploading APNs auth key...");
28455
- const credential = yield* uploadCredential(api, {
28456
- platform: "ios",
28457
- type: "push-key",
28458
- name: resolved.name,
28459
- filePath: resolved.p8Path,
28460
- keyId: resolved.keyId,
28461
- appleTeamIdentifier: resolved.appleTeamIdentifier
28462
- });
28463
- yield* printHuman("APNs push key registered.");
28464
- yield* printHumanKeyValue([
28465
- ["ID", credential.id],
28466
- ["Key ID", resolved.keyId],
28467
- ["Apple team", resolved.appleTeamIdentifier]
28468
- ]);
28469
- return credential;
28470
- }), {
28471
- exits: GENERATE_EXIT_EXTRAS,
28472
- json: "value"
28473
- })
28474
- });
28475
- const resolvePushKeyInput = (api, args) => Effect.gen(function* () {
28476
- const derivedTeamId = yield* resolveAppleTeamFromAscKey(api, args["asc-key-id"]);
28477
- const keyId = yield* validateKeyId((args["key-id"] ?? (yield* promptText("APNs key ID (10 uppercase alphanumeric)"))).trim().toUpperCase());
28478
- const appleTeamIdentifier = yield* validateAppleTeamId((args["apple-team-id"] ?? derivedTeamId ?? (yield* promptText("Apple Team identifier (10 uppercase alphanumeric)"))).trim().toUpperCase());
28479
- const p8Path = args.p8 ?? (yield* promptText("Path to the AuthKey_XXXXXXXXXX.p8 file you downloaded"));
28480
- if (p8Path.trim().length === 0) return yield* new CredentialValidationError({ message: "Missing --p8 path" });
28481
- return {
28482
- keyId,
28483
- appleTeamIdentifier,
28484
- p8Path,
28485
- name: args.name ?? keyId
28486
- };
28487
- });
28488
28715
  const GSA_FIREBASE_URL = "https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk";
28489
28716
  const GSA_GCP_URL = "https://console.cloud.google.com/iam-admin/serviceaccounts";
28490
28717
  const gsaKeyCommand = defineCommand({
@@ -28551,7 +28778,7 @@ const generateCommand$1 = defineCommand({
28551
28778
  keystore: keystoreCommand,
28552
28779
  "distribution-certificate": distributionCertificateCommand$1,
28553
28780
  "provisioning-profile": provisioningProfileCommand,
28554
- "push-key": pushKeyCommand,
28781
+ "push-key": pushKeyCommand$1,
28555
28782
  "gsa-key": gsaKeyCommand
28556
28783
  }
28557
28784
  });
@@ -28956,7 +29183,10 @@ const resolveType = (raw, available) => Effect.gen(function* () {
28956
29183
  //#region src/commands/credentials/revoke.ts
28957
29184
  const REVOKE_EXIT_EXTRAS = {
28958
29185
  CredentialValidationError: 2,
28959
- GenerateFailedError: 6
29186
+ GenerateFailedError: 6,
29187
+ AppleIdGenerateFailedError: 6,
29188
+ AppleAuthError: 4,
29189
+ InteractiveProhibitedError: 4
28960
29190
  };
28961
29191
  const resolveAscKeyId = (api, raw) => Effect.gen(function* () {
28962
29192
  if (raw !== void 0 && raw.length > 0) return raw;
@@ -29011,12 +29241,74 @@ const distributionCertificateCommand = defineCommand({
29011
29241
  json: "value"
29012
29242
  })
29013
29243
  });
29244
+ const resolvePushKeyTarget = (api, idArg) => Effect.gen(function* () {
29245
+ const { items } = yield* api.applePushKeys.list();
29246
+ if (items.length === 0) return yield* new CredentialValidationError({ message: "No APNs push keys stored. Nothing to revoke." });
29247
+ if (idArg !== void 0 && idArg.length > 0) {
29248
+ const match = items.find((entry) => entry.id === idArg);
29249
+ if (match === void 0) return yield* new CredentialValidationError({ message: `Push key ${idArg} not found.` });
29250
+ return match;
29251
+ }
29252
+ if (items.length === 1) {
29253
+ const [only] = items;
29254
+ if (only !== void 0) return only;
29255
+ }
29256
+ const chosen = yield* promptSelect("Select a push key to revoke", items.map((entry) => ({
29257
+ value: entry.id,
29258
+ label: `${entry.keyId} (team ${entry.appleTeamId})`
29259
+ })));
29260
+ const match = items.find((entry) => entry.id === chosen);
29261
+ if (match === void 0) return yield* new CredentialValidationError({ message: `Selected push key ${chosen} not found after listing.` });
29262
+ return match;
29263
+ });
29264
+ const pushKeyCommand = defineCommand({
29265
+ meta: {
29266
+ name: "push-key",
29267
+ description: "Revoke an APNs auth key on the Apple Developer Portal (via Apple ID login) and delete it from this account"
29268
+ },
29269
+ args: {
29270
+ id: {
29271
+ type: "string",
29272
+ description: "Local push key ID (prompts if omitted)"
29273
+ },
29274
+ "keep-local": {
29275
+ type: "boolean",
29276
+ description: "Revoke on Apple but keep the credential in this account"
29277
+ }
29278
+ },
29279
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
29280
+ const api = yield* apiClient;
29281
+ const target = yield* resolvePushKeyTarget(api, args.id);
29282
+ const auth = yield* AppleAuth;
29283
+ const session = yield* auth.ensureLoggedIn();
29284
+ const result = yield* revokeLocalApnsKey(api, {
29285
+ context: auth.buildRequestContext(session),
29286
+ pushKeyId: target.id,
29287
+ keyId: target.keyId,
29288
+ keepLocal: args["keep-local"] ?? false
29289
+ });
29290
+ yield* printHuman("APNs push key revoke complete.");
29291
+ yield* printHumanKeyValue([
29292
+ ["Local ID", result.localId],
29293
+ ["Key ID", result.keyId],
29294
+ ["Revoked on Apple", result.revokedOnApple ? "yes" : "no (not present on portal)"],
29295
+ ["Deleted locally", result.deletedLocally ? "yes" : "no (--keep-local)"]
29296
+ ]);
29297
+ return result;
29298
+ }), {
29299
+ exits: REVOKE_EXIT_EXTRAS,
29300
+ json: "value"
29301
+ })
29302
+ });
29014
29303
  const revokeCommand = defineCommand({
29015
29304
  meta: {
29016
29305
  name: "revoke",
29017
29306
  description: "Revoke credentials on the upstream provider"
29018
29307
  },
29019
- subCommands: { "distribution-certificate": distributionCertificateCommand }
29308
+ subCommands: {
29309
+ "distribution-certificate": distributionCertificateCommand,
29310
+ "push-key": pushKeyCommand
29311
+ }
29020
29312
  });
29021
29313
 
29022
29314
  //#endregion