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