@better-update/cli 0.31.1 → 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 +610 -105
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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.
|
|
37
|
+
var version = "0.33.0";
|
|
38
38
|
|
|
39
39
|
//#endregion
|
|
40
40
|
//#region src/lib/interactive-mode.ts
|
|
@@ -1297,8 +1297,8 @@ var ChannelsGroup = class extends HttpApiGroup.make("channels").add(HttpApiEndpo
|
|
|
1297
1297
|
//#endregion
|
|
1298
1298
|
//#region ../../packages/api/src/domain/device.ts
|
|
1299
1299
|
const DeviceClass = Schema.Literal("IPHONE", "IPAD", "MAC", "UNKNOWN");
|
|
1300
|
-
const IDENTIFIER_PATTERN = /^(?:[A-Fa-f0-9]{40}|[A-Fa-f0-9]{8}-[A-Fa-f0-9]{16}|[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12})$/u;
|
|
1301
|
-
const DeviceIdentifier = Schema.String.pipe(Schema.pattern(IDENTIFIER_PATTERN, { message: () => "Identifier must be an Apple UDID: 40 hex chars, 8-16 hex, or UUID (8-4-4-4-12 hex)" }));
|
|
1300
|
+
const IDENTIFIER_PATTERN$1 = /^(?:[A-Fa-f0-9]{40}|[A-Fa-f0-9]{8}-[A-Fa-f0-9]{16}|[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12})$/u;
|
|
1301
|
+
const DeviceIdentifier = Schema.String.pipe(Schema.pattern(IDENTIFIER_PATTERN$1, { message: () => "Identifier must be an Apple UDID: 40 hex chars, 8-16 hex, or UUID (8-4-4-4-12 hex)" }));
|
|
1302
1302
|
var Device = class extends Schema.Class("Device")({
|
|
1303
1303
|
id: Id,
|
|
1304
1304
|
organizationId: Id,
|
|
@@ -1324,6 +1324,34 @@ const UpdateDeviceBody = Schema.Struct({
|
|
|
1324
1324
|
enabled: Schema.optional(Schema.Boolean),
|
|
1325
1325
|
appleTeamId: Schema.optional(Schema.NullOr(Id))
|
|
1326
1326
|
});
|
|
1327
|
+
/**
|
|
1328
|
+
* One device in an App Store Connect snapshot, as reconciled by `syncDevices`.
|
|
1329
|
+
* `appleDevicePortalId` is the device's id on Apple's portal — its presence is
|
|
1330
|
+
* what the dashboard surfaces as "synced".
|
|
1331
|
+
*/
|
|
1332
|
+
const SyncDeviceEntry = Schema.Struct({
|
|
1333
|
+
identifier: DeviceIdentifier,
|
|
1334
|
+
name: Name120,
|
|
1335
|
+
deviceClass: DeviceClass,
|
|
1336
|
+
appleDevicePortalId: Schema.String
|
|
1337
|
+
});
|
|
1338
|
+
/**
|
|
1339
|
+
* Reconcile the org's device roster for one Apple team against a snapshot of
|
|
1340
|
+
* App Store Connect devices: link portal ids onto existing rows and import any
|
|
1341
|
+
* device that only exists on Apple. `appleTeamId` is the internal team Id (UUID).
|
|
1342
|
+
*/
|
|
1343
|
+
const SyncDevicesBody = Schema.Struct({
|
|
1344
|
+
appleTeamId: Id,
|
|
1345
|
+
devices: Schema.Array(SyncDeviceEntry)
|
|
1346
|
+
});
|
|
1347
|
+
const SyncDevicesResult = Schema.Struct({
|
|
1348
|
+
/** Devices that existed only on Apple and were imported locally. */
|
|
1349
|
+
created: Schema.Number,
|
|
1350
|
+
/** Local devices that gained (or changed) their Apple portal id. */
|
|
1351
|
+
linked: Schema.Number,
|
|
1352
|
+
/** Local devices already in sync — nothing to do. */
|
|
1353
|
+
unchanged: Schema.Number
|
|
1354
|
+
});
|
|
1327
1355
|
const DeleteDeviceResult = DeletedResult;
|
|
1328
1356
|
const DeviceSortColumn = Schema.Literal("name", "createdAt", "deviceClass");
|
|
1329
1357
|
const DeviceSort = sortParam(DeviceSortColumn);
|
|
@@ -1374,6 +1402,9 @@ var DevicesGroup = class extends HttpApiGroup.make("devices").add(HttpApiEndpoin
|
|
|
1374
1402
|
}))).add(HttpApiEndpoint.del("delete")`/api/devices/${idParam}`.addSuccess(DeleteDeviceResult).annotateContext(OpenApi.annotations({
|
|
1375
1403
|
title: "Delete device",
|
|
1376
1404
|
description: "Remove a registered device from the organization"
|
|
1405
|
+
}))).add(HttpApiEndpoint.post("syncDevices", "/api/devices/sync").setPayload(SyncDevicesBody).addSuccess(SyncDevicesResult).annotateContext(OpenApi.annotations({
|
|
1406
|
+
title: "Sync devices with App Store Connect",
|
|
1407
|
+
description: "Reconcile the org's device roster for one Apple team against an App Store Connect snapshot: link Apple portal ids onto existing devices and import devices that only exist on Apple"
|
|
1377
1408
|
}))).add(HttpApiEndpoint.post("createRegistrationRequest", "/api/devices/registration-requests").setPayload(CreateRegistrationRequestBody).addSuccess(DeviceRegistrationRequest, { status: 201 }).annotateContext(OpenApi.annotations({
|
|
1378
1409
|
title: "Create device registration request",
|
|
1379
1410
|
description: "Generate a URL + QR code for self-service device enrollment via Safari on iOS"
|
|
@@ -3047,8 +3078,10 @@ const AppleSessionStoreLive = Layer.effect(AppleSessionStore, Effect.gen(functio
|
|
|
3047
3078
|
const defaultAppleUtils = {
|
|
3048
3079
|
Auth: AppleUtils.Auth,
|
|
3049
3080
|
Session: AppleUtils.Session,
|
|
3081
|
+
Teams: AppleUtils.Teams,
|
|
3050
3082
|
CookieFileCache: AppleUtils.CookieFileCache
|
|
3051
3083
|
};
|
|
3084
|
+
const TEN_CHAR_TEAM_ID = /^[A-Z0-9]{10}$/u;
|
|
3052
3085
|
var AppleAuth = class extends Context.Tag("cli/AppleAuth")() {};
|
|
3053
3086
|
const sessionFromAuthState = (state) => ({
|
|
3054
3087
|
username: state.username,
|
|
@@ -3062,11 +3095,19 @@ const sessionFromInfo = (username, info) => ({
|
|
|
3062
3095
|
teamName: info.provider.name,
|
|
3063
3096
|
providerId: info.provider.providerId
|
|
3064
3097
|
});
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
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;
|
|
3070
3111
|
});
|
|
3071
3112
|
const restoreFromCookies = (appleUtils, cookies) => Effect.tryPromise({
|
|
3072
3113
|
try: async () => appleUtils.Auth.loginWithCookiesAsync({ cookies }),
|
|
@@ -3081,10 +3122,16 @@ const restoreFromCookies = (appleUtils, cookies) => Effect.tryPromise({
|
|
|
3081
3122
|
const resolveSessionTeam = (appleUtils, state) => Effect.gen(function* () {
|
|
3082
3123
|
const { availableProviders } = state.session;
|
|
3083
3124
|
const resolution = yield* resolveProvider(appleUtils, availableProviders, state.context.providerId ?? state.session.provider.providerId);
|
|
3084
|
-
|
|
3085
|
-
const
|
|
3086
|
-
if (
|
|
3087
|
-
|
|
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
|
+
};
|
|
3088
3135
|
});
|
|
3089
3136
|
const loginWithCredentials = (appleUtils, credentials) => Effect.tryPromise({
|
|
3090
3137
|
try: async () => appleUtils.Auth.loginWithUserCredentialsAsync(credentials, { autoResolveProvider: true }),
|
|
@@ -19357,12 +19404,13 @@ const toAscDevice = (value) => {
|
|
|
19357
19404
|
if (!isRecord$1(value)) return null;
|
|
19358
19405
|
const { id, attributes } = value;
|
|
19359
19406
|
if (typeof id !== "string" || !isRecord$1(attributes)) return null;
|
|
19360
|
-
const { udid, name } = attributes;
|
|
19407
|
+
const { udid, name, deviceClass } = attributes;
|
|
19361
19408
|
if (typeof udid !== "string" || typeof name !== "string") return null;
|
|
19362
19409
|
return {
|
|
19363
19410
|
id,
|
|
19364
19411
|
udid,
|
|
19365
|
-
name
|
|
19412
|
+
name,
|
|
19413
|
+
deviceClass: typeof deviceClass === "string" ? deviceClass : null
|
|
19366
19414
|
};
|
|
19367
19415
|
};
|
|
19368
19416
|
const extractList = (body, map) => {
|
|
@@ -19373,6 +19421,18 @@ const extractSingle = (body, map) => {
|
|
|
19373
19421
|
if (!isRecord$1(body)) return null;
|
|
19374
19422
|
return map(body["data"]);
|
|
19375
19423
|
};
|
|
19424
|
+
/**
|
|
19425
|
+
* App Store Connect paginates list responses (default 200/page) and returns the
|
|
19426
|
+
* absolute URL of the next page under `links.next`. Strip the base so it can be
|
|
19427
|
+
* fed back into `fetchRaw`; return null when there is no further page.
|
|
19428
|
+
*/
|
|
19429
|
+
const nextPagePath = (body) => {
|
|
19430
|
+
if (!isRecord$1(body)) return null;
|
|
19431
|
+
const { links } = body;
|
|
19432
|
+
if (!isRecord$1(links) || typeof links["next"] !== "string") return null;
|
|
19433
|
+
const { next } = links;
|
|
19434
|
+
return next.startsWith(API_BASE) ? next.slice(37) : next;
|
|
19435
|
+
};
|
|
19376
19436
|
const malformed = (resource) => new AscApiError({
|
|
19377
19437
|
status: 500,
|
|
19378
19438
|
message: `Malformed ${resource} response`,
|
|
@@ -19420,7 +19480,29 @@ const createBundleId = (credentials, params) => withJwt(credentials, (jwt) => Ef
|
|
|
19420
19480
|
return resource;
|
|
19421
19481
|
}));
|
|
19422
19482
|
const listDevices = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
19423
|
-
|
|
19483
|
+
const devices = [];
|
|
19484
|
+
let path = "/v1/devices?limit=200";
|
|
19485
|
+
while (path !== null) {
|
|
19486
|
+
const body = yield* fetchRaw(jwt, path);
|
|
19487
|
+
devices.push(...extractList(body, toAscDevice));
|
|
19488
|
+
path = nextPagePath(body);
|
|
19489
|
+
}
|
|
19490
|
+
return devices;
|
|
19491
|
+
}));
|
|
19492
|
+
const createDevice = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
19493
|
+
const resource = extractSingle(yield* fetchRaw(jwt, "/v1/devices", {
|
|
19494
|
+
method: "POST",
|
|
19495
|
+
body: JSON.stringify({ data: {
|
|
19496
|
+
type: "devices",
|
|
19497
|
+
attributes: {
|
|
19498
|
+
name: params.name,
|
|
19499
|
+
udid: params.udid,
|
|
19500
|
+
platform: "IOS"
|
|
19501
|
+
}
|
|
19502
|
+
} })
|
|
19503
|
+
}), toAscDevice);
|
|
19504
|
+
if (resource === null) return yield* malformed("device");
|
|
19505
|
+
return resource;
|
|
19424
19506
|
}));
|
|
19425
19507
|
const createProvisioningProfile = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
19426
19508
|
const relationships = {
|
|
@@ -20689,6 +20771,7 @@ const DISTRIBUTION_TO_CERTIFICATE_TYPE = {
|
|
|
20689
20771
|
DEVELOPMENT: AppleUtils.CertificateType.IOS_DEVELOPMENT
|
|
20690
20772
|
};
|
|
20691
20773
|
var AppleIdGenerateFailedError = class extends Data.TaggedError("AppleIdGenerateFailedError") {};
|
|
20774
|
+
var ApnsKeyLimitError = class extends Data.TaggedError("ApnsKeyLimitError") {};
|
|
20692
20775
|
const CERT_LIMIT_PATTERN = /already have a current.*certificate|pending certificate request/iu;
|
|
20693
20776
|
const messageOf = (cause) => cause instanceof Error ? cause.message : String(cause);
|
|
20694
20777
|
const wrap = (step, run) => Effect.tryPromise({
|
|
@@ -20833,6 +20916,86 @@ const generateAndUploadProvisioningProfileViaAppleId = (api, input) => Effect.ge
|
|
|
20833
20916
|
developerPortalIdentifier: created.developerPortalIdentifier
|
|
20834
20917
|
};
|
|
20835
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
|
+
});
|
|
20836
20999
|
|
|
20837
21000
|
//#endregion
|
|
20838
21001
|
//#region src/lib/ios-bundle-config-upsert.ts
|
|
@@ -20905,6 +21068,36 @@ const interactiveAppleIdCertLimitRecover = (ctx) => Effect.gen(function* () {
|
|
|
20905
21068
|
yield* Effect.forEach(toRevoke, (id) => revokeDistributionCertViaAppleId(ctx, id), { concurrency: "inherit" });
|
|
20906
21069
|
yield* Console.log(`Revoked ${toRevoke.length} certificate(s); retrying generation...`);
|
|
20907
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
|
+
});
|
|
20908
21101
|
const generateDistributionCertViaAppleIdInteractive = (api, ctx) => Effect.gen(function* () {
|
|
20909
21102
|
yield* Console.log("Generating distribution certificate via Apple ID...");
|
|
20910
21103
|
const generate = generateAndUploadDistributionCertificateViaAppleId(api, { context: ctx });
|
|
@@ -26926,6 +27119,39 @@ const revokeIosDistributionCert = (ctx) => Effect.gen(function* () {
|
|
|
26926
27119
|
["Deleted locally", result.deletedLocally ? "yes" : "no (kept)"]
|
|
26927
27120
|
]);
|
|
26928
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
|
+
});
|
|
26929
27155
|
|
|
26930
27156
|
//#endregion
|
|
26931
27157
|
//#region src/application/credentials-manager-ios.ts
|
|
@@ -26990,8 +27216,26 @@ const generateNewIosDistributionCert = (ctx) => Effect.gen(function* () {
|
|
|
26990
27216
|
["Apple team", created.appleTeamIdentifier]
|
|
26991
27217
|
]);
|
|
26992
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
|
+
}]);
|
|
26993
27226
|
const addIosPushKey = (ctx) => Effect.gen(function* () {
|
|
26994
|
-
yield*
|
|
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.");
|
|
26995
27239
|
yield* printHuman(`Create one here, download .p8, then return: ${APPLE_PUSH_KEY_PORTAL_URL$1}`);
|
|
26996
27240
|
const keyId = (yield* promptText("APNs key ID (10 uppercase alphanumeric)")).trim().toUpperCase();
|
|
26997
27241
|
if (!APPLE_TEN_CHARS.test(keyId)) return yield* new CredentialValidationError({ message: `Push key ID "${keyId}" must be 10 uppercase alphanumeric characters.` });
|
|
@@ -27053,7 +27297,12 @@ const setupProjectPushNotifications = (ctx) => Effect.gen(function* () {
|
|
|
27053
27297
|
yield* Console.log(`Push notifications set up: key ${pushKeyId} bound to ${config.bundleIdentifier} (${config.distributionType}).`);
|
|
27054
27298
|
});
|
|
27055
27299
|
const createNewPushKeyForBundle = (ctx, fallbackTeamId) => Effect.gen(function* () {
|
|
27056
|
-
yield*
|
|
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.");
|
|
27057
27306
|
yield* printHuman(`Create one here, download .p8, then return: ${APPLE_PUSH_KEY_PORTAL_URL$1}`);
|
|
27058
27307
|
const rawKeyId = (yield* promptText("APNs key ID (10 uppercase alphanumeric)")).trim().toUpperCase();
|
|
27059
27308
|
if (!APPLE_TEN_CHARS.test(rawKeyId)) return yield* new CredentialValidationError({ message: `Push key ID "${rawKeyId}" must be 10 uppercase alphanumeric characters.` });
|
|
@@ -27140,9 +27389,13 @@ const iosPushKeysMenu = (ctx) => Effect.gen(function* () {
|
|
|
27140
27389
|
value: "bind",
|
|
27141
27390
|
label: "Use an existing push key"
|
|
27142
27391
|
},
|
|
27392
|
+
{
|
|
27393
|
+
value: "revoke",
|
|
27394
|
+
label: "Revoke a push key (Apple Developer Portal)"
|
|
27395
|
+
},
|
|
27143
27396
|
{
|
|
27144
27397
|
value: "remove",
|
|
27145
|
-
label: "Remove a push key"
|
|
27398
|
+
label: "Remove a push key (local only)"
|
|
27146
27399
|
},
|
|
27147
27400
|
{
|
|
27148
27401
|
value: BACK,
|
|
@@ -27153,6 +27406,7 @@ const iosPushKeysMenu = (ctx) => Effect.gen(function* () {
|
|
|
27153
27406
|
if (choice === "setup") yield* safely("set up push notifications", setupProjectPushNotifications(ctx));
|
|
27154
27407
|
else if (choice === "add") yield* safely("add push key", addIosPushKey(ctx));
|
|
27155
27408
|
else if (choice === "bind") yield* safely("bind push key", bindIosPushKey(ctx));
|
|
27409
|
+
else if (choice === "revoke") yield* safely("revoke push key", revokeIosPushKey(ctx));
|
|
27156
27410
|
else if (choice === "remove") yield* safely("remove push key", pickAndDelete(ctx, "push-key", "APNs push key"));
|
|
27157
27411
|
yield* iosPushKeysMenu(ctx);
|
|
27158
27412
|
});
|
|
@@ -28127,6 +28381,129 @@ const downloadCommand = defineCommand({
|
|
|
28127
28381
|
}), { json: "value" })
|
|
28128
28382
|
});
|
|
28129
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
|
+
|
|
28130
28507
|
//#endregion
|
|
28131
28508
|
//#region src/commands/credentials/generate.ts
|
|
28132
28509
|
const GENERATE_EXIT_EXTRAS = {
|
|
@@ -28335,90 +28712,6 @@ const parseDeviceIds = (raw) => {
|
|
|
28335
28712
|
const ids = raw.split(",").map((id) => id.trim()).filter((id) => id.length > 0);
|
|
28336
28713
|
return ids.length === 0 ? void 0 : ids;
|
|
28337
28714
|
};
|
|
28338
|
-
const APPLE_PUSH_KEY_PORTAL_URL = "https://developer.apple.com/account/resources/authkeys/list";
|
|
28339
|
-
const KEY_ID_PATTERN = /^[A-Z0-9]{10}$/u;
|
|
28340
|
-
const APPLE_TEAM_ID_PATTERN = /^[A-Z0-9]{10}$/u;
|
|
28341
|
-
const resolveAppleTeamFromAscKey = (api, ascApiKeyId) => Effect.gen(function* () {
|
|
28342
|
-
if (ascApiKeyId === void 0) return;
|
|
28343
|
-
const teamId = (yield* api.ascApiKeys.list()).items.find((entry) => entry.id === ascApiKeyId)?.appleTeamId;
|
|
28344
|
-
return typeof teamId === "string" ? teamId : void 0;
|
|
28345
|
-
});
|
|
28346
|
-
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.` }));
|
|
28347
|
-
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.` }));
|
|
28348
|
-
const pushKeyCommand = defineCommand({
|
|
28349
|
-
meta: {
|
|
28350
|
-
name: "push-key",
|
|
28351
|
-
description: "Register an APNs auth key (.p8) — guides you through creating one in the Apple Developer portal, then uploads it"
|
|
28352
|
-
},
|
|
28353
|
-
args: {
|
|
28354
|
-
"key-id": {
|
|
28355
|
-
type: "string",
|
|
28356
|
-
description: "APNs key ID (10 uppercase alphanumeric)"
|
|
28357
|
-
},
|
|
28358
|
-
"apple-team-id": {
|
|
28359
|
-
type: "string",
|
|
28360
|
-
description: "Apple Team identifier"
|
|
28361
|
-
},
|
|
28362
|
-
p8: {
|
|
28363
|
-
type: "string",
|
|
28364
|
-
description: "Path to the AuthKey_XXXXXXXXXX.p8 file"
|
|
28365
|
-
},
|
|
28366
|
-
"asc-key-id": {
|
|
28367
|
-
type: "string",
|
|
28368
|
-
description: "ASC API key ID to derive --apple-team-id automatically"
|
|
28369
|
-
},
|
|
28370
|
-
name: {
|
|
28371
|
-
type: "string",
|
|
28372
|
-
description: "Display name (defaults to the key ID)"
|
|
28373
|
-
},
|
|
28374
|
-
"skip-portal-hint": {
|
|
28375
|
-
type: "boolean",
|
|
28376
|
-
description: "Skip the Apple Developer portal URL hint (already created the key)"
|
|
28377
|
-
}
|
|
28378
|
-
},
|
|
28379
|
-
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
28380
|
-
const api = yield* apiClient;
|
|
28381
|
-
if (args["skip-portal-hint"] !== true) {
|
|
28382
|
-
yield* printHuman("Apple does not expose APNs key creation via the public ASC API.");
|
|
28383
|
-
yield* printHuman("Create the key here, download the .p8, then come back:");
|
|
28384
|
-
yield* printHuman(` ${APPLE_PUSH_KEY_PORTAL_URL}`);
|
|
28385
|
-
yield* printHuman("");
|
|
28386
|
-
}
|
|
28387
|
-
const resolved = yield* resolvePushKeyInput(api, args);
|
|
28388
|
-
yield* printHuman("Uploading APNs auth key...");
|
|
28389
|
-
const credential = yield* uploadCredential(api, {
|
|
28390
|
-
platform: "ios",
|
|
28391
|
-
type: "push-key",
|
|
28392
|
-
name: resolved.name,
|
|
28393
|
-
filePath: resolved.p8Path,
|
|
28394
|
-
keyId: resolved.keyId,
|
|
28395
|
-
appleTeamIdentifier: resolved.appleTeamIdentifier
|
|
28396
|
-
});
|
|
28397
|
-
yield* printHuman("APNs push key registered.");
|
|
28398
|
-
yield* printHumanKeyValue([
|
|
28399
|
-
["ID", credential.id],
|
|
28400
|
-
["Key ID", resolved.keyId],
|
|
28401
|
-
["Apple team", resolved.appleTeamIdentifier]
|
|
28402
|
-
]);
|
|
28403
|
-
return credential;
|
|
28404
|
-
}), {
|
|
28405
|
-
exits: GENERATE_EXIT_EXTRAS,
|
|
28406
|
-
json: "value"
|
|
28407
|
-
})
|
|
28408
|
-
});
|
|
28409
|
-
const resolvePushKeyInput = (api, args) => Effect.gen(function* () {
|
|
28410
|
-
const derivedTeamId = yield* resolveAppleTeamFromAscKey(api, args["asc-key-id"]);
|
|
28411
|
-
const keyId = yield* validateKeyId((args["key-id"] ?? (yield* promptText("APNs key ID (10 uppercase alphanumeric)"))).trim().toUpperCase());
|
|
28412
|
-
const appleTeamIdentifier = yield* validateAppleTeamId((args["apple-team-id"] ?? derivedTeamId ?? (yield* promptText("Apple Team identifier (10 uppercase alphanumeric)"))).trim().toUpperCase());
|
|
28413
|
-
const p8Path = args.p8 ?? (yield* promptText("Path to the AuthKey_XXXXXXXXXX.p8 file you downloaded"));
|
|
28414
|
-
if (p8Path.trim().length === 0) return yield* new CredentialValidationError({ message: "Missing --p8 path" });
|
|
28415
|
-
return {
|
|
28416
|
-
keyId,
|
|
28417
|
-
appleTeamIdentifier,
|
|
28418
|
-
p8Path,
|
|
28419
|
-
name: args.name ?? keyId
|
|
28420
|
-
};
|
|
28421
|
-
});
|
|
28422
28715
|
const GSA_FIREBASE_URL = "https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk";
|
|
28423
28716
|
const GSA_GCP_URL = "https://console.cloud.google.com/iam-admin/serviceaccounts";
|
|
28424
28717
|
const gsaKeyCommand = defineCommand({
|
|
@@ -28485,7 +28778,7 @@ const generateCommand$1 = defineCommand({
|
|
|
28485
28778
|
keystore: keystoreCommand,
|
|
28486
28779
|
"distribution-certificate": distributionCertificateCommand$1,
|
|
28487
28780
|
"provisioning-profile": provisioningProfileCommand,
|
|
28488
|
-
"push-key": pushKeyCommand,
|
|
28781
|
+
"push-key": pushKeyCommand$1,
|
|
28489
28782
|
"gsa-key": gsaKeyCommand
|
|
28490
28783
|
}
|
|
28491
28784
|
});
|
|
@@ -28890,7 +29183,10 @@ const resolveType = (raw, available) => Effect.gen(function* () {
|
|
|
28890
29183
|
//#region src/commands/credentials/revoke.ts
|
|
28891
29184
|
const REVOKE_EXIT_EXTRAS = {
|
|
28892
29185
|
CredentialValidationError: 2,
|
|
28893
|
-
GenerateFailedError: 6
|
|
29186
|
+
GenerateFailedError: 6,
|
|
29187
|
+
AppleIdGenerateFailedError: 6,
|
|
29188
|
+
AppleAuthError: 4,
|
|
29189
|
+
InteractiveProhibitedError: 4
|
|
28894
29190
|
};
|
|
28895
29191
|
const resolveAscKeyId = (api, raw) => Effect.gen(function* () {
|
|
28896
29192
|
if (raw !== void 0 && raw.length > 0) return raw;
|
|
@@ -28945,12 +29241,74 @@ const distributionCertificateCommand = defineCommand({
|
|
|
28945
29241
|
json: "value"
|
|
28946
29242
|
})
|
|
28947
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
|
+
});
|
|
28948
29303
|
const revokeCommand = defineCommand({
|
|
28949
29304
|
meta: {
|
|
28950
29305
|
name: "revoke",
|
|
28951
29306
|
description: "Revoke credentials on the upstream provider"
|
|
28952
29307
|
},
|
|
28953
|
-
subCommands: {
|
|
29308
|
+
subCommands: {
|
|
29309
|
+
"distribution-certificate": distributionCertificateCommand,
|
|
29310
|
+
"push-key": pushKeyCommand
|
|
29311
|
+
}
|
|
28954
29312
|
});
|
|
28955
29313
|
|
|
28956
29314
|
//#endregion
|
|
@@ -30143,6 +30501,7 @@ const listDevicesCommand = defineCommand({
|
|
|
30143
30501
|
"Class",
|
|
30144
30502
|
"UDID",
|
|
30145
30503
|
"Team",
|
|
30504
|
+
"Synced",
|
|
30146
30505
|
"Enabled"
|
|
30147
30506
|
], items.map((device) => [
|
|
30148
30507
|
device.id,
|
|
@@ -30150,6 +30509,7 @@ const listDevicesCommand = defineCommand({
|
|
|
30150
30509
|
device.deviceClass,
|
|
30151
30510
|
device.identifier,
|
|
30152
30511
|
device.appleTeamId ?? "—",
|
|
30512
|
+
device.appleDevicePortalId === null ? "no" : "yes",
|
|
30153
30513
|
device.enabled ? "yes" : "no"
|
|
30154
30514
|
]));
|
|
30155
30515
|
return {
|
|
@@ -30190,6 +30550,150 @@ const renameDeviceCommand = defineCommand({
|
|
|
30190
30550
|
}))
|
|
30191
30551
|
});
|
|
30192
30552
|
|
|
30553
|
+
//#endregion
|
|
30554
|
+
//#region src/commands/devices/sync.ts
|
|
30555
|
+
const IDENTIFIER_PATTERN = /^(?:[A-Fa-f0-9]{40}|[A-Fa-f0-9]{8}-[A-Fa-f0-9]{16}|[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12})$/u;
|
|
30556
|
+
const APPLE_DEVICE_CLASS = {
|
|
30557
|
+
IPHONE: "IPHONE",
|
|
30558
|
+
IPAD: "IPAD",
|
|
30559
|
+
MAC: "MAC"
|
|
30560
|
+
};
|
|
30561
|
+
const toDeviceClass = (raw) => raw === null ? "UNKNOWN" : APPLE_DEVICE_CLASS[raw] ?? "UNKNOWN";
|
|
30562
|
+
const ascErrorMessage = (error) => error._tag === "AscApiError" ? error.message : `Apple request failed: ${String(error.cause)}`;
|
|
30563
|
+
const LIST_LIMIT = 100;
|
|
30564
|
+
/**
|
|
30565
|
+
* Resolve which ASC key authenticates the sync and which internal team it
|
|
30566
|
+
* targets. Either flag suffices: an ASC key already carries its team, and a team
|
|
30567
|
+
* resolves to the ASC key uploaded for it.
|
|
30568
|
+
*/
|
|
30569
|
+
const resolveTarget = (api, args) => Effect.gen(function* () {
|
|
30570
|
+
const keyArg = args["asc-api-key-id"];
|
|
30571
|
+
const teamArg = args["apple-team-id"];
|
|
30572
|
+
const ascKeys = yield* api.ascApiKeys.list();
|
|
30573
|
+
if (keyArg !== void 0) {
|
|
30574
|
+
const match = ascKeys.items.find((key) => key.id === keyArg);
|
|
30575
|
+
if (match === void 0) return yield* new InvalidArgumentError({ message: `ASC API key "${keyArg}" not found in this organization.` });
|
|
30576
|
+
const appleTeamId = teamArg ?? match.appleTeamId;
|
|
30577
|
+
if (!appleTeamId) return yield* new InvalidArgumentError({ message: `ASC API key "${keyArg}" is not linked to an Apple team; pass --apple-team-id <uuid>.` });
|
|
30578
|
+
return {
|
|
30579
|
+
ascApiKeyId: keyArg,
|
|
30580
|
+
appleTeamId
|
|
30581
|
+
};
|
|
30582
|
+
}
|
|
30583
|
+
if (teamArg !== void 0) {
|
|
30584
|
+
const match = ascKeys.items.find((key) => key.appleTeamId === teamArg);
|
|
30585
|
+
if (match === void 0) return yield* new InvalidArgumentError({ message: `No ASC API key found for team "${teamArg}". Upload one with \`better-update credentials upload-asc-key\`.` });
|
|
30586
|
+
return {
|
|
30587
|
+
ascApiKeyId: match.id,
|
|
30588
|
+
appleTeamId: teamArg
|
|
30589
|
+
};
|
|
30590
|
+
}
|
|
30591
|
+
return yield* new InvalidArgumentError({ message: "Pass --apple-team-id <uuid> or --asc-api-key-id <id> to choose what to sync." });
|
|
30592
|
+
});
|
|
30593
|
+
const listAllLocalDevices = (api, appleTeamId) => Effect.gen(function* () {
|
|
30594
|
+
const items = [];
|
|
30595
|
+
let page = 1;
|
|
30596
|
+
let total = Number.POSITIVE_INFINITY;
|
|
30597
|
+
while (items.length < total) {
|
|
30598
|
+
const result = yield* api.devices.list({ urlParams: {
|
|
30599
|
+
appleTeamId,
|
|
30600
|
+
page,
|
|
30601
|
+
limit: LIST_LIMIT
|
|
30602
|
+
} });
|
|
30603
|
+
({total} = result);
|
|
30604
|
+
if (result.items.length === 0) break;
|
|
30605
|
+
items.push(...result.items);
|
|
30606
|
+
page += 1;
|
|
30607
|
+
}
|
|
30608
|
+
return items;
|
|
30609
|
+
});
|
|
30610
|
+
const syncDeviceCommand = defineCommand({
|
|
30611
|
+
meta: {
|
|
30612
|
+
name: "sync",
|
|
30613
|
+
description: "Sync devices with Apple App Store Connect: register local-only devices on Apple and import devices already registered there"
|
|
30614
|
+
},
|
|
30615
|
+
args: {
|
|
30616
|
+
"apple-team-id": {
|
|
30617
|
+
type: "string",
|
|
30618
|
+
description: "Internal team Id (UUID) to sync; derived from --asc-api-key-id if omitted"
|
|
30619
|
+
},
|
|
30620
|
+
"asc-api-key-id": {
|
|
30621
|
+
type: "string",
|
|
30622
|
+
description: "ASC API key to authenticate with; derived from --apple-team-id if omitted"
|
|
30623
|
+
},
|
|
30624
|
+
push: {
|
|
30625
|
+
type: "boolean",
|
|
30626
|
+
default: true,
|
|
30627
|
+
description: "Register local-only devices on Apple",
|
|
30628
|
+
negativeDescription: "Skip registering local devices on Apple (--no-push)"
|
|
30629
|
+
},
|
|
30630
|
+
pull: {
|
|
30631
|
+
type: "boolean",
|
|
30632
|
+
default: true,
|
|
30633
|
+
description: "Import Apple-registered devices into better-update",
|
|
30634
|
+
negativeDescription: "Skip importing Apple devices (--no-pull)"
|
|
30635
|
+
}
|
|
30636
|
+
},
|
|
30637
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
30638
|
+
const api = yield* apiClient;
|
|
30639
|
+
const target = yield* resolveTarget(api, args);
|
|
30640
|
+
const creds = yield* fetchAscCredentials(api, target.ascApiKeyId);
|
|
30641
|
+
const ascCreds = {
|
|
30642
|
+
keyId: creds.keyId,
|
|
30643
|
+
issuerId: creds.issuerId,
|
|
30644
|
+
p8Pem: creds.p8Pem
|
|
30645
|
+
};
|
|
30646
|
+
const appleDevices = yield* listDevices(ascCreds);
|
|
30647
|
+
const local = yield* listAllLocalDevices(api, target.appleTeamId);
|
|
30648
|
+
const localUdids = new Set(local.map((device) => device.identifier.toLowerCase()));
|
|
30649
|
+
const pushed = [];
|
|
30650
|
+
const pushFailures = [];
|
|
30651
|
+
if (args.push) {
|
|
30652
|
+
const appleUdids = new Set(appleDevices.map((device) => device.udid.toLowerCase()));
|
|
30653
|
+
const toPush = local.filter((device) => !appleUdids.has(device.identifier.toLowerCase()));
|
|
30654
|
+
for (const device of toPush) {
|
|
30655
|
+
const result = yield* Effect.either(createDevice(ascCreds, {
|
|
30656
|
+
name: device.name,
|
|
30657
|
+
udid: device.identifier
|
|
30658
|
+
}));
|
|
30659
|
+
if (Either.isRight(result)) pushed.push(result.right);
|
|
30660
|
+
else pushFailures.push({
|
|
30661
|
+
identifier: device.identifier,
|
|
30662
|
+
message: ascErrorMessage(result.left)
|
|
30663
|
+
});
|
|
30664
|
+
}
|
|
30665
|
+
}
|
|
30666
|
+
const reconcileEntries = [...appleDevices, ...pushed].filter((device) => args.pull || localUdids.has(device.udid.toLowerCase())).filter((device) => IDENTIFIER_PATTERN.test(device.udid) && device.name.length > 0).map((device) => ({
|
|
30667
|
+
identifier: device.udid,
|
|
30668
|
+
name: device.name.slice(0, 120),
|
|
30669
|
+
deviceClass: toDeviceClass(device.deviceClass),
|
|
30670
|
+
appleDevicePortalId: device.id
|
|
30671
|
+
}));
|
|
30672
|
+
const summary = reconcileEntries.length > 0 ? yield* api.devices.syncDevices({ payload: {
|
|
30673
|
+
appleTeamId: target.appleTeamId,
|
|
30674
|
+
devices: reconcileEntries
|
|
30675
|
+
} }) : {
|
|
30676
|
+
created: 0,
|
|
30677
|
+
linked: 0,
|
|
30678
|
+
unchanged: 0
|
|
30679
|
+
};
|
|
30680
|
+
yield* printHumanKeyValue([
|
|
30681
|
+
["Apple devices", String(appleDevices.length + pushed.length)],
|
|
30682
|
+
["Pushed to Apple", String(pushed.length)],
|
|
30683
|
+
["Imported locally", String(summary.created)],
|
|
30684
|
+
["Linked (portal id set)", String(summary.linked)],
|
|
30685
|
+
["Already synced", String(summary.unchanged)]
|
|
30686
|
+
]);
|
|
30687
|
+
for (const failure of pushFailures) yield* printHuman(`⚠ Could not push ${failure.identifier} to Apple: ${failure.message}`);
|
|
30688
|
+
return {
|
|
30689
|
+
appleTeamId: target.appleTeamId,
|
|
30690
|
+
pushed: pushed.length,
|
|
30691
|
+
...summary,
|
|
30692
|
+
pushFailures
|
|
30693
|
+
};
|
|
30694
|
+
}), { json: "value" })
|
|
30695
|
+
});
|
|
30696
|
+
|
|
30193
30697
|
//#endregion
|
|
30194
30698
|
//#region src/commands/devices/view.ts
|
|
30195
30699
|
const viewDeviceCommand = defineCommand({
|
|
@@ -30230,6 +30734,7 @@ const devicesCommand = defineCommand({
|
|
|
30230
30734
|
add: addDeviceCommand,
|
|
30231
30735
|
list: listDevicesCommand,
|
|
30232
30736
|
view: viewDeviceCommand,
|
|
30737
|
+
sync: syncDeviceCommand,
|
|
30233
30738
|
rename: renameDeviceCommand,
|
|
30234
30739
|
enable: enableDeviceCommand,
|
|
30235
30740
|
disable: disableDeviceCommand,
|